reflectapi-runtime 0.1.0__py3-none-any.whl
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.
- reflectapi_runtime/__init__.py +109 -0
- reflectapi_runtime/auth.py +507 -0
- reflectapi_runtime/batch.py +165 -0
- reflectapi_runtime/client.py +924 -0
- reflectapi_runtime/exceptions.py +120 -0
- reflectapi_runtime/hypothesis_strategies.py +275 -0
- reflectapi_runtime/middleware.py +254 -0
- reflectapi_runtime/option.py +295 -0
- reflectapi_runtime/response.py +126 -0
- reflectapi_runtime/streaming.py +435 -0
- reflectapi_runtime/testing.py +380 -0
- reflectapi_runtime/types.py +32 -0
- reflectapi_runtime-0.1.0.dist-info/METADATA +36 -0
- reflectapi_runtime-0.1.0.dist-info/RECORD +15 -0
- reflectapi_runtime-0.1.0.dist-info/WHEEL +4 -0
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
"""ReflectAPI Python Runtime Library.
|
|
2
|
+
|
|
3
|
+
This package provides the core runtime components for ReflectAPI-generated clients.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from .auth import (
|
|
7
|
+
APIKeyAuth,
|
|
8
|
+
AuthHandler,
|
|
9
|
+
AuthToken,
|
|
10
|
+
BasicAuth,
|
|
11
|
+
BearerTokenAuth,
|
|
12
|
+
CustomAuth,
|
|
13
|
+
OAuth2AuthorizationCodeAuth,
|
|
14
|
+
OAuth2ClientCredentialsAuth,
|
|
15
|
+
api_key,
|
|
16
|
+
basic_auth,
|
|
17
|
+
bearer_token,
|
|
18
|
+
oauth2_authorization_code,
|
|
19
|
+
oauth2_client_credentials,
|
|
20
|
+
)
|
|
21
|
+
from .batch import BatchClient
|
|
22
|
+
from .client import AsyncClientBase, ClientBase
|
|
23
|
+
from .exceptions import (
|
|
24
|
+
ApiError,
|
|
25
|
+
ApplicationError,
|
|
26
|
+
NetworkError,
|
|
27
|
+
TimeoutError,
|
|
28
|
+
ValidationError,
|
|
29
|
+
)
|
|
30
|
+
from .hypothesis_strategies import (
|
|
31
|
+
HAS_HYPOTHESIS,
|
|
32
|
+
api_model_strategy,
|
|
33
|
+
enhanced_strategy_for_type,
|
|
34
|
+
strategy_for_pydantic_model,
|
|
35
|
+
strategy_for_type,
|
|
36
|
+
)
|
|
37
|
+
from .middleware import AsyncMiddleware
|
|
38
|
+
from .option import (
|
|
39
|
+
Option,
|
|
40
|
+
ReflectapiOption,
|
|
41
|
+
Undefined,
|
|
42
|
+
none,
|
|
43
|
+
serialize_option_dict,
|
|
44
|
+
some,
|
|
45
|
+
undefined,
|
|
46
|
+
)
|
|
47
|
+
from .response import ApiResponse, TransportMetadata
|
|
48
|
+
from .streaming import AsyncStreamingClient, StreamingResponse
|
|
49
|
+
from .testing import (
|
|
50
|
+
AsyncCassetteMiddleware,
|
|
51
|
+
CassetteClient,
|
|
52
|
+
CassetteMiddleware,
|
|
53
|
+
MockClient,
|
|
54
|
+
TestClientMixin,
|
|
55
|
+
)
|
|
56
|
+
from .types import BatchResult, ReflectapiEmpty, ReflectapiInfallible
|
|
57
|
+
|
|
58
|
+
__version__ = "0.1.0"
|
|
59
|
+
|
|
60
|
+
__all__ = [
|
|
61
|
+
# Authentication
|
|
62
|
+
"APIKeyAuth",
|
|
63
|
+
"AuthHandler",
|
|
64
|
+
"AuthToken",
|
|
65
|
+
"BasicAuth",
|
|
66
|
+
"BearerTokenAuth",
|
|
67
|
+
"CustomAuth",
|
|
68
|
+
"OAuth2AuthorizationCodeAuth",
|
|
69
|
+
"OAuth2ClientCredentialsAuth",
|
|
70
|
+
"api_key",
|
|
71
|
+
"basic_auth",
|
|
72
|
+
"bearer_token",
|
|
73
|
+
"oauth2_authorization_code",
|
|
74
|
+
"oauth2_client_credentials",
|
|
75
|
+
# Core
|
|
76
|
+
"ApiError",
|
|
77
|
+
"ApiResponse",
|
|
78
|
+
"ApplicationError",
|
|
79
|
+
"AsyncCassetteMiddleware",
|
|
80
|
+
"AsyncClientBase",
|
|
81
|
+
"AsyncStreamingClient",
|
|
82
|
+
"BatchClient",
|
|
83
|
+
"BatchResult",
|
|
84
|
+
"CassetteClient",
|
|
85
|
+
"CassetteMiddleware",
|
|
86
|
+
"ClientBase",
|
|
87
|
+
"HAS_HYPOTHESIS",
|
|
88
|
+
"AsyncMiddleware",
|
|
89
|
+
"MockClient",
|
|
90
|
+
"NetworkError",
|
|
91
|
+
"Option",
|
|
92
|
+
"ReflectapiEmpty",
|
|
93
|
+
"ReflectapiInfallible",
|
|
94
|
+
"ReflectapiOption",
|
|
95
|
+
"StreamingResponse",
|
|
96
|
+
"TestClientMixin",
|
|
97
|
+
"TimeoutError",
|
|
98
|
+
"TransportMetadata",
|
|
99
|
+
"Undefined",
|
|
100
|
+
"ValidationError",
|
|
101
|
+
"api_model_strategy",
|
|
102
|
+
"enhanced_strategy_for_type",
|
|
103
|
+
"none",
|
|
104
|
+
"serialize_option_dict",
|
|
105
|
+
"some",
|
|
106
|
+
"strategy_for_pydantic_model",
|
|
107
|
+
"strategy_for_type",
|
|
108
|
+
"undefined",
|
|
109
|
+
]
|
|
@@ -0,0 +1,507 @@
|
|
|
1
|
+
"""Authentication handlers for ReflectAPI Python clients."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import asyncio
|
|
6
|
+
import time
|
|
7
|
+
from abc import ABC, abstractmethod
|
|
8
|
+
from dataclasses import dataclass
|
|
9
|
+
from typing import Any, Callable
|
|
10
|
+
|
|
11
|
+
import httpx
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@dataclass
|
|
15
|
+
class AuthToken:
|
|
16
|
+
"""Represents an authentication token with metadata."""
|
|
17
|
+
access_token: str
|
|
18
|
+
token_type: str = "Bearer"
|
|
19
|
+
expires_in: int | None = None
|
|
20
|
+
refresh_token: str | None = None
|
|
21
|
+
scope: str | None = None
|
|
22
|
+
|
|
23
|
+
# Internal tracking
|
|
24
|
+
_created_at: float | None = None
|
|
25
|
+
|
|
26
|
+
def __post_init__(self):
|
|
27
|
+
"""Initialize creation timestamp."""
|
|
28
|
+
if self._created_at is None:
|
|
29
|
+
self._created_at = time.time()
|
|
30
|
+
|
|
31
|
+
@property
|
|
32
|
+
def is_expired(self) -> bool:
|
|
33
|
+
"""Check if the token is expired."""
|
|
34
|
+
if self.expires_in is None:
|
|
35
|
+
return False
|
|
36
|
+
|
|
37
|
+
elapsed = time.time() - self._created_at
|
|
38
|
+
# Add 60 second buffer for clock skew and network latency
|
|
39
|
+
return elapsed >= (self.expires_in - 60)
|
|
40
|
+
|
|
41
|
+
@property
|
|
42
|
+
def expires_at(self) -> float | None:
|
|
43
|
+
"""Get the absolute expiration timestamp."""
|
|
44
|
+
if self.expires_in is None:
|
|
45
|
+
return None
|
|
46
|
+
return self._created_at + self.expires_in
|
|
47
|
+
|
|
48
|
+
def to_header_value(self) -> str:
|
|
49
|
+
"""Generate the Authorization header value."""
|
|
50
|
+
return f"{self.token_type} {self.access_token}"
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
class AuthHandler(httpx.Auth):
|
|
54
|
+
"""Base class for authentication handlers that integrates directly with httpx."""
|
|
55
|
+
|
|
56
|
+
@abstractmethod
|
|
57
|
+
def apply_auth(self, request: httpx.Request) -> httpx.Request:
|
|
58
|
+
"""Apply authentication to a synchronous request."""
|
|
59
|
+
pass
|
|
60
|
+
|
|
61
|
+
@abstractmethod
|
|
62
|
+
async def apply_auth_async(self, request: httpx.Request) -> httpx.Request:
|
|
63
|
+
"""Apply authentication to an asynchronous request."""
|
|
64
|
+
pass
|
|
65
|
+
|
|
66
|
+
def auth_flow(self, request: httpx.Request):
|
|
67
|
+
"""httpx sync auth flow implementation."""
|
|
68
|
+
yield self.apply_auth(request)
|
|
69
|
+
|
|
70
|
+
async def async_auth_flow(self, request: httpx.Request):
|
|
71
|
+
"""httpx async auth flow implementation."""
|
|
72
|
+
yield await self.apply_auth_async(request)
|
|
73
|
+
|
|
74
|
+
def __call__(self, request: httpx.Request) -> httpx.Request:
|
|
75
|
+
"""Allow handler to be used as a callable for httpx auth parameter."""
|
|
76
|
+
return self.apply_auth(request)
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
class BearerTokenAuth(AuthHandler):
|
|
80
|
+
"""Simple Bearer token authentication handler."""
|
|
81
|
+
|
|
82
|
+
def __init__(self, token: str):
|
|
83
|
+
"""Initialize with a bearer token.
|
|
84
|
+
|
|
85
|
+
Args:
|
|
86
|
+
token: The bearer token string
|
|
87
|
+
"""
|
|
88
|
+
self.token = token
|
|
89
|
+
|
|
90
|
+
def apply_auth(self, request: httpx.Request) -> httpx.Request:
|
|
91
|
+
"""Apply Bearer token to request headers."""
|
|
92
|
+
request.headers["Authorization"] = f"Bearer {self.token}"
|
|
93
|
+
return request
|
|
94
|
+
|
|
95
|
+
async def apply_auth_async(self, request: httpx.Request) -> httpx.Request:
|
|
96
|
+
"""Apply Bearer token to async request headers."""
|
|
97
|
+
return self.apply_auth(request)
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
class APIKeyAuth(AuthHandler):
|
|
101
|
+
"""API Key authentication handler with flexible placement options."""
|
|
102
|
+
|
|
103
|
+
def __init__(self, api_key: str, header_name: str = "X-API-Key", param_name: str | None = None):
|
|
104
|
+
"""Initialize with API key and placement options.
|
|
105
|
+
|
|
106
|
+
Args:
|
|
107
|
+
api_key: The API key string
|
|
108
|
+
header_name: Header name for the API key (default: X-API-Key)
|
|
109
|
+
param_name: If provided, add API key as query parameter instead of header
|
|
110
|
+
"""
|
|
111
|
+
self.api_key = api_key
|
|
112
|
+
self.header_name = header_name
|
|
113
|
+
self.param_name = param_name
|
|
114
|
+
|
|
115
|
+
def apply_auth(self, request: httpx.Request) -> httpx.Request:
|
|
116
|
+
"""Apply API key to request."""
|
|
117
|
+
if self.param_name:
|
|
118
|
+
# Add as query parameter
|
|
119
|
+
url = request.url
|
|
120
|
+
params = dict(url.params)
|
|
121
|
+
params[self.param_name] = self.api_key
|
|
122
|
+
request.url = url.copy_with(params=params)
|
|
123
|
+
else:
|
|
124
|
+
# Add as header
|
|
125
|
+
request.headers[self.header_name] = self.api_key
|
|
126
|
+
return request
|
|
127
|
+
|
|
128
|
+
async def apply_auth_async(self, request: httpx.Request) -> httpx.Request:
|
|
129
|
+
"""Apply API key to async request."""
|
|
130
|
+
return self.apply_auth(request)
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
class BasicAuth(AuthHandler):
|
|
134
|
+
"""HTTP Basic authentication handler."""
|
|
135
|
+
|
|
136
|
+
def __init__(self, username: str, password: str):
|
|
137
|
+
"""Initialize with username and password.
|
|
138
|
+
|
|
139
|
+
Args:
|
|
140
|
+
username: Username for basic auth
|
|
141
|
+
password: Password for basic auth
|
|
142
|
+
"""
|
|
143
|
+
self.auth = httpx.BasicAuth(username, password)
|
|
144
|
+
|
|
145
|
+
def apply_auth(self, request: httpx.Request) -> httpx.Request:
|
|
146
|
+
"""Apply basic authentication."""
|
|
147
|
+
# httpx.BasicAuth.auth_flow returns a generator, so we need to get the first (and only) result
|
|
148
|
+
auth_flow = self.auth.auth_flow(request)
|
|
149
|
+
return next(auth_flow)
|
|
150
|
+
|
|
151
|
+
async def apply_auth_async(self, request: httpx.Request) -> httpx.Request:
|
|
152
|
+
"""Apply basic authentication to async request."""
|
|
153
|
+
return self.apply_auth(request)
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
class OAuth2ClientCredentialsAuth(AuthHandler):
|
|
157
|
+
"""OAuth2 Client Credentials flow authentication handler."""
|
|
158
|
+
|
|
159
|
+
def __init__(
|
|
160
|
+
self,
|
|
161
|
+
token_url: str,
|
|
162
|
+
client_id: str,
|
|
163
|
+
client_secret: str,
|
|
164
|
+
scope: str | None = None,
|
|
165
|
+
client: httpx.Client | httpx.AsyncClient | None = None
|
|
166
|
+
):
|
|
167
|
+
"""Initialize OAuth2 client credentials authentication.
|
|
168
|
+
|
|
169
|
+
Args:
|
|
170
|
+
token_url: OAuth2 token endpoint URL
|
|
171
|
+
client_id: OAuth2 client ID
|
|
172
|
+
client_secret: OAuth2 client secret
|
|
173
|
+
scope: Optional scope for the token request
|
|
174
|
+
client: Optional HTTP client for token requests (will create if not provided)
|
|
175
|
+
"""
|
|
176
|
+
self.token_url = token_url
|
|
177
|
+
self.client_id = client_id
|
|
178
|
+
self.client_secret = client_secret
|
|
179
|
+
self.scope = scope
|
|
180
|
+
|
|
181
|
+
# Token storage
|
|
182
|
+
self._token: AuthToken | None = None
|
|
183
|
+
self._token_lock = asyncio.Lock()
|
|
184
|
+
|
|
185
|
+
# HTTP clients for token requests
|
|
186
|
+
self._sync_client = client if isinstance(client, httpx.Client) else httpx.Client()
|
|
187
|
+
self._async_client = client if isinstance(client, httpx.AsyncClient) else httpx.AsyncClient()
|
|
188
|
+
self._owns_clients = client is None
|
|
189
|
+
|
|
190
|
+
def _prepare_token_request(self) -> dict[str, Any]:
|
|
191
|
+
"""Prepare the token request data."""
|
|
192
|
+
data = {
|
|
193
|
+
"grant_type": "client_credentials",
|
|
194
|
+
"client_id": self.client_id,
|
|
195
|
+
"client_secret": self.client_secret,
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
if self.scope:
|
|
199
|
+
data["scope"] = self.scope
|
|
200
|
+
|
|
201
|
+
return data
|
|
202
|
+
|
|
203
|
+
def _get_valid_token_sync(self) -> AuthToken:
|
|
204
|
+
"""Get a valid token, refreshing if necessary (synchronous)."""
|
|
205
|
+
if self._token and not self._token.is_expired:
|
|
206
|
+
return self._token
|
|
207
|
+
|
|
208
|
+
# Request new token
|
|
209
|
+
data = self._prepare_token_request()
|
|
210
|
+
response = self._sync_client.post(
|
|
211
|
+
self.token_url,
|
|
212
|
+
data=data,
|
|
213
|
+
headers={"Content-Type": "application/x-www-form-urlencoded"}
|
|
214
|
+
)
|
|
215
|
+
response.raise_for_status()
|
|
216
|
+
|
|
217
|
+
token_data = response.json()
|
|
218
|
+
self._token = AuthToken(
|
|
219
|
+
access_token=token_data["access_token"],
|
|
220
|
+
token_type=token_data.get("token_type", "Bearer"),
|
|
221
|
+
expires_in=token_data.get("expires_in"),
|
|
222
|
+
refresh_token=token_data.get("refresh_token"),
|
|
223
|
+
scope=token_data.get("scope")
|
|
224
|
+
)
|
|
225
|
+
|
|
226
|
+
return self._token
|
|
227
|
+
|
|
228
|
+
async def _get_valid_token_async(self) -> AuthToken:
|
|
229
|
+
"""Get a valid token, refreshing if necessary (asynchronous)."""
|
|
230
|
+
async with self._token_lock:
|
|
231
|
+
if self._token and not self._token.is_expired:
|
|
232
|
+
return self._token
|
|
233
|
+
|
|
234
|
+
# Request new token
|
|
235
|
+
data = self._prepare_token_request()
|
|
236
|
+
response = await self._async_client.post(
|
|
237
|
+
self.token_url,
|
|
238
|
+
data=data,
|
|
239
|
+
headers={"Content-Type": "application/x-www-form-urlencoded"}
|
|
240
|
+
)
|
|
241
|
+
response.raise_for_status()
|
|
242
|
+
|
|
243
|
+
token_data = response.json()
|
|
244
|
+
self._token = AuthToken(
|
|
245
|
+
access_token=token_data["access_token"],
|
|
246
|
+
token_type=token_data.get("token_type", "Bearer"),
|
|
247
|
+
expires_in=token_data.get("expires_in"),
|
|
248
|
+
refresh_token=token_data.get("refresh_token"),
|
|
249
|
+
scope=token_data.get("scope")
|
|
250
|
+
)
|
|
251
|
+
|
|
252
|
+
return self._token
|
|
253
|
+
|
|
254
|
+
def apply_auth(self, request: httpx.Request) -> httpx.Request:
|
|
255
|
+
"""Apply OAuth2 authentication to synchronous request."""
|
|
256
|
+
token = self._get_valid_token_sync()
|
|
257
|
+
request.headers["Authorization"] = token.to_header_value()
|
|
258
|
+
return request
|
|
259
|
+
|
|
260
|
+
async def apply_auth_async(self, request: httpx.Request) -> httpx.Request:
|
|
261
|
+
"""Apply OAuth2 authentication to asynchronous request."""
|
|
262
|
+
token = await self._get_valid_token_async()
|
|
263
|
+
request.headers["Authorization"] = token.to_header_value()
|
|
264
|
+
return request
|
|
265
|
+
|
|
266
|
+
def close(self) -> None:
|
|
267
|
+
"""Close HTTP clients if we own them."""
|
|
268
|
+
if self._owns_clients:
|
|
269
|
+
self._sync_client.close()
|
|
270
|
+
|
|
271
|
+
async def aclose(self) -> None:
|
|
272
|
+
"""Close async HTTP clients if we own them."""
|
|
273
|
+
if self._owns_clients:
|
|
274
|
+
await self._async_client.aclose()
|
|
275
|
+
|
|
276
|
+
|
|
277
|
+
class OAuth2AuthorizationCodeAuth(AuthHandler):
|
|
278
|
+
"""OAuth2 Authorization Code flow authentication handler."""
|
|
279
|
+
|
|
280
|
+
def __init__(
|
|
281
|
+
self,
|
|
282
|
+
access_token: str,
|
|
283
|
+
refresh_token: str | None = None,
|
|
284
|
+
token_url: str | None = None,
|
|
285
|
+
client_id: str | None = None,
|
|
286
|
+
client_secret: str | None = None,
|
|
287
|
+
expires_in: int | None = None,
|
|
288
|
+
client: httpx.Client | httpx.AsyncClient | None = None
|
|
289
|
+
):
|
|
290
|
+
"""Initialize OAuth2 authorization code authentication.
|
|
291
|
+
|
|
292
|
+
Args:
|
|
293
|
+
access_token: Initial access token
|
|
294
|
+
refresh_token: Refresh token for token renewal
|
|
295
|
+
token_url: Token endpoint URL (required if refresh_token provided)
|
|
296
|
+
client_id: OAuth2 client ID (required if refresh_token provided)
|
|
297
|
+
client_secret: OAuth2 client secret (required if refresh_token provided)
|
|
298
|
+
expires_in: Token expiration time in seconds
|
|
299
|
+
client: Optional HTTP client for token refresh requests
|
|
300
|
+
"""
|
|
301
|
+
self._token = AuthToken(
|
|
302
|
+
access_token=access_token,
|
|
303
|
+
refresh_token=refresh_token,
|
|
304
|
+
expires_in=expires_in
|
|
305
|
+
)
|
|
306
|
+
|
|
307
|
+
self.token_url = token_url
|
|
308
|
+
self.client_id = client_id
|
|
309
|
+
self.client_secret = client_secret
|
|
310
|
+
self._token_lock = asyncio.Lock()
|
|
311
|
+
|
|
312
|
+
# Validate refresh configuration
|
|
313
|
+
if refresh_token and not all([token_url, client_id, client_secret]):
|
|
314
|
+
raise ValueError(
|
|
315
|
+
"token_url, client_id, and client_secret are required when refresh_token is provided"
|
|
316
|
+
)
|
|
317
|
+
|
|
318
|
+
# HTTP clients for token refresh
|
|
319
|
+
self._sync_client = client if isinstance(client, httpx.Client) else httpx.Client()
|
|
320
|
+
self._async_client = client if isinstance(client, httpx.AsyncClient) else httpx.AsyncClient()
|
|
321
|
+
self._owns_clients = client is None
|
|
322
|
+
|
|
323
|
+
def _refresh_token_sync(self) -> AuthToken:
|
|
324
|
+
"""Refresh the access token (synchronous)."""
|
|
325
|
+
if not self._token.refresh_token:
|
|
326
|
+
raise RuntimeError("No refresh token available")
|
|
327
|
+
|
|
328
|
+
data = {
|
|
329
|
+
"grant_type": "refresh_token",
|
|
330
|
+
"refresh_token": self._token.refresh_token,
|
|
331
|
+
"client_id": self.client_id,
|
|
332
|
+
"client_secret": self.client_secret,
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
response = self._sync_client.post(
|
|
336
|
+
self.token_url,
|
|
337
|
+
data=data,
|
|
338
|
+
headers={"Content-Type": "application/x-www-form-urlencoded"}
|
|
339
|
+
)
|
|
340
|
+
response.raise_for_status()
|
|
341
|
+
|
|
342
|
+
token_data = response.json()
|
|
343
|
+
self._token = AuthToken(
|
|
344
|
+
access_token=token_data["access_token"],
|
|
345
|
+
token_type=token_data.get("token_type", "Bearer"),
|
|
346
|
+
expires_in=token_data.get("expires_in"),
|
|
347
|
+
refresh_token=token_data.get("refresh_token", self._token.refresh_token),
|
|
348
|
+
scope=token_data.get("scope")
|
|
349
|
+
)
|
|
350
|
+
|
|
351
|
+
return self._token
|
|
352
|
+
|
|
353
|
+
async def _refresh_token_async(self) -> AuthToken:
|
|
354
|
+
"""Refresh the access token (asynchronous)."""
|
|
355
|
+
if not self._token.refresh_token:
|
|
356
|
+
raise RuntimeError("No refresh token available")
|
|
357
|
+
|
|
358
|
+
data = {
|
|
359
|
+
"grant_type": "refresh_token",
|
|
360
|
+
"refresh_token": self._token.refresh_token,
|
|
361
|
+
"client_id": self.client_id,
|
|
362
|
+
"client_secret": self.client_secret,
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
response = await self._async_client.post(
|
|
366
|
+
self.token_url,
|
|
367
|
+
data=data,
|
|
368
|
+
headers={"Content-Type": "application/x-www-form-urlencoded"}
|
|
369
|
+
)
|
|
370
|
+
response.raise_for_status()
|
|
371
|
+
|
|
372
|
+
token_data = response.json()
|
|
373
|
+
self._token = AuthToken(
|
|
374
|
+
access_token=token_data["access_token"],
|
|
375
|
+
token_type=token_data.get("token_type", "Bearer"),
|
|
376
|
+
expires_in=token_data.get("expires_in"),
|
|
377
|
+
refresh_token=token_data.get("refresh_token", self._token.refresh_token),
|
|
378
|
+
scope=token_data.get("scope")
|
|
379
|
+
)
|
|
380
|
+
|
|
381
|
+
return self._token
|
|
382
|
+
|
|
383
|
+
def _get_valid_token_sync(self) -> AuthToken:
|
|
384
|
+
"""Get a valid token, refreshing if necessary (synchronous)."""
|
|
385
|
+
if not self._token.is_expired:
|
|
386
|
+
return self._token
|
|
387
|
+
|
|
388
|
+
if self._token.refresh_token:
|
|
389
|
+
return self._refresh_token_sync()
|
|
390
|
+
|
|
391
|
+
raise RuntimeError("Access token is expired and no refresh token available")
|
|
392
|
+
|
|
393
|
+
async def _get_valid_token_async(self) -> AuthToken:
|
|
394
|
+
"""Get a valid token, refreshing if necessary (asynchronous)."""
|
|
395
|
+
async with self._token_lock:
|
|
396
|
+
if not self._token.is_expired:
|
|
397
|
+
return self._token
|
|
398
|
+
|
|
399
|
+
if self._token.refresh_token:
|
|
400
|
+
return await self._refresh_token_async()
|
|
401
|
+
|
|
402
|
+
raise RuntimeError("Access token is expired and no refresh token available")
|
|
403
|
+
|
|
404
|
+
def apply_auth(self, request: httpx.Request) -> httpx.Request:
|
|
405
|
+
"""Apply OAuth2 authentication to synchronous request."""
|
|
406
|
+
token = self._get_valid_token_sync()
|
|
407
|
+
request.headers["Authorization"] = token.to_header_value()
|
|
408
|
+
return request
|
|
409
|
+
|
|
410
|
+
async def apply_auth_async(self, request: httpx.Request) -> httpx.Request:
|
|
411
|
+
"""Apply OAuth2 authentication to asynchronous request."""
|
|
412
|
+
token = await self._get_valid_token_async()
|
|
413
|
+
request.headers["Authorization"] = token.to_header_value()
|
|
414
|
+
return request
|
|
415
|
+
|
|
416
|
+
@property
|
|
417
|
+
def access_token(self) -> str:
|
|
418
|
+
"""Get the current access token."""
|
|
419
|
+
return self._token.access_token
|
|
420
|
+
|
|
421
|
+
@property
|
|
422
|
+
def is_expired(self) -> bool:
|
|
423
|
+
"""Check if the current token is expired."""
|
|
424
|
+
return self._token.is_expired
|
|
425
|
+
|
|
426
|
+
def close(self) -> None:
|
|
427
|
+
"""Close HTTP clients if we own them."""
|
|
428
|
+
if self._owns_clients:
|
|
429
|
+
self._sync_client.close()
|
|
430
|
+
|
|
431
|
+
async def aclose(self) -> None:
|
|
432
|
+
"""Close async HTTP clients if we own them."""
|
|
433
|
+
if self._owns_clients:
|
|
434
|
+
await self._async_client.aclose()
|
|
435
|
+
|
|
436
|
+
|
|
437
|
+
class CustomAuth(AuthHandler):
|
|
438
|
+
"""Custom authentication handler using user-provided functions."""
|
|
439
|
+
|
|
440
|
+
def __init__(
|
|
441
|
+
self,
|
|
442
|
+
sync_auth_func: Callable[[httpx.Request], httpx.Request] | None = None,
|
|
443
|
+
async_auth_func: Callable[[httpx.Request], httpx.Request] | None = None
|
|
444
|
+
):
|
|
445
|
+
"""Initialize with custom authentication functions.
|
|
446
|
+
|
|
447
|
+
Args:
|
|
448
|
+
sync_auth_func: Synchronous function that takes httpx.Request and returns httpx.Request
|
|
449
|
+
async_auth_func: Asynchronous function that takes httpx.Request and returns httpx.Request
|
|
450
|
+
"""
|
|
451
|
+
if not sync_auth_func and not async_auth_func:
|
|
452
|
+
raise ValueError("At least one of sync_auth_func or async_auth_func must be provided")
|
|
453
|
+
|
|
454
|
+
self.sync_auth_func = sync_auth_func
|
|
455
|
+
self.async_auth_func = async_auth_func
|
|
456
|
+
|
|
457
|
+
def apply_auth(self, request: httpx.Request) -> httpx.Request:
|
|
458
|
+
"""Apply custom synchronous authentication."""
|
|
459
|
+
if not self.sync_auth_func:
|
|
460
|
+
raise RuntimeError("No synchronous auth function provided")
|
|
461
|
+
return self.sync_auth_func(request)
|
|
462
|
+
|
|
463
|
+
async def apply_auth_async(self, request: httpx.Request) -> httpx.Request:
|
|
464
|
+
"""Apply custom asynchronous authentication."""
|
|
465
|
+
if not self.async_auth_func:
|
|
466
|
+
raise RuntimeError("No asynchronous auth function provided")
|
|
467
|
+
return await self.async_auth_func(request)
|
|
468
|
+
|
|
469
|
+
|
|
470
|
+
# Convenience factory functions
|
|
471
|
+
def bearer_token(token: str) -> BearerTokenAuth:
|
|
472
|
+
"""Create a Bearer token authentication handler."""
|
|
473
|
+
return BearerTokenAuth(token)
|
|
474
|
+
|
|
475
|
+
|
|
476
|
+
def api_key(key: str, header_name: str = "X-API-Key", param_name: str | None = None) -> APIKeyAuth:
|
|
477
|
+
"""Create an API key authentication handler."""
|
|
478
|
+
return APIKeyAuth(key, header_name, param_name)
|
|
479
|
+
|
|
480
|
+
|
|
481
|
+
def basic_auth(username: str, password: str) -> BasicAuth:
|
|
482
|
+
"""Create a Basic authentication handler."""
|
|
483
|
+
return BasicAuth(username, password)
|
|
484
|
+
|
|
485
|
+
|
|
486
|
+
def oauth2_client_credentials(
|
|
487
|
+
token_url: str,
|
|
488
|
+
client_id: str,
|
|
489
|
+
client_secret: str,
|
|
490
|
+
scope: str | None = None
|
|
491
|
+
) -> OAuth2ClientCredentialsAuth:
|
|
492
|
+
"""Create an OAuth2 client credentials authentication handler."""
|
|
493
|
+
return OAuth2ClientCredentialsAuth(token_url, client_id, client_secret, scope)
|
|
494
|
+
|
|
495
|
+
|
|
496
|
+
def oauth2_authorization_code(
|
|
497
|
+
access_token: str,
|
|
498
|
+
refresh_token: str | None = None,
|
|
499
|
+
token_url: str | None = None,
|
|
500
|
+
client_id: str | None = None,
|
|
501
|
+
client_secret: str | None = None,
|
|
502
|
+
expires_in: int | None = None
|
|
503
|
+
) -> OAuth2AuthorizationCodeAuth:
|
|
504
|
+
"""Create an OAuth2 authorization code authentication handler."""
|
|
505
|
+
return OAuth2AuthorizationCodeAuth(
|
|
506
|
+
access_token, refresh_token, token_url, client_id, client_secret, expires_in
|
|
507
|
+
)
|