Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3,816 changes: 3,816 additions & 0 deletions openapi-spec.json

Large diffs are not rendered by default.

Empty file added scripts/__init__.py
Empty file.
14 changes: 14 additions & 0 deletions scripts/generate_openapi.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
"""Generate openapi-spec.json from the FastAPI app.

Usage: uv run python -m scripts.generate_openapi
"""

import json
from pathlib import Path

from src.main import app

spec = app.openapi()
out = Path(__file__).resolve().parent.parent / "openapi-spec.json"
out.write_text(json.dumps(spec, indent=2) + "\n")
print(f"Wrote {out} ({len(spec['paths'])} paths)")
11 changes: 6 additions & 5 deletions src/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,7 @@ async def lifespan(_app: FastAPI) -> AsyncIterator[None]:
app = FastAPI(
title="GeoStorm",
description="Observability for AI-driven software discovery",
version="0.1.0",
lifespan=lifespan,
)

Expand Down Expand Up @@ -144,7 +145,7 @@ def _get_version() -> str:
return "dev"


@app.get("/api/version")
@app.get("/api/version", tags=["Meta"], operation_id="getVersion")
async def get_version() -> VersionResponse:
"""Return the application version and build time."""
settings = get_settings()
Expand All @@ -154,7 +155,7 @@ async def get_version() -> VersionResponse:
)


@app.get("/health")
@app.get("/health", tags=["Meta"], operation_id="healthCheck")
async def health_check() -> HealthResponse:
"""Return service health status with a real database ping."""
db_ok = await check_database_health()
Expand All @@ -171,18 +172,18 @@ async def health_check() -> HealthResponse:
if _ASTRO_ASSETS.is_dir():
app.mount("/_astro", StaticFiles(directory=str(_ASTRO_ASSETS)), name="astro_assets")
if (_STATIC_DIR / "favicon.svg").is_file():
@app.get("/favicon.svg", response_model=None)
@app.get("/favicon.svg", response_model=None, include_in_schema=False)
async def favicon_svg() -> FileResponse:
"""Serve favicon."""
return FileResponse(str(_STATIC_DIR / "favicon.svg"))

@app.get("/favicon.ico", response_model=None)
@app.get("/favicon.ico", response_model=None, include_in_schema=False)
async def favicon_ico() -> FileResponse:
"""Serve favicon."""
return FileResponse(str(_STATIC_DIR / "favicon.ico"))


@app.get("/{_full_path:path}", response_model=None)
@app.get("/{_full_path:path}", response_model=None, include_in_schema=False)
async def serve_spa(_full_path: str) -> FileResponse | JSONResponse:
"""Serve static Astro pages.

Expand Down
10 changes: 5 additions & 5 deletions src/routes/alerts.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,10 @@
UpdateAlertConfigRequest,
)

router = APIRouter(prefix="/api")
router = APIRouter(prefix="/api", tags=["Alerts"])


@router.get("/alerts")
@router.get("/alerts", operation_id="listAlerts")
async def get_alerts(
project_id: str = Query(...),
limit: int = Query(default=50),
Expand Down Expand Up @@ -68,15 +68,15 @@ async def get_alerts(
)


@router.post("/alerts/{alert_id}/acknowledge")
@router.post("/alerts/{alert_id}/acknowledge", operation_id="acknowledgeAlert")
async def post_acknowledge_alert(alert_id: str) -> dict[str, Any]:
result = await alert_service.acknowledge_alert(alert_id)
if not result:
raise HTTPException(status_code=404, detail="Alert not found")
return {"status": "acknowledged"}


@router.get("/alerts/config")
@router.get("/alerts/config", operation_id="getAlertConfig")
async def get_alert_config(
project_id: str = Query(...),
) -> list[AlertConfigResponse]:
Expand All @@ -98,7 +98,7 @@ async def get_alert_config(
]


@router.patch("/alerts/config")
@router.patch("/alerts/config", operation_id="updateAlertConfig")
async def update_alert_config(
body: UpdateAlertConfigRequest,
project_id: str = Query(...),
Expand Down
24 changes: 12 additions & 12 deletions src/routes/projects.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,15 +23,15 @@
)
from src.services.project_service import clear_and_trigger_monitoring

router = APIRouter(prefix="/api")
router = APIRouter(prefix="/api", tags=["Projects"])


@router.get("/projects")
@router.get("/projects", operation_id="listProjects")
async def list_projects() -> list[ProjectResponse]:
return await project_service.list_projects()


@router.post("/projects", status_code=201)
@router.post("/projects", status_code=201, operation_id="createProject")
async def create_project(req: CreateProjectRequest) -> ProjectCreatedResponse:
return await project_service.create_project(
name=req.name,
Expand All @@ -43,13 +43,13 @@ async def create_project(req: CreateProjectRequest) -> ProjectCreatedResponse:
)


@router.get("/projects/{project_id}")
@router.get("/projects/{project_id}", operation_id="getProject")
async def get_project(project_id: str) -> ProjectDetailResponse:
project = await get_project_or_404(project_id)
return await project_service.get_project_detail(project_id, project)


@router.patch("/projects/{project_id}")
@router.patch("/projects/{project_id}", operation_id="updateProject")
async def update_project(project_id: str, req: UpdateProjectRequest) -> ProjectResponse:
await get_writable_project_or_403(project_id)

Expand All @@ -69,14 +69,14 @@ async def update_project(project_id: str, req: UpdateProjectRequest) -> ProjectR
return result


@router.delete("/projects/{project_id}")
@router.delete("/projects/{project_id}", operation_id="deleteProject")
async def delete_project(project_id: str) -> Response:
await get_writable_project_or_403(project_id)
await project_service.soft_delete_project(project_id)
return Response(status_code=204)


@router.post("/projects/{project_id}/monitor", status_code=202)
@router.post("/projects/{project_id}/monitor", status_code=202, operation_id="triggerMonitoring")
async def trigger_monitoring(project_id: str) -> dict[str, str]:
await get_writable_project_or_403(project_id)
result = await clear_and_trigger_monitoring(project_id)
Expand All @@ -99,7 +99,7 @@ async def trigger_monitoring(project_id: str) -> dict[str, str]:
return result


@router.get("/projects/{project_id}/brand")
@router.get("/projects/{project_id}/brand", operation_id="getProjectBrand")
async def get_brand(project_id: str) -> BrandResponse:
await get_project_or_404(project_id)
brand = await project_service.get_brand(project_id)
Expand All @@ -108,7 +108,7 @@ async def get_brand(project_id: str) -> BrandResponse:
return brand


@router.put("/projects/{project_id}/brand")
@router.put("/projects/{project_id}/brand", operation_id="updateProjectBrand")
async def update_brand(project_id: str, req: UpdateBrandRequest) -> BrandResponse:
await get_writable_project_or_403(project_id)

Expand All @@ -132,21 +132,21 @@ async def update_brand(project_id: str, req: UpdateBrandRequest) -> BrandRespons
return brand


@router.get("/projects/{project_id}/competitors")
@router.get("/projects/{project_id}/competitors", operation_id="listProjectCompetitors")
async def list_competitors(project_id: str) -> list[CompetitorResponse]:
await get_project_or_404(project_id)
return await project_service.list_competitors(project_id)


@router.post("/projects/{project_id}/competitors", status_code=201)
@router.post("/projects/{project_id}/competitors", status_code=201, operation_id="createProjectCompetitor")
async def create_competitor(project_id: str, req: CreateCompetitorRequest) -> CompetitorResponse:
await get_writable_project_or_403(project_id)
return await project_service.create_competitor(
project_id, req.name, req.aliases, req.website,
)


@router.delete("/projects/{project_id}/competitors/{competitor_id}")
@router.delete("/projects/{project_id}/competitors/{competitor_id}", operation_id="deleteProjectCompetitor")
async def delete_competitor(project_id: str, competitor_id: str) -> Response:
await get_writable_project_or_403(project_id)
rowcount = await project_service.delete_competitor(competitor_id, project_id)
Expand Down
10 changes: 5 additions & 5 deletions src/routes/providers.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,10 @@
UpdateProviderRequest,
)

router = APIRouter(prefix="/api")
router = APIRouter(prefix="/api", tags=["Providers"])


@router.get("/projects/{project_id}/providers")
@router.get("/projects/{project_id}/providers", operation_id="listProviders")
async def list_providers(project_id: str) -> list[LLMProviderResponse]:
await get_project_or_404(project_id)
rows = await provider_service.list_providers(project_id)
Expand All @@ -37,7 +37,7 @@ async def list_providers(project_id: str) -> list[LLMProviderResponse]:
]


@router.post("/projects/{project_id}/providers", status_code=201)
@router.post("/projects/{project_id}/providers", status_code=201, operation_id="createProvider")
async def create_provider(project_id: str, body: CreateProviderRequest) -> LLMProviderResponse:
await get_writable_project_or_403(project_id)
provider_id = uuid.uuid4().hex
Expand All @@ -58,7 +58,7 @@ async def create_provider(project_id: str, body: CreateProviderRequest) -> LLMPr
)


@router.patch("/projects/{project_id}/providers/{provider_id}")
@router.patch("/projects/{project_id}/providers/{provider_id}", operation_id="updateProvider")
async def update_provider(
project_id: str, provider_id: str, body: UpdateProviderRequest,
) -> LLMProviderResponse:
Expand Down Expand Up @@ -90,7 +90,7 @@ async def update_provider(
)


@router.delete("/projects/{project_id}/providers/{provider_id}")
@router.delete("/projects/{project_id}/providers/{provider_id}", operation_id="deleteProvider")
async def delete_provider(project_id: str, provider_id: str) -> Response:
await get_writable_project_or_403(project_id)
rowcount = await provider_service.delete_provider(provider_id, project_id)
Expand Down
14 changes: 7 additions & 7 deletions src/routes/runs.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@
# ---------------------------------------------------------------------------


@router.get("/projects/{project_id}/runs")
@router.get("/projects/{project_id}/runs", operation_id="listRuns", tags=["Runs"])
async def list_runs(
project_id: str,
limit: int = Query(default=50, ge=1, le=200),
Expand All @@ -51,7 +51,7 @@ async def list_runs(
# ---------------------------------------------------------------------------


@router.get("/runs/{run_id}")
@router.get("/runs/{run_id}", operation_id="getRun", tags=["Runs"])
async def get_run(run_id: str) -> RunDetailResponse:
"""Run detail with aggregate perception score and competitors detected."""
result = await run_service.get_run_detail(run_id)
Expand All @@ -68,7 +68,7 @@ async def get_run(run_id: str) -> RunDetailResponse:
# ---------------------------------------------------------------------------


@router.get("/runs/{run_id}/progress")
@router.get("/runs/{run_id}/progress", operation_id="streamRunProgress", tags=["Runs"])
async def stream_run_progress(run_id: str) -> StreamingResponse:
"""Server-Sent Events stream for real-time run progress."""
run = await run_service.get_run_status(run_id)
Expand Down Expand Up @@ -131,7 +131,7 @@ async def event_generator() -> AsyncGenerator[str, None]:
# ---------------------------------------------------------------------------


@router.get("/runs/{run_id}/responses")
@router.get("/runs/{run_id}/responses", operation_id="listRunResponses", tags=["Runs"])
async def list_responses(
run_id: str,
limit: int = Query(default=50, ge=1, le=200),
Expand All @@ -149,7 +149,7 @@ async def list_responses(
# ---------------------------------------------------------------------------


@router.get("/projects/{project_id}/perception")
@router.get("/projects/{project_id}/perception", operation_id="getPerception", tags=["Perception"])
async def get_perception(
project_id: str,
start_date: str | None = Query(default=None),
Expand All @@ -165,7 +165,7 @@ async def get_perception(
# ---------------------------------------------------------------------------


@router.get("/projects/{project_id}/trajectory")
@router.get("/projects/{project_id}/trajectory", operation_id="getTrajectory", tags=["Perception"])
async def get_trajectory(
project_id: str,
start_date: str | None = Query(default=None),
Expand All @@ -182,7 +182,7 @@ async def get_trajectory(
# ---------------------------------------------------------------------------


@router.get("/projects/{project_id}/perception/breakdown")
@router.get("/projects/{project_id}/perception/breakdown", operation_id="getPerceptionBreakdown", tags=["Perception"])
async def get_perception_breakdown(project_id: str) -> PerceptionBreakdownResponse:
"""Per-term and per-provider perception breakdown from the latest scoring period."""
await get_project_or_404(project_id)
Expand Down
6 changes: 3 additions & 3 deletions src/routes/schedule.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,10 @@
from src.routes.deps import get_project_or_404, get_writable_project_or_403
from src.schemas import ScheduleResponse, UpdateScheduleRequest

router = APIRouter(prefix="/api")
router = APIRouter(prefix="/api", tags=["Schedules"])


@router.get("/projects/{project_id}/schedule")
@router.get("/projects/{project_id}/schedule", operation_id="getSchedule")
async def get_schedule(project_id: str) -> ScheduleResponse:
await get_project_or_404(project_id)
result = await schedule_service.get_schedule(project_id)
Expand All @@ -23,7 +23,7 @@ async def get_schedule(project_id: str) -> ScheduleResponse:
return ScheduleResponse(**result)


@router.patch("/projects/{project_id}/schedule")
@router.patch("/projects/{project_id}/schedule", operation_id="updateSchedule")
async def update_schedule(
project_id: str, body: UpdateScheduleRequest,
) -> ScheduleResponse:
Expand Down
12 changes: 6 additions & 6 deletions src/routes/setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,17 +28,17 @@
router = APIRouter(prefix="/api")


@router.get("/setup/status")
@router.get("/setup/status", operation_id="getSetupStatus", tags=["Setup"])
async def get_setup_status() -> SetupStatusResponse:
return await settings_service.get_setup_status()


@router.get("/settings/api-key-status")
@router.get("/settings/api-key-status", operation_id="getApiKeyStatus", tags=["Settings"])
async def get_api_key_status() -> ApiKeyStatusResponse:
return await settings_service.get_api_key_status()


@router.post("/settings/api-key")
@router.post("/settings/api-key", operation_id="storeApiKey", tags=["Settings"])
async def store_api_key(req: StoreApiKeyRequest) -> dict[str, Any]:
now = datetime.now(tz=UTC).isoformat()
try:
Expand All @@ -48,13 +48,13 @@ async def store_api_key(req: StoreApiKeyRequest) -> dict[str, Any]:
return {"status": "stored"}


@router.delete("/settings/api-key")
@router.delete("/settings/api-key", operation_id="deleteApiKey", tags=["Settings"])
async def delete_api_key() -> Response:
await settings_service.delete_api_key()
return Response(status_code=204)


@router.get("/settings/models")
@router.get("/settings/models", operation_id="listRecommendedModels", tags=["Settings"])
async def get_recommended_models() -> list[dict[str, str]]:
"""Return the curated list of models available through OpenRouter."""
return RECOMMENDED_MODELS
Expand Down Expand Up @@ -102,7 +102,7 @@ async def get_recommended_models() -> list[dict[str, str]]:
"""


@router.post("/setup/autofill")
@router.post("/setup/autofill", operation_id="autofillProject", tags=["Setup"])
async def autofill_project(req: AutofillRequest) -> AutofillResponse:
api_key = await get_api_key(ProviderType.OPENROUTER)
if not api_key:
Expand Down
8 changes: 4 additions & 4 deletions src/routes/terms.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,10 @@
from src.routes.deps import get_project_or_404, get_writable_project_or_403
from src.schemas import CreateTermRequest, TermResponse

router = APIRouter(prefix="/api")
router = APIRouter(prefix="/api", tags=["Terms"])


@router.get("/projects/{project_id}/terms")
@router.get("/projects/{project_id}/terms", operation_id="listTerms")
async def get_terms(project_id: str) -> list[TermResponse]:
await get_project_or_404(project_id)
rows = await term_service.list_terms(project_id)
Expand All @@ -32,7 +32,7 @@ async def get_terms(project_id: str) -> list[TermResponse]:
]


@router.post("/projects/{project_id}/terms", status_code=201)
@router.post("/projects/{project_id}/terms", status_code=201, operation_id="createTerm")
async def create_term(project_id: str, body: CreateTermRequest) -> TermResponse:
await get_writable_project_or_403(project_id)
term_id = uuid.uuid4().hex
Expand All @@ -49,7 +49,7 @@ async def create_term(project_id: str, body: CreateTermRequest) -> TermResponse:
)


@router.delete("/projects/{project_id}/terms/{term_id}")
@router.delete("/projects/{project_id}/terms/{term_id}", operation_id="deleteTerm")
async def delete_term(project_id: str, term_id: str) -> Response:
await get_writable_project_or_403(project_id)
rowcount = await term_service.delete_term(term_id, project_id)
Expand Down
Loading
Loading