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
27 changes: 27 additions & 0 deletions SPECIFICATION.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
# CSV Export Feature Specification

## Goal

Provide an endpoint that exports reports as CSV.

## Endpoint

GET /reports/export

## Requirements

1. Export reports in CSV format.
2. Support existing filters.
3. Support existing sorting.
4. Exclude internal_id.
5. Exclude owner_email.
6. Follow RFC4180 CSV rules.
7. Return downloadable file.
8. Content-Type must be text/csv.

## Acceptance Criteria

- CSV downloads successfully.
- Internal fields never appear.
- Commas, quotes and newlines are handled correctly.
- Existing filters work exactly as in /reports.
53 changes: 53 additions & 0 deletions app/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,12 @@

from __future__ import annotations

import csv
import io
from datetime import datetime

from fastapi import FastAPI, HTTPException, Query
from fastapi.responses import StreamingResponse

from app.models import ReportListResponse, ReportPublic, ReportStatus
from app.reports import query
Expand Down Expand Up @@ -51,3 +54,53 @@ def list_reports(
offset=offset,
limit=limit,
)


@app.get("/reports/export")
def export_reports(
status: ReportStatus | None = Query(None, description="Filter by status"),
date_from: datetime | None = Query(None, description="Lower bound on created_at (inclusive)"),
date_to: datetime | None = Query(None, description="Upper bound on created_at (inclusive)"),
sort: str = Query("created_at", description="Sort field"),
descending: bool = Query(True, description="Sort descending"),
) -> StreamingResponse:
"""Export reports as a downloadable CSV.

Reuses the filtering and sorting logic, excludes internal fields (internal_id,
owner_email), and produces an RFC 4180 compliant CSV.
"""
try:
rows = query(
status=status,
date_from=date_from,
date_to=date_to,
sort=sort,
descending=descending,
)
except ValueError as e:
raise HTTPException(status_code=400, detail=str(e)) from e

def csv_generator():
output = io.StringIO()
writer = csv.writer(output, lineterminator="\r\n")

# Write CSV Header
writer.writerow(["id", "title", "status", "owner", "amount", "created_at"])
yield output.getvalue()
output.seek(0)
output.truncate(0)

# Write CSV Data Rows
for r in rows:
created_at_str = r.created_at.isoformat()
writer.writerow([r.id, r.title, r.status, r.owner, r.amount, created_at_str])
yield output.getvalue()
output.seek(0)
output.truncate(0)

headers = {
"Content-Disposition": 'attachment; filename="reports.csv"',
"Access-Control-Expose-Headers": "Content-Disposition",
}
return StreamingResponse(csv_generator(), media_type="text/csv", headers=headers)