ofspectrum 1.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.
- ofspectrum/__init__.py +91 -0
- ofspectrum/client.py +314 -0
- ofspectrum/exceptions.py +247 -0
- ofspectrum/models/__init__.py +21 -0
- ofspectrum/models/audio.py +102 -0
- ofspectrum/models/notebook.py +114 -0
- ofspectrum/models/quota.py +81 -0
- ofspectrum/models/token.py +74 -0
- ofspectrum/py.typed +0 -0
- ofspectrum/resources/__init__.py +17 -0
- ofspectrum/resources/audio.py +196 -0
- ofspectrum/resources/base.py +88 -0
- ofspectrum/resources/notebooks.py +325 -0
- ofspectrum/resources/quotas.py +98 -0
- ofspectrum/resources/tokens.py +152 -0
- ofspectrum/resources/webhooks.py +232 -0
- ofspectrum/utils/__init__.py +7 -0
- ofspectrum/utils/retry.py +155 -0
- ofspectrum-1.1.1.dist-info/METADATA +241 -0
- ofspectrum-1.1.1.dist-info/RECORD +22 -0
- ofspectrum-1.1.1.dist-info/WHEEL +5 -0
- ofspectrum-1.1.1.dist-info/top_level.txt +1 -0
ofspectrum/__init__.py
ADDED
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
"""
|
|
2
|
+
OfSpectrum Python SDK
|
|
3
|
+
|
|
4
|
+
Audio watermarking and AI detection API client.
|
|
5
|
+
|
|
6
|
+
Example:
|
|
7
|
+
from ofspectrum import OfSpectrum
|
|
8
|
+
|
|
9
|
+
client = OfSpectrum(api_key="your_api_key")
|
|
10
|
+
|
|
11
|
+
# Create a token
|
|
12
|
+
token = client.tokens.create(name="My Token", token_type="creator")
|
|
13
|
+
|
|
14
|
+
# Encode watermark
|
|
15
|
+
result = client.audio.encode(
|
|
16
|
+
audio="input.mp3",
|
|
17
|
+
token_id=token.id,
|
|
18
|
+
output_path="watermarked.mp3"
|
|
19
|
+
)
|
|
20
|
+
print(f"Encoded {result.audio_duration}s of audio")
|
|
21
|
+
|
|
22
|
+
# Decode watermark
|
|
23
|
+
decode = client.audio.decode("suspect.mp3")
|
|
24
|
+
if decode.watermarked:
|
|
25
|
+
print(f"Found watermark: {decode.token_id}")
|
|
26
|
+
|
|
27
|
+
# Check quota
|
|
28
|
+
quota = client.quotas.get_encode_quota()
|
|
29
|
+
print(f"Remaining: {quota.remaining}/{quota.quota_limit}")
|
|
30
|
+
"""
|
|
31
|
+
|
|
32
|
+
__version__ = "1.0.0"
|
|
33
|
+
__author__ = "OfSpectrum"
|
|
34
|
+
|
|
35
|
+
from .client import OfSpectrum, AsyncOfSpectrum
|
|
36
|
+
from .exceptions import (
|
|
37
|
+
OfSpectrumError,
|
|
38
|
+
AuthenticationError,
|
|
39
|
+
RateLimitError,
|
|
40
|
+
QuotaExceededError,
|
|
41
|
+
ResourceNotFoundError,
|
|
42
|
+
ValidationError,
|
|
43
|
+
WatermarkExistsError,
|
|
44
|
+
TimeoutError,
|
|
45
|
+
ServiceUnavailableError,
|
|
46
|
+
NetworkError,
|
|
47
|
+
)
|
|
48
|
+
from .models import (
|
|
49
|
+
Token,
|
|
50
|
+
TokenCreateParams,
|
|
51
|
+
TokenUpdateParams,
|
|
52
|
+
Notebook,
|
|
53
|
+
NotebookMedia,
|
|
54
|
+
NotebookCreateParams,
|
|
55
|
+
EncodeResult,
|
|
56
|
+
DecodeResult,
|
|
57
|
+
Quota,
|
|
58
|
+
QuotaList,
|
|
59
|
+
)
|
|
60
|
+
from .utils import RetryConfig, with_retry
|
|
61
|
+
|
|
62
|
+
__all__ = [
|
|
63
|
+
# Client
|
|
64
|
+
"OfSpectrum",
|
|
65
|
+
"AsyncOfSpectrum",
|
|
66
|
+
# Exceptions
|
|
67
|
+
"OfSpectrumError",
|
|
68
|
+
"AuthenticationError",
|
|
69
|
+
"RateLimitError",
|
|
70
|
+
"QuotaExceededError",
|
|
71
|
+
"ResourceNotFoundError",
|
|
72
|
+
"ValidationError",
|
|
73
|
+
"WatermarkExistsError",
|
|
74
|
+
"TimeoutError",
|
|
75
|
+
"ServiceUnavailableError",
|
|
76
|
+
"NetworkError",
|
|
77
|
+
# Models
|
|
78
|
+
"Token",
|
|
79
|
+
"TokenCreateParams",
|
|
80
|
+
"TokenUpdateParams",
|
|
81
|
+
"Notebook",
|
|
82
|
+
"NotebookMedia",
|
|
83
|
+
"NotebookCreateParams",
|
|
84
|
+
"EncodeResult",
|
|
85
|
+
"DecodeResult",
|
|
86
|
+
"Quota",
|
|
87
|
+
"QuotaList",
|
|
88
|
+
# Utils
|
|
89
|
+
"RetryConfig",
|
|
90
|
+
"with_retry",
|
|
91
|
+
]
|
ofspectrum/client.py
ADDED
|
@@ -0,0 +1,314 @@
|
|
|
1
|
+
"""
|
|
2
|
+
OfSpectrum API Client
|
|
3
|
+
|
|
4
|
+
Main entry point for the SDK.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from typing import Optional, Dict, Any
|
|
8
|
+
import httpx
|
|
9
|
+
|
|
10
|
+
from .resources import (
|
|
11
|
+
TokensResource,
|
|
12
|
+
NotebooksResource,
|
|
13
|
+
AudioResource,
|
|
14
|
+
QuotasResource,
|
|
15
|
+
# WebhooksResource, # Not yet available
|
|
16
|
+
)
|
|
17
|
+
from .exceptions import (
|
|
18
|
+
OfSpectrumError,
|
|
19
|
+
AuthenticationError,
|
|
20
|
+
NetworkError,
|
|
21
|
+
raise_for_error,
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class OfSpectrum:
|
|
26
|
+
"""
|
|
27
|
+
Synchronous OfSpectrum API client.
|
|
28
|
+
|
|
29
|
+
Example:
|
|
30
|
+
from ofspectrum import OfSpectrum
|
|
31
|
+
|
|
32
|
+
client = OfSpectrum(api_key="your_api_key")
|
|
33
|
+
|
|
34
|
+
# List tokens
|
|
35
|
+
tokens = client.tokens.list()
|
|
36
|
+
|
|
37
|
+
# Encode watermark
|
|
38
|
+
result = client.audio.encode(
|
|
39
|
+
audio="input.mp3",
|
|
40
|
+
token_id=tokens[0].id,
|
|
41
|
+
output_path="watermarked.mp3"
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
# Check quota
|
|
45
|
+
quota = client.quotas.get_encode_quota()
|
|
46
|
+
print(f"Remaining: {quota.remaining}")
|
|
47
|
+
"""
|
|
48
|
+
|
|
49
|
+
DEFAULT_BASE_URL = "https://api.ofspectrum.com/api/v1"
|
|
50
|
+
DEFAULT_TIMEOUT = 120.0
|
|
51
|
+
|
|
52
|
+
def __init__(
|
|
53
|
+
self,
|
|
54
|
+
api_key: str,
|
|
55
|
+
base_url: Optional[str] = None,
|
|
56
|
+
timeout: float = DEFAULT_TIMEOUT,
|
|
57
|
+
):
|
|
58
|
+
"""
|
|
59
|
+
Initialize the OfSpectrum client.
|
|
60
|
+
|
|
61
|
+
Args:
|
|
62
|
+
api_key: Your OfSpectrum API key (64-character hex string)
|
|
63
|
+
base_url: Optional custom API base URL
|
|
64
|
+
timeout: Request timeout in seconds (default 120)
|
|
65
|
+
"""
|
|
66
|
+
if not api_key:
|
|
67
|
+
raise ValueError("api_key is required")
|
|
68
|
+
|
|
69
|
+
self._api_key = api_key
|
|
70
|
+
self._base_url = (base_url or self.DEFAULT_BASE_URL).rstrip("/")
|
|
71
|
+
self._timeout = timeout
|
|
72
|
+
|
|
73
|
+
# Initialize HTTP client
|
|
74
|
+
self._client = httpx.Client(
|
|
75
|
+
base_url=self._base_url,
|
|
76
|
+
timeout=httpx.Timeout(timeout),
|
|
77
|
+
headers=self._default_headers(),
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
# Initialize resources
|
|
81
|
+
self.tokens = TokensResource(self)
|
|
82
|
+
self.notebooks = NotebooksResource(self)
|
|
83
|
+
self.audio = AudioResource(self)
|
|
84
|
+
self.quotas = QuotasResource(self)
|
|
85
|
+
# self.webhooks = WebhooksResource(self) # Not yet available
|
|
86
|
+
|
|
87
|
+
def _default_headers(self) -> Dict[str, str]:
|
|
88
|
+
"""Get default request headers"""
|
|
89
|
+
return {
|
|
90
|
+
"Authorization": f"Bearer {self._api_key}",
|
|
91
|
+
"User-Agent": "OfSpectrum-Python-SDK/1.0.0",
|
|
92
|
+
"Accept": "application/json",
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
def _request(
|
|
96
|
+
self,
|
|
97
|
+
method: str,
|
|
98
|
+
path: str,
|
|
99
|
+
params: Optional[Dict[str, Any]] = None,
|
|
100
|
+
json: Optional[Dict[str, Any]] = None,
|
|
101
|
+
data: Optional[Dict[str, Any]] = None,
|
|
102
|
+
files: Optional[Dict[str, Any]] = None,
|
|
103
|
+
timeout: Optional[float] = None,
|
|
104
|
+
) -> httpx.Response:
|
|
105
|
+
"""
|
|
106
|
+
Make an HTTP request to the API.
|
|
107
|
+
|
|
108
|
+
Args:
|
|
109
|
+
method: HTTP method
|
|
110
|
+
path: API path
|
|
111
|
+
params: Query parameters
|
|
112
|
+
json: JSON body
|
|
113
|
+
data: Form data
|
|
114
|
+
files: Files to upload
|
|
115
|
+
timeout: Optional request timeout
|
|
116
|
+
|
|
117
|
+
Returns:
|
|
118
|
+
httpx.Response
|
|
119
|
+
|
|
120
|
+
Raises:
|
|
121
|
+
AuthenticationError: If API key is invalid
|
|
122
|
+
NetworkError: If network error occurs
|
|
123
|
+
OfSpectrumError: For other API errors
|
|
124
|
+
"""
|
|
125
|
+
url = path if path.startswith("/") else f"/{path}"
|
|
126
|
+
|
|
127
|
+
request_kwargs = {
|
|
128
|
+
"method": method,
|
|
129
|
+
"url": url,
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
if params:
|
|
133
|
+
request_kwargs["params"] = params
|
|
134
|
+
|
|
135
|
+
if json:
|
|
136
|
+
request_kwargs["json"] = json
|
|
137
|
+
|
|
138
|
+
if data:
|
|
139
|
+
request_kwargs["data"] = data
|
|
140
|
+
|
|
141
|
+
if files:
|
|
142
|
+
request_kwargs["files"] = files
|
|
143
|
+
|
|
144
|
+
if timeout:
|
|
145
|
+
request_kwargs["timeout"] = timeout
|
|
146
|
+
|
|
147
|
+
try:
|
|
148
|
+
response = self._client.request(**request_kwargs)
|
|
149
|
+
|
|
150
|
+
# Check for authentication errors
|
|
151
|
+
if response.status_code == 401:
|
|
152
|
+
raise AuthenticationError(
|
|
153
|
+
message="Invalid or expired API key",
|
|
154
|
+
status_code=401,
|
|
155
|
+
)
|
|
156
|
+
|
|
157
|
+
return response
|
|
158
|
+
|
|
159
|
+
except httpx.TimeoutException as e:
|
|
160
|
+
raise NetworkError(f"Request timed out: {e}")
|
|
161
|
+
except httpx.ConnectError as e:
|
|
162
|
+
raise NetworkError(f"Connection failed: {e}")
|
|
163
|
+
except httpx.RequestError as e:
|
|
164
|
+
raise NetworkError(f"Network error: {e}")
|
|
165
|
+
|
|
166
|
+
def close(self):
|
|
167
|
+
"""Close the HTTP client"""
|
|
168
|
+
self._client.close()
|
|
169
|
+
|
|
170
|
+
def __enter__(self):
|
|
171
|
+
return self
|
|
172
|
+
|
|
173
|
+
def __exit__(self, exc_type, exc_val, exc_tb):
|
|
174
|
+
self.close()
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
class AsyncOfSpectrum:
|
|
178
|
+
"""
|
|
179
|
+
Asynchronous OfSpectrum API client.
|
|
180
|
+
|
|
181
|
+
Example:
|
|
182
|
+
from ofspectrum import AsyncOfSpectrum
|
|
183
|
+
|
|
184
|
+
async with AsyncOfSpectrum(api_key="your_api_key") as client:
|
|
185
|
+
tokens = await client.tokens.list()
|
|
186
|
+
"""
|
|
187
|
+
|
|
188
|
+
DEFAULT_BASE_URL = "https://api.ofspectrum.com/api/v1"
|
|
189
|
+
DEFAULT_TIMEOUT = 120.0
|
|
190
|
+
|
|
191
|
+
def __init__(
|
|
192
|
+
self,
|
|
193
|
+
api_key: str,
|
|
194
|
+
base_url: Optional[str] = None,
|
|
195
|
+
timeout: float = DEFAULT_TIMEOUT,
|
|
196
|
+
):
|
|
197
|
+
"""
|
|
198
|
+
Initialize the async OfSpectrum client.
|
|
199
|
+
|
|
200
|
+
Args:
|
|
201
|
+
api_key: Your OfSpectrum API key
|
|
202
|
+
base_url: Optional custom API base URL
|
|
203
|
+
timeout: Request timeout in seconds
|
|
204
|
+
"""
|
|
205
|
+
if not api_key:
|
|
206
|
+
raise ValueError("api_key is required")
|
|
207
|
+
|
|
208
|
+
self._api_key = api_key
|
|
209
|
+
self._base_url = (base_url or self.DEFAULT_BASE_URL).rstrip("/")
|
|
210
|
+
self._timeout = timeout
|
|
211
|
+
self._client: Optional[httpx.AsyncClient] = None
|
|
212
|
+
|
|
213
|
+
# Resources will be initialized when client is opened
|
|
214
|
+
self.tokens: Optional[TokensResource] = None
|
|
215
|
+
self.notebooks: Optional[NotebooksResource] = None
|
|
216
|
+
self.audio: Optional[AudioResource] = None
|
|
217
|
+
self.quotas: Optional[QuotasResource] = None
|
|
218
|
+
# self.webhooks: Optional[WebhooksResource] = None # Not yet available
|
|
219
|
+
|
|
220
|
+
def _default_headers(self) -> Dict[str, str]:
|
|
221
|
+
return {
|
|
222
|
+
"Authorization": f"Bearer {self._api_key}",
|
|
223
|
+
"User-Agent": "OfSpectrum-Python-SDK/1.0.0",
|
|
224
|
+
"Accept": "application/json",
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
async def __aenter__(self):
|
|
228
|
+
self._client = httpx.AsyncClient(
|
|
229
|
+
base_url=self._base_url,
|
|
230
|
+
timeout=httpx.Timeout(self._timeout),
|
|
231
|
+
headers=self._default_headers(),
|
|
232
|
+
)
|
|
233
|
+
|
|
234
|
+
# Create a sync client wrapper for resources
|
|
235
|
+
# Note: For true async, resources would need async versions
|
|
236
|
+
self.tokens = TokensResource(self)
|
|
237
|
+
self.notebooks = NotebooksResource(self)
|
|
238
|
+
self.audio = AudioResource(self)
|
|
239
|
+
self.quotas = QuotasResource(self)
|
|
240
|
+
# self.webhooks = WebhooksResource(self) # Not yet available
|
|
241
|
+
|
|
242
|
+
return self
|
|
243
|
+
|
|
244
|
+
async def __aexit__(self, exc_type, exc_val, exc_tb):
|
|
245
|
+
if self._client:
|
|
246
|
+
await self._client.aclose()
|
|
247
|
+
|
|
248
|
+
def _request(
|
|
249
|
+
self,
|
|
250
|
+
method: str,
|
|
251
|
+
path: str,
|
|
252
|
+
**kwargs
|
|
253
|
+
) -> httpx.Response:
|
|
254
|
+
"""
|
|
255
|
+
Sync request wrapper for resource compatibility.
|
|
256
|
+
For truly async operations, use the async client methods directly.
|
|
257
|
+
"""
|
|
258
|
+
# For now, use a sync approach within async context
|
|
259
|
+
# A full async implementation would require async resource methods
|
|
260
|
+
import asyncio
|
|
261
|
+
|
|
262
|
+
async def _async_request():
|
|
263
|
+
if not self._client:
|
|
264
|
+
raise RuntimeError("Client not initialized. Use 'async with' context.")
|
|
265
|
+
|
|
266
|
+
url = path if path.startswith("/") else f"/{path}"
|
|
267
|
+
kwargs["url"] = url
|
|
268
|
+
kwargs["method"] = method
|
|
269
|
+
|
|
270
|
+
try:
|
|
271
|
+
response = await self._client.request(**kwargs)
|
|
272
|
+
if response.status_code == 401:
|
|
273
|
+
raise AuthenticationError(
|
|
274
|
+
message="Invalid or expired API key",
|
|
275
|
+
status_code=401,
|
|
276
|
+
)
|
|
277
|
+
return response
|
|
278
|
+
except httpx.TimeoutException as e:
|
|
279
|
+
raise NetworkError(f"Request timed out: {e}")
|
|
280
|
+
except httpx.RequestError as e:
|
|
281
|
+
raise NetworkError(f"Network error: {e}")
|
|
282
|
+
|
|
283
|
+
try:
|
|
284
|
+
loop = asyncio.get_event_loop()
|
|
285
|
+
if loop.is_running():
|
|
286
|
+
# We're in an async context, can't use run_until_complete
|
|
287
|
+
# Fall back to sync client for now
|
|
288
|
+
import warnings
|
|
289
|
+
warnings.warn(
|
|
290
|
+
"AsyncOfSpectrum resource methods are currently sync. "
|
|
291
|
+
"Use await client._async_request() for true async."
|
|
292
|
+
)
|
|
293
|
+
with httpx.Client(
|
|
294
|
+
base_url=self._base_url,
|
|
295
|
+
timeout=httpx.Timeout(self._timeout),
|
|
296
|
+
headers=self._default_headers(),
|
|
297
|
+
) as sync_client:
|
|
298
|
+
url = path if path.startswith("/") else f"/{path}"
|
|
299
|
+
kwargs["url"] = url
|
|
300
|
+
kwargs["method"] = method
|
|
301
|
+
return sync_client.request(**kwargs)
|
|
302
|
+
else:
|
|
303
|
+
return loop.run_until_complete(_async_request())
|
|
304
|
+
except RuntimeError:
|
|
305
|
+
# No event loop, use sync
|
|
306
|
+
with httpx.Client(
|
|
307
|
+
base_url=self._base_url,
|
|
308
|
+
timeout=httpx.Timeout(self._timeout),
|
|
309
|
+
headers=self._default_headers(),
|
|
310
|
+
) as sync_client:
|
|
311
|
+
url = path if path.startswith("/") else f"/{path}"
|
|
312
|
+
kwargs["url"] = url
|
|
313
|
+
kwargs["method"] = method
|
|
314
|
+
return sync_client.request(**kwargs)
|
ofspectrum/exceptions.py
ADDED
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
"""
|
|
2
|
+
OfSpectrum SDK Exceptions
|
|
3
|
+
|
|
4
|
+
All exceptions inherit from OfSpectrumError for easy catching.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from typing import Optional, Dict, Any
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class OfSpectrumError(Exception):
|
|
11
|
+
"""Base exception for all OfSpectrum SDK errors"""
|
|
12
|
+
|
|
13
|
+
def __init__(
|
|
14
|
+
self,
|
|
15
|
+
message: str,
|
|
16
|
+
code: Optional[str] = None,
|
|
17
|
+
status_code: Optional[int] = None,
|
|
18
|
+
details: Optional[Dict[str, Any]] = None,
|
|
19
|
+
):
|
|
20
|
+
super().__init__(message)
|
|
21
|
+
self.message = message
|
|
22
|
+
self.code = code
|
|
23
|
+
self.status_code = status_code
|
|
24
|
+
self.details = details or {}
|
|
25
|
+
|
|
26
|
+
def __str__(self) -> str:
|
|
27
|
+
if self.code:
|
|
28
|
+
return f"[{self.code}] {self.message}"
|
|
29
|
+
return self.message
|
|
30
|
+
|
|
31
|
+
def __repr__(self) -> str:
|
|
32
|
+
return f"{self.__class__.__name__}(message={self.message!r}, code={self.code!r})"
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class AuthenticationError(OfSpectrumError):
|
|
36
|
+
"""Raised when authentication fails (invalid API key, expired token, etc.)"""
|
|
37
|
+
|
|
38
|
+
def __init__(self, message: str = "Authentication failed", **kwargs):
|
|
39
|
+
super().__init__(message, **kwargs)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class RateLimitError(OfSpectrumError):
|
|
43
|
+
"""Raised when rate limit is exceeded"""
|
|
44
|
+
|
|
45
|
+
def __init__(
|
|
46
|
+
self,
|
|
47
|
+
message: str = "Rate limit exceeded",
|
|
48
|
+
retry_after: Optional[int] = None,
|
|
49
|
+
**kwargs
|
|
50
|
+
):
|
|
51
|
+
super().__init__(message, **kwargs)
|
|
52
|
+
self.retry_after = retry_after
|
|
53
|
+
|
|
54
|
+
def __str__(self) -> str:
|
|
55
|
+
base = super().__str__()
|
|
56
|
+
if self.retry_after:
|
|
57
|
+
return f"{base} (retry after {self.retry_after}s)"
|
|
58
|
+
return base
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
class QuotaExceededError(OfSpectrumError):
|
|
62
|
+
"""Raised when service quota is exceeded"""
|
|
63
|
+
|
|
64
|
+
def __init__(
|
|
65
|
+
self,
|
|
66
|
+
message: str = "Quota exceeded",
|
|
67
|
+
service: Optional[str] = None,
|
|
68
|
+
remaining: int = 0,
|
|
69
|
+
reset_at: Optional[str] = None,
|
|
70
|
+
**kwargs
|
|
71
|
+
):
|
|
72
|
+
super().__init__(message, **kwargs)
|
|
73
|
+
self.service = service
|
|
74
|
+
self.remaining = remaining
|
|
75
|
+
self.reset_at = reset_at
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
class ResourceNotFoundError(OfSpectrumError):
|
|
79
|
+
"""Raised when a requested resource is not found"""
|
|
80
|
+
|
|
81
|
+
def __init__(
|
|
82
|
+
self,
|
|
83
|
+
message: str = "Resource not found",
|
|
84
|
+
resource_type: Optional[str] = None,
|
|
85
|
+
resource_id: Optional[str] = None,
|
|
86
|
+
**kwargs
|
|
87
|
+
):
|
|
88
|
+
super().__init__(message, **kwargs)
|
|
89
|
+
self.resource_type = resource_type
|
|
90
|
+
self.resource_id = resource_id
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
class ValidationError(OfSpectrumError):
|
|
94
|
+
"""Raised when request validation fails"""
|
|
95
|
+
|
|
96
|
+
def __init__(
|
|
97
|
+
self,
|
|
98
|
+
message: str = "Validation error",
|
|
99
|
+
field: Optional[str] = None,
|
|
100
|
+
**kwargs
|
|
101
|
+
):
|
|
102
|
+
super().__init__(message, **kwargs)
|
|
103
|
+
self.field = field
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
class WatermarkExistsError(OfSpectrumError):
|
|
107
|
+
"""Raised when trying to encode a watermark on already watermarked audio"""
|
|
108
|
+
|
|
109
|
+
def __init__(self, message: str = "Audio already contains watermark", **kwargs):
|
|
110
|
+
super().__init__(message, **kwargs)
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
class TimeoutError(OfSpectrumError):
|
|
114
|
+
"""Raised when a request times out"""
|
|
115
|
+
|
|
116
|
+
def __init__(self, message: str = "Request timed out", **kwargs):
|
|
117
|
+
super().__init__(message, **kwargs)
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
class ServiceUnavailableError(OfSpectrumError):
|
|
121
|
+
"""Raised when the service is temporarily unavailable"""
|
|
122
|
+
|
|
123
|
+
def __init__(
|
|
124
|
+
self,
|
|
125
|
+
message: str = "Service temporarily unavailable",
|
|
126
|
+
retry_after: Optional[int] = None,
|
|
127
|
+
**kwargs
|
|
128
|
+
):
|
|
129
|
+
super().__init__(message, **kwargs)
|
|
130
|
+
self.retry_after = retry_after
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
class NetworkError(OfSpectrumError):
|
|
134
|
+
"""Raised when a network error occurs"""
|
|
135
|
+
|
|
136
|
+
def __init__(self, message: str = "Network error", **kwargs):
|
|
137
|
+
super().__init__(message, **kwargs)
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
# Mapping from API error codes to exception classes
|
|
141
|
+
ERROR_CODE_MAP = {
|
|
142
|
+
"AUTH_1001": AuthenticationError,
|
|
143
|
+
"AUTH_1002": AuthenticationError,
|
|
144
|
+
"AUTH_1003": AuthenticationError,
|
|
145
|
+
"AUTH_1004": AuthenticationError,
|
|
146
|
+
"AUTH_1005": RateLimitError,
|
|
147
|
+
"AUTH_1006": AuthenticationError,
|
|
148
|
+
"AUTH_1007": AuthenticationError,
|
|
149
|
+
"RES_2001": ResourceNotFoundError,
|
|
150
|
+
"RES_2002": OfSpectrumError, # Forbidden
|
|
151
|
+
"RES_2003": OfSpectrumError, # Conflict
|
|
152
|
+
"RES_2004": OfSpectrumError, # Already exists
|
|
153
|
+
"QUOTA_3001": QuotaExceededError,
|
|
154
|
+
"QUOTA_3002": QuotaExceededError,
|
|
155
|
+
"QUOTA_3003": QuotaExceededError,
|
|
156
|
+
"QUOTA_3004": QuotaExceededError,
|
|
157
|
+
"PROC_4001": TimeoutError,
|
|
158
|
+
"PROC_4002": ValidationError,
|
|
159
|
+
"PROC_4003": WatermarkExistsError,
|
|
160
|
+
"PROC_4004": ServiceUnavailableError,
|
|
161
|
+
"PROC_4005": ValidationError,
|
|
162
|
+
"PROC_4006": ValidationError,
|
|
163
|
+
"SYS_5001": OfSpectrumError,
|
|
164
|
+
"SYS_5002": OfSpectrumError,
|
|
165
|
+
"SYS_5003": OfSpectrumError,
|
|
166
|
+
"SYS_5004": ServiceUnavailableError,
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
def raise_for_error(response_data, status_code: int):
|
|
171
|
+
"""
|
|
172
|
+
Parse API error response and raise appropriate exception.
|
|
173
|
+
|
|
174
|
+
Args:
|
|
175
|
+
response_data: The JSON response from the API (dict or list)
|
|
176
|
+
status_code: HTTP status code
|
|
177
|
+
|
|
178
|
+
Raises:
|
|
179
|
+
OfSpectrumError: Appropriate exception based on error code
|
|
180
|
+
"""
|
|
181
|
+
# If response is a list (e.g., tokens.list returns a list), it's not an error
|
|
182
|
+
if not isinstance(response_data, dict):
|
|
183
|
+
return
|
|
184
|
+
|
|
185
|
+
# Check for direct error format: {"error": "ErrorCode", "message": "..."}
|
|
186
|
+
# This is used by tokens_router and other legacy endpoints
|
|
187
|
+
if "error" in response_data and isinstance(response_data.get("error"), str):
|
|
188
|
+
error_code = response_data.get("error")
|
|
189
|
+
message = response_data.get("message", error_code)
|
|
190
|
+
|
|
191
|
+
# Map common error codes
|
|
192
|
+
if error_code == "QuotaExceeded":
|
|
193
|
+
raise QuotaExceededError(message=message, status_code=status_code or 429)
|
|
194
|
+
elif error_code == "QuotaMissing" or error_code == "QuotaCheckFailed":
|
|
195
|
+
raise QuotaExceededError(message=message, status_code=status_code or 500)
|
|
196
|
+
elif error_code == "Unauthorized":
|
|
197
|
+
raise AuthenticationError(message=message, status_code=status_code or 403)
|
|
198
|
+
elif error_code == "DuplicateName":
|
|
199
|
+
raise ValidationError(message=message, status_code=status_code or 400)
|
|
200
|
+
elif error_code == "Missing required fields" or error_code == "InvalidField":
|
|
201
|
+
raise ValidationError(message=message, status_code=status_code or 400)
|
|
202
|
+
elif error_code == "UnableToGenerate":
|
|
203
|
+
raise OfSpectrumError(message=message, code=error_code, status_code=status_code or 500)
|
|
204
|
+
else:
|
|
205
|
+
raise OfSpectrumError(message=message, code=error_code, status_code=status_code or 500)
|
|
206
|
+
|
|
207
|
+
if response_data.get("status") != "error":
|
|
208
|
+
# Also check for FastAPI validation errors (detail field)
|
|
209
|
+
if "detail" in response_data and status_code >= 400:
|
|
210
|
+
detail = response_data.get("detail")
|
|
211
|
+
if isinstance(detail, str):
|
|
212
|
+
raise OfSpectrumError(message=detail, status_code=status_code)
|
|
213
|
+
elif isinstance(detail, list):
|
|
214
|
+
# FastAPI validation error format
|
|
215
|
+
messages = [f"{d.get('loc', ['?'])[-1]}: {d.get('msg', '?')}" for d in detail]
|
|
216
|
+
raise ValidationError(message="; ".join(messages), status_code=status_code)
|
|
217
|
+
return
|
|
218
|
+
|
|
219
|
+
error = response_data.get("error", {})
|
|
220
|
+
code = error.get("code")
|
|
221
|
+
message = error.get("message", "Unknown error")
|
|
222
|
+
details = error.get("details", {})
|
|
223
|
+
|
|
224
|
+
# Get appropriate exception class
|
|
225
|
+
exc_class = ERROR_CODE_MAP.get(code, OfSpectrumError)
|
|
226
|
+
|
|
227
|
+
# Build kwargs based on exception type
|
|
228
|
+
kwargs = {
|
|
229
|
+
"message": message,
|
|
230
|
+
"code": code,
|
|
231
|
+
"status_code": status_code,
|
|
232
|
+
"details": details,
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
if exc_class == RateLimitError:
|
|
236
|
+
kwargs["retry_after"] = details.get("retry_after")
|
|
237
|
+
elif exc_class == QuotaExceededError:
|
|
238
|
+
kwargs["service"] = details.get("service")
|
|
239
|
+
kwargs["remaining"] = details.get("remaining", 0)
|
|
240
|
+
kwargs["reset_at"] = details.get("reset_at")
|
|
241
|
+
elif exc_class == ResourceNotFoundError:
|
|
242
|
+
kwargs["resource_type"] = details.get("resource_type")
|
|
243
|
+
kwargs["resource_id"] = details.get("resource_id")
|
|
244
|
+
elif exc_class == ValidationError:
|
|
245
|
+
kwargs["field"] = details.get("field")
|
|
246
|
+
|
|
247
|
+
raise exc_class(**kwargs)
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
"""
|
|
2
|
+
OfSpectrum SDK Data Models
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from .token import Token, TokenCreateParams, TokenUpdateParams
|
|
6
|
+
from .notebook import Notebook, NotebookMedia, NotebookCreateParams
|
|
7
|
+
from .audio import EncodeResult, DecodeResult
|
|
8
|
+
from .quota import Quota, QuotaList
|
|
9
|
+
|
|
10
|
+
__all__ = [
|
|
11
|
+
"Token",
|
|
12
|
+
"TokenCreateParams",
|
|
13
|
+
"TokenUpdateParams",
|
|
14
|
+
"Notebook",
|
|
15
|
+
"NotebookMedia",
|
|
16
|
+
"NotebookCreateParams",
|
|
17
|
+
"EncodeResult",
|
|
18
|
+
"DecodeResult",
|
|
19
|
+
"Quota",
|
|
20
|
+
"QuotaList",
|
|
21
|
+
]
|