interloper-api 0.6.0__tar.gz → 0.8.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (28) hide show
  1. {interloper_api-0.6.0 → interloper_api-0.8.0}/PKG-INFO +1 -1
  2. {interloper_api-0.6.0 → interloper_api-0.8.0}/pyproject.toml +1 -1
  3. {interloper_api-0.6.0 → interloper_api-0.8.0}/src/interloper_api/routes/runs.py +57 -3
  4. {interloper_api-0.6.0 → interloper_api-0.8.0}/README.md +0 -0
  5. {interloper_api-0.6.0 → interloper_api-0.8.0}/src/interloper_api/__init__.py +0 -0
  6. {interloper_api-0.6.0 → interloper_api-0.8.0}/src/interloper_api/app.py +0 -0
  7. {interloper_api-0.6.0 → interloper_api-0.8.0}/src/interloper_api/dependencies.py +0 -0
  8. {interloper_api-0.6.0 → interloper_api-0.8.0}/src/interloper_api/email.py +0 -0
  9. {interloper_api-0.6.0 → interloper_api-0.8.0}/src/interloper_api/routes/__init__.py +0 -0
  10. {interloper_api-0.6.0 → interloper_api-0.8.0}/src/interloper_api/routes/admin.py +0 -0
  11. {interloper_api-0.6.0 → interloper_api-0.8.0}/src/interloper_api/routes/agent.py +0 -0
  12. {interloper_api-0.6.0 → interloper_api-0.8.0}/src/interloper_api/routes/assets.py +0 -0
  13. {interloper_api-0.6.0 → interloper_api-0.8.0}/src/interloper_api/routes/auth.py +0 -0
  14. {interloper_api-0.6.0 → interloper_api-0.8.0}/src/interloper_api/routes/backfills.py +0 -0
  15. {interloper_api-0.6.0 → interloper_api-0.8.0}/src/interloper_api/routes/catalog.py +0 -0
  16. {interloper_api-0.6.0 → interloper_api-0.8.0}/src/interloper_api/routes/destinations.py +0 -0
  17. {interloper_api-0.6.0 → interloper_api-0.8.0}/src/interloper_api/routes/external/__init__.py +0 -0
  18. {interloper_api-0.6.0 → interloper_api-0.8.0}/src/interloper_api/routes/external/amazon_ads.py +0 -0
  19. {interloper_api-0.6.0 → interloper_api-0.8.0}/src/interloper_api/routes/external/facebook_ads.py +0 -0
  20. {interloper_api-0.6.0 → interloper_api-0.8.0}/src/interloper_api/routes/external/google_ads.py +0 -0
  21. {interloper_api-0.6.0 → interloper_api-0.8.0}/src/interloper_api/routes/external/pinterest_ads.py +0 -0
  22. {interloper_api-0.6.0 → interloper_api-0.8.0}/src/interloper_api/routes/external/snapchat_ads.py +0 -0
  23. {interloper_api-0.6.0 → interloper_api-0.8.0}/src/interloper_api/routes/jobs.py +0 -0
  24. {interloper_api-0.6.0 → interloper_api-0.8.0}/src/interloper_api/routes/oauth.py +0 -0
  25. {interloper_api-0.6.0 → interloper_api-0.8.0}/src/interloper_api/routes/organisations.py +0 -0
  26. {interloper_api-0.6.0 → interloper_api-0.8.0}/src/interloper_api/routes/resources.py +0 -0
  27. {interloper_api-0.6.0 → interloper_api-0.8.0}/src/interloper_api/routes/sources.py +0 -0
  28. {interloper_api-0.6.0 → interloper_api-0.8.0}/src/interloper_api/routes/ws.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: interloper-api
3
- Version: 0.6.0
3
+ Version: 0.8.0
4
4
  Summary: Interloper FastAPI routes
5
5
  Author: Guillaume Onfroy
6
6
  Author-email: Guillaume Onfroy <guillaume@digitlcloud.com>
@@ -3,7 +3,7 @@
3
3
  # ###############
4
4
  [project]
5
5
  name = "interloper-api"
6
- version = "0.6.0"
6
+ version = "0.8.0"
7
7
  description = "Interloper FastAPI routes"
8
8
  readme = "README.md"
9
9
  authors = [{ name = "Guillaume Onfroy", email = "guillaume@digitlcloud.com" }]
@@ -3,6 +3,7 @@
3
3
  from __future__ import annotations
4
4
 
5
5
  import datetime as dt
6
+ from typing import Literal
6
7
  from uuid import UUID
7
8
 
8
9
  from fastapi import APIRouter, Depends, HTTPException, Response
@@ -11,10 +12,15 @@ from interloper_db import Profile, Store
11
12
  from interloper_db.models import Event, Run
12
13
  from pydantic import BaseModel
13
14
 
14
- from interloper_api.dependencies import get_org_id, get_store, require_viewer
15
+ from interloper_api.dependencies import get_org_id, get_store, require_editor, require_viewer
15
16
 
16
17
  router = APIRouter()
17
18
 
19
+ #: Hard cap on the number of events returned in a single page, regardless of
20
+ #: the requested ``limit``. Keeps a pathological ``?limit=1000000`` from loading
21
+ #: an entire run's history into memory at once.
22
+ MAX_EVENTS_PAGE_SIZE = 1000
23
+
18
24
 
19
25
  class RunResponse(BaseModel):
20
26
  """Response body for a run."""
@@ -25,11 +31,20 @@ class RunResponse(BaseModel):
25
31
  backfill_id: UUID | None
26
32
  partition_date: dt.date | None
27
33
  status: str
34
+ retry_of: UUID | None = None
35
+ attempt: int = 1
36
+ retry_scope: str | None = None
28
37
  started_at: str | None = None
29
38
  completed_at: str | None = None
30
39
  created_at: str | None = None
31
40
 
32
41
 
42
+ class RetryRequest(BaseModel):
43
+ """Request body for retrying a failed run."""
44
+
45
+ scope: Literal["all", "failed"] = "all"
46
+
47
+
33
48
  class AssetExecutionResponse(BaseModel):
34
49
  """Response body for an asset execution (from the asset_executions view)."""
35
50
 
@@ -75,6 +90,9 @@ def _run_to_response(run: Run) -> RunResponse:
75
90
  backfill_id=run.backfill_id,
76
91
  partition_date=run.partition_date,
77
92
  status=run.status,
93
+ retry_of=run.retry_of,
94
+ attempt=run.attempt,
95
+ retry_scope=run.retry_scope,
78
96
  started_at=str(run.started_at) if run.started_at else None,
79
97
  completed_at=str(run.completed_at) if run.completed_at else None,
80
98
  created_at=str(run.created_at) if run.created_at else None,
@@ -172,13 +190,49 @@ def list_asset_executions(
172
190
  ]
173
191
 
174
192
 
193
+ @router.post("/{run_id}/retry")
194
+ def retry_run(
195
+ run_id: UUID,
196
+ body: RetryRequest | None = None,
197
+ user: Profile = Depends(require_editor),
198
+ store: Store = Depends(get_store),
199
+ ) -> dict[str, str]:
200
+ """Queue a retry of a failed run.
201
+
202
+ Creates a new run linked to the original via ``retry_of``. With
203
+ ``scope="all"`` the whole DAG re-runs; with ``scope="failed"`` only the
204
+ previously failed/cancelled assets re-run.
205
+ """
206
+ scope = body.scope if body else "all"
207
+ try:
208
+ run = store.retry_run(run_id, scope=scope)
209
+ except NotFoundError:
210
+ raise HTTPException(status_code=404, detail=f"Run {run_id} not found")
211
+ except ValueError as e:
212
+ raise HTTPException(status_code=409, detail=str(e))
213
+ return {"status": "queued", "run_id": str(run.id)}
214
+
215
+
175
216
  @router.get("/{run_id}/events")
176
217
  def list_run_events(
177
218
  run_id: UUID,
219
+ response: Response,
178
220
  limit: int = 100,
221
+ offset: int = 0,
179
222
  user: Profile = Depends(require_viewer),
180
223
  store: Store = Depends(get_store),
181
224
  ) -> list[EventResponse]:
182
- """List events for a run."""
183
- events = store.list_events(run_id=run_id, limit=limit)
225
+ """List events for a run, oldest first.
226
+
227
+ Events are ordered ``timestamp ASC`` and paged with ``limit``/``offset``.
228
+ The total number of events for the run (ignoring ``limit``/``offset``) is
229
+ returned in the ``X-Total-Count`` response header so clients can page
230
+ through every event — including the terminal/outcome events
231
+ (``asset_completed``, ``asset_failed``, ``run_failed``, …) that sort last.
232
+ """
233
+ limit = max(1, min(limit, MAX_EVENTS_PAGE_SIZE))
234
+ offset = max(0, offset)
235
+ total = store.count_events(run_id=run_id)
236
+ response.headers["X-Total-Count"] = str(total)
237
+ events = store.list_events(run_id=run_id, limit=limit, offset=offset)
184
238
  return [_event_to_response(e) for e in events]
File without changes