interloper-api 0.82.0__tar.gz → 0.83.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.82.0 → interloper_api-0.83.0}/PKG-INFO +1 -1
  2. {interloper_api-0.82.0 → interloper_api-0.83.0}/pyproject.toml +1 -1
  3. {interloper_api-0.82.0 → interloper_api-0.83.0}/pyproject.toml.orig +1 -1
  4. {interloper_api-0.82.0 → interloper_api-0.83.0}/src/interloper_api/routes/components.py +109 -32
  5. {interloper_api-0.82.0 → interloper_api-0.83.0}/README.md +0 -0
  6. {interloper_api-0.82.0 → interloper_api-0.83.0}/src/interloper_api/__init__.py +0 -0
  7. {interloper_api-0.82.0 → interloper_api-0.83.0}/src/interloper_api/app.py +0 -0
  8. {interloper_api-0.82.0 → interloper_api-0.83.0}/src/interloper_api/dependencies/__init__.py +0 -0
  9. {interloper_api-0.82.0 → interloper_api-0.83.0}/src/interloper_api/dependencies/auth.py +0 -0
  10. {interloper_api-0.82.0 → interloper_api-0.83.0}/src/interloper_api/dependencies/rbac.py +0 -0
  11. {interloper_api-0.82.0 → interloper_api-0.83.0}/src/interloper_api/dependencies/state.py +0 -0
  12. {interloper_api-0.82.0 → interloper_api-0.83.0}/src/interloper_api/notifications/__init__.py +0 -0
  13. {interloper_api-0.82.0 → interloper_api-0.83.0}/src/interloper_api/notifications/invitations.py +0 -0
  14. {interloper_api-0.82.0 → interloper_api-0.83.0}/src/interloper_api/routes/__init__.py +0 -0
  15. {interloper_api-0.82.0 → interloper_api-0.83.0}/src/interloper_api/routes/admin.py +0 -0
  16. {interloper_api-0.82.0 → interloper_api-0.83.0}/src/interloper_api/routes/agent.py +0 -0
  17. {interloper_api-0.82.0 → interloper_api-0.83.0}/src/interloper_api/routes/auth.py +0 -0
  18. {interloper_api-0.82.0 → interloper_api-0.83.0}/src/interloper_api/routes/backfills.py +0 -0
  19. {interloper_api-0.82.0 → interloper_api-0.83.0}/src/interloper_api/routes/catalog.py +0 -0
  20. {interloper_api-0.82.0 → interloper_api-0.83.0}/src/interloper_api/routes/health.py +0 -0
  21. {interloper_api-0.82.0 → interloper_api-0.83.0}/src/interloper_api/routes/oauth.py +0 -0
  22. {interloper_api-0.82.0 → interloper_api-0.83.0}/src/interloper_api/routes/organisations.py +0 -0
  23. {interloper_api-0.82.0 → interloper_api-0.83.0}/src/interloper_api/routes/runs.py +0 -0
  24. {interloper_api-0.82.0 → interloper_api-0.83.0}/src/interloper_api/routes/tokens.py +0 -0
  25. {interloper_api-0.82.0 → interloper_api-0.83.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.82.0
3
+ Version: 0.83.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.82.0"
3
+ version = "0.83.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.82.0"
6
+ version = "0.83.0"
7
7
  description = "Interloper FastAPI routes"
8
8
  readme = "README.md"
9
9
  authors = [{ name = "Guillaume Onfroy", email = "guillaume@digitlcloud.com" }]
@@ -7,8 +7,10 @@ checking a connection (``/check``).
7
7
 
8
8
  The response shape is kind-agnostic — identity, drift ``status``, ``config``
9
9
  (decoded for secret kinds on detail responses; the schema's ``x-public``
10
- subset elsewhere), machine-owned ``state``, typed ``relations``, and one
11
- level of ``children`` (a source's assets).
10
+ subset elsewhere), machine-owned ``state``, typed ``relations``, and the
11
+ components a row owns under ``children`` (a source's assets). The owner is
12
+ the unit: an owned component rides inside its owner and never lists on its
13
+ own, the way the catalog reaches an owned definition through its owner.
12
14
  What a kind's config looks like and which relation types it may declare
13
15
  come from the catalog (``/catalog``), not from this router.
14
16
  """
@@ -36,7 +38,7 @@ from interloper.errors import (
36
38
  from interloper.resource.fields import is_fetch_field_provider
37
39
  from interloper.utils.concurrency import invoke
38
40
  from interloper.utils.imports import import_from_path
39
- from interloper_db import Component, ComponentStatus, Store
41
+ from interloper_db import Component, ComponentStatus, DeleteImpact, Store
40
42
  from pydantic import BaseModel, Field, ValidationError
41
43
 
42
44
  from interloper_api.dependencies import (
@@ -70,10 +72,16 @@ class RelationCreateRequest(RelationEntry):
70
72
 
71
73
 
72
74
  class RelationRef(BaseModel):
73
- """One relation binding in a component response."""
75
+ """One relation binding in a component response, with enough of its target to name it.
76
+
77
+ ``dst_key`` and ``dst_name`` ride along so a surface can label what a
78
+ component is bound to without holding that component's own row.
79
+ """
74
80
 
75
81
  dst_id: UUID
76
82
  dst_kind: str
83
+ dst_key: str
84
+ dst_name: str | None = None
77
85
 
78
86
 
79
87
  class RelationResponse(BaseModel):
@@ -90,6 +98,37 @@ class RelationResponse(BaseModel):
90
98
  dst_kind: str
91
99
 
92
100
 
101
+ class UsedByRef(BaseModel):
102
+ """A component bound to something about to be deleted, as the 409 ``used_by`` payload names it."""
103
+
104
+ id: str
105
+ kind: str
106
+ key: str
107
+ name: str | None = None
108
+
109
+
110
+ class DeleteImpactResponse(BaseModel):
111
+ """The preview behind a delete confirmation: who blocks it, who merely detaches."""
112
+
113
+ blocking: list[UsedByRef]
114
+ detaching: list[UsedByRef]
115
+
116
+ @classmethod
117
+ def from_impact(cls, impact: DeleteImpact) -> DeleteImpactResponse:
118
+ """Convert the store's preview to its response model.
119
+
120
+ Args:
121
+ impact: The store's blocking and detaching referrers.
122
+
123
+ Returns:
124
+ The response model.
125
+ """
126
+ return cls(
127
+ blocking=[UsedByRef.model_validate(ref) for ref in impact.blocking],
128
+ detaching=[UsedByRef.model_validate(ref) for ref in impact.detaching],
129
+ )
130
+
131
+
93
132
  class ComponentCreateRequest(BaseModel):
94
133
  """Request body for creating a component of any kind.
95
134
 
@@ -154,37 +193,34 @@ class ComponentResponse(BaseModel):
154
193
  ) -> ComponentResponse:
155
194
  """Convert a component row to its response model.
156
195
 
157
- ``status`` is the usability state hydration gates on: catalog resolution
158
- (drift detection) plus, for an encrypted row, whether its payload
159
- decrypts. Secret kinds expose their decoded payload as ``config`` only
160
- when *include_config* is set (detail responses); otherwise ``config``
161
- carries just the schema's ``x-public`` subset (operational fields such
162
- as a connection's ``auto_renew``). An ``unreadable`` row carries no
163
- ``config`` either way: the reason rides its ``status``, so the
164
- collection still lists and the UI can say what is wrong instead of the
165
- request failing over one row.
196
+ The row is read once: ``status`` is the usability state hydration gates
197
+ on (catalog resolution, then whether the payload decodes) and every
198
+ view of the payload comes off that same reading. Secret kinds expose
199
+ their decoded payload as ``config`` only when *include_config* is set
200
+ (detail responses); otherwise ``config`` carries just the schema's
201
+ ``x-public`` subset (operational fields such as a connection's
202
+ ``auto_renew``). An ``unreadable`` row carries no ``config`` either
203
+ way: the reason rides its ``status``, so the collection still lists and
204
+ the UI can say what is wrong instead of the request failing over one
205
+ row.
166
206
 
167
207
  Args:
168
208
  row: The component row to convert.
169
209
  store: The Store instance.
170
210
  include_config: Whether a secret kind's decoded config is exposed.
171
- parent_key: The parent source's key when the caller already knows it,
172
- sparing a lazy load of ``row.parent`` for asset rows.
173
- with_children: Whether the row's children are nested in the response.
211
+ parent_key: The owner's key when the caller already knows it,
212
+ sparing a lookup for an owned row.
213
+ with_children: Whether the components the row owns are nested in
214
+ the response.
174
215
 
175
216
  Returns:
176
217
  The response model.
177
218
  """
178
- status = store.components.status(row, parent_key=parent_key)
219
+ reading = store.components.read(row, parent_key=parent_key)
179
220
 
180
- config: dict[str, Any] | None = row.config
181
- if KINDS[row.kind].sensitive:
182
- if status is ComponentStatus.UNREADABLE:
183
- config = None
184
- elif include_config:
185
- config = store.components.decode_config(row)
186
- else:
187
- config = store.components.public_config(row)
221
+ config = reading.config
222
+ if KINDS[row.kind].sensitive and not include_config:
223
+ config = None if reading.status is ComponentStatus.UNREADABLE else reading.public_config
188
224
 
189
225
  return cls(
190
226
  id=row.id,
@@ -192,8 +228,8 @@ class ComponentResponse(BaseModel):
192
228
  kind=row.kind,
193
229
  key=row.key,
194
230
  name=row.name,
195
- discriminator=store.components.discriminator(row),
196
- status=status,
231
+ discriminator=reading.discriminator,
232
+ status=reading.status,
197
233
  config=config,
198
234
  state=row.state,
199
235
  encrypted=row.encrypted,
@@ -242,7 +278,12 @@ def _relations_of(row: Component) -> dict[str, list[RelationRef]]:
242
278
  grouped: dict[str, list[RelationRef]] = {}
243
279
  for relation in row.out_relations:
244
280
  grouped.setdefault(relation.name, []).append(
245
- RelationRef(dst_id=relation.dst_id, dst_kind=relation.dst_kind)
281
+ RelationRef(
282
+ dst_id=relation.dst_id,
283
+ dst_kind=relation.dst_kind,
284
+ dst_key=relation.dst.key if relation.dst else "",
285
+ dst_name=relation.dst.name if relation.dst else None,
286
+ )
246
287
  )
247
288
  return grouped
248
289
 
@@ -272,18 +313,22 @@ def list_components(
272
313
  store: StoreDep,
273
314
  kind: Annotated[list[str] | None, Query()] = None,
274
315
  ) -> list[ComponentResponse]:
275
- """List the organisation's components, optionally filtered by kind(s).
316
+ """List the organisation's root components, optionally filtered by kind(s).
317
+
318
+ An owned component (a source's asset) rides under its owner's
319
+ ``children`` rather than listing on its own, so ``kind=asset`` yields the
320
+ standalone assets alone.
276
321
 
277
322
  Args:
278
- kind: The component kinds to keep; None lists every kind.
323
+ kind: The root kinds to keep; None lists every kind.
279
324
  user: The authenticated user.
280
325
  org_id: The active organisation UUID.
281
326
  store: The Store instance.
282
327
 
283
328
  Returns:
284
- The organisation's components, secret configs withheld.
329
+ The organisation's root components, secret configs withheld.
285
330
  """
286
- rows = store.components.list_all(org_id, kinds=kind)
331
+ rows = store.components.list_roots(org_id, kinds=kind)
287
332
  return [ComponentResponse.from_row(row, store, include_config=False) for row in rows]
288
333
 
289
334
 
@@ -321,6 +366,38 @@ def list_relations(
321
366
  ]
322
367
 
323
368
 
369
+ @router.get("/delete-impact")
370
+ def get_delete_impact(
371
+ user: CurrentUserDep,
372
+ store: StoreDep,
373
+ component_id: Annotated[list[UUID], Query(alias="id")],
374
+ ) -> DeleteImpactResponse:
375
+ """Preview what deleting the given components does to the components bound to them.
376
+
377
+ The rule the delete guard enforces, evaluated without deleting, so a
378
+ confirmation can say up front what refuses the deletion and what merely
379
+ loses a binding.
380
+
381
+ Args:
382
+ user: The authenticated user.
383
+ store: The Store instance.
384
+ component_id: The components about to be deleted, repeated per id.
385
+
386
+ Returns:
387
+ The blocking and detaching referrers.
388
+
389
+ Raises:
390
+ HTTPException: 404 when any of the ids is unknown.
391
+ """
392
+ for one in component_id:
393
+ load_authorized(store.components.get, one, user, store, label="Component")
394
+ try:
395
+ impact = store.components.delete_impact(component_id)
396
+ except NotFoundError as e:
397
+ raise HTTPException(status_code=404, detail=str(e))
398
+ return DeleteImpactResponse.from_impact(impact)
399
+
400
+
324
401
  @router.post("/", status_code=201)
325
402
  def create_component(
326
403
  body: ComponentCreateRequest,