from __future__ import annotations from datetime import datetime from pathlib import Path from typing import Annotated, Literal from fastapi import FastAPI, HTTPException from pydantic import BaseModel, Field from .service import CivicSwarmService class ProposalCreate(BaseModel): city: str title: str body: str geography: str = "" tags: list[str] = Field(default_factory=list) class ResidentCreate(BaseModel): resident_key: str geography: str = "" interests: list[str] = Field(default_factory=list) lived_experience: list[str] = Field(default_factory=list) languages: list[str] = Field(default_factory=list) class CommentCreate(BaseModel): text: str channel: str = "mobile" resident_key: str | None = None geography: str = "" metadata: dict = Field(default_factory=dict) class PreferenceCreate(BaseModel): resident_key: str score: float channel: str = "mobile" class BatchResident(BaseModel): kind: Literal["resident"] = "resident" resident_key: str geography: str = "" interests: list[str] = Field(default_factory=list) lived_experience: list[str] = Field(default_factory=list) languages: list[str] = Field(default_factory=list) class BatchComment(BaseModel): kind: Literal["comment"] = "comment" proposal_id: int text: str resident_key: str | None = None channel: str = "mobile" geography: str = "" metadata: dict = Field(default_factory=dict) class BatchPreference(BaseModel): kind: Literal["preference"] = "preference" proposal_id: int resident_key: str score: float channel: str = "mobile" class BatchLedger(BaseModel): kind: Literal["ledger"] = "ledger" proposal_id: int entry_kind: str payload: dict = Field(default_factory=dict) class BatchProposalVersion(BaseModel): kind: Literal["proposal_version"] = "proposal_version" proposal_id: int rationale: str = "" objections: list[str] = Field(default_factory=list) resolution_status: str = "open" CaptureItem = Annotated[ BatchResident | BatchComment | BatchPreference | BatchLedger | BatchProposalVersion, Field(discriminator="kind"), ] class CaptureBatchCreate(BaseModel): batch_key: str source: str = "mobile" captured_at: datetime | None = None items: list[CaptureItem] = Field(default_factory=list) def create_app(db_path: str | Path = ":memory:") -> FastAPI: service = CivicSwarmService.from_path(db_path) app = FastAPI(title="CivicSwarm", version="0.1.0") app.state.service = service @app.post("/proposals") def create_proposal(payload: ProposalCreate): return service.create_proposal(payload.city, payload.title, payload.body, payload.geography, payload.tags) @app.post("/residents") def create_resident(payload: ResidentCreate): return service.register_resident(payload.resident_key, payload.geography, payload.interests, payload.lived_experience, payload.languages) @app.post("/proposals/{proposal_id}/comments") def create_comment(proposal_id: int, payload: CommentCreate): try: return service.add_comment(proposal_id, payload.text, payload.channel, payload.resident_key, payload.geography, payload.metadata) except KeyError as exc: raise HTTPException(status_code=404, detail=str(exc)) from exc @app.post("/proposals/{proposal_id}/preferences") def create_preference(proposal_id: int, payload: PreferenceCreate): return service.submit_preference(proposal_id, payload.resident_key, payload.score, payload.channel) @app.post("/capture-batches") def ingest_capture_batch(payload: CaptureBatchCreate): return service.ingest_capture_batch(payload.batch_key, payload.source, [item.model_dump() for item in payload.items], payload.captured_at) @app.get("/capture-batches/{batch_key}") def get_capture_batch(batch_key: str): try: return service.get_capture_batch(batch_key) except KeyError as exc: raise HTTPException(status_code=404, detail=str(exc)) from exc @app.get("/proposals/{proposal_id}/route") def route_proposal(proposal_id: int, top_n: int = 3): return service.route_proposal(proposal_id, top_n=top_n) @app.get("/proposals/{proposal_id}/dashboard") def dashboard(proposal_id: int): return service.build_dashboard(proposal_id) @app.get("/proposals/{proposal_id}/brief") def brief(proposal_id: int): return service.export_brief(proposal_id) @app.get("/proposals/{proposal_id}/ledger") def ledger(proposal_id: int): return service.list_ledger(proposal_id) return app