interloper-api 0.71.0__tar.gz → 0.72.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.71.0 → interloper_api-0.72.0}/PKG-INFO +1 -1
  2. {interloper_api-0.71.0 → interloper_api-0.72.0}/pyproject.toml +1 -1
  3. {interloper_api-0.71.0 → interloper_api-0.72.0}/pyproject.toml.orig +1 -1
  4. {interloper_api-0.71.0 → interloper_api-0.72.0}/src/interloper_api/app.py +21 -1
  5. {interloper_api-0.71.0 → interloper_api-0.72.0}/src/interloper_api/routes/components.py +21 -15
  6. {interloper_api-0.71.0 → interloper_api-0.72.0}/README.md +0 -0
  7. {interloper_api-0.71.0 → interloper_api-0.72.0}/src/interloper_api/__init__.py +0 -0
  8. {interloper_api-0.71.0 → interloper_api-0.72.0}/src/interloper_api/dependencies/__init__.py +0 -0
  9. {interloper_api-0.71.0 → interloper_api-0.72.0}/src/interloper_api/dependencies/auth.py +0 -0
  10. {interloper_api-0.71.0 → interloper_api-0.72.0}/src/interloper_api/dependencies/rbac.py +0 -0
  11. {interloper_api-0.71.0 → interloper_api-0.72.0}/src/interloper_api/dependencies/state.py +0 -0
  12. {interloper_api-0.71.0 → interloper_api-0.72.0}/src/interloper_api/notifications/__init__.py +0 -0
  13. {interloper_api-0.71.0 → interloper_api-0.72.0}/src/interloper_api/notifications/invitations.py +0 -0
  14. {interloper_api-0.71.0 → interloper_api-0.72.0}/src/interloper_api/routes/__init__.py +0 -0
  15. {interloper_api-0.71.0 → interloper_api-0.72.0}/src/interloper_api/routes/admin.py +0 -0
  16. {interloper_api-0.71.0 → interloper_api-0.72.0}/src/interloper_api/routes/agent.py +0 -0
  17. {interloper_api-0.71.0 → interloper_api-0.72.0}/src/interloper_api/routes/auth.py +0 -0
  18. {interloper_api-0.71.0 → interloper_api-0.72.0}/src/interloper_api/routes/backfills.py +0 -0
  19. {interloper_api-0.71.0 → interloper_api-0.72.0}/src/interloper_api/routes/catalog.py +0 -0
  20. {interloper_api-0.71.0 → interloper_api-0.72.0}/src/interloper_api/routes/health.py +0 -0
  21. {interloper_api-0.71.0 → interloper_api-0.72.0}/src/interloper_api/routes/oauth.py +0 -0
  22. {interloper_api-0.71.0 → interloper_api-0.72.0}/src/interloper_api/routes/organisations.py +0 -0
  23. {interloper_api-0.71.0 → interloper_api-0.72.0}/src/interloper_api/routes/runs.py +0 -0
  24. {interloper_api-0.71.0 → interloper_api-0.72.0}/src/interloper_api/routes/tokens.py +0 -0
  25. {interloper_api-0.71.0 → interloper_api-0.72.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.71.0
3
+ Version: 0.72.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.71.0"
3
+ version = "0.72.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.71.0"
6
+ version = "0.72.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, FastAPI, Request
10
10
  from fastapi.middleware.cors import CORSMiddleware
11
11
  from fastapi.responses import JSONResponse
12
12
  from interloper.catalog.base import Catalog
13
- from interloper.errors import ComponentDriftError, NotFoundError, QuotaExceededError
13
+ from interloper.errors import ComponentDriftError, HydrationError, NotFoundError, QuotaExceededError
14
14
  from interloper_db import Store
15
15
 
16
16
  from interloper_api.dependencies import (
@@ -87,6 +87,25 @@ async def _component_drift(_request: Request, exception: ComponentDriftError) ->
87
87
  return JSONResponse(status_code=409, content={"detail": str(exception)})
88
88
 
89
89
 
90
+ async def _hydration_failed(_request: Request, exception: HydrationError) -> JSONResponse:
91
+ """Render an unreadable stored record as a conflict rather than a 500.
92
+
93
+ A record that cannot be rebuilt from the database (a payload the active
94
+ ``INTERLOPER_ENCRYPTION_KEY`` cannot decrypt, a stored config the class no
95
+ longer accepts) stays broken until an operator re-keys or rewrites it, so
96
+ like drift it surfaces as a 409 carrying the reason instead of an opaque
97
+ 500 that only the server log explains.
98
+
99
+ Args:
100
+ _request: The incoming request, unused.
101
+ exception: The raised :class:`HydrationError`.
102
+
103
+ Returns:
104
+ A 409 response carrying the exception message as ``detail``.
105
+ """
106
+ return JSONResponse(status_code=409, content={"detail": str(exception)})
107
+
108
+
90
109
  async def _quota_exceeded(_request: Request, exception: QuotaExceededError) -> JSONResponse:
91
110
  """Render store-level quota enforcement as a 429 with structured context.
92
111
 
@@ -116,6 +135,7 @@ async def _quota_exceeded(_request: Request, exception: QuotaExceededError) -> J
116
135
  _ERROR_HANDLERS: dict[type[Exception], Any] = {
117
136
  NotFoundError: _not_found,
118
137
  ComponentDriftError: _component_drift,
138
+ HydrationError: _hydration_failed,
119
139
  QuotaExceededError: _quota_exceeded,
120
140
  }
121
141
 
@@ -6,8 +6,9 @@ unsaved config — resolving a FetchField's options (``/resolve``) and
6
6
  checking a connection (``/check``).
7
7
 
8
8
  The response shape is kind-agnostic — identity, drift ``status``, ``config``
9
- (decoded for secret kinds on detail responses), machine-owned ``state``,
10
- typed ``relations``, and one level of ``children`` (a source's assets).
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).
11
12
  What a kind's config looks like and which relation types it may declare
12
13
  come from the catalog (``/catalog``), not from this router.
13
14
  """
@@ -120,9 +121,9 @@ class ComponentUpdateRequest(BaseModel):
120
121
  class ComponentResponse(BaseModel):
121
122
  """Response body for a component of any kind.
122
123
 
123
- ``auto_renew`` surfaces a connection's renewal toggle even in list
124
- responses, where a secret kind's ``config`` stays undisclosed — the
125
- toggle is operational metadata, not a credential.
124
+ A secret kind whose payload does not decrypt carries ``status``
125
+ ``unreadable`` and no ``config`` at all, rather than a subset that was
126
+ never read.
126
127
  """
127
128
 
128
129
  id: UUID
@@ -134,7 +135,6 @@ class ComponentResponse(BaseModel):
134
135
  config: dict[str, Any] | None = None
135
136
  state: dict[str, Any] | None = None
136
137
  encrypted: bool = False
137
- auto_renew: bool | None = None
138
138
  parent_id: UUID | None = None
139
139
  relations: dict[str, list[RelationRef]] = {}
140
140
  children: list[ComponentResponse] = []
@@ -152,9 +152,15 @@ class ComponentResponse(BaseModel):
152
152
  ) -> ComponentResponse:
153
153
  """Convert a component row to its response model.
154
154
 
155
- ``status`` is the catalog-resolution state (drift detection), derived from
156
- the same resolver hydration uses. Secret kinds expose their decoded
157
- payload as ``config`` only when *include_config* is set (detail responses).
155
+ ``status`` is the usability state hydration gates on: catalog resolution
156
+ (drift detection) plus, for an encrypted row, whether its payload
157
+ decrypts. Secret kinds expose their decoded payload as ``config`` only
158
+ when *include_config* is set (detail responses); otherwise ``config``
159
+ carries just the schema's ``x-public`` subset (operational fields such
160
+ as a connection's ``auto_renew``). An ``unreadable`` row carries no
161
+ ``config`` either way: the reason rides its ``status``, so the
162
+ collection still lists and the UI can say what is wrong instead of the
163
+ request failing over one row.
158
164
 
159
165
  Args:
160
166
  row: The component row to convert.
@@ -171,11 +177,12 @@ class ComponentResponse(BaseModel):
171
177
 
172
178
  config: dict[str, Any] | None = row.config
173
179
  if KINDS[row.kind].sensitive:
174
- config = store.components.decode_config(row) if include_config else None
175
-
176
- auto_renew: bool | None = None
177
- if row.kind == "connection":
178
- auto_renew = bool((config or store.components.decode_config(row)).get("auto_renew", True))
180
+ if status is ComponentStatus.UNREADABLE:
181
+ config = None
182
+ elif include_config:
183
+ config = store.components.decode_config(row)
184
+ else:
185
+ config = store.components.public_config(row)
179
186
 
180
187
  return cls(
181
188
  id=row.id,
@@ -187,7 +194,6 @@ class ComponentResponse(BaseModel):
187
194
  config=config,
188
195
  state=row.state,
189
196
  encrypted=row.encrypted,
190
- auto_renew=auto_renew,
191
197
  parent_id=row.parent_id,
192
198
  relations=_relations_of(row),
193
199
  children=[