interloper-api 0.84.0__tar.gz → 0.86.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 (25) hide show
  1. {interloper_api-0.84.0 → interloper_api-0.86.0}/PKG-INFO +1 -1
  2. {interloper_api-0.84.0 → interloper_api-0.86.0}/pyproject.toml +1 -1
  3. {interloper_api-0.84.0 → interloper_api-0.86.0}/pyproject.toml.orig +1 -1
  4. {interloper_api-0.84.0 → interloper_api-0.86.0}/src/interloper_api/routes/runs.py +32 -5
  5. {interloper_api-0.84.0 → interloper_api-0.86.0}/README.md +0 -0
  6. {interloper_api-0.84.0 → interloper_api-0.86.0}/src/interloper_api/__init__.py +0 -0
  7. {interloper_api-0.84.0 → interloper_api-0.86.0}/src/interloper_api/app.py +0 -0
  8. {interloper_api-0.84.0 → interloper_api-0.86.0}/src/interloper_api/dependencies/__init__.py +0 -0
  9. {interloper_api-0.84.0 → interloper_api-0.86.0}/src/interloper_api/dependencies/auth.py +0 -0
  10. {interloper_api-0.84.0 → interloper_api-0.86.0}/src/interloper_api/dependencies/rbac.py +0 -0
  11. {interloper_api-0.84.0 → interloper_api-0.86.0}/src/interloper_api/dependencies/state.py +0 -0
  12. {interloper_api-0.84.0 → interloper_api-0.86.0}/src/interloper_api/notifications/__init__.py +0 -0
  13. {interloper_api-0.84.0 → interloper_api-0.86.0}/src/interloper_api/notifications/invitations.py +0 -0
  14. {interloper_api-0.84.0 → interloper_api-0.86.0}/src/interloper_api/routes/__init__.py +0 -0
  15. {interloper_api-0.84.0 → interloper_api-0.86.0}/src/interloper_api/routes/admin.py +0 -0
  16. {interloper_api-0.84.0 → interloper_api-0.86.0}/src/interloper_api/routes/agent.py +0 -0
  17. {interloper_api-0.84.0 → interloper_api-0.86.0}/src/interloper_api/routes/auth.py +0 -0
  18. {interloper_api-0.84.0 → interloper_api-0.86.0}/src/interloper_api/routes/backfills.py +0 -0
  19. {interloper_api-0.84.0 → interloper_api-0.86.0}/src/interloper_api/routes/catalog.py +0 -0
  20. {interloper_api-0.84.0 → interloper_api-0.86.0}/src/interloper_api/routes/components.py +0 -0
  21. {interloper_api-0.84.0 → interloper_api-0.86.0}/src/interloper_api/routes/health.py +0 -0
  22. {interloper_api-0.84.0 → interloper_api-0.86.0}/src/interloper_api/routes/oauth.py +0 -0
  23. {interloper_api-0.84.0 → interloper_api-0.86.0}/src/interloper_api/routes/organisations.py +0 -0
  24. {interloper_api-0.84.0 → interloper_api-0.86.0}/src/interloper_api/routes/tokens.py +0 -0
  25. {interloper_api-0.84.0 → interloper_api-0.86.0}/src/interloper_api/routes/websocket.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: interloper-api
3
- Version: 0.84.0
3
+ Version: 0.86.0
4
4
  Summary: Interloper FastAPI routes
5
5
  Author: Guillaume Onfroy
6
6
  Author-email: Guillaume Onfroy <guillaume@digitlcloud.com>
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "interloper-api"
3
- version = "0.84.0"
3
+ version = "0.86.0"
4
4
  description = "Interloper FastAPI routes"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.10"
@@ -3,7 +3,7 @@
3
3
  # ###############
4
4
  [project]
5
5
  name = "interloper-api"
6
- version = "0.84.0"
6
+ version = "0.86.0"
7
7
  description = "Interloper FastAPI routes"
8
8
  readme = "README.md"
9
9
  authors = [{ name = "Guillaume Onfroy", email = "guillaume@digitlcloud.com" }]
@@ -10,7 +10,7 @@ from fastapi import APIRouter, HTTPException, Query, Response
10
10
  from interloper.errors import NotFoundError
11
11
  from interloper_db import Profile, Store
12
12
  from interloper_db.models import Event, Execution, Run
13
- from pydantic import BaseModel
13
+ from pydantic import BaseModel, Field
14
14
 
15
15
  from interloper_api.dependencies import (
16
16
  CurrentUserDep,
@@ -40,6 +40,15 @@ class RunResponse(BaseModel):
40
40
  target kinds they do not know about. All three ``component_*`` identity
41
41
  fields are ``None`` exactly when the target was deleted
42
42
  (``component_id`` nulls on deletion).
43
+
44
+ A response is one attempt. In a stack-native listing it is the stack's
45
+ latest, so ``attempt`` is also how many attempts the stack took; fetching
46
+ an older attempt by id gives that attempt's own number. ``root_run_id``
47
+ is what groups them, and lists them through ``?root_run_id=``.
48
+
49
+ ``execution_counts`` is the attempt's own operation executions per status
50
+ (a failed-scope retry only re-executes what had not succeeded), so a
51
+ listing can draw each attempt's progress without fetching its executions.
43
52
  """
44
53
 
45
54
  id: UUID
@@ -52,18 +61,23 @@ class RunResponse(BaseModel):
52
61
  partition_key: str | None
53
62
  status: str
54
63
  retry_of: UUID | None = None
64
+ root_run_id: UUID | None = None
55
65
  attempt: int = 1
56
66
  retry_scope: str | None = None
67
+ scheduled_for: str | None = None
57
68
  started_at: str | None = None
58
69
  completed_at: str | None = None
59
70
  created_at: str | None = None
71
+ execution_counts: dict[str, int] = Field(default_factory=dict)
60
72
 
61
73
  @classmethod
62
- def from_run(cls, run: Run) -> RunResponse:
74
+ def from_run(cls, run: Run, execution_counts: dict[str, int] | None = None) -> RunResponse:
63
75
  """Convert a DB Run to a RunResponse.
64
76
 
65
77
  Args:
66
78
  run: The DB Run row, with its ``target`` relationship loaded.
79
+ execution_counts: The run's execution count per status; None (a
80
+ run just queued) reads as no executions yet.
67
81
 
68
82
  Returns:
69
83
  The response model.
@@ -79,11 +93,14 @@ class RunResponse(BaseModel):
79
93
  partition_key=run.partition_key,
80
94
  status=run.status,
81
95
  retry_of=run.retry_of,
96
+ root_run_id=run.root_run_id,
82
97
  attempt=run.attempt,
83
98
  retry_scope=run.retry_scope,
99
+ scheduled_for=str(run.scheduled_for) if run.scheduled_for else None,
84
100
  started_at=str(run.started_at) if run.started_at else None,
85
101
  completed_at=str(run.completed_at) if run.completed_at else None,
86
102
  created_at=str(run.created_at) if run.created_at else None,
103
+ execution_counts=execution_counts or {},
87
104
  )
88
105
 
89
106
 
@@ -222,10 +239,16 @@ def list_runs(
222
239
  q: str | None = None,
223
240
  component_kind: str | None = None,
224
241
  component_key: str | None = None,
242
+ root_run_id: UUID | None = None,
225
243
  limit: int = 50,
226
244
  offset: int = 0,
227
245
  ) -> list[RunResponse]:
228
- """List runs with optional filters.
246
+ """List one row per run stack, or one stack's attempts.
247
+
248
+ A stack is one piece of work, so a listing carries its latest attempt and
249
+ every filter reads that attempt: a stack whose first attempt failed and
250
+ whose second succeeded is a success. Passing ``root_run_id`` asks for one
251
+ stack's attempts instead, newest first.
229
252
 
230
253
  ``after``/``before`` bound the runs to those whose execution overlaps that
231
254
  window — a run occupies ``started_at`` → ``completed_at`` (open-ended while
@@ -247,6 +270,7 @@ def list_runs(
247
270
  component_kind: Keep only runs targeting a component of this kind; None applies no filter.
248
271
  component_key: Keep only runs targeting a component of this type (catalog key); None
249
272
  applies no filter.
273
+ root_run_id: List this stack's attempts rather than one row per stack.
250
274
  limit: Maximum number of runs on the page.
251
275
  offset: Number of matching runs to skip before the page starts.
252
276
  user: The authenticated user, required to hold at least the ``viewer`` role.
@@ -266,6 +290,7 @@ def list_runs(
266
290
  q=q,
267
291
  component_kind=component_kind,
268
292
  component_key=component_key,
293
+ root_run_id=root_run_id,
269
294
  )
270
295
  response.headers["X-Total-Count"] = str(total)
271
296
  runs = store.runs.list_all(
@@ -278,10 +303,12 @@ def list_runs(
278
303
  q=q,
279
304
  component_kind=component_kind,
280
305
  component_key=component_key,
306
+ root_run_id=root_run_id,
281
307
  limit=limit,
282
308
  offset=offset,
283
309
  )
284
- return [RunResponse.from_run(r) for r in runs]
310
+ counts = store.events.count_executions([run.id for run in runs])
311
+ return [RunResponse.from_run(run, counts.get(run.id)) for run in runs]
285
312
 
286
313
 
287
314
  @router.post("/", status_code=201)
@@ -348,7 +375,7 @@ def get_run(
348
375
  The run, as a response model.
349
376
  """
350
377
  run = _load_authorized_run(run_id, user, store)
351
- return RunResponse.from_run(run)
378
+ return RunResponse.from_run(run, store.events.count_executions([run_id]).get(run_id))
352
379
 
353
380
 
354
381
  @router.get("/{run_id}/executions")