11"""
2- GET /api/v1/export — export URL stats as CSV, XLSX, JSON, or XML.
2+ GET /api/v1/export — export URL stats as CSV, XLSX, JSON, or XML.
3+ GET /api/v1/export/links/{id} — export twin of the per-link stats endpoint.
34
45Auth is optional for scope=anon (public stats); scope=all requires auth.
6+ The per-link endpoint always requires auth — you must own the URL.
57API key users require ``stats:read``, ``urls:read``, or ``admin:all``.
68"""
79
810from __future__ import annotations
911
1012from typing import Annotated
1113
12- from fastapi import APIRouter , Depends , Query , Request
14+ from fastapi import APIRouter , Depends , Path , Query , Request
1315from fastapi .responses import Response
1416
1517from dependencies import (
1618 STATS_SCOPES ,
1719 CurrentUser ,
1820 ExportSvc ,
21+ UrlSvc ,
1922 optional_scopes ,
23+ require_scopes ,
2024)
2125from middleware .openapi import EXPORT_RESPONSES , OPTIONAL_AUTH_SECURITY
2226from middleware .rate_limiter import Limits , dynamic_limit , limiter
23- from schemas .dto .requests .stats import ExportQuery
27+ from routes .api_v1 ._helpers import parse_url_id
28+ from schemas .dto .requests .stats import ExportQuery , LinkExportQuery
2429
2530router = APIRouter (tags = ["Statistics" ])
2631
2732_export_limit , _export_key = dynamic_limit (
2833 Limits .API_EXPORT_AUTHED , Limits .API_EXPORT_ANON
2934)
3035
31-
32- @router .get (
33- "/export" ,
34- responses = {
35- ** EXPORT_RESPONSES ,
36- 200 : {
37- "description" : "Export file download" ,
38- "content" : {
39- "application/json" : {
40- "schema" : {"type" : "string" , "format" : "binary" },
41- },
42- "application/xml" : {
43- "schema" : {"type" : "string" , "format" : "binary" },
44- },
45- "application/zip" : {
46- "schema" : {"type" : "string" , "format" : "binary" },
47- "x-description" : "CSV export — ZIP archive containing summary.csv plus one file per dimension" ,
48- },
49- "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet" : {
50- "schema" : {"type" : "string" , "format" : "binary" },
51- "x-description" : "XLSX export — Excel workbook with multiple sheets" ,
52- },
36+ # Shared 200 documentation for both export routes — the download body is
37+ # format-dependent, never JSON-schema'd.
38+ _EXPORT_200_RESPONSES = {
39+ ** EXPORT_RESPONSES ,
40+ 200 : {
41+ "description" : "Export file download" ,
42+ "content" : {
43+ "application/json" : {
44+ "schema" : {"type" : "string" , "format" : "binary" },
45+ },
46+ "application/xml" : {
47+ "schema" : {"type" : "string" , "format" : "binary" },
48+ },
49+ "application/zip" : {
50+ "schema" : {"type" : "string" , "format" : "binary" },
51+ "x-description" : "CSV export — ZIP archive containing summary.csv plus one file per dimension" ,
52+ },
53+ "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet" : {
54+ "schema" : {"type" : "string" , "format" : "binary" },
55+ "x-description" : "XLSX export — Excel workbook with multiple sheets" ,
5356 },
5457 },
5558 },
59+ }
60+
61+
62+ @router .get (
63+ "/export" ,
64+ responses = _EXPORT_200_RESPONSES ,
5665 openapi_extra = OPTIONAL_AUTH_SECURITY ,
5766 operation_id = "exportStats" ,
5867 summary = "Export Statistics" ,
@@ -86,6 +95,10 @@ async def export_v1(
8695 - `xlsx` — Excel spreadsheet with multiple sheets
8796 - `csv` — **ZIP archive** containing `summary.csv` plus one CSV file per metrics dimension
8897
98+ **Filtering**: Same as `GET /stats` — including the `url_id` filter to
99+ slice the export to specific URLs you own. To export a single link,
100+ prefer `GET /export/links/{url_id}`.
101+
89102 **Note**: Export generation is resource-intensive. Lower rate limits apply
90103 compared to other endpoints.
91104 """
@@ -96,3 +109,59 @@ async def export_v1(
96109 media_type = result .mimetype ,
97110 headers = {"Content-Disposition" : f'attachment; filename="{ result .filename } "' },
98111 )
112+
113+
114+ @router .get (
115+ "/export/links/{url_id}" ,
116+ responses = _EXPORT_200_RESPONSES ,
117+ operation_id = "exportLinkStats" ,
118+ summary = "Export Link Statistics" ,
119+ )
120+ @limiter .limit (Limits .API_EXPORT_AUTHED )
121+ async def export_link_v1 (
122+ request : Request ,
123+ url_id : Annotated [
124+ str ,
125+ Path (description = "Unique identifier of the URL (MongoDB ObjectId)." ),
126+ ],
127+ query : Annotated [LinkExportQuery , Query ()],
128+ export_service : ExportSvc ,
129+ url_service : UrlSvc ,
130+ user : CurrentUser = Depends (require_scopes (STATS_SCOPES )), # noqa: B008
131+ ) -> Response :
132+ """Export click statistics for a single URL you own.
133+
134+ The export twin of `GET /stats/links/{url_id}` — the same formats as
135+ `GET /export`, pre-scoped to one link. The suggested filename carries
136+ the link's alias.
137+
138+ **Authentication**: Required — you must own the URL.
139+
140+ **API Key Scope**: `stats:read`, `urls:read`, or `admin:all`
141+
142+ **Rate Limits**: 30/min, 1,000/day
143+
144+ **Export Formats**:
145+
146+ - `json` — single JSON file
147+ - `xml` — single XML file
148+ - `xlsx` — Excel spreadsheet with multiple sheets
149+ - `csv` — **ZIP archive** containing `summary.csv` plus one CSV file per metrics dimension
150+
151+ **Errors**:
152+
153+ - `400` — malformed id (not a valid ObjectId)
154+ - `404` — no URL with that id in your account. A URL owned by someone
155+ else answers identically; this endpoint never confirms foreign ids.
156+
157+ **Note**: Export generation is resource-intensive. Lower rate limits apply
158+ compared to other endpoints.
159+ """
160+ oid = parse_url_id (url_id )
161+ url = await url_service .get_owned (oid , user .user_id )
162+ result = await export_service .export_link (query , url )
163+ return Response (
164+ content = result .content ,
165+ media_type = result .mimetype ,
166+ headers = {"Content-Disposition" : f'attachment; filename="{ result .filename } "' },
167+ )
0 commit comments