working dev environment
This commit is contained in:
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