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 files

The 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>

Files changed (1) hide show
  1. 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
- "id": state_row.id,
85
- "analysis_title": state_row.analysis_title,
86
- "problem_statement": state_row.problem_statement,
87
- "problem_validated": state_row.problem_validated,
88
- "owner_id": state_row.owner_id,
89
- "report_id": state_row.report_id,
90
- "data_source_ids": bound_ids,
91
- "created_at": state_row.created_at.isoformat() if state_row.created_at else None,
92
- "updated_at": state_row.updated_at.isoformat() if state_row.updated_at else None,
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)}