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.
@@ -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
+ )