From 7825d269379db56c74634cb3a15b8485891c5e79 Mon Sep 17 00:00:00 2001 From: Raaz Rishi Date: Mon, 25 May 2026 19:56:30 +0530 Subject: [PATCH 1/4] spec: add project context and feature proposal --- openspec/changes/get-report-by-id/proposal.md | 24 +++++++++++++++ .../get-report-by-id/specs/reports/spec.md | 29 +++++++++++++++++++ openspec/changes/get-report-by-id/tasks.md | 7 +++++ openspec/project.md | 18 ++++++++++++ 4 files changed, 78 insertions(+) create mode 100644 openspec/changes/get-report-by-id/proposal.md create mode 100644 openspec/changes/get-report-by-id/specs/reports/spec.md create mode 100644 openspec/changes/get-report-by-id/tasks.md create mode 100644 openspec/project.md 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 From e082be55990ff09d71e75ffeb55a37916c3cbee7 Mon Sep 17 00:00:00 2001 From: Raaz Rishi Date: Mon, 25 May 2026 19:57:14 +0530 Subject: [PATCH 2/4] feat: implement GET /reports/{id} --- app/main.py | 16 +++++++++++++++- app/reports.py | 11 +++++++++++ 2 files changed, 26 insertions(+), 1 deletion(-) diff --git a/app/main.py b/app/main.py index 684c6c2..bf7c6c4 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/{report_id}", response_model=ReportPublic) +def get_report(report_id: int) -> ReportPublic: + """Return one report by ID.""" + + report = get_report_by_id(report_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 From 7e70ceb9671d181e7787ac0065994f615deb07ea Mon Sep 17 00:00:00 2001 From: Raaz Rishi Date: Mon, 25 May 2026 19:57:44 +0530 Subject: [PATCH 3/4] test: add endpoint tests --- tests/test_reports.py | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) create mode 100644 tests/test_reports.py diff --git a/tests/test_reports.py b/tests/test_reports.py new file mode 100644 index 0000000..5b0c09b --- /dev/null +++ b/tests/test_reports.py @@ -0,0 +1,30 @@ +from fastapi.testclient import TestClient + +from app.main import app + + +client = TestClient(app) + + +def test_get_report_found(): + + response = client.get("/reports/1") + + assert response.status_code == 200 + + data = response.json() + + assert data["id"] == 1 + + assert "internal_id" not in data + + assert "owner_email" not in data + + +def test_get_report_missing(): + + response = client.get("/reports/99999") + + assert response.status_code == 404 + + assert response.json()["detail"] == "Report not found" \ No newline at end of file From b4a45c2e3480d2102ff9ea8e6ff8d7477de8344d Mon Sep 17 00:00:00 2001 From: Raaz Rishi Date: Mon, 25 May 2026 22:12:05 +0530 Subject: [PATCH 4/4] fix: align endpoint naming and stabilize tests --- app/main.py | 6 +++--- tests/test_reports.py | 14 +++++++++++--- 2 files changed, 14 insertions(+), 6 deletions(-) diff --git a/app/main.py b/app/main.py index bf7c6c4..886983f 100644 --- a/app/main.py +++ b/app/main.py @@ -52,11 +52,11 @@ def list_reports( limit=limit, ) -@app.get("/reports/{report_id}", response_model=ReportPublic) -def get_report(report_id: int) -> ReportPublic: +@app.get("/reports/{id}", response_model=ReportPublic) +def get_report(id: int) -> ReportPublic: """Return one report by ID.""" - report = get_report_by_id(report_id) + report = get_report_by_id(id) if report is None: raise HTTPException( diff --git a/tests/test_reports.py b/tests/test_reports.py index 5b0c09b..27522f5 100644 --- a/tests/test_reports.py +++ b/tests/test_reports.py @@ -8,13 +8,17 @@ def test_get_report_found(): - response = client.get("/reports/1") + 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"] == 1 + assert data["id"] == existing_id assert "internal_id" not in data @@ -23,7 +27,11 @@ def test_get_report_found(): def test_get_report_missing(): - response = client.get("/reports/99999") + 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