sdkrouter 0.1.1__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.
- sdkrouter/__init__.py +110 -0
- sdkrouter/_api/__init__.py +28 -0
- sdkrouter/_api/client.py +204 -0
- sdkrouter/_api/generated/__init__.py +21 -0
- sdkrouter/_api/generated/cdn/__init__.py +209 -0
- sdkrouter/_api/generated/cdn/cdn__api__cdn/__init__.py +7 -0
- sdkrouter/_api/generated/cdn/cdn__api__cdn/client.py +133 -0
- sdkrouter/_api/generated/cdn/cdn__api__cdn/models.py +163 -0
- sdkrouter/_api/generated/cdn/cdn__api__cdn/sync_client.py +132 -0
- sdkrouter/_api/generated/cdn/client.py +75 -0
- sdkrouter/_api/generated/cdn/logger.py +256 -0
- sdkrouter/_api/generated/cdn/pyproject.toml +55 -0
- sdkrouter/_api/generated/cdn/retry.py +272 -0
- sdkrouter/_api/generated/cdn/sync_client.py +58 -0
- sdkrouter/_api/generated/cleaner/__init__.py +212 -0
- sdkrouter/_api/generated/cleaner/cleaner__api__cleaner/__init__.py +7 -0
- sdkrouter/_api/generated/cleaner/cleaner__api__cleaner/client.py +83 -0
- sdkrouter/_api/generated/cleaner/cleaner__api__cleaner/models.py +117 -0
- sdkrouter/_api/generated/cleaner/cleaner__api__cleaner/sync_client.py +82 -0
- sdkrouter/_api/generated/cleaner/client.py +75 -0
- sdkrouter/_api/generated/cleaner/enums.py +55 -0
- sdkrouter/_api/generated/cleaner/logger.py +256 -0
- sdkrouter/_api/generated/cleaner/pyproject.toml +55 -0
- sdkrouter/_api/generated/cleaner/retry.py +272 -0
- sdkrouter/_api/generated/cleaner/sync_client.py +58 -0
- sdkrouter/_api/generated/keys/__init__.py +212 -0
- sdkrouter/_api/generated/keys/client.py +75 -0
- sdkrouter/_api/generated/keys/enums.py +64 -0
- sdkrouter/_api/generated/keys/keys__api__keys/__init__.py +7 -0
- sdkrouter/_api/generated/keys/keys__api__keys/client.py +150 -0
- sdkrouter/_api/generated/keys/keys__api__keys/models.py +152 -0
- sdkrouter/_api/generated/keys/keys__api__keys/sync_client.py +149 -0
- sdkrouter/_api/generated/keys/logger.py +256 -0
- sdkrouter/_api/generated/keys/pyproject.toml +55 -0
- sdkrouter/_api/generated/keys/retry.py +272 -0
- sdkrouter/_api/generated/keys/sync_client.py +58 -0
- sdkrouter/_api/generated/models/__init__.py +209 -0
- sdkrouter/_api/generated/models/client.py +75 -0
- sdkrouter/_api/generated/models/logger.py +256 -0
- sdkrouter/_api/generated/models/models__api__llm_models/__init__.py +7 -0
- sdkrouter/_api/generated/models/models__api__llm_models/client.py +99 -0
- sdkrouter/_api/generated/models/models__api__llm_models/models.py +206 -0
- sdkrouter/_api/generated/models/models__api__llm_models/sync_client.py +99 -0
- sdkrouter/_api/generated/models/pyproject.toml +55 -0
- sdkrouter/_api/generated/models/retry.py +272 -0
- sdkrouter/_api/generated/models/sync_client.py +58 -0
- sdkrouter/_api/generated/shortlinks/__init__.py +209 -0
- sdkrouter/_api/generated/shortlinks/client.py +75 -0
- sdkrouter/_api/generated/shortlinks/logger.py +256 -0
- sdkrouter/_api/generated/shortlinks/pyproject.toml +55 -0
- sdkrouter/_api/generated/shortlinks/retry.py +272 -0
- sdkrouter/_api/generated/shortlinks/shortlinks__api__shortlinks/__init__.py +7 -0
- sdkrouter/_api/generated/shortlinks/shortlinks__api__shortlinks/client.py +137 -0
- sdkrouter/_api/generated/shortlinks/shortlinks__api__shortlinks/models.py +153 -0
- sdkrouter/_api/generated/shortlinks/shortlinks__api__shortlinks/sync_client.py +136 -0
- sdkrouter/_api/generated/shortlinks/sync_client.py +58 -0
- sdkrouter/_api/generated/vision/__init__.py +212 -0
- sdkrouter/_api/generated/vision/client.py +75 -0
- sdkrouter/_api/generated/vision/enums.py +40 -0
- sdkrouter/_api/generated/vision/logger.py +256 -0
- sdkrouter/_api/generated/vision/pyproject.toml +55 -0
- sdkrouter/_api/generated/vision/retry.py +272 -0
- sdkrouter/_api/generated/vision/sync_client.py +58 -0
- sdkrouter/_api/generated/vision/vision__api__vision/__init__.py +7 -0
- sdkrouter/_api/generated/vision/vision__api__vision/client.py +65 -0
- sdkrouter/_api/generated/vision/vision__api__vision/models.py +138 -0
- sdkrouter/_api/generated/vision/vision__api__vision/sync_client.py +65 -0
- sdkrouter/_client.py +432 -0
- sdkrouter/_config.py +74 -0
- sdkrouter/_constants.py +21 -0
- sdkrouter/_internal/__init__.py +1 -0
- sdkrouter/_types/__init__.py +30 -0
- sdkrouter/_types/cdn.py +27 -0
- sdkrouter/_types/models.py +26 -0
- sdkrouter/_types/ocr.py +24 -0
- sdkrouter/_types/parsed.py +101 -0
- sdkrouter/_types/shortlinks.py +27 -0
- sdkrouter/_types/vision.py +29 -0
- sdkrouter/_version.py +3 -0
- sdkrouter/helpers/__init__.py +13 -0
- sdkrouter/helpers/formatting.py +15 -0
- sdkrouter/helpers/html.py +100 -0
- sdkrouter/helpers/json_cleaner.py +53 -0
- sdkrouter/tools/__init__.py +129 -0
- sdkrouter/tools/cdn.py +285 -0
- sdkrouter/tools/cleaner.py +186 -0
- sdkrouter/tools/keys.py +215 -0
- sdkrouter/tools/models.py +196 -0
- sdkrouter/tools/shortlinks.py +165 -0
- sdkrouter/tools/vision.py +173 -0
- sdkrouter/utils/__init__.py +27 -0
- sdkrouter/utils/parsing.py +109 -0
- sdkrouter/utils/tokens.py +375 -0
- sdkrouter-0.1.1.dist-info/METADATA +411 -0
- sdkrouter-0.1.1.dist-info/RECORD +96 -0
- sdkrouter-0.1.1.dist-info/WHEEL +4 -0
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
# Auto-generated by DjangoCFG - see CLAUDE.md
|
|
2
|
+
[tool.poetry]
|
|
3
|
+
name = "api-client"
|
|
4
|
+
version = "1.0.0"
|
|
5
|
+
description = "Auto-generated Python client for SDKRouter API"
|
|
6
|
+
authors = ["Author \u003cauthor@example.com\u003e"]
|
|
7
|
+
license = "MIT"
|
|
8
|
+
readme = "README.md"
|
|
9
|
+
keywords = ["api", "client", "python", "openapi"]
|
|
10
|
+
classifiers = [
|
|
11
|
+
"Development Status :: 5 - Production/Stable",
|
|
12
|
+
"Intended Audience :: Developers",
|
|
13
|
+
"License :: OSI Approved :: MIT License",
|
|
14
|
+
"Programming Language :: Python :: 3",
|
|
15
|
+
"Programming Language :: Python :: 3.12",
|
|
16
|
+
"Typing :: Typed",
|
|
17
|
+
]
|
|
18
|
+
|
|
19
|
+
[tool.poetry.dependencies]
|
|
20
|
+
python = "^3.12"
|
|
21
|
+
pydantic = "^2.12"
|
|
22
|
+
httpx = "^0.28"
|
|
23
|
+
tenacity = "^9.1"
|
|
24
|
+
rich = "^14.1.0"
|
|
25
|
+
|
|
26
|
+
[tool.poetry.group.dev.dependencies]
|
|
27
|
+
pytest = "^8.0"
|
|
28
|
+
pytest-asyncio = "^0.24"
|
|
29
|
+
pytest-cov = "^6.0"
|
|
30
|
+
mypy = "^1.18"
|
|
31
|
+
ruff = "^0.13"
|
|
32
|
+
|
|
33
|
+
[build-system]
|
|
34
|
+
requires = ["poetry-core"]
|
|
35
|
+
build-backend = "poetry.core.masonry.api"
|
|
36
|
+
|
|
37
|
+
[tool.mypy]
|
|
38
|
+
python_version = "3.12"
|
|
39
|
+
strict = true
|
|
40
|
+
warn_return_any = true
|
|
41
|
+
warn_unused_configs = true
|
|
42
|
+
disallow_untyped_defs = true
|
|
43
|
+
|
|
44
|
+
[tool.ruff]
|
|
45
|
+
line-length = 100
|
|
46
|
+
target-version = "py312"
|
|
47
|
+
|
|
48
|
+
[tool.ruff.lint]
|
|
49
|
+
select = ["E", "F", "I", "N", "UP", "B"]
|
|
50
|
+
ignore = []
|
|
51
|
+
|
|
52
|
+
[tool.pytest.ini_options]
|
|
53
|
+
asyncio_mode = "auto"
|
|
54
|
+
testpaths = ["tests"]
|
|
55
|
+
addopts = "--cov=api_client --cov-report=html --cov-report=term"
|
|
@@ -0,0 +1,272 @@
|
|
|
1
|
+
# Auto-generated by DjangoCFG - see CLAUDE.md
|
|
2
|
+
"""
|
|
3
|
+
Retry Configuration and Utilities
|
|
4
|
+
|
|
5
|
+
Provides automatic retry logic for failed HTTP requests using tenacity.
|
|
6
|
+
Retries only on network errors and server errors (5xx), not client errors (4xx).
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
from dataclasses import dataclass, field
|
|
12
|
+
from typing import Callable, Any
|
|
13
|
+
import httpx
|
|
14
|
+
from tenacity import (
|
|
15
|
+
retry,
|
|
16
|
+
stop_after_attempt,
|
|
17
|
+
wait_exponential,
|
|
18
|
+
retry_if_exception,
|
|
19
|
+
RetryCallState,
|
|
20
|
+
before_sleep_log,
|
|
21
|
+
)
|
|
22
|
+
import logging
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
@dataclass
|
|
26
|
+
class RetryConfig:
|
|
27
|
+
"""
|
|
28
|
+
Retry configuration options.
|
|
29
|
+
|
|
30
|
+
Uses exponential backoff with jitter by default to avoid thundering herd.
|
|
31
|
+
"""
|
|
32
|
+
|
|
33
|
+
max_attempts: int = 3
|
|
34
|
+
"""Maximum number of retry attempts (default: 3)"""
|
|
35
|
+
|
|
36
|
+
min_wait: float = 1.0
|
|
37
|
+
"""Minimum wait time between retries in seconds (default: 1.0)"""
|
|
38
|
+
|
|
39
|
+
max_wait: float = 60.0
|
|
40
|
+
"""Maximum wait time between retries in seconds (default: 60.0)"""
|
|
41
|
+
|
|
42
|
+
multiplier: float = 2.0
|
|
43
|
+
"""Exponential backoff multiplier (default: 2.0)"""
|
|
44
|
+
|
|
45
|
+
on_retry: Callable[[RetryCallState], None] | None = None
|
|
46
|
+
"""Callback called on each retry attempt"""
|
|
47
|
+
|
|
48
|
+
logger: logging.Logger | None = None
|
|
49
|
+
"""Logger for retry attempts (default: None)"""
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
DEFAULT_RETRY_CONFIG = RetryConfig()
|
|
53
|
+
"""Default retry configuration"""
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def should_retry(exception: BaseException) -> bool:
|
|
57
|
+
"""
|
|
58
|
+
Determine if an error should trigger a retry.
|
|
59
|
+
|
|
60
|
+
Retries on:
|
|
61
|
+
- Network errors (connection refused, timeout, etc.)
|
|
62
|
+
- Server errors (5xx status codes)
|
|
63
|
+
- Rate limiting (429 status code)
|
|
64
|
+
|
|
65
|
+
Does NOT retry on:
|
|
66
|
+
- Client errors (4xx except 429)
|
|
67
|
+
- Authentication errors (401, 403)
|
|
68
|
+
- Not found (404)
|
|
69
|
+
|
|
70
|
+
Args:
|
|
71
|
+
exception: The exception to check
|
|
72
|
+
|
|
73
|
+
Returns:
|
|
74
|
+
True if should retry, False otherwise
|
|
75
|
+
"""
|
|
76
|
+
# Always retry network errors
|
|
77
|
+
if isinstance(exception, (
|
|
78
|
+
httpx.NetworkError,
|
|
79
|
+
httpx.TimeoutException,
|
|
80
|
+
httpx.ConnectError,
|
|
81
|
+
httpx.ReadError,
|
|
82
|
+
httpx.WriteError,
|
|
83
|
+
httpx.PoolTimeout,
|
|
84
|
+
)):
|
|
85
|
+
return True
|
|
86
|
+
|
|
87
|
+
# For HTTP errors, check status code
|
|
88
|
+
if isinstance(exception, httpx.HTTPStatusError):
|
|
89
|
+
status = exception.response.status_code
|
|
90
|
+
|
|
91
|
+
# Retry on 5xx server errors
|
|
92
|
+
if 500 <= status < 600:
|
|
93
|
+
return True
|
|
94
|
+
|
|
95
|
+
# Retry on 429 (rate limit)
|
|
96
|
+
if status == 429:
|
|
97
|
+
return True
|
|
98
|
+
|
|
99
|
+
# Do NOT retry on 4xx client errors
|
|
100
|
+
return False
|
|
101
|
+
|
|
102
|
+
# Don't retry on unknown errors
|
|
103
|
+
return False
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def create_retry_decorator(config: RetryConfig | None = None):
|
|
107
|
+
"""
|
|
108
|
+
Create a retry decorator with the given configuration.
|
|
109
|
+
|
|
110
|
+
Args:
|
|
111
|
+
config: Retry configuration (uses defaults if None)
|
|
112
|
+
|
|
113
|
+
Returns:
|
|
114
|
+
Tenacity retry decorator
|
|
115
|
+
|
|
116
|
+
Example:
|
|
117
|
+
>>> retry_decorator = create_retry_decorator(RetryConfig(max_attempts=5))
|
|
118
|
+
>>> @retry_decorator
|
|
119
|
+
... async def fetch_data():
|
|
120
|
+
... async with httpx.AsyncClient() as client:
|
|
121
|
+
... response = await client.get('https://api.example.com/users')
|
|
122
|
+
... response.raise_for_status()
|
|
123
|
+
... return response.json()
|
|
124
|
+
"""
|
|
125
|
+
cfg = config or DEFAULT_RETRY_CONFIG
|
|
126
|
+
|
|
127
|
+
# Build retry decorator
|
|
128
|
+
retry_args = {
|
|
129
|
+
'stop': stop_after_attempt(cfg.max_attempts),
|
|
130
|
+
'wait': wait_exponential(
|
|
131
|
+
multiplier=cfg.multiplier,
|
|
132
|
+
min=cfg.min_wait,
|
|
133
|
+
max=cfg.max_wait,
|
|
134
|
+
),
|
|
135
|
+
'retry': retry_if_exception(should_retry),
|
|
136
|
+
'reraise': True,
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
# Add logger if provided
|
|
140
|
+
if cfg.logger:
|
|
141
|
+
retry_args['before_sleep'] = before_sleep_log(cfg.logger, logging.WARNING)
|
|
142
|
+
|
|
143
|
+
# Add custom callback if provided
|
|
144
|
+
if cfg.on_retry:
|
|
145
|
+
original_before_sleep = retry_args.get('before_sleep')
|
|
146
|
+
|
|
147
|
+
def combined_before_sleep(retry_state: RetryCallState):
|
|
148
|
+
if original_before_sleep:
|
|
149
|
+
original_before_sleep(retry_state)
|
|
150
|
+
if cfg.on_retry:
|
|
151
|
+
cfg.on_retry(retry_state)
|
|
152
|
+
|
|
153
|
+
retry_args['before_sleep'] = combined_before_sleep
|
|
154
|
+
|
|
155
|
+
return retry(**retry_args)
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
async def with_retry(
|
|
159
|
+
fn: Callable[..., Any],
|
|
160
|
+
config: RetryConfig | None = None,
|
|
161
|
+
*args,
|
|
162
|
+
**kwargs
|
|
163
|
+
) -> Any:
|
|
164
|
+
"""
|
|
165
|
+
Execute an async function with retry logic.
|
|
166
|
+
|
|
167
|
+
Args:
|
|
168
|
+
fn: Async function to retry
|
|
169
|
+
config: Retry configuration (uses defaults if None)
|
|
170
|
+
*args: Positional arguments for fn
|
|
171
|
+
**kwargs: Keyword arguments for fn
|
|
172
|
+
|
|
173
|
+
Returns:
|
|
174
|
+
Result of the function
|
|
175
|
+
|
|
176
|
+
Example:
|
|
177
|
+
>>> async def fetch_users():
|
|
178
|
+
... async with httpx.AsyncClient() as client:
|
|
179
|
+
... response = await client.get('https://api.example.com/users')
|
|
180
|
+
... response.raise_for_status()
|
|
181
|
+
... return response.json()
|
|
182
|
+
>>>
|
|
183
|
+
>>> result = await with_retry(fetch_users, RetryConfig(max_attempts=5))
|
|
184
|
+
"""
|
|
185
|
+
retry_decorator = create_retry_decorator(config)
|
|
186
|
+
retryable_fn = retry_decorator(fn)
|
|
187
|
+
return await retryable_fn(*args, **kwargs)
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
class RetryAsyncClient:
|
|
191
|
+
"""
|
|
192
|
+
HTTP client wrapper that adds automatic retry logic.
|
|
193
|
+
|
|
194
|
+
Wraps httpx.AsyncClient and applies retry logic to all HTTP methods.
|
|
195
|
+
Transparently retries on network errors, 5xx status codes, and 429 rate limits.
|
|
196
|
+
|
|
197
|
+
Example:
|
|
198
|
+
>>> async with RetryAsyncClient('https://api.example.com', retry_config=RetryConfig(max_attempts=5)) as client:
|
|
199
|
+
... response = await client.get('/users')
|
|
200
|
+
... response.raise_for_status()
|
|
201
|
+
"""
|
|
202
|
+
|
|
203
|
+
def __init__(
|
|
204
|
+
self,
|
|
205
|
+
base_url: str | None = None,
|
|
206
|
+
retry_config: RetryConfig | None = None,
|
|
207
|
+
**kwargs: Any
|
|
208
|
+
):
|
|
209
|
+
"""
|
|
210
|
+
Initialize retry-enabled HTTP client.
|
|
211
|
+
|
|
212
|
+
Args:
|
|
213
|
+
base_url: Base URL for all requests
|
|
214
|
+
retry_config: Retry configuration (None to disable retry)
|
|
215
|
+
**kwargs: Additional httpx.AsyncClient kwargs
|
|
216
|
+
"""
|
|
217
|
+
self._client = httpx.AsyncClient(base_url=base_url, **kwargs)
|
|
218
|
+
self.retry_config = retry_config
|
|
219
|
+
self._retry_decorator = create_retry_decorator(retry_config) if retry_config else None
|
|
220
|
+
|
|
221
|
+
async def __aenter__(self) -> 'RetryAsyncClient':
|
|
222
|
+
await self._client.__aenter__()
|
|
223
|
+
return self
|
|
224
|
+
|
|
225
|
+
async def __aexit__(self, *args: Any) -> None:
|
|
226
|
+
await self._client.__aexit__(*args)
|
|
227
|
+
|
|
228
|
+
async def aclose(self) -> None:
|
|
229
|
+
"""Close the HTTP client."""
|
|
230
|
+
await self._client.aclose()
|
|
231
|
+
|
|
232
|
+
def _wrap_with_retry(self, method: str):
|
|
233
|
+
"""Wrap HTTP method with retry logic."""
|
|
234
|
+
original_method = getattr(self._client, method)
|
|
235
|
+
|
|
236
|
+
if self._retry_decorator:
|
|
237
|
+
async def wrapped(*args, **kwargs):
|
|
238
|
+
@self._retry_decorator
|
|
239
|
+
async def _do_request():
|
|
240
|
+
return await original_method(*args, **kwargs)
|
|
241
|
+
return await _do_request()
|
|
242
|
+
return wrapped
|
|
243
|
+
else:
|
|
244
|
+
return original_method
|
|
245
|
+
|
|
246
|
+
async def get(self, *args, **kwargs) -> httpx.Response:
|
|
247
|
+
"""GET request with retry."""
|
|
248
|
+
return await self._wrap_with_retry('get')(*args, **kwargs)
|
|
249
|
+
|
|
250
|
+
async def post(self, *args, **kwargs) -> httpx.Response:
|
|
251
|
+
"""POST request with retry."""
|
|
252
|
+
return await self._wrap_with_retry('post')(*args, **kwargs)
|
|
253
|
+
|
|
254
|
+
async def put(self, *args, **kwargs) -> httpx.Response:
|
|
255
|
+
"""PUT request with retry."""
|
|
256
|
+
return await self._wrap_with_retry('put')(*args, **kwargs)
|
|
257
|
+
|
|
258
|
+
async def patch(self, *args, **kwargs) -> httpx.Response:
|
|
259
|
+
"""PATCH request with retry."""
|
|
260
|
+
return await self._wrap_with_retry('patch')(*args, **kwargs)
|
|
261
|
+
|
|
262
|
+
async def delete(self, *args, **kwargs) -> httpx.Response:
|
|
263
|
+
"""DELETE request with retry."""
|
|
264
|
+
return await self._wrap_with_retry('delete')(*args, **kwargs)
|
|
265
|
+
|
|
266
|
+
async def head(self, *args, **kwargs) -> httpx.Response:
|
|
267
|
+
"""HEAD request with retry."""
|
|
268
|
+
return await self._wrap_with_retry('head')(*args, **kwargs)
|
|
269
|
+
|
|
270
|
+
async def options(self, *args, **kwargs) -> httpx.Response:
|
|
271
|
+
"""OPTIONS request with retry."""
|
|
272
|
+
return await self._wrap_with_retry('options')(*args, **kwargs)
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Any, Optional
|
|
4
|
+
|
|
5
|
+
import httpx
|
|
6
|
+
|
|
7
|
+
from .models__api__llm_models.sync_client import SyncModelsLlmModelsAPI
|
|
8
|
+
from .logger import APILogger, LoggerConfig
|
|
9
|
+
from .retry import RetryConfig, RetryAsyncClient
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class SyncAPIClient:
|
|
13
|
+
"""
|
|
14
|
+
Synchronous API client for SDKRouter API.
|
|
15
|
+
|
|
16
|
+
Usage:
|
|
17
|
+
>>> with SyncAPIClient(base_url='https://api.example.com') as client:
|
|
18
|
+
... users = client.users.list()
|
|
19
|
+
... post = client.posts.create(data=new_post)
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
def __init__(
|
|
23
|
+
self,
|
|
24
|
+
base_url: str,
|
|
25
|
+
logger_config: Optional[LoggerConfig] = None,
|
|
26
|
+
**kwargs: Any,
|
|
27
|
+
):
|
|
28
|
+
"""
|
|
29
|
+
Initialize sync API client.
|
|
30
|
+
|
|
31
|
+
Args:
|
|
32
|
+
base_url: Base API URL (e.g., 'https://api.example.com')
|
|
33
|
+
logger_config: Logger configuration (None to disable logging)
|
|
34
|
+
**kwargs: Additional httpx.Client kwargs
|
|
35
|
+
"""
|
|
36
|
+
self.base_url = base_url.rstrip('/')
|
|
37
|
+
self._client = httpx.Client(
|
|
38
|
+
base_url=self.base_url,
|
|
39
|
+
**kwargs,
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
# Initialize logger
|
|
43
|
+
self.logger: Optional[APILogger] = None
|
|
44
|
+
if logger_config is not None:
|
|
45
|
+
self.logger = APILogger(logger_config)
|
|
46
|
+
|
|
47
|
+
# Initialize sub-clients
|
|
48
|
+
self.models_llm_models = SyncModelsLlmModelsAPI(self._client)
|
|
49
|
+
|
|
50
|
+
def __enter__(self) -> 'SyncAPIClient':
|
|
51
|
+
return self
|
|
52
|
+
|
|
53
|
+
def __exit__(self, *args: Any) -> None:
|
|
54
|
+
self._client.close()
|
|
55
|
+
|
|
56
|
+
def close(self) -> None:
|
|
57
|
+
"""Close HTTP client."""
|
|
58
|
+
self._client.close()
|
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
# Auto-generated by DjangoCFG - see CLAUDE.md
|
|
2
|
+
"""
|
|
3
|
+
SDKRouter API - API Client with JWT Management
|
|
4
|
+
|
|
5
|
+
Usage:
|
|
6
|
+
>>> from api import API
|
|
7
|
+
>>>
|
|
8
|
+
>>> api = API('https://api.example.com')
|
|
9
|
+
>>>
|
|
10
|
+
>>> # Set JWT token
|
|
11
|
+
>>> api.set_token('your-jwt-token', 'refresh-token')
|
|
12
|
+
>>>
|
|
13
|
+
>>> # Use API
|
|
14
|
+
>>> async with api:
|
|
15
|
+
... posts = await api.posts.list()
|
|
16
|
+
... user = await api.users.retrieve(1)
|
|
17
|
+
>>>
|
|
18
|
+
>>> # Check authentication
|
|
19
|
+
>>> if api.is_authenticated():
|
|
20
|
+
... # ...
|
|
21
|
+
>>>
|
|
22
|
+
>>> # Get OpenAPI schema path
|
|
23
|
+
>>> schema_path = api.get_schema_path()
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
from __future__ import annotations
|
|
27
|
+
|
|
28
|
+
import threading
|
|
29
|
+
from typing import Any
|
|
30
|
+
|
|
31
|
+
import httpx
|
|
32
|
+
|
|
33
|
+
from .client import APIClient
|
|
34
|
+
from .logger import LoggerConfig
|
|
35
|
+
from .retry import RetryConfig
|
|
36
|
+
from .shortlinks__api__shortlinks import ShortlinksShortlinksAPI
|
|
37
|
+
|
|
38
|
+
TOKEN_KEY = "auth_token"
|
|
39
|
+
REFRESH_TOKEN_KEY = "refresh_token"
|
|
40
|
+
|
|
41
|
+
# Auto-generated by DjangoCFG - see CLAUDE.md
|
|
42
|
+
class API:
|
|
43
|
+
"""
|
|
44
|
+
API Client wrapper with JWT token management.
|
|
45
|
+
|
|
46
|
+
This class provides:
|
|
47
|
+
- Thread-safe JWT token storage
|
|
48
|
+
- Automatic Authorization header injection
|
|
49
|
+
- Context manager support for async operations
|
|
50
|
+
- Optional retry and logging configuration
|
|
51
|
+
|
|
52
|
+
Example:
|
|
53
|
+
>>> api = API('https://api.example.com')
|
|
54
|
+
>>> api.set_token('jwt-token')
|
|
55
|
+
>>> async with api:
|
|
56
|
+
... users = await api.users.list()
|
|
57
|
+
>>>
|
|
58
|
+
>>> # With retry and logging
|
|
59
|
+
>>> api = API(
|
|
60
|
+
... 'https://api.example.com',
|
|
61
|
+
... retry_config=RetryConfig(max_attempts=5),
|
|
62
|
+
... logger_config=LoggerConfig(enabled=True)
|
|
63
|
+
... )
|
|
64
|
+
"""
|
|
65
|
+
|
|
66
|
+
def __init__(
|
|
67
|
+
self,
|
|
68
|
+
base_url: str,
|
|
69
|
+
logger_config: LoggerConfig | None = None,
|
|
70
|
+
retry_config: RetryConfig | None = None,
|
|
71
|
+
**kwargs: Any
|
|
72
|
+
):
|
|
73
|
+
"""
|
|
74
|
+
Initialize API client.
|
|
75
|
+
|
|
76
|
+
Args:
|
|
77
|
+
base_url: Base API URL (e.g., 'https://api.example.com')
|
|
78
|
+
logger_config: Logger configuration (None to disable logging)
|
|
79
|
+
retry_config: Retry configuration (None to disable retry)
|
|
80
|
+
**kwargs: Additional httpx.AsyncClient kwargs
|
|
81
|
+
"""
|
|
82
|
+
self.base_url = base_url.rstrip('/')
|
|
83
|
+
self._kwargs = kwargs
|
|
84
|
+
self._logger_config = logger_config
|
|
85
|
+
self._retry_config = retry_config
|
|
86
|
+
self._token: str | None = None
|
|
87
|
+
self._refresh_token: str | None = None
|
|
88
|
+
self._lock = threading.Lock()
|
|
89
|
+
self._client: APIClient | None = None
|
|
90
|
+
self._init_clients()
|
|
91
|
+
|
|
92
|
+
def _init_clients(self) -> None:
|
|
93
|
+
"""Initialize API client with current token."""
|
|
94
|
+
# Create httpx client with auth header if token exists
|
|
95
|
+
headers = {}
|
|
96
|
+
if self._token:
|
|
97
|
+
headers['Authorization'] = f'Bearer {self._token}'
|
|
98
|
+
|
|
99
|
+
kwargs = {**self._kwargs}
|
|
100
|
+
if headers:
|
|
101
|
+
kwargs['headers'] = headers
|
|
102
|
+
|
|
103
|
+
# Create new APIClient
|
|
104
|
+
self._client = APIClient(
|
|
105
|
+
self.base_url,
|
|
106
|
+
logger_config=self._logger_config,
|
|
107
|
+
retry_config=self._retry_config,
|
|
108
|
+
**kwargs
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
@property
|
|
112
|
+
def shortlinks_shortlinks(self) -> ShortlinksShortlinksAPI:
|
|
113
|
+
"""Access shortlinks endpoints."""
|
|
114
|
+
return self._client.shortlinks_shortlinks
|
|
115
|
+
|
|
116
|
+
def get_token(self) -> str | None:
|
|
117
|
+
"""Get current JWT token."""
|
|
118
|
+
with self._lock:
|
|
119
|
+
return self._token
|
|
120
|
+
|
|
121
|
+
def get_refresh_token(self) -> str | None:
|
|
122
|
+
"""Get current refresh token."""
|
|
123
|
+
with self._lock:
|
|
124
|
+
return self._refresh_token
|
|
125
|
+
|
|
126
|
+
def set_token(self, token: str, refresh_token: str | None = None) -> None:
|
|
127
|
+
"""
|
|
128
|
+
Set JWT token and refresh token.
|
|
129
|
+
|
|
130
|
+
Args:
|
|
131
|
+
token: JWT access token
|
|
132
|
+
refresh_token: JWT refresh token (optional)
|
|
133
|
+
"""
|
|
134
|
+
with self._lock:
|
|
135
|
+
self._token = token
|
|
136
|
+
if refresh_token:
|
|
137
|
+
self._refresh_token = refresh_token
|
|
138
|
+
|
|
139
|
+
# Reinitialize clients with new token
|
|
140
|
+
self._init_clients()
|
|
141
|
+
|
|
142
|
+
def clear_tokens(self) -> None:
|
|
143
|
+
"""Clear all tokens."""
|
|
144
|
+
with self._lock:
|
|
145
|
+
self._token = None
|
|
146
|
+
self._refresh_token = None
|
|
147
|
+
|
|
148
|
+
# Reinitialize clients without token
|
|
149
|
+
self._init_clients()
|
|
150
|
+
|
|
151
|
+
def is_authenticated(self) -> bool:
|
|
152
|
+
"""Check if user is authenticated."""
|
|
153
|
+
return self.get_token() is not None
|
|
154
|
+
|
|
155
|
+
def set_base_url(self, url: str) -> None:
|
|
156
|
+
"""
|
|
157
|
+
Update base URL and reinitialize clients.
|
|
158
|
+
|
|
159
|
+
Args:
|
|
160
|
+
url: New base URL
|
|
161
|
+
"""
|
|
162
|
+
self.base_url = url.rstrip('/')
|
|
163
|
+
self._init_clients()
|
|
164
|
+
|
|
165
|
+
def get_base_url(self) -> str:
|
|
166
|
+
"""Get current base URL."""
|
|
167
|
+
return self.base_url
|
|
168
|
+
|
|
169
|
+
def get_schema_path(self) -> str:
|
|
170
|
+
"""
|
|
171
|
+
Get OpenAPI schema path.
|
|
172
|
+
|
|
173
|
+
Returns:
|
|
174
|
+
Path to the OpenAPI schema JSON file
|
|
175
|
+
|
|
176
|
+
Note:
|
|
177
|
+
The OpenAPI schema is available in the schema.json file.
|
|
178
|
+
You can load it dynamically using:
|
|
179
|
+
```python
|
|
180
|
+
import json
|
|
181
|
+
from pathlib import Path
|
|
182
|
+
|
|
183
|
+
schema_path = Path(__file__).parent / 'schema.json'
|
|
184
|
+
with open(schema_path) as f:
|
|
185
|
+
schema = json.load(f)
|
|
186
|
+
```
|
|
187
|
+
"""
|
|
188
|
+
return './schema.json'
|
|
189
|
+
|
|
190
|
+
async def __aenter__(self) -> 'API':
|
|
191
|
+
"""Async context manager entry."""
|
|
192
|
+
if self._client:
|
|
193
|
+
await self._client.__aenter__()
|
|
194
|
+
return self
|
|
195
|
+
|
|
196
|
+
async def __aexit__(self, *args: Any) -> None:
|
|
197
|
+
"""Async context manager exit."""
|
|
198
|
+
if self._client:
|
|
199
|
+
await self._client.__aexit__(*args)
|
|
200
|
+
|
|
201
|
+
async def close(self) -> None:
|
|
202
|
+
"""Close HTTP client."""
|
|
203
|
+
if self._client:
|
|
204
|
+
await self._client.close()
|
|
205
|
+
|
|
206
|
+
__all__ = [
|
|
207
|
+
"API",
|
|
208
|
+
"APIClient",
|
|
209
|
+
]
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Any, Optional
|
|
4
|
+
|
|
5
|
+
import httpx
|
|
6
|
+
|
|
7
|
+
from .shortlinks__api__shortlinks import ShortlinksShortlinksAPI
|
|
8
|
+
from .logger import APILogger, LoggerConfig
|
|
9
|
+
from .retry import RetryConfig, RetryAsyncClient
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class APIClient:
|
|
13
|
+
"""
|
|
14
|
+
Async API client for SDKRouter API.
|
|
15
|
+
|
|
16
|
+
Usage:
|
|
17
|
+
>>> async with APIClient(base_url='https://api.example.com') as client:
|
|
18
|
+
... users = await client.users.list()
|
|
19
|
+
... post = await client.posts.create(data=new_post)
|
|
20
|
+
>>>
|
|
21
|
+
>>> # With retry configuration
|
|
22
|
+
>>> retry_config = RetryConfig(max_attempts=5, min_wait=2.0)
|
|
23
|
+
>>> async with APIClient(base_url='https://api.example.com', retry_config=retry_config) as client:
|
|
24
|
+
... users = await client.users.list()
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
def __init__(
|
|
28
|
+
self,
|
|
29
|
+
base_url: str,
|
|
30
|
+
logger_config: Optional[LoggerConfig] = None,
|
|
31
|
+
retry_config: Optional[RetryConfig] = None,
|
|
32
|
+
**kwargs: Any,
|
|
33
|
+
):
|
|
34
|
+
"""
|
|
35
|
+
Initialize API client.
|
|
36
|
+
|
|
37
|
+
Args:
|
|
38
|
+
base_url: Base API URL (e.g., 'https://api.example.com')
|
|
39
|
+
logger_config: Logger configuration (None to disable logging)
|
|
40
|
+
retry_config: Retry configuration (None to disable retry)
|
|
41
|
+
**kwargs: Additional httpx.AsyncClient kwargs
|
|
42
|
+
"""
|
|
43
|
+
self.base_url = base_url.rstrip('/')
|
|
44
|
+
|
|
45
|
+
# Create HTTP client with or without retry
|
|
46
|
+
if retry_config is not None:
|
|
47
|
+
self._client = RetryAsyncClient(
|
|
48
|
+
base_url=self.base_url,
|
|
49
|
+
retry_config=retry_config,
|
|
50
|
+
**kwargs,
|
|
51
|
+
)
|
|
52
|
+
else:
|
|
53
|
+
self._client = httpx.AsyncClient(
|
|
54
|
+
base_url=self.base_url,
|
|
55
|
+
**kwargs,
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
# Initialize logger
|
|
59
|
+
self.logger: Optional[APILogger] = None
|
|
60
|
+
if logger_config is not None:
|
|
61
|
+
self.logger = APILogger(logger_config)
|
|
62
|
+
|
|
63
|
+
# Initialize sub-clients
|
|
64
|
+
self.shortlinks_shortlinks = ShortlinksShortlinksAPI(self._client)
|
|
65
|
+
|
|
66
|
+
async def __aenter__(self) -> 'APIClient':
|
|
67
|
+
await self._client.__aenter__()
|
|
68
|
+
return self
|
|
69
|
+
|
|
70
|
+
async def __aexit__(self, *args: Any) -> None:
|
|
71
|
+
await self._client.__aexit__(*args)
|
|
72
|
+
|
|
73
|
+
async def close(self) -> None:
|
|
74
|
+
"""Close HTTP client."""
|
|
75
|
+
await self._client.aclose()
|