Rifqi Hafizuddin Claude Opus 4.8 commited on
Commit ·
4c5cff8
1
Parent(s): 61282c5
[KM-652] feat(analysis): GET /analysis/{id} + GET /analysis (read endpoints)
Browse filesThe FE workspace + sidebar need to read analysis state back; only create
existed. Add:
- GET /analysis/{id} — full state (8 fields) + bound data_source_ids; 404 if absent
- GET /analysis?user_id= — a user's analyses, most-recently-updated first (summary)
Extract _serialize_state so create + detail share one payload shape.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- src/api/v1/analysis.py +66 -11
src/api/v1/analysis.py
CHANGED
|
@@ -13,6 +13,7 @@ import uuid
|
|
| 13 |
|
| 14 |
from fastapi import APIRouter, Depends, HTTPException
|
| 15 |
from pydantic import BaseModel, Field
|
|
|
|
| 16 |
from sqlalchemy.ext.asyncio import AsyncSession
|
| 17 |
|
| 18 |
from src.db.postgres.connection import get_db
|
|
@@ -24,6 +25,30 @@ logger = get_logger("analysis_api")
|
|
| 24 |
router = APIRouter(prefix="/api/v1", tags=["Analysis"])
|
| 25 |
|
| 26 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 27 |
class CreateAnalysisRequest(BaseModel):
|
| 28 |
user_id: str
|
| 29 |
analysis_title: str = "New analysis"
|
|
@@ -80,15 +105,45 @@ async def create_analysis(
|
|
| 80 |
return {
|
| 81 |
"status": "success",
|
| 82 |
"message": "Analysis created successfully",
|
| 83 |
-
"data":
|
| 84 |
-
|
| 85 |
-
|
| 86 |
-
|
| 87 |
-
|
| 88 |
-
|
| 89 |
-
|
| 90 |
-
|
| 91 |
-
|
| 92 |
-
|
| 93 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 94 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 13 |
|
| 14 |
from fastapi import APIRouter, Depends, HTTPException
|
| 15 |
from pydantic import BaseModel, Field
|
| 16 |
+
from sqlalchemy import select
|
| 17 |
from sqlalchemy.ext.asyncio import AsyncSession
|
| 18 |
|
| 19 |
from src.db.postgres.connection import get_db
|
|
|
|
| 25 |
router = APIRouter(prefix="/api/v1", tags=["Analysis"])
|
| 26 |
|
| 27 |
|
| 28 |
+
def _serialize_state(row: AnalysisStateRow, data_source_ids: list[str]) -> dict:
|
| 29 |
+
"""The full analysis payload: the 8 state fields + the bound source ids."""
|
| 30 |
+
return {
|
| 31 |
+
"id": row.id,
|
| 32 |
+
"analysis_title": row.analysis_title,
|
| 33 |
+
"problem_statement": row.problem_statement,
|
| 34 |
+
"problem_validated": row.problem_validated,
|
| 35 |
+
"owner_id": row.owner_id,
|
| 36 |
+
"report_id": row.report_id,
|
| 37 |
+
"data_source_ids": data_source_ids,
|
| 38 |
+
"created_at": row.created_at.isoformat() if row.created_at else None,
|
| 39 |
+
"updated_at": row.updated_at.isoformat() if row.updated_at else None,
|
| 40 |
+
}
|
| 41 |
+
|
| 42 |
+
|
| 43 |
+
async def _bound_source_ids(db: AsyncSession, analysis_id: str) -> list[str]:
|
| 44 |
+
result = await db.execute(
|
| 45 |
+
select(AnalysisDataSourceRow.source_id).where(
|
| 46 |
+
AnalysisDataSourceRow.analysis_id == analysis_id
|
| 47 |
+
)
|
| 48 |
+
)
|
| 49 |
+
return list(result.scalars().all())
|
| 50 |
+
|
| 51 |
+
|
| 52 |
class CreateAnalysisRequest(BaseModel):
|
| 53 |
user_id: str
|
| 54 |
analysis_title: str = "New analysis"
|
|
|
|
| 105 |
return {
|
| 106 |
"status": "success",
|
| 107 |
"message": "Analysis created successfully",
|
| 108 |
+
"data": _serialize_state(state_row, bound_ids),
|
| 109 |
+
}
|
| 110 |
+
|
| 111 |
+
|
| 112 |
+
@router.get("/analysis")
|
| 113 |
+
@log_execution(logger)
|
| 114 |
+
async def list_analyses(user_id: str, db: AsyncSession = Depends(get_db)):
|
| 115 |
+
"""List a user's analyses, most-recently-updated first (Analysis sidebar).
|
| 116 |
+
|
| 117 |
+
Summary fields only (no per-row source bindings — fetch those via the detail
|
| 118 |
+
endpoint) to keep the list a single query.
|
| 119 |
+
"""
|
| 120 |
+
result = await db.execute(
|
| 121 |
+
select(AnalysisStateRow)
|
| 122 |
+
.where(AnalysisStateRow.owner_id == user_id)
|
| 123 |
+
.order_by(AnalysisStateRow.updated_at.desc())
|
| 124 |
+
)
|
| 125 |
+
rows = result.scalars().all()
|
| 126 |
+
return {
|
| 127 |
+
"status": "success",
|
| 128 |
+
"data": [
|
| 129 |
+
{
|
| 130 |
+
"id": r.id,
|
| 131 |
+
"analysis_title": r.analysis_title,
|
| 132 |
+
"problem_validated": r.problem_validated,
|
| 133 |
+
"report_id": r.report_id,
|
| 134 |
+
"updated_at": r.updated_at.isoformat() if r.updated_at else None,
|
| 135 |
+
}
|
| 136 |
+
for r in rows
|
| 137 |
+
],
|
| 138 |
}
|
| 139 |
+
|
| 140 |
+
|
| 141 |
+
@router.get("/analysis/{analysis_id}")
|
| 142 |
+
@log_execution(logger)
|
| 143 |
+
async def get_analysis(analysis_id: str, db: AsyncSession = Depends(get_db)):
|
| 144 |
+
"""Read one analysis's state + bound data sources (the FE workspace render)."""
|
| 145 |
+
row = await db.get(AnalysisStateRow, analysis_id)
|
| 146 |
+
if row is None:
|
| 147 |
+
raise HTTPException(status_code=404, detail=f"Analysis {analysis_id!r} not found.")
|
| 148 |
+
data_source_ids = await _bound_source_ids(db, analysis_id)
|
| 149 |
+
return {"status": "success", "data": _serialize_state(row, data_source_ids)}
|