idea198-civicswarm-privacy-.../civicswarm/api.py

150 lines
4.6 KiB
Python

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