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