working 3 tier approach
This commit is contained in:
1 parent
4482a30793
commit
e5fef8b181
47 files changed
+1288
-200
No files matched your search
Vendored
+18
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"version": "0.2.0",
|
||||
"configurations": [
|
||||
{
|
||||
"name": "FastAPI (uvicorn)",
|
||||
"type": "python",
|
||||
"request": "launch",
|
||||
"module": "uvicorn",
|
||||
"args": ["app.main:app", "--reload", "--host", "0.0.0.0", "--port", "8000"],
|
||||
"cwd": "${workspaceFolder}",
|
||||
"env": {
|
||||
"DATABASE_URL": "postgresql+asyncpg://postgres:postgres@localhost:5432/kmountain"
|
||||
},
|
||||
"django": false,
|
||||
"console": "integratedTerminal"
|
||||
}
|
||||
]
|
||||
}
|
||||
Vendored
+13
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"python.defaultInterpreterPath": "${workspaceFolder}/.venv/bin/python",
|
||||
"python.linting.enabled": true,
|
||||
"python.linting.flake8Enabled": true,
|
||||
"python.linting.pylanceEnabled": true,
|
||||
"python.formatting.provider": "black",
|
||||
"editor.formatOnSave": true,
|
||||
"editor.tabSize": 4,
|
||||
"[python]": {
|
||||
"editor.defaultFormatter": "ms-python.black-formatter",
|
||||
"editor.tabSize": 4
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
FROM docker.io/library/python:3.12-slim AS base
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
RUN pip install --no-cache-dir gunicorn uvicorn psycopg2-binary
|
||||
|
||||
COPY requirements.txt .
|
||||
RUN pip install --no-cache-dir -r requirements.txt
|
||||
|
||||
COPY . .
|
||||
|
||||
CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000", "--workers", "2"]
|
||||
Binary file not shown.
Whitespace-only changes.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Whitespace-only changes.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,70 @@
|
||||
from typing import Optional
|
||||
|
||||
from fastapi import APIRouter, Depends, Query
|
||||
from sqlalchemy import select
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.database import get_session
|
||||
from app.models import Event
|
||||
from app.schemas import EventCreate, EventUpdate, EventResponse
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
@router.get("/", response_model=list[EventResponse])
|
||||
async def list_events(
|
||||
active: Optional[bool] = Query(None, description="Filter by active status"),
|
||||
session: AsyncSession = Depends(get_session),
|
||||
):
|
||||
query = select(Event).order_by(Event.date)
|
||||
if active is not None:
|
||||
query = query.where(Event.active == active)
|
||||
result = await session.execute(query)
|
||||
return list(result.scalars().all())
|
||||
|
||||
|
||||
@router.get("/{event_id}", response_model=EventResponse)
|
||||
async def get_event(event_id: int, session: AsyncSession = Depends(get_session)):
|
||||
result = await session.execute(select(Event).where(Event.id == event_id))
|
||||
event = result.scalar_one_or_none()
|
||||
if not event:
|
||||
return {"error": "Event not found"}
|
||||
return event
|
||||
|
||||
|
||||
@router.post("/", response_model=EventResponse, status_code=201)
|
||||
async def create_event(
|
||||
payload: EventCreate, session: AsyncSession = Depends(get_session)
|
||||
):
|
||||
event = Event(**payload.model_dump(), active=True)
|
||||
session.add(event)
|
||||
await session.commit()
|
||||
await session.refresh(event)
|
||||
return event
|
||||
|
||||
|
||||
@router.put("/{event_id}", response_model=EventResponse)
|
||||
async def update_event(
|
||||
event_id: int,
|
||||
payload: EventUpdate,
|
||||
session: AsyncSession = Depends(get_session),
|
||||
):
|
||||
result = await session.execute(select(Event).where(Event.id == event_id))
|
||||
event = result.scalar_one_or_none()
|
||||
if not event:
|
||||
return {"error": "Event not found"}
|
||||
for key, value in payload.model_dump(exclude_unset=True).items():
|
||||
setattr(event, key, value)
|
||||
await session.commit()
|
||||
await session.refresh(event)
|
||||
return event
|
||||
|
||||
|
||||
@router.delete("/{event_id}", status_code=204)
|
||||
async def delete_event(event_id: int, session: AsyncSession = Depends(get_session)):
|
||||
result = await session.execute(select(Event).where(Event.id == event_id))
|
||||
event = result.scalar_one_or_none()
|
||||
if not event:
|
||||
return {"error": "Event not found"}
|
||||
await session.delete(event)
|
||||
await session.commit()
|
||||
@@ -0,0 +1,75 @@
|
||||
from typing import Optional
|
||||
|
||||
from fastapi import APIRouter, Depends, Query
|
||||
from sqlalchemy import select, func
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.database import get_session
|
||||
from app.models import Program
|
||||
from app.schemas import ProgramCreate, ProgramUpdate, ProgramResponse
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
def _day_filter(day: int):
|
||||
"""Return the SQLAlchemy filter for a given day_of_week."""
|
||||
return Program.day_of_week == day
|
||||
|
||||
|
||||
@router.get("/", response_model=list[ProgramResponse])
|
||||
async def list_programs(
|
||||
day: Optional[int] = Query(None, description="Filter by day_of_week (1=Mon … 7=Sun)"),
|
||||
session: AsyncSession = Depends(get_session),
|
||||
):
|
||||
query = select(Program).order_by(Program.day_of_week, Program.time)
|
||||
if day is not None:
|
||||
query = query.filter(_day_filter(day))
|
||||
result = await session.execute(query)
|
||||
return list(result.scalars().all())
|
||||
|
||||
|
||||
@router.get("/{program_id}", response_model=ProgramResponse)
|
||||
async def get_program(program_id: int, session: AsyncSession = Depends(get_session)):
|
||||
result = await session.execute(select(Program).where(Program.id == program_id))
|
||||
program = result.scalar_one_or_none()
|
||||
if not program:
|
||||
return {"error": "Program not found"}
|
||||
return program
|
||||
|
||||
|
||||
@router.post("/", response_model=ProgramResponse, status_code=201)
|
||||
async def create_program(
|
||||
payload: ProgramCreate, session: AsyncSession = Depends(get_session)
|
||||
):
|
||||
program = Program(**payload.model_dump())
|
||||
session.add(program)
|
||||
await session.commit()
|
||||
await session.refresh(program)
|
||||
return program
|
||||
|
||||
|
||||
@router.put("/{program_id}", response_model=ProgramResponse)
|
||||
async def update_program(
|
||||
program_id: int,
|
||||
payload: ProgramUpdate,
|
||||
session: AsyncSession = Depends(get_session),
|
||||
):
|
||||
result = await session.execute(select(Program).where(Program.id == program_id))
|
||||
program = result.scalar_one_or_none()
|
||||
if not program:
|
||||
return {"error": "Program not found"}
|
||||
for key, value in payload.model_dump(exclude_unset=True).items():
|
||||
setattr(program, key, value)
|
||||
await session.commit()
|
||||
await session.refresh(program)
|
||||
return program
|
||||
|
||||
|
||||
@router.delete("/{program_id}", status_code=204)
|
||||
async def delete_program(program_id: int, session: AsyncSession = Depends(get_session)):
|
||||
result = await session.execute(select(Program).where(Program.id == program_id))
|
||||
program = result.scalar_one_or_none()
|
||||
if not program:
|
||||
return {"error": "Program not found"}
|
||||
await session.delete(program)
|
||||
await session.commit()
|
||||
@@ -0,0 +1,66 @@
|
||||
from typing import Optional
|
||||
|
||||
from fastapi import APIRouter, Depends
|
||||
from sqlalchemy import select
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.database import get_session
|
||||
from app.models import DonationTier
|
||||
from app.schemas import TierCreate, TierUpdate, TierResponse
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
@router.get("/", response_model=list[TierResponse])
|
||||
async def list_tiers(session: AsyncSession = Depends(get_session)):
|
||||
result = await session.execute(
|
||||
select(DonationTier).order_by(DonationTier.display_order)
|
||||
)
|
||||
return list(result.scalars().all())
|
||||
|
||||
|
||||
@router.get("/{tier_id}", response_model=TierResponse)
|
||||
async def get_tier(tier_id: int, session: AsyncSession = Depends(get_session)):
|
||||
result = await session.execute(select(DonationTier).where(DonationTier.id == tier_id))
|
||||
tier = result.scalar_one_or_none()
|
||||
if not tier:
|
||||
return {"error": "Tier not found"}
|
||||
return tier
|
||||
|
||||
|
||||
@router.post("/", response_model=TierResponse, status_code=201)
|
||||
async def create_tier(
|
||||
payload: TierCreate, session: AsyncSession = Depends(get_session)
|
||||
):
|
||||
tier = DonationTier(**payload.model_dump())
|
||||
session.add(tier)
|
||||
await session.commit()
|
||||
await session.refresh(tier)
|
||||
return tier
|
||||
|
||||
|
||||
@router.put("/{tier_id}", response_model=TierResponse)
|
||||
async def update_tier(
|
||||
tier_id: int,
|
||||
payload: TierUpdate,
|
||||
session: AsyncSession = Depends(get_session),
|
||||
):
|
||||
result = await session.execute(select(DonationTier).where(DonationTier.id == tier_id))
|
||||
tier = result.scalar_one_or_none()
|
||||
if not tier:
|
||||
return {"error": "Tier not found"}
|
||||
for key, value in payload.model_dump(exclude_unset=True).items():
|
||||
setattr(tier, key, value)
|
||||
await session.commit()
|
||||
await session.refresh(tier)
|
||||
return tier
|
||||
|
||||
|
||||
@router.delete("/{tier_id}", status_code=204)
|
||||
async def delete_tier(tier_id: int, session: AsyncSession = Depends(get_session)):
|
||||
result = await session.execute(select(DonationTier).where(DonationTier.id == tier_id))
|
||||
tier = result.scalar_one_or_none()
|
||||
if not tier:
|
||||
return {"error": "Tier not found"}
|
||||
await session.delete(tier)
|
||||
await session.commit()
|
||||
@@ -0,0 +1,11 @@
|
||||
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"]
|
||||
|
||||
model_config = {"env_prefix": "KMTN_"}
|
||||
|
||||
|
||||
settings = Settings()
|
||||
@@ -0,0 +1,11 @@
|
||||
from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker, create_async_engine
|
||||
|
||||
from app.config import settings
|
||||
|
||||
engine = create_async_engine(settings.DATABASE_URL, echo=False)
|
||||
async_session = async_sessionmaker(engine, class_=AsyncSession, expire_on_commit=False)
|
||||
|
||||
|
||||
async def get_session() -> AsyncSession:
|
||||
async with async_session() as session:
|
||||
yield session
|
||||
@@ -0,0 +1,38 @@
|
||||
from contextlib import asynccontextmanager
|
||||
|
||||
from fastapi import FastAPI
|
||||
from fastapi.middleware.cors import CORSMiddleware
|
||||
|
||||
from app.config import settings
|
||||
from app.database import engine
|
||||
from app.models import Base
|
||||
from app.api import programs, events, tiers
|
||||
|
||||
|
||||
@asynccontextmanager
|
||||
async def lifespan(app: FastAPI):
|
||||
# Create tables on startup (seed.py handles data seeding)
|
||||
async with engine.begin() as conn:
|
||||
await conn.run_sync(Base.metadata.create_all)
|
||||
yield
|
||||
|
||||
|
||||
app = FastAPI(
|
||||
title="KMountain Flower Radio API",
|
||||
description="Backend API for the KMountain Flower Radio Station website.",
|
||||
version="1.0.0",
|
||||
lifespan=lifespan,
|
||||
)
|
||||
|
||||
app.add_middleware(
|
||||
CORSMiddleware,
|
||||
allow_origins=settings.CORS_ORIGINS,
|
||||
allow_credentials=True,
|
||||
allow_methods=["*"],
|
||||
allow_headers=["*"],
|
||||
)
|
||||
|
||||
# Register routers
|
||||
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"])
|
||||
@@ -0,0 +1,51 @@
|
||||
from sqlalchemy import (
|
||||
Column,
|
||||
Integer,
|
||||
String,
|
||||
Text,
|
||||
Numeric,
|
||||
Boolean,
|
||||
Date,
|
||||
ARRAY,
|
||||
)
|
||||
from sqlalchemy.orm import DeclarativeBase
|
||||
|
||||
|
||||
class Base(DeclarativeBase):
|
||||
pass
|
||||
|
||||
|
||||
class Program(Base):
|
||||
__tablename__ = "programs"
|
||||
|
||||
id = Column(Integer, primary_key=True, autoincrement=True)
|
||||
day_of_week = Column(Integer, nullable=False) # 1=Mon … 7=Sun
|
||||
day_label = Column(String(10), nullable=False) # "Monday", …
|
||||
time = Column(String(10), nullable=False) # "6:00 AM"
|
||||
title = Column(String(200), nullable=False)
|
||||
host = Column(String(100), nullable=False)
|
||||
genre = Column(String(80), nullable=False)
|
||||
|
||||
|
||||
class Event(Base):
|
||||
__tablename__ = "events"
|
||||
|
||||
id = Column(Integer, primary_key=True, autoincrement=True)
|
||||
date = Column(Date, nullable=False)
|
||||
title = Column(String(200), nullable=False)
|
||||
description = Column(Text, nullable=False)
|
||||
location = Column(String(200), nullable=False)
|
||||
icon = Column(String(10), nullable=False)
|
||||
rsvp_url = Column(String(500), nullable=True)
|
||||
active = Column(Boolean, default=True)
|
||||
|
||||
|
||||
class DonationTier(Base):
|
||||
__tablename__ = "donation_tiers"
|
||||
|
||||
id = Column(Integer, primary_key=True, autoincrement=True)
|
||||
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)
|
||||
display_order = Column(Integer, nullable=False)
|
||||
@@ -0,0 +1,99 @@
|
||||
from datetime import date
|
||||
from typing import Optional
|
||||
|
||||
from pydantic import BaseModel, field_validator
|
||||
|
||||
|
||||
# ── Program ──────────────────────────────────────────────
|
||||
|
||||
class ProgramCreate(BaseModel):
|
||||
day_of_week: int
|
||||
day_label: str
|
||||
time: str
|
||||
title: str
|
||||
host: str
|
||||
genre: str
|
||||
|
||||
|
||||
class ProgramUpdate(BaseModel):
|
||||
day_of_week: Optional[int] = None
|
||||
day_label: Optional[str] = None
|
||||
time: Optional[str] = None
|
||||
title: Optional[str] = None
|
||||
host: Optional[str] = None
|
||||
genre: Optional[str] = None
|
||||
|
||||
|
||||
class ProgramResponse(BaseModel):
|
||||
id: int
|
||||
day_of_week: int
|
||||
day_label: str
|
||||
time: str
|
||||
title: str
|
||||
host: str
|
||||
genre: str
|
||||
|
||||
model_config = {"from_attributes": True}
|
||||
|
||||
|
||||
# ── Event ────────────────────────────────────────────────
|
||||
|
||||
class EventCreate(BaseModel):
|
||||
date: date
|
||||
title: str
|
||||
description: str
|
||||
location: str
|
||||
icon: str
|
||||
rsvp_url: Optional[str] = None
|
||||
|
||||
|
||||
class EventUpdate(BaseModel):
|
||||
date: Optional[date] = None
|
||||
title: Optional[str] = None
|
||||
description: Optional[str] = None
|
||||
location: Optional[str] = None
|
||||
icon: Optional[str] = None
|
||||
rsvp_url: Optional[str] = None
|
||||
active: Optional[bool] = None
|
||||
|
||||
|
||||
class EventResponse(BaseModel):
|
||||
id: int
|
||||
date: date
|
||||
title: str
|
||||
description: str
|
||||
location: str
|
||||
icon: str
|
||||
rsvp_url: Optional[str]
|
||||
active: bool
|
||||
|
||||
model_config = {"from_attributes": True}
|
||||
|
||||
|
||||
# ── DonationTier ─────────────────────────────────────────
|
||||
|
||||
class TierCreate(BaseModel):
|
||||
name: str
|
||||
amount: float
|
||||
icon: str
|
||||
benefits: list[str]
|
||||
display_order: int
|
||||
|
||||
|
||||
class TierUpdate(BaseModel):
|
||||
name: Optional[str] = None
|
||||
amount: Optional[float] = None
|
||||
icon: Optional[str] = None
|
||||
benefits: Optional[list[str]] = None
|
||||
display_order: Optional[int] = None
|
||||
|
||||
|
||||
class TierResponse(BaseModel):
|
||||
id: int
|
||||
name: str
|
||||
amount: float
|
||||
icon: str
|
||||
benefits: list[str]
|
||||
display_order: int
|
||||
|
||||
model_config = {"from_attributes": True}
|
||||
@@ -0,0 +1,9 @@
|
||||
fastapi==0.115.6
|
||||
uvicorn[standard]==0.34.0
|
||||
psycopg2-binary==2.9.10
|
||||
asyncpg==0.30.0
|
||||
sqlalchemy[asyncio]==2.0.36
|
||||
alembic==1.14.1
|
||||
python-multipart==0.0.20
|
||||
pydantic==2.10.4
|
||||
pydantic-settings==2.7.1
|
||||
+118
@@ -0,0 +1,118 @@
|
||||
"""
|
||||
Seed script: populates the database with station data.
|
||||
|
||||
Usage:
|
||||
DATABASE_URL=postgresql://... python -m app.seed
|
||||
(or from the repo root: cd backend && python seed.py)
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
from datetime import date
|
||||
|
||||
from app.database import async_session
|
||||
from app.models import Program, Event, DonationTier
|
||||
|
||||
# ── Seed data ──────────────────────────────────────────────
|
||||
|
||||
PROGRAMS: list[dict] = [
|
||||
{"day_of_week": 1, "day_label": "Monday", "time": "6:00 AM", "title": "Morning Mountain Mist", "host": "Diana Walsh", "genre": "Ambient / Nature"},
|
||||
{"day_of_week": 1, "day_label": "Monday", "time": "8:00 AM", "title": "Community Roundup", "host": "Tom Breen", "genre": "News / Talk"},
|
||||
{"day_of_week": 1, "day_label": "Monday", "time": "10:00 AM", "title": "Bluegrass Trails", "host": "Sarah Lynn", "genre": "Bluegrass"},
|
||||
{"day_of_week": 1, "day_label": "Monday", "time": "12:00 PM", "title": "Lunchtime Jazz", "host": "Mike Darrow", "genre": "Jazz"},
|
||||
{"day_of_week": 1, "day_label": "Monday", "time": "2:00 PM", "title": "Folk Roots Hour", "host": "Nadia Cole", "genre": "Folk"},
|
||||
{"day_of_week": 1, "day_label": "Monday", "time": "4:00 PM", "title": "Afternoon Acoustics", "host": "Jen Reeves", "genre": "Acoustic"},
|
||||
{"day_of_week": 1, "day_label": "Monday", "time": "6:00 PM", "title": "Evening Echoes", "host": "Carlos Mendez", "genre": "Indie / Alternative"},
|
||||
{"day_of_week": 1, "day_label": "Monday", "time": "8:00 PM", "title": "Classical Mountains", "host": "Elena Cross", "genre": "Classical"},
|
||||
{"day_of_week": 1, "day_label": "Monday", "time": "10:00 PM", "title": "Night Owl Session", "host": "DJ Kofi", "genre": "Electronic"},
|
||||
{"day_of_week": 1, "day_label": "Monday", "time": "12:00 AM", "title": "Late Night Jazz", "host": "Mike Darrow", "genre": "Jazz"},
|
||||
]
|
||||
|
||||
EVENTS: list[dict] = [
|
||||
{
|
||||
"date": date(2026, 7, 12),
|
||||
"title": "Summer Sounds Festival",
|
||||
"description": "A full day of live local music, community booths, and station meet-and-greet at the foothills park. Free admission — donations welcome.",
|
||||
"location": "Foothills Community Park",
|
||||
"icon": "🎵",
|
||||
},
|
||||
{
|
||||
"date": date(2026, 8, 5),
|
||||
"title": "Annual Fundraiser Gala",
|
||||
"description": "Our biggest fundraiser of the year! Enjoy dinner, silent auction, and performances from past guest artists.",
|
||||
"location": "Mountain View Ballroom",
|
||||
"icon": "🎙️",
|
||||
},
|
||||
{
|
||||
"date": date(2026, 9, 18),
|
||||
"title": "Volunteer Open House",
|
||||
"description": "Tour the studio, meet the team, and learn how you can help keep our free stream alive. New volunteers always welcome!",
|
||||
"location": "KMTN Studio — 42 Pine St",
|
||||
"icon": "🤝",
|
||||
},
|
||||
{
|
||||
"date": date(2026, 10, 30),
|
||||
"title": "Harvest Moon Broadcast",
|
||||
"description": "Special live broadcast from the harvest moon gathering. Featuring folk, bluegrass, and indigenous music.",
|
||||
"location": "Ridgefield Amphitheater",
|
||||
"icon": "🌙",
|
||||
},
|
||||
]
|
||||
|
||||
TIERS: list[dict] = [
|
||||
{
|
||||
"name": "Seed",
|
||||
"amount": 5.00,
|
||||
"icon": "🌱",
|
||||
"benefits": ["Digital thank-you card", "Name listed on our website", "Quarterly newsletter"],
|
||||
"display_order": 1,
|
||||
},
|
||||
{
|
||||
"name": "Stream",
|
||||
"amount": 15.00,
|
||||
"icon": "🎵",
|
||||
"benefits": ["All Seed benefits", "KMountain Flower pin", "Priority show-request line", "Annual station update call"],
|
||||
"display_order": 2,
|
||||
},
|
||||
{
|
||||
"name": "Ridge",
|
||||
"amount": 30.00,
|
||||
"icon": "🏔️",
|
||||
"benefits": ["All Stream benefits", "Official member patch", "Exclusive show invitations", "Annual station tour"],
|
||||
"display_order": 3,
|
||||
},
|
||||
{
|
||||
"name": "Summit",
|
||||
"amount": 60.00,
|
||||
"icon": "⭐",
|
||||
"benefits": ["All Ridge benefits", "Featured on the Summit Wall", "Name on-air during anniversary special", "Lifetime patron recognition"],
|
||||
"display_order": 4,
|
||||
},
|
||||
]
|
||||
|
||||
|
||||
# ── Main ──────────────────────────────────────────────────
|
||||
|
||||
async def seed():
|
||||
async with async_session() as session:
|
||||
# Programs
|
||||
for data in PROGRAMS:
|
||||
session.add(Program(**data))
|
||||
await session.commit()
|
||||
print(f" ✓ Seeded {len(PROGRAMS)} programs")
|
||||
|
||||
# Events
|
||||
for data in EVENTS:
|
||||
event = Event(**data, active=True)
|
||||
session.add(event)
|
||||
await session.commit()
|
||||
print(f" ✓ Seeded {len(EVENTS)} events")
|
||||
|
||||
# Tiers
|
||||
for data in TIERS:
|
||||
session.add(DonationTier(**data))
|
||||
await session.commit()
|
||||
print(f" ✓ Seeded {len(TIERS)} donation tiers")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(seed())
|
||||
Reference in new issue
Block a user