working dev environment
This commit is contained in:
1 parent
6fc00b8ce9
commit
57b882128d
21 files changed
+200
-210
No files matched your search
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,26 @@
|
||||
from fastapi import APIRouter
|
||||
|
||||
from app.config import settings
|
||||
from app.database import get_session
|
||||
from app.models import DonationTier, Event, Program
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
@router.post("/reset")
|
||||
async def reset_database():
|
||||
"""Truncate all tables and re-seed. Dev-only — disabled in production."""
|
||||
if settings.ENV == "production":
|
||||
return {"error": "Admin reset is disabled in production"}
|
||||
|
||||
from app.database import async_session
|
||||
from seed import seed as run_seed
|
||||
|
||||
async with async_session() as session:
|
||||
await session.execute(Program.__table__.delete())
|
||||
await session.execute(Event.__table__.delete())
|
||||
await session.execute(DonationTier.__table__.delete())
|
||||
await session.commit()
|
||||
|
||||
await run_seed()
|
||||
return {"status": "reset complete"}
|
||||
@@ -4,6 +4,7 @@ from pydantic_settings import BaseSettings
|
||||
class Settings(BaseSettings):
|
||||
DATABASE_URL: str = "postgresql+asyncpg://postgres:postgres@localhost:5432/kmountain"
|
||||
CORS_ORIGINS: list[str] = ["http://localhost:4200", "http://localhost:3000"]
|
||||
ENV: str = "development"
|
||||
|
||||
model_config = {"env_prefix": "KMTN_"}
|
||||
|
||||
|
||||
+19
-3
@@ -2,18 +2,29 @@ from contextlib import asynccontextmanager
|
||||
|
||||
from fastapi import FastAPI
|
||||
from fastapi.middleware.cors import CORSMiddleware
|
||||
from sqlalchemy import func, select
|
||||
|
||||
from app.config import settings
|
||||
from app.database import engine
|
||||
from app.models import Base
|
||||
from app.database import async_session, engine
|
||||
from app.models import Base, Program
|
||||
from app.api import programs, events, tiers
|
||||
|
||||
|
||||
@asynccontextmanager
|
||||
async def lifespan(app: FastAPI):
|
||||
# Create tables on startup (seed.py handles data seeding)
|
||||
# Create tables on startup
|
||||
async with engine.begin() as conn:
|
||||
await conn.run_sync(Base.metadata.create_all)
|
||||
|
||||
# Auto-seed if database is empty
|
||||
async with async_session() as session:
|
||||
result = await session.execute(select(func.count(Program.id)))
|
||||
count = result.scalar()
|
||||
if count == 0:
|
||||
print(" → Database is empty — running seed...")
|
||||
from seed import seed as run_seed
|
||||
await run_seed()
|
||||
|
||||
yield
|
||||
|
||||
|
||||
@@ -36,3 +47,8 @@ app.add_middleware(
|
||||
app.include_router(programs.router, prefix="/api/programs", tags=["programs"])
|
||||
app.include_router(events.router, prefix="/api/events", tags=["events"])
|
||||
app.include_router(tiers.router, prefix="/api/tiers", tags=["tiers"])
|
||||
|
||||
# Dev-only admin router (disabled in production)
|
||||
if settings.ENV != "production":
|
||||
from app.api import admin
|
||||
app.include_router(admin.router, prefix="/api/admin", tags=["admin"])
|
||||
@@ -6,7 +6,7 @@ from sqlalchemy import (
|
||||
Numeric,
|
||||
Boolean,
|
||||
Date,
|
||||
ARRAY,
|
||||
JSON,
|
||||
)
|
||||
from sqlalchemy.orm import DeclarativeBase
|
||||
|
||||
@@ -47,5 +47,5 @@ class DonationTier(Base):
|
||||
name = Column(String(50), unique=True, nullable=False)
|
||||
amount = Column(Numeric(10, 2), nullable=False)
|
||||
icon = Column(String(10), nullable=False)
|
||||
benefits = Column(ARRAY(Text), nullable=False)
|
||||
benefits = Column(JSON, nullable=False)
|
||||
display_order = Column(Integer, nullable=False)
|
||||
@@ -2,6 +2,7 @@ fastapi==0.115.6
|
||||
uvicorn[standard]==0.34.0
|
||||
psycopg2-binary==2.9.10
|
||||
asyncpg==0.30.0
|
||||
aiosqlite==0.20.0
|
||||
sqlalchemy[asyncio]==2.0.36
|
||||
alembic==1.14.1
|
||||
python-multipart==0.0.20
|
||||
|
||||
+68
-10
@@ -1,14 +1,18 @@
|
||||
"""
|
||||
Seed script: populates the database with station data.
|
||||
|
||||
Idempotent — safe to run multiple times. Uses get-or-create (upsert) for each entity.
|
||||
|
||||
Usage:
|
||||
DATABASE_URL=postgresql://... python -m app.seed
|
||||
(or from the repo root: cd backend && python seed.py)
|
||||
KMTN_DATABASE_URL=... python seed.py
|
||||
(run from the backend/ directory)
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
from datetime import date
|
||||
|
||||
from sqlalchemy import select
|
||||
|
||||
from app.database import async_session
|
||||
from app.models import Program, Event, DonationTier
|
||||
|
||||
@@ -90,29 +94,83 @@ TIERS: list[dict] = [
|
||||
]
|
||||
|
||||
|
||||
# ── Helpers ────────────────────────────────────────────────
|
||||
|
||||
async def _upsert_program(session, data: dict) -> None:
|
||||
"""Get-or-create a program matched on (day_of_week, time)."""
|
||||
existing = await session.execute(
|
||||
select(Program).where(
|
||||
Program.day_of_week == data["day_of_week"],
|
||||
Program.time == data["time"],
|
||||
)
|
||||
)
|
||||
program = existing.scalar_one_or_none()
|
||||
if program:
|
||||
for key, value in data.items():
|
||||
setattr(program, key, value)
|
||||
else:
|
||||
session.add(Program(**data))
|
||||
|
||||
|
||||
async def _upsert_event(session, data: dict) -> None:
|
||||
"""Get-or-create an event matched on (date, title)."""
|
||||
existing = await session.execute(
|
||||
select(Event).where(
|
||||
Event.date == data["date"],
|
||||
Event.title == data["title"],
|
||||
)
|
||||
)
|
||||
event = existing.scalar_one_or_none()
|
||||
if event:
|
||||
for key, value in data.items():
|
||||
setattr(event, key, value)
|
||||
else:
|
||||
session.add(Event(**data, active=True))
|
||||
|
||||
|
||||
async def _upsert_tier(session, data: dict) -> None:
|
||||
"""Get-or-create a donation tier matched on name."""
|
||||
existing = await session.execute(
|
||||
select(DonationTier).where(DonationTier.name == data["name"])
|
||||
)
|
||||
tier = existing.scalar_one_or_none()
|
||||
if tier:
|
||||
for key, value in data.items():
|
||||
setattr(tier, key, value)
|
||||
else:
|
||||
session.add(DonationTier(**data))
|
||||
|
||||
|
||||
# ── Main ──────────────────────────────────────────────────
|
||||
|
||||
async def seed():
|
||||
async def seed() -> None:
|
||||
"""Populate the database with station data (idempotent)."""
|
||||
async with async_session() as session:
|
||||
# Programs
|
||||
for data in PROGRAMS:
|
||||
session.add(Program(**data))
|
||||
await _upsert_program(session, data)
|
||||
await session.commit()
|
||||
print(f" ✓ Seeded {len(PROGRAMS)} programs")
|
||||
|
||||
# Events
|
||||
for data in EVENTS:
|
||||
event = Event(**data, active=True)
|
||||
session.add(event)
|
||||
await _upsert_event(session, data)
|
||||
await session.commit()
|
||||
print(f" ✓ Seeded {len(EVENTS)} events")
|
||||
|
||||
# Tiers
|
||||
for data in TIERS:
|
||||
session.add(DonationTier(**data))
|
||||
await _upsert_tier(session, data)
|
||||
await session.commit()
|
||||
print(f" ✓ Seeded {len(TIERS)} donation tiers")
|
||||
|
||||
|
||||
async def truncate_all() -> None:
|
||||
"""Remove all seed data from the database."""
|
||||
async with async_session() as session:
|
||||
await session.execute(Program.__table__.delete())
|
||||
await session.execute(Event.__table__.delete())
|
||||
await session.execute(DonationTier.__table__.delete())
|
||||
await session.commit()
|
||||
print(" ✓ Truncated all tables")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(seed())
|
||||
Reference in new issue
Block a user