diff --git a/app/main.py b/app/main.py index 684c6c2..886983f 100644 --- a/app/main.py +++ b/app/main.py @@ -7,7 +7,7 @@ from fastapi import FastAPI, HTTPException, Query from app.models import ReportListResponse, ReportPublic, ReportStatus -from app.reports import query +from app.reports import query, get_report_by_id app = FastAPI(title="SDD Workshop — Reports API", version="0.1.0") @@ -51,3 +51,17 @@ def list_reports( offset=offset, limit=limit, ) + +@app.get("/reports/{id}", response_model=ReportPublic) +def get_report(id: int) -> ReportPublic: + """Return one report by ID.""" + + report = get_report_by_id(id) + + if report is None: + raise HTTPException( + status_code=404, + detail="Report not found", + ) + + return ReportPublic.from_internal(report) \ No newline at end of file diff --git a/app/reports.py b/app/reports.py index 2d7bf93..96a8f2e 100644 --- a/app/reports.py +++ b/app/reports.py @@ -39,3 +39,14 @@ def query( rows = (r for r in rows if r.created_at <= date_to) return sorted(rows, key=lambda r: getattr(r, sort), reverse=descending) + +def get_report_by_id(report_id: int) -> Report | None: + """Return one report by ID or None if missing.""" + + rows = all_reports() + + for report in rows: + if report.id == report_id: + return report + + return None diff --git a/openspec/changes/get-report-by-id/proposal.md b/openspec/changes/get-report-by-id/proposal.md new file mode 100644 index 0000000..ff7fa8f --- /dev/null +++ b/openspec/changes/get-report-by-id/proposal.md @@ -0,0 +1,24 @@ +# Proposal + +## Motivation + +Clients currently fetch all reports and filter client-side to find one record. + +Direct lookup reduces payload and improves usability. + +## Changes + +ADD: + +GET /reports/{id} + +Behavior: + +- Return report if found +- Return 404 if missing + +## Non-goals + +- Existing routes unchanged +- No authentication changes +- No changes to PublicReport model \ No newline at end of file diff --git a/openspec/changes/get-report-by-id/specs/reports/spec.md b/openspec/changes/get-report-by-id/specs/reports/spec.md new file mode 100644 index 0000000..ffedd21 --- /dev/null +++ b/openspec/changes/get-report-by-id/specs/reports/spec.md @@ -0,0 +1,29 @@ +# Spec Delta + +## ADD + +### Endpoint + +GET /reports/{id} + +### Parameters + +id: +- type: integer +- required: true + +### Responses + +200: +Return existing public report model + +404: +{ + "detail": "Report not found" +} + +### Constraints + +- Existing `/reports` endpoint behavior remains unchanged +- Existing pagination behavior remains unchanged +- Reuse existing response model \ No newline at end of file diff --git a/openspec/changes/get-report-by-id/tasks.md b/openspec/changes/get-report-by-id/tasks.md new file mode 100644 index 0000000..8386c30 --- /dev/null +++ b/openspec/changes/get-report-by-id/tasks.md @@ -0,0 +1,7 @@ +# Tasks + +- [x] Add helper function in app/reports.py +- [x] Add route in app/main.py +- [x] Add success test +- [x] Add 404 test +- [x] Verify all tests pass \ No newline at end of file diff --git a/openspec/project.md b/openspec/project.md new file mode 100644 index 0000000..af59c3b --- /dev/null +++ b/openspec/project.md @@ -0,0 +1,18 @@ +# Project Context + +## Tech Stack +- Python +- FastAPI +- Pydantic +- In-memory dataset + +## Conventions +- All routes return JSON +- Existing routes must remain unchanged +- Public models never expose internal fields +- Existing API behavior preserved + +## Constraints +- Preserve `/reports` contract +- Preserve pagination behavior +- No breaking changes to existing endpoints \ No newline at end of file diff --git a/tests/test_reports.py b/tests/test_reports.py new file mode 100644 index 0000000..27522f5 --- /dev/null +++ b/tests/test_reports.py @@ -0,0 +1,38 @@ +from fastapi.testclient import TestClient + +from app.main import app + + +client = TestClient(app) + + +def test_get_report_found(): + + reports = client.get("/reports").json()["items"] + + existing_id = reports[0]["id"] + + response = client.get(f"/reports/{existing_id}") + + assert response.status_code == 200 + + data = response.json() + + assert data["id"] == existing_id + + assert "internal_id" not in data + + assert "owner_email" not in data + + +def test_get_report_missing(): + + reports = client.get("/reports").json()["items"] + + missing_id = max(r["id"] for r in reports) + 1 + + response = client.get(f"/reports/{missing_id}") + + assert response.status_code == 404 + + assert response.json()["detail"] == "Report not found" \ No newline at end of file