interloper-api 0.70.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.70.0 → interloper_api-0.72.0}/PKG-INFO +1 -1
  2. {interloper_api-0.70.0 → interloper_api-0.72.0}/pyproject.toml +1 -1
  3. {interloper_api-0.70.0 → interloper_api-0.72.0}/pyproject.toml.orig +1 -1
  4. {interloper_api-0.70.0 → interloper_api-0.72.0}/src/interloper_api/app.py +21 -1
  5. {interloper_api-0.70.0 → interloper_api-0.72.0}/src/interloper_api/routes/components.py +24 -7
  6. {interloper_api-0.70.0 → interloper_api-0.72.0}/README.md +0 -0
  7. {interloper_api-0.70.0 → interloper_api-0.72.0}/src/interloper_api/__init__.py +0 -0
  8. {interloper_api-0.70.0 → interloper_api-0.72.0}/src/interloper_api/dependencies/__init__.py +0 -0
  9. {interloper_api-0.70.0 → interloper_api-0.72.0}/src/interloper_api/dependencies/auth.py +0 -0
  10. {interloper_api-0.70.0 → interloper_api-0.72.0}/src/interloper_api/dependencies/rbac.py +0 -0
  11. {interloper_api-0.70.0 → interloper_api-0.72.0}/src/interloper_api/dependencies/state.py +0 -0
  12. {interloper_api-0.70.0 → interloper_api-0.72.0}/src/interloper_api/notifications/__init__.py +0 -0
  13. {interloper_api-0.70.0 → interloper_api-0.72.0}/src/interloper_api/notifications/invitations.py +0 -0
  14. {interloper_api-0.70.0 → interloper_api-0.72.0}/src/interloper_api/routes/__init__.py +0 -0
  15. {interloper_api-0.70.0 → interloper_api-0.72.0}/src/interloper_api/routes/admin.py +0 -0
  16. {interloper_api-0.70.0 → interloper_api-0.72.0}/src/interloper_api/routes/agent.py +0 -0
  17. {interloper_api-0.70.0 → interloper_api-0.72.0}/src/interloper_api/routes/auth.py +0 -0
  18. {interloper_api-0.70.0 → interloper_api-0.72.0}/src/interloper_api/routes/backfills.py +0 -0
  19. {interloper_api-0.70.0 → interloper_api-0.72.0}/src/interloper_api/routes/catalog.py +0 -0
  20. {interloper_api-0.70.0 → interloper_api-0.72.0}/src/interloper_api/routes/health.py +0 -0
  21. {interloper_api-0.70.0 → interloper_api-0.72.0}/src/interloper_api/routes/oauth.py +0 -0
  22. {interloper_api-0.70.0 → interloper_api-0.72.0}/src/interloper_api/routes/organisations.py +0 -0
  23. {interloper_api-0.70.0 → interloper_api-0.72.0}/src/interloper_api/routes/runs.py +0 -0
  24. {interloper_api-0.70.0 → interloper_api-0.72.0}/src/interloper_api/routes/tokens.py +0 -0
  25. {interloper_api-0.70.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.70.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.70.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.70.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
  """
@@ -118,7 +119,12 @@ class ComponentUpdateRequest(BaseModel):
118
119
 
119
120
 
120
121
  class ComponentResponse(BaseModel):
121
- """Response body for a component of any kind."""
122
+ """Response body for a component of any kind.
123
+
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.
127
+ """
122
128
 
123
129
  id: UUID
124
130
  org_id: UUID
@@ -146,9 +152,15 @@ class ComponentResponse(BaseModel):
146
152
  ) -> ComponentResponse:
147
153
  """Convert a component row to its response model.
148
154
 
149
- ``status`` is the catalog-resolution state (drift detection), derived from
150
- the same resolver hydration uses. Secret kinds expose their decoded
151
- 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.
152
164
 
153
165
  Args:
154
166
  row: The component row to convert.
@@ -165,7 +177,12 @@ class ComponentResponse(BaseModel):
165
177
 
166
178
  config: dict[str, Any] | None = row.config
167
179
  if KINDS[row.kind].sensitive:
168
- config = store.components.decode_config(row) if include_config else None
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)
169
186
 
170
187
  return cls(
171
188
  id=row.id,