working dev environment

This commit is contained in:
2026-06-18 08:29:03 +00:00
parent 6fc00b8ce9
commit 57b882128d
21 changed files with 200 additions and 210 deletions
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.
+26
View File
@@ -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"}
+1
View File
@@ -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
View File
@@ -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"])
+2 -2
View File
@@ -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)
+1
View File
@@ -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
View File
@@ -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())