interloper-api 0.69.0__tar.gz → 0.70.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.0 → interloper_api-0.70.0}/PKG-INFO +1 -1
  2. {interloper_api-0.69.0 → interloper_api-0.70.0}/pyproject.toml +1 -1
  3. {interloper_api-0.69.0 → interloper_api-0.70.0}/pyproject.toml.orig +1 -1
  4. {interloper_api-0.69.0 → interloper_api-0.70.0}/src/interloper_api/routes/oauth.py +16 -60
  5. {interloper_api-0.69.0 → interloper_api-0.70.0}/README.md +0 -0
  6. {interloper_api-0.69.0 → interloper_api-0.70.0}/src/interloper_api/__init__.py +0 -0
  7. {interloper_api-0.69.0 → interloper_api-0.70.0}/src/interloper_api/app.py +0 -0
  8. {interloper_api-0.69.0 → interloper_api-0.70.0}/src/interloper_api/dependencies/__init__.py +0 -0
  9. {interloper_api-0.69.0 → interloper_api-0.70.0}/src/interloper_api/dependencies/auth.py +0 -0
  10. {interloper_api-0.69.0 → interloper_api-0.70.0}/src/interloper_api/dependencies/rbac.py +0 -0
  11. {interloper_api-0.69.0 → interloper_api-0.70.0}/src/interloper_api/dependencies/state.py +0 -0
  12. {interloper_api-0.69.0 → interloper_api-0.70.0}/src/interloper_api/notifications/__init__.py +0 -0
  13. {interloper_api-0.69.0 → interloper_api-0.70.0}/src/interloper_api/notifications/invitations.py +0 -0
  14. {interloper_api-0.69.0 → interloper_api-0.70.0}/src/interloper_api/routes/__init__.py +0 -0
  15. {interloper_api-0.69.0 → interloper_api-0.70.0}/src/interloper_api/routes/admin.py +0 -0
  16. {interloper_api-0.69.0 → interloper_api-0.70.0}/src/interloper_api/routes/agent.py +0 -0
  17. {interloper_api-0.69.0 → interloper_api-0.70.0}/src/interloper_api/routes/auth.py +0 -0
  18. {interloper_api-0.69.0 → interloper_api-0.70.0}/src/interloper_api/routes/backfills.py +0 -0
  19. {interloper_api-0.69.0 → interloper_api-0.70.0}/src/interloper_api/routes/catalog.py +0 -0
  20. {interloper_api-0.69.0 → interloper_api-0.70.0}/src/interloper_api/routes/components.py +0 -0
  21. {interloper_api-0.69.0 → interloper_api-0.70.0}/src/interloper_api/routes/health.py +0 -0
  22. {interloper_api-0.69.0 → interloper_api-0.70.0}/src/interloper_api/routes/organisations.py +0 -0
  23. {interloper_api-0.69.0 → interloper_api-0.70.0}/src/interloper_api/routes/runs.py +0 -0
  24. {interloper_api-0.69.0 → interloper_api-0.70.0}/src/interloper_api/routes/tokens.py +0 -0
  25. {interloper_api-0.69.0 → interloper_api-0.70.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.0
3
+ Version: 0.70.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.0"
3
+ version = "0.70.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.0"
6
+ version = "0.70.0"
7
7
  description = "Interloper FastAPI routes"
8
8
  readme = "README.md"
9
9
  authors = [{ name = "Guillaume Onfroy", email = "guillaume@digitlcloud.com" }]
@@ -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)