interloper-api 0.69.1__tar.gz → 0.71.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.69.1 → interloper_api-0.71.0}/PKG-INFO +1 -1
  2. {interloper_api-0.69.1 → interloper_api-0.71.0}/pyproject.toml +1 -1
  3. {interloper_api-0.69.1 → interloper_api-0.71.0}/pyproject.toml.orig +1 -1
  4. {interloper_api-0.69.1 → interloper_api-0.71.0}/src/interloper_api/routes/components.py +12 -1
  5. {interloper_api-0.69.1 → interloper_api-0.71.0}/src/interloper_api/routes/oauth.py +16 -60
  6. {interloper_api-0.69.1 → interloper_api-0.71.0}/README.md +0 -0
  7. {interloper_api-0.69.1 → interloper_api-0.71.0}/src/interloper_api/__init__.py +0 -0
  8. {interloper_api-0.69.1 → interloper_api-0.71.0}/src/interloper_api/app.py +0 -0
  9. {interloper_api-0.69.1 → interloper_api-0.71.0}/src/interloper_api/dependencies/__init__.py +0 -0
  10. {interloper_api-0.69.1 → interloper_api-0.71.0}/src/interloper_api/dependencies/auth.py +0 -0
  11. {interloper_api-0.69.1 → interloper_api-0.71.0}/src/interloper_api/dependencies/rbac.py +0 -0
  12. {interloper_api-0.69.1 → interloper_api-0.71.0}/src/interloper_api/dependencies/state.py +0 -0
  13. {interloper_api-0.69.1 → interloper_api-0.71.0}/src/interloper_api/notifications/__init__.py +0 -0
  14. {interloper_api-0.69.1 → interloper_api-0.71.0}/src/interloper_api/notifications/invitations.py +0 -0
  15. {interloper_api-0.69.1 → interloper_api-0.71.0}/src/interloper_api/routes/__init__.py +0 -0
  16. {interloper_api-0.69.1 → interloper_api-0.71.0}/src/interloper_api/routes/admin.py +0 -0
  17. {interloper_api-0.69.1 → interloper_api-0.71.0}/src/interloper_api/routes/agent.py +0 -0
  18. {interloper_api-0.69.1 → interloper_api-0.71.0}/src/interloper_api/routes/auth.py +0 -0
  19. {interloper_api-0.69.1 → interloper_api-0.71.0}/src/interloper_api/routes/backfills.py +0 -0
  20. {interloper_api-0.69.1 → interloper_api-0.71.0}/src/interloper_api/routes/catalog.py +0 -0
  21. {interloper_api-0.69.1 → interloper_api-0.71.0}/src/interloper_api/routes/health.py +0 -0
  22. {interloper_api-0.69.1 → interloper_api-0.71.0}/src/interloper_api/routes/organisations.py +0 -0
  23. {interloper_api-0.69.1 → interloper_api-0.71.0}/src/interloper_api/routes/runs.py +0 -0
  24. {interloper_api-0.69.1 → interloper_api-0.71.0}/src/interloper_api/routes/tokens.py +0 -0
  25. {interloper_api-0.69.1 → interloper_api-0.71.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.69.1
3
+ Version: 0.71.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.69.1"
3
+ version = "0.71.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.69.1"
6
+ version = "0.71.0"
7
7
  description = "Interloper FastAPI routes"
8
8
  readme = "README.md"
9
9
  authors = [{ name = "Guillaume Onfroy", email = "guillaume@digitlcloud.com" }]
@@ -118,7 +118,12 @@ class ComponentUpdateRequest(BaseModel):
118
118
 
119
119
 
120
120
  class ComponentResponse(BaseModel):
121
- """Response body for a component of any kind."""
121
+ """Response body for a component of any kind.
122
+
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.
126
+ """
122
127
 
123
128
  id: UUID
124
129
  org_id: UUID
@@ -129,6 +134,7 @@ class ComponentResponse(BaseModel):
129
134
  config: dict[str, Any] | None = None
130
135
  state: dict[str, Any] | None = None
131
136
  encrypted: bool = False
137
+ auto_renew: bool | None = None
132
138
  parent_id: UUID | None = None
133
139
  relations: dict[str, list[RelationRef]] = {}
134
140
  children: list[ComponentResponse] = []
@@ -167,6 +173,10 @@ class ComponentResponse(BaseModel):
167
173
  if KINDS[row.kind].sensitive:
168
174
  config = store.components.decode_config(row) if include_config else None
169
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))
179
+
170
180
  return cls(
171
181
  id=row.id,
172
182
  org_id=row.org_id,
@@ -177,6 +187,7 @@ class ComponentResponse(BaseModel):
177
187
  config=config,
178
188
  state=row.state,
179
189
  encrypted=row.encrypted,
190
+ auto_renew=auto_renew,
180
191
  parent_id=row.parent_id,
181
192
  relations=_relations_of(row),
182
193
  children=[
@@ -1,10 +1,9 @@
1
1
  """OAuth2 token exchange routes.
2
2
 
3
3
  Providers come from the core registry (``interloper.oauth``): each
4
- ``OAuthProvider`` carries a declarative token-exchange spec (URL, method,
5
- encoding, parameter names), so the exchange is performed generically —
6
- adding a provider is an ``interloper.oauth_providers`` entry point, not a
7
- new route.
4
+ ``OAuthProvider`` builds its own token requests (its dialect included), so
5
+ the exchange is performed generically — adding a provider is an
6
+ ``interloper.oauth_providers`` entry point, not a new route.
8
7
 
9
8
  The in-house *OAuth* credentials (``client_id`` / ``client_secret`` /
10
9
  ``redirect_uri``) are read from provider-scoped environment variables
@@ -19,14 +18,13 @@ buttons to render.
19
18
 
20
19
  from __future__ import annotations
21
20
 
22
- import base64
23
21
  import logging
24
22
  import os
25
23
  from typing import Any
26
24
 
27
25
  import httpx
28
26
  from fastapi import APIRouter, Depends, HTTPException
29
- from interloper.oauth import PROVIDERS, OAuthAppCredentials, OAuthProvider
27
+ from interloper.oauth import PROVIDERS, OAuthAppCredentials
30
28
  from interloper_db import Profile
31
29
  from pydantic import BaseModel
32
30
 
@@ -55,59 +53,10 @@ def log_provider_status() -> None:
55
53
  logger.info("OAuth sign-in providers: %s", ", ".join(sorted(active)) if active else "none configured")
56
54
 
57
55
 
58
- # -- Generic token exchange ----------------------------------------------------
59
-
60
-
61
- async def _exchange(
62
- client: httpx.AsyncClient,
63
- spec: OAuthProvider,
64
- config: OAuthAppCredentials,
65
- code: str,
66
- ) -> dict[str, Any]:
67
- """Exchange an authorization code for tokens, driven by the provider spec.
68
-
69
- Args:
70
- client: The HTTP client to use.
71
- spec: The provider's token-exchange spec.
72
- config: The in-house OAuth credentials.
73
- code: The authorization code.
74
-
75
- Returns:
76
- The provider's raw token response (e.g. ``refresh_token``). The OAuth
77
- credentials are *not* injected: they are the in-house per-provider
78
- values resolved from env at runtime, so the secret never leaves the
79
- server.
80
- """
81
- logical_values = {
82
- "grant_type": "authorization_code",
83
- "code": code,
84
- "redirect_uri": config.redirect_uri,
85
- "client_id": config.client_id,
86
- "client_secret": config.client_secret,
87
- }
88
- params = {wire: logical_values[logical] for logical, wire in spec.token_params.items()}
89
-
90
- headers: dict[str, str] = {}
91
- if spec.token_basic_auth:
92
- creds = base64.b64encode(f"{config.client_id}:{config.client_secret}".encode()).decode()
93
- headers["Authorization"] = f"Basic {creds}"
94
-
95
- if spec.token_method == "get":
96
- response = await client.get(spec.token_url, params=params, headers=headers)
97
- elif spec.token_encoding == "form":
98
- headers["Content-Type"] = "application/x-www-form-urlencoded"
99
- response = await client.post(spec.token_url, data=params, headers=headers)
100
- else:
101
- response = await client.post(spec.token_url, json=params, headers=headers)
102
- response.raise_for_status()
103
-
104
- return response.json()
105
-
106
-
107
56
  # -- Routes --------------------------------------------------------------------
108
57
 
109
58
 
110
- class TokenExchangeRequest(BaseModel):
59
+ class AuthorizationCodeExchangeRequest(BaseModel):
111
60
  """Request body for exchanging an authorization code for tokens."""
112
61
 
113
62
  code: str
@@ -151,9 +100,9 @@ def list_providers() -> list[ProviderInfo]:
151
100
 
152
101
 
153
102
  @router.post("/{provider}")
154
- async def exchange_token(
103
+ async def exchange_authorization_code(
155
104
  provider: str,
156
- body: TokenExchangeRequest,
105
+ body: AuthorizationCodeExchangeRequest,
157
106
  _user: Profile = Depends(get_current_user),
158
107
  ) -> dict[str, Any]:
159
108
  """Exchange an authorization code for tokens. Requires authentication.
@@ -185,10 +134,17 @@ async def exchange_token(
185
134
 
186
135
  try:
187
136
  logger.info("Exchanging auth code for provider %s", provider)
137
+ request = spec.authorization_code_request(
138
+ code=body.code,
139
+ redirect_uri=config.redirect_uri,
140
+ client_id=config.client_id,
141
+ client_secret=config.client_secret,
142
+ )
188
143
  async with httpx.AsyncClient(timeout=30, follow_redirects=True) as client:
189
- result = await _exchange(client, spec, config, body.code)
144
+ response = await client.send(request)
145
+ response.raise_for_status()
190
146
  logger.info("Successfully exchanged auth code for provider %s", provider)
191
- return result
147
+ return response.json()
192
148
  except httpx.HTTPStatusError as exception:
193
149
  detail = exception.response.text
194
150
  logger.error("Token exchange failed for %s: %s %s", provider, exception.response.status_code, detail)