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.
Files changed (96) hide show
  1. sdkrouter/__init__.py +110 -0
  2. sdkrouter/_api/__init__.py +28 -0
  3. sdkrouter/_api/client.py +204 -0
  4. sdkrouter/_api/generated/__init__.py +21 -0
  5. sdkrouter/_api/generated/cdn/__init__.py +209 -0
  6. sdkrouter/_api/generated/cdn/cdn__api__cdn/__init__.py +7 -0
  7. sdkrouter/_api/generated/cdn/cdn__api__cdn/client.py +133 -0
  8. sdkrouter/_api/generated/cdn/cdn__api__cdn/models.py +163 -0
  9. sdkrouter/_api/generated/cdn/cdn__api__cdn/sync_client.py +132 -0
  10. sdkrouter/_api/generated/cdn/client.py +75 -0
  11. sdkrouter/_api/generated/cdn/logger.py +256 -0
  12. sdkrouter/_api/generated/cdn/pyproject.toml +55 -0
  13. sdkrouter/_api/generated/cdn/retry.py +272 -0
  14. sdkrouter/_api/generated/cdn/sync_client.py +58 -0
  15. sdkrouter/_api/generated/cleaner/__init__.py +212 -0
  16. sdkrouter/_api/generated/cleaner/cleaner__api__cleaner/__init__.py +7 -0
  17. sdkrouter/_api/generated/cleaner/cleaner__api__cleaner/client.py +83 -0
  18. sdkrouter/_api/generated/cleaner/cleaner__api__cleaner/models.py +117 -0
  19. sdkrouter/_api/generated/cleaner/cleaner__api__cleaner/sync_client.py +82 -0
  20. sdkrouter/_api/generated/cleaner/client.py +75 -0
  21. sdkrouter/_api/generated/cleaner/enums.py +55 -0
  22. sdkrouter/_api/generated/cleaner/logger.py +256 -0
  23. sdkrouter/_api/generated/cleaner/pyproject.toml +55 -0
  24. sdkrouter/_api/generated/cleaner/retry.py +272 -0
  25. sdkrouter/_api/generated/cleaner/sync_client.py +58 -0
  26. sdkrouter/_api/generated/keys/__init__.py +212 -0
  27. sdkrouter/_api/generated/keys/client.py +75 -0
  28. sdkrouter/_api/generated/keys/enums.py +64 -0
  29. sdkrouter/_api/generated/keys/keys__api__keys/__init__.py +7 -0
  30. sdkrouter/_api/generated/keys/keys__api__keys/client.py +150 -0
  31. sdkrouter/_api/generated/keys/keys__api__keys/models.py +152 -0
  32. sdkrouter/_api/generated/keys/keys__api__keys/sync_client.py +149 -0
  33. sdkrouter/_api/generated/keys/logger.py +256 -0
  34. sdkrouter/_api/generated/keys/pyproject.toml +55 -0
  35. sdkrouter/_api/generated/keys/retry.py +272 -0
  36. sdkrouter/_api/generated/keys/sync_client.py +58 -0
  37. sdkrouter/_api/generated/models/__init__.py +209 -0
  38. sdkrouter/_api/generated/models/client.py +75 -0
  39. sdkrouter/_api/generated/models/logger.py +256 -0
  40. sdkrouter/_api/generated/models/models__api__llm_models/__init__.py +7 -0
  41. sdkrouter/_api/generated/models/models__api__llm_models/client.py +99 -0
  42. sdkrouter/_api/generated/models/models__api__llm_models/models.py +206 -0
  43. sdkrouter/_api/generated/models/models__api__llm_models/sync_client.py +99 -0
  44. sdkrouter/_api/generated/models/pyproject.toml +55 -0
  45. sdkrouter/_api/generated/models/retry.py +272 -0
  46. sdkrouter/_api/generated/models/sync_client.py +58 -0
  47. sdkrouter/_api/generated/shortlinks/__init__.py +209 -0
  48. sdkrouter/_api/generated/shortlinks/client.py +75 -0
  49. sdkrouter/_api/generated/shortlinks/logger.py +256 -0
  50. sdkrouter/_api/generated/shortlinks/pyproject.toml +55 -0
  51. sdkrouter/_api/generated/shortlinks/retry.py +272 -0
  52. sdkrouter/_api/generated/shortlinks/shortlinks__api__shortlinks/__init__.py +7 -0
  53. sdkrouter/_api/generated/shortlinks/shortlinks__api__shortlinks/client.py +137 -0
  54. sdkrouter/_api/generated/shortlinks/shortlinks__api__shortlinks/models.py +153 -0
  55. sdkrouter/_api/generated/shortlinks/shortlinks__api__shortlinks/sync_client.py +136 -0
  56. sdkrouter/_api/generated/shortlinks/sync_client.py +58 -0
  57. sdkrouter/_api/generated/vision/__init__.py +212 -0
  58. sdkrouter/_api/generated/vision/client.py +75 -0
  59. sdkrouter/_api/generated/vision/enums.py +40 -0
  60. sdkrouter/_api/generated/vision/logger.py +256 -0
  61. sdkrouter/_api/generated/vision/pyproject.toml +55 -0
  62. sdkrouter/_api/generated/vision/retry.py +272 -0
  63. sdkrouter/_api/generated/vision/sync_client.py +58 -0
  64. sdkrouter/_api/generated/vision/vision__api__vision/__init__.py +7 -0
  65. sdkrouter/_api/generated/vision/vision__api__vision/client.py +65 -0
  66. sdkrouter/_api/generated/vision/vision__api__vision/models.py +138 -0
  67. sdkrouter/_api/generated/vision/vision__api__vision/sync_client.py +65 -0
  68. sdkrouter/_client.py +432 -0
  69. sdkrouter/_config.py +74 -0
  70. sdkrouter/_constants.py +21 -0
  71. sdkrouter/_internal/__init__.py +1 -0
  72. sdkrouter/_types/__init__.py +30 -0
  73. sdkrouter/_types/cdn.py +27 -0
  74. sdkrouter/_types/models.py +26 -0
  75. sdkrouter/_types/ocr.py +24 -0
  76. sdkrouter/_types/parsed.py +101 -0
  77. sdkrouter/_types/shortlinks.py +27 -0
  78. sdkrouter/_types/vision.py +29 -0
  79. sdkrouter/_version.py +3 -0
  80. sdkrouter/helpers/__init__.py +13 -0
  81. sdkrouter/helpers/formatting.py +15 -0
  82. sdkrouter/helpers/html.py +100 -0
  83. sdkrouter/helpers/json_cleaner.py +53 -0
  84. sdkrouter/tools/__init__.py +129 -0
  85. sdkrouter/tools/cdn.py +285 -0
  86. sdkrouter/tools/cleaner.py +186 -0
  87. sdkrouter/tools/keys.py +215 -0
  88. sdkrouter/tools/models.py +196 -0
  89. sdkrouter/tools/shortlinks.py +165 -0
  90. sdkrouter/tools/vision.py +173 -0
  91. sdkrouter/utils/__init__.py +27 -0
  92. sdkrouter/utils/parsing.py +109 -0
  93. sdkrouter/utils/tokens.py +375 -0
  94. sdkrouter-0.1.1.dist-info/METADATA +411 -0
  95. sdkrouter-0.1.1.dist-info/RECORD +96 -0
  96. sdkrouter-0.1.1.dist-info/WHEEL +4 -0
sdkrouter/tools/cdn.py ADDED
@@ -0,0 +1,285 @@
1
+ """CDN file storage tool using generated API client."""
2
+
3
+ from pathlib import Path
4
+ from typing import Optional, BinaryIO, Union
5
+
6
+ import httpx
7
+
8
+ from .._config import SDKConfig
9
+ from .._api.client import (
10
+ BaseResource,
11
+ AsyncBaseResource,
12
+ SyncCdnCdnAPI,
13
+ CdnCdnAPI,
14
+ )
15
+ from .._api.generated.cdn.cdn__api__cdn.models import (
16
+ CDNFileList,
17
+ CDNFileDetail,
18
+ CDNFileUpload,
19
+ CDNFileUploadRequest,
20
+ PaginatedCDNFileListList,
21
+ CDNStats,
22
+ )
23
+
24
+
25
+ class CDNUploadResponse:
26
+ """Response from CDN upload - can be immediate result or async job."""
27
+
28
+ def __init__(self, data: dict, is_async: bool = False):
29
+ self._data = data
30
+ self.is_async = is_async
31
+
32
+ if is_async:
33
+ self.status = data.get("status")
34
+ self.job_id = data.get("job_id")
35
+ self.message = data.get("message")
36
+ self.uuid = None
37
+ self.filename = None
38
+ self.url = None
39
+ else:
40
+ self.uuid = data.get("uuid")
41
+ self.filename = data.get("filename")
42
+ self.url = data.get("url")
43
+ self.short_url = data.get("short_url")
44
+ self.content_type = data.get("content_type")
45
+ self.size_bytes = data.get("size_bytes")
46
+ self.status = "completed"
47
+ self.job_id = None
48
+ self.message = None
49
+
50
+ def __repr__(self):
51
+ if self.is_async:
52
+ return f"CDNUploadResponse(status={self.status}, job_id={self.job_id})"
53
+ return f"CDNUploadResponse(uuid={self.uuid}, filename={self.filename})"
54
+
55
+
56
+ class CDNResource(BaseResource):
57
+ """CDN file storage tool (sync).
58
+
59
+ Uses generated SyncCdnCdnAPI client.
60
+ """
61
+
62
+ def __init__(self, config: SDKConfig):
63
+ super().__init__(config)
64
+ self._api = SyncCdnCdnAPI(self._http_client)
65
+
66
+ def upload(
67
+ self,
68
+ file: Path | BinaryIO | bytes | None = None,
69
+ *,
70
+ url: Optional[str] = None,
71
+ filename: Optional[str] = None,
72
+ ttl: Optional[str] = None,
73
+ is_public: bool = True,
74
+ metadata: Optional[dict] = None,
75
+ ) -> Union[CDNFileDetail, CDNUploadResponse]:
76
+ """
77
+ Upload a file to CDN.
78
+
79
+ Supports two modes:
80
+ - file: Direct upload from Path, bytes, or file-like object
81
+ - url: Server downloads from URL (may be processed async)
82
+
83
+ Note: File upload requires multipart/form-data, so we use httpx directly.
84
+ """
85
+ if not file and not url:
86
+ raise ValueError("Either 'file' or 'url' must be provided")
87
+ if file and url:
88
+ raise ValueError("Provide either 'file' or 'url', not both")
89
+
90
+ data = {"is_public": str(is_public).lower()}
91
+ if ttl:
92
+ data["ttl"] = ttl
93
+ if metadata:
94
+ import json
95
+ data["metadata"] = json.dumps(metadata)
96
+
97
+ if url:
98
+ # URL mode - use httpx directly since response varies (async job vs file)
99
+ json_data = {"url": url, "is_public": is_public}
100
+ if filename is not None:
101
+ json_data["filename"] = filename
102
+ if ttl is not None:
103
+ json_data["ttl"] = ttl
104
+ if metadata is not None:
105
+ json_data["metadata"] = metadata
106
+
107
+ response = self._http_client.post("/api/cdn/", json=json_data)
108
+ response.raise_for_status()
109
+ response_data = response.json()
110
+
111
+ # Check if async processing
112
+ if response.status_code == 202 or "job_id" in response_data or response_data.get("status") == "processing":
113
+ return CDNUploadResponse(response_data, is_async=True)
114
+ return CDNFileDetail.model_validate(response_data)
115
+ else:
116
+ # File upload mode - use httpx directly (multipart)
117
+ if isinstance(file, Path):
118
+ filename = filename or file.name
119
+ with open(file, "rb") as f:
120
+ content = f.read()
121
+ elif isinstance(file, bytes):
122
+ content = file
123
+ if not filename:
124
+ raise ValueError("filename required when uploading bytes")
125
+ else:
126
+ # BinaryIO
127
+ content = file.read()
128
+ filename = filename or getattr(file, "name", "file")
129
+
130
+ files = {"file": (filename, content)}
131
+ response = self._http_client.post("/api/cdn/", files=files, data=data)
132
+
133
+ response.raise_for_status()
134
+ response_data = response.json()
135
+
136
+ if response.status_code == 202 or "job_id" in response_data:
137
+ return CDNUploadResponse(response_data, is_async=True)
138
+
139
+ return CDNFileDetail.model_validate(response_data)
140
+
141
+ def get(self, uuid: str) -> CDNFileDetail:
142
+ """Get file details by UUID."""
143
+ return self._api.retrieve(uuid)
144
+
145
+ def list(
146
+ self,
147
+ *,
148
+ page: int = 1,
149
+ page_size: int = 20,
150
+ ) -> PaginatedCDNFileListList:
151
+ """List files."""
152
+ return self._api.list(page=page, page_size=page_size)
153
+
154
+ def delete(self, uuid: str) -> bool:
155
+ """Delete a file by UUID."""
156
+ try:
157
+ self._api.destroy(uuid)
158
+ return True
159
+ except httpx.HTTPStatusError:
160
+ return False
161
+
162
+ def stats(self) -> CDNStats:
163
+ """Get storage statistics."""
164
+ return self._api.stats_retrieve()
165
+
166
+
167
+ class AsyncCDNResource(AsyncBaseResource):
168
+ """CDN file storage tool (async).
169
+
170
+ Uses generated CdnCdnAPI client.
171
+ """
172
+
173
+ def __init__(self, config: SDKConfig):
174
+ super().__init__(config)
175
+ self._api = CdnCdnAPI(self._http_client)
176
+
177
+ async def upload(
178
+ self,
179
+ file: Path | BinaryIO | bytes | None = None,
180
+ *,
181
+ url: Optional[str] = None,
182
+ filename: Optional[str] = None,
183
+ ttl: Optional[str] = None,
184
+ is_public: bool = True,
185
+ metadata: Optional[dict] = None,
186
+ ) -> Union[CDNFileDetail, CDNUploadResponse]:
187
+ """
188
+ Upload a file to CDN.
189
+
190
+ Note: File upload requires multipart/form-data, so we use httpx directly.
191
+ """
192
+ if not file and not url:
193
+ raise ValueError("Either 'file' or 'url' must be provided")
194
+ if file and url:
195
+ raise ValueError("Provide either 'file' or 'url', not both")
196
+
197
+ data = {"is_public": str(is_public).lower()}
198
+ if ttl:
199
+ data["ttl"] = ttl
200
+ if metadata:
201
+ import json
202
+ data["metadata"] = json.dumps(metadata)
203
+
204
+ if url:
205
+ # URL mode - use httpx directly since response varies (async job vs file)
206
+ json_data = {"url": url, "is_public": is_public}
207
+ if filename is not None:
208
+ json_data["filename"] = filename
209
+ if ttl is not None:
210
+ json_data["ttl"] = ttl
211
+ if metadata is not None:
212
+ json_data["metadata"] = metadata
213
+
214
+ response = await self._http_client.post("/api/cdn/", json=json_data)
215
+ response.raise_for_status()
216
+ response_data = response.json()
217
+
218
+ # Check if async processing
219
+ if response.status_code == 202 or "job_id" in response_data or response_data.get("status") == "processing":
220
+ return CDNUploadResponse(response_data, is_async=True)
221
+ return CDNFileDetail.model_validate(response_data)
222
+ else:
223
+ # File upload mode - use httpx directly (multipart)
224
+ if isinstance(file, Path):
225
+ filename = filename or file.name
226
+ with open(file, "rb") as f:
227
+ content = f.read()
228
+ elif isinstance(file, bytes):
229
+ content = file
230
+ if not filename:
231
+ raise ValueError("filename required when uploading bytes")
232
+ else:
233
+ # BinaryIO
234
+ content = file.read()
235
+ filename = filename or getattr(file, "name", "file")
236
+
237
+ files = {"file": (filename, content)}
238
+ response = await self._http_client.post("/api/cdn/", files=files, data=data)
239
+
240
+ response.raise_for_status()
241
+ response_data = response.json()
242
+
243
+ if response.status_code == 202 or "job_id" in response_data:
244
+ return CDNUploadResponse(response_data, is_async=True)
245
+
246
+ return CDNFileDetail.model_validate(response_data)
247
+
248
+ async def get(self, uuid: str) -> CDNFileDetail:
249
+ """Get file details by UUID."""
250
+ return await self._api.retrieve(uuid)
251
+
252
+ async def list(
253
+ self,
254
+ *,
255
+ page: int = 1,
256
+ page_size: int = 20,
257
+ ) -> PaginatedCDNFileListList:
258
+ """List files."""
259
+ return await self._api.list(page=page, page_size=page_size)
260
+
261
+ async def delete(self, uuid: str) -> bool:
262
+ """Delete a file by UUID."""
263
+ try:
264
+ await self._api.destroy(uuid)
265
+ return True
266
+ except httpx.HTTPStatusError:
267
+ return False
268
+
269
+ async def stats(self) -> CDNStats:
270
+ """Get storage statistics."""
271
+ return await self._api.stats_retrieve()
272
+
273
+
274
+ __all__ = [
275
+ "CDNResource",
276
+ "AsyncCDNResource",
277
+ "CDNUploadResponse",
278
+ "CDNStats",
279
+ # Models
280
+ "CDNFileList",
281
+ "CDNFileDetail",
282
+ "CDNFileUpload",
283
+ "CDNFileUploadRequest",
284
+ "PaginatedCDNFileListList",
285
+ ]
@@ -0,0 +1,186 @@
1
+ """HTML Cleaner tool using generated API client."""
2
+
3
+ from pathlib import Path
4
+ from typing import Optional
5
+
6
+ from .._config import SDKConfig
7
+ from .._api.client import (
8
+ BaseResource,
9
+ AsyncBaseResource,
10
+ SyncCleanerCleanerAPI,
11
+ CleanerCleanerAPI,
12
+ )
13
+ from .._api.generated.cleaner.cleaner__api__cleaner.models import (
14
+ CleanRequestRequest,
15
+ CleanResponse,
16
+ CleaningRequestDetail,
17
+ CleaningStats,
18
+ )
19
+ from .._api.generated.cleaner.enums import (
20
+ CleanRequestRequestOutputFormat,
21
+ CleaningRequestDetailStatus,
22
+ CleaningRequestListStatus,
23
+ )
24
+
25
+
26
+ class CleanerResource(BaseResource):
27
+ """HTML Cleaner tool (sync).
28
+
29
+ Uses generated SyncCleanerCleanerAPI client.
30
+ """
31
+
32
+ def __init__(self, config: SDKConfig):
33
+ super().__init__(config)
34
+ self._api = SyncCleanerCleanerAPI(self._http_client)
35
+
36
+ def clean(
37
+ self,
38
+ html: bytes | str,
39
+ *,
40
+ filename: str = "input.html",
41
+ output_format: CleanRequestRequestOutputFormat | str = CleanRequestRequestOutputFormat.HTML,
42
+ max_tokens: int = 10000,
43
+ remove_scripts: bool = True,
44
+ remove_styles: bool = True,
45
+ remove_comments: bool = True,
46
+ remove_hidden: bool = True,
47
+ filter_classes: bool = True,
48
+ class_threshold: float = 0.3,
49
+ try_hydration: bool = True,
50
+ preserve_selectors: Optional[list[str]] = None,
51
+ ) -> CleanResponse:
52
+ """Clean HTML content via API."""
53
+ if isinstance(html, str):
54
+ content = html.encode("utf-8")
55
+ else:
56
+ content = html
57
+
58
+ # Handle string or enum for output_format
59
+ format_value = output_format.value if hasattr(output_format, "value") else output_format
60
+
61
+ files = {"file": (filename, content, "text/html")}
62
+ form_data = {
63
+ "output_format": format_value,
64
+ "max_tokens": str(max_tokens),
65
+ "remove_scripts": str(remove_scripts).lower(),
66
+ "remove_styles": str(remove_styles).lower(),
67
+ "remove_comments": str(remove_comments).lower(),
68
+ "remove_hidden": str(remove_hidden).lower(),
69
+ "filter_classes": str(filter_classes).lower(),
70
+ "class_threshold": str(class_threshold),
71
+ "try_hydration": str(try_hydration).lower(),
72
+ }
73
+ if preserve_selectors:
74
+ form_data["preserve_selectors"] = ",".join(preserve_selectors)
75
+
76
+ response = self._http_client.post("/api/cleaner/clean/", files=files, data=form_data)
77
+ response.raise_for_status()
78
+ return CleanResponse.model_validate(response.json())
79
+
80
+ def clean_file(self, file_path: str | Path, **kwargs) -> CleanResponse:
81
+ """Clean HTML file."""
82
+ path = Path(file_path)
83
+ content = path.read_bytes()
84
+ filename = kwargs.pop("filename", path.name)
85
+ return self.clean(content, filename=filename, **kwargs)
86
+
87
+ def get(self, uuid: str) -> CleaningRequestDetail:
88
+ """Get cleaning request details."""
89
+ return self._api.retrieve(uuid)
90
+
91
+ def list(self):
92
+ """List cleaning requests."""
93
+ return self._api.list()
94
+
95
+ def stats(self) -> CleaningStats:
96
+ """Get cleaning statistics."""
97
+ return self._api.stats_retrieve()
98
+
99
+
100
+ class AsyncCleanerResource(AsyncBaseResource):
101
+ """HTML Cleaner tool (async).
102
+
103
+ Uses generated CleanerCleanerAPI client.
104
+ """
105
+
106
+ def __init__(self, config: SDKConfig):
107
+ super().__init__(config)
108
+ self._api = CleanerCleanerAPI(self._http_client)
109
+
110
+ async def clean(
111
+ self,
112
+ html: bytes | str,
113
+ *,
114
+ filename: str = "input.html",
115
+ output_format: CleanRequestRequestOutputFormat | str = CleanRequestRequestOutputFormat.HTML,
116
+ max_tokens: int = 10000,
117
+ remove_scripts: bool = True,
118
+ remove_styles: bool = True,
119
+ remove_comments: bool = True,
120
+ remove_hidden: bool = True,
121
+ filter_classes: bool = True,
122
+ class_threshold: float = 0.3,
123
+ try_hydration: bool = True,
124
+ preserve_selectors: Optional[list[str]] = None,
125
+ ) -> CleanResponse:
126
+ """Clean HTML content via API."""
127
+ if isinstance(html, str):
128
+ content = html.encode("utf-8")
129
+ else:
130
+ content = html
131
+
132
+ # Handle string or enum for output_format
133
+ format_value = output_format.value if hasattr(output_format, "value") else output_format
134
+
135
+ files = {"file": (filename, content, "text/html")}
136
+ form_data = {
137
+ "output_format": format_value,
138
+ "max_tokens": str(max_tokens),
139
+ "remove_scripts": str(remove_scripts).lower(),
140
+ "remove_styles": str(remove_styles).lower(),
141
+ "remove_comments": str(remove_comments).lower(),
142
+ "remove_hidden": str(remove_hidden).lower(),
143
+ "filter_classes": str(filter_classes).lower(),
144
+ "class_threshold": str(class_threshold),
145
+ "try_hydration": str(try_hydration).lower(),
146
+ }
147
+ if preserve_selectors:
148
+ form_data["preserve_selectors"] = ",".join(preserve_selectors)
149
+
150
+ response = await self._http_client.post("/api/cleaner/clean/", files=files, data=form_data)
151
+ response.raise_for_status()
152
+ return CleanResponse.model_validate(response.json())
153
+
154
+ async def clean_file(self, file_path: str | Path, **kwargs) -> CleanResponse:
155
+ """Clean HTML file."""
156
+ path = Path(file_path)
157
+ content = path.read_bytes()
158
+ filename = kwargs.pop("filename", path.name)
159
+ return await self.clean(content, filename=filename, **kwargs)
160
+
161
+ async def get(self, uuid: str) -> CleaningRequestDetail:
162
+ """Get cleaning request details."""
163
+ return await self._api.retrieve(uuid)
164
+
165
+ async def list(self):
166
+ """List cleaning requests."""
167
+ return await self._api.list()
168
+
169
+ async def stats(self) -> CleaningStats:
170
+ """Get cleaning statistics."""
171
+ return await self._api.stats_retrieve()
172
+
173
+
174
+ __all__ = [
175
+ "CleanerResource",
176
+ "AsyncCleanerResource",
177
+ # Models
178
+ "CleanRequestRequest",
179
+ "CleanResponse",
180
+ "CleaningRequestDetail",
181
+ "CleaningStats",
182
+ # Enums
183
+ "CleanRequestRequestOutputFormat",
184
+ "CleaningRequestDetailStatus",
185
+ "CleaningRequestListStatus",
186
+ ]
@@ -0,0 +1,215 @@
1
+ """API Keys management tool using generated API client."""
2
+
3
+ from datetime import datetime
4
+ from decimal import Decimal
5
+ from typing import Optional
6
+
7
+ from .._config import SDKConfig
8
+ from .._api.client import (
9
+ BaseResource,
10
+ AsyncBaseResource,
11
+ SyncKeysKeysAPI,
12
+ KeysKeysAPI,
13
+ )
14
+ from .._api.generated.keys.keys__api__keys.models import (
15
+ APIKeyList,
16
+ APIKeyDetail,
17
+ APIKeyCreate,
18
+ APIKeyCreateRequest,
19
+ PaginatedAPIKeyListList,
20
+ )
21
+ from .._api.generated.keys.enums import (
22
+ APIKeyCreatePermission,
23
+ APIKeyCreateRequestPermission,
24
+ APIKeyDetailPermission,
25
+ APIKeyListPermission,
26
+ )
27
+
28
+
29
+ class KeysResource(BaseResource):
30
+ """API Keys management tool (sync).
31
+
32
+ Uses generated SyncKeysKeysAPI client.
33
+
34
+ Example:
35
+ ```python
36
+ from sdkrouter import SDKRouter
37
+
38
+ client = SDKRouter(api_key="your-api-key")
39
+
40
+ # Create a new key
41
+ new_key = client.keys.create(name="Production Key")
42
+ print(f"New key: {new_key.plain_key}") # Save this!
43
+
44
+ # List all keys
45
+ keys = client.keys.list()
46
+ for key in keys.results:
47
+ print(f"{key.name}: {key.key_prefix}...")
48
+
49
+ # Rotate a key
50
+ rotated = client.keys.rotate(key_id)
51
+ print(f"New rotated key: {rotated.plain_key}")
52
+ ```
53
+ """
54
+
55
+ def __init__(self, config: SDKConfig):
56
+ super().__init__(config)
57
+ self._api = SyncKeysKeysAPI(self._http_client)
58
+
59
+ def create(
60
+ self,
61
+ name: str,
62
+ *,
63
+ permission: APIKeyCreateRequestPermission = APIKeyCreateRequestPermission.WRITE,
64
+ rate_limit_rpm: Optional[int] = None,
65
+ rate_limit_rpd: Optional[int] = None,
66
+ quota_monthly_usd: Optional[Decimal] = None,
67
+ expires_at: Optional[datetime] = None,
68
+ metadata: Optional[dict] = None,
69
+ is_test: bool = False,
70
+ ) -> APIKeyCreate:
71
+ """
72
+ Create a new API key.
73
+
74
+ Args:
75
+ name: Human-readable name for the key
76
+ permission: Permission level (read, write, admin)
77
+ rate_limit_rpm: Max requests per minute (None = unlimited)
78
+ rate_limit_rpd: Max requests per day (None = unlimited)
79
+ quota_monthly_usd: Monthly spending limit in USD
80
+ expires_at: When the key expires
81
+ metadata: Additional metadata
82
+ is_test: Create a test key (sk_test_ prefix)
83
+
84
+ Returns:
85
+ APIKeyCreate with key info including `plain_key` (only shown once!)
86
+ """
87
+ request = APIKeyCreateRequest(
88
+ name=name,
89
+ permission=permission,
90
+ rate_limit_rpm=rate_limit_rpm,
91
+ rate_limit_rpd=rate_limit_rpd,
92
+ quota_monthly_usd=str(quota_monthly_usd) if quota_monthly_usd else None,
93
+ expires_at=expires_at.isoformat() if expires_at else None,
94
+ metadata=metadata,
95
+ is_test=is_test,
96
+ )
97
+ return self._api.create(request)
98
+
99
+ def get(self, key_id: str) -> APIKeyDetail:
100
+ """Get API key details by ID."""
101
+ return self._api.retrieve(key_id)
102
+
103
+ def list(
104
+ self,
105
+ *,
106
+ page: int = 1,
107
+ page_size: int = 20,
108
+ ) -> PaginatedAPIKeyListList:
109
+ """List your API keys."""
110
+ return self._api.list(page=page, page_size=page_size)
111
+
112
+ def delete(self, key_id: str) -> bool:
113
+ """Deactivate an API key (soft delete)."""
114
+ try:
115
+ self._api.destroy(key_id)
116
+ return True
117
+ except httpx.HTTPStatusError:
118
+ return False
119
+
120
+ def rotate(self, key_id: str) -> APIKeyList:
121
+ """
122
+ Rotate an API key (generate new key).
123
+
124
+ The old key will stop working immediately.
125
+
126
+ Returns:
127
+ APIKeyList with `plain_key` (the new key - only shown once!)
128
+ """
129
+ return self._api.rotate_create(key_id)
130
+
131
+ def reactivate(self, key_id: str) -> APIKeyList:
132
+ """Reactivate a deactivated API key."""
133
+ return self._api.reactivate_create(key_id)
134
+
135
+
136
+ class AsyncKeysResource(AsyncBaseResource):
137
+ """API Keys management tool (async).
138
+
139
+ Uses generated KeysKeysAPI client.
140
+ """
141
+
142
+ def __init__(self, config: SDKConfig):
143
+ super().__init__(config)
144
+ self._api = KeysKeysAPI(self._http_client)
145
+
146
+ async def create(
147
+ self,
148
+ name: str,
149
+ *,
150
+ permission: APIKeyCreateRequestPermission = APIKeyCreateRequestPermission.WRITE,
151
+ rate_limit_rpm: Optional[int] = None,
152
+ rate_limit_rpd: Optional[int] = None,
153
+ quota_monthly_usd: Optional[Decimal] = None,
154
+ expires_at: Optional[datetime] = None,
155
+ metadata: Optional[dict] = None,
156
+ is_test: bool = False,
157
+ ) -> APIKeyCreate:
158
+ """Create a new API key."""
159
+ request = APIKeyCreateRequest(
160
+ name=name,
161
+ permission=permission,
162
+ rate_limit_rpm=rate_limit_rpm,
163
+ rate_limit_rpd=rate_limit_rpd,
164
+ quota_monthly_usd=str(quota_monthly_usd) if quota_monthly_usd else None,
165
+ expires_at=expires_at.isoformat() if expires_at else None,
166
+ metadata=metadata,
167
+ is_test=is_test,
168
+ )
169
+ return await self._api.create(request)
170
+
171
+ async def get(self, key_id: str) -> APIKeyDetail:
172
+ """Get API key details by ID."""
173
+ return await self._api.retrieve(key_id)
174
+
175
+ async def list(
176
+ self,
177
+ *,
178
+ page: int = 1,
179
+ page_size: int = 20,
180
+ ) -> PaginatedAPIKeyListList:
181
+ """List your API keys."""
182
+ return await self._api.list(page=page, page_size=page_size)
183
+
184
+ async def delete(self, key_id: str) -> bool:
185
+ """Deactivate an API key (soft delete)."""
186
+ try:
187
+ await self._api.destroy(key_id)
188
+ return True
189
+ except httpx.HTTPStatusError:
190
+ return False
191
+
192
+ async def rotate(self, key_id: str) -> APIKeyList:
193
+ """Rotate an API key (generate new key)."""
194
+ return await self._api.rotate_create(key_id)
195
+
196
+ async def reactivate(self, key_id: str) -> APIKeyList:
197
+ """Reactivate a deactivated API key."""
198
+ return await self._api.reactivate_create(key_id)
199
+
200
+
201
+ __all__ = [
202
+ "KeysResource",
203
+ "AsyncKeysResource",
204
+ # Models
205
+ "APIKeyList",
206
+ "APIKeyDetail",
207
+ "APIKeyCreate",
208
+ "APIKeyCreateRequest",
209
+ "PaginatedAPIKeyListList",
210
+ # Enums
211
+ "APIKeyCreatePermission",
212
+ "APIKeyCreateRequestPermission",
213
+ "APIKeyDetailPermission",
214
+ "APIKeyListPermission",
215
+ ]