turboapi-sdk 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- turboapi/__init__.py +56 -0
- turboapi/client.py +377 -0
- turboapi/errors.py +118 -0
- turboapi/models.py +273 -0
- turboapi_sdk-0.1.0.dist-info/METADATA +139 -0
- turboapi_sdk-0.1.0.dist-info/RECORD +7 -0
- turboapi_sdk-0.1.0.dist-info/WHEEL +4 -0
turboapi/__init__.py
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
"""
|
|
2
|
+
TurboAPI SDK - Call AI services through the TurboAPI platform.
|
|
3
|
+
|
|
4
|
+
Usage:
|
|
5
|
+
from turboapi import TurboAPIClient
|
|
6
|
+
|
|
7
|
+
client = TurboAPIClient(api_key="tbp_xxxxx")
|
|
8
|
+
|
|
9
|
+
# Create a task and wait for result
|
|
10
|
+
result = client.call.create_and_wait(
|
|
11
|
+
slug_id="karaoke-maker",
|
|
12
|
+
input={"audio_file": "https://..."},
|
|
13
|
+
timeout=300,
|
|
14
|
+
)
|
|
15
|
+
print(result.output)
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
from turboapi.client import TurboAPIClient
|
|
19
|
+
from turboapi.models import (
|
|
20
|
+
APIListData,
|
|
21
|
+
APIResponse,
|
|
22
|
+
CategoryResponse,
|
|
23
|
+
TaskResponse,
|
|
24
|
+
TaskLogItem,
|
|
25
|
+
TaskListResponse,
|
|
26
|
+
TaskStatus,
|
|
27
|
+
TaskPriority,
|
|
28
|
+
)
|
|
29
|
+
from turboapi.errors import (
|
|
30
|
+
TurboAPIError,
|
|
31
|
+
AuthenticationError,
|
|
32
|
+
RateLimitError,
|
|
33
|
+
TaskError,
|
|
34
|
+
NotFoundError,
|
|
35
|
+
ValidationError,
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
__all__ = [
|
|
39
|
+
"TurboAPIClient",
|
|
40
|
+
# Models
|
|
41
|
+
"APIListData",
|
|
42
|
+
"APIResponse",
|
|
43
|
+
"CategoryResponse",
|
|
44
|
+
"TaskResponse",
|
|
45
|
+
"TaskLogItem",
|
|
46
|
+
"TaskListResponse",
|
|
47
|
+
"TaskStatus",
|
|
48
|
+
"TaskPriority",
|
|
49
|
+
# Errors
|
|
50
|
+
"TurboAPIError",
|
|
51
|
+
"AuthenticationError",
|
|
52
|
+
"RateLimitError",
|
|
53
|
+
"TaskError",
|
|
54
|
+
"NotFoundError",
|
|
55
|
+
"ValidationError",
|
|
56
|
+
]
|
turboapi/client.py
ADDED
|
@@ -0,0 +1,377 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import time
|
|
4
|
+
from typing import Any, Dict, Optional
|
|
5
|
+
|
|
6
|
+
import httpx
|
|
7
|
+
|
|
8
|
+
from turboapi.errors import (
|
|
9
|
+
TurboAPIError,
|
|
10
|
+
NetworkError,
|
|
11
|
+
TimeoutError,
|
|
12
|
+
)
|
|
13
|
+
from turboapi.models import (
|
|
14
|
+
APIListData,
|
|
15
|
+
APIResponse,
|
|
16
|
+
CategoryResponse,
|
|
17
|
+
TaskListResponse,
|
|
18
|
+
TaskResponse,
|
|
19
|
+
TaskStatus,
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
DEFAULT_BASE_URL = "https://api.turboapi.ai/api/v1"
|
|
23
|
+
DEFAULT_TIMEOUT = 30.0
|
|
24
|
+
DEFAULT_POLL_INTERVAL = 2.0
|
|
25
|
+
DEFAULT_POLL_TIMEOUT = 300.0
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class _CallModule:
|
|
29
|
+
"""Task creation and management."""
|
|
30
|
+
|
|
31
|
+
def __init__(self, client: "TurboAPIClient"):
|
|
32
|
+
self._client = client
|
|
33
|
+
|
|
34
|
+
def create(
|
|
35
|
+
self,
|
|
36
|
+
slug_id: str,
|
|
37
|
+
input: Dict[str, Any],
|
|
38
|
+
*,
|
|
39
|
+
prefer_wait: bool = False,
|
|
40
|
+
) -> TaskResponse:
|
|
41
|
+
"""Submit a task for execution.
|
|
42
|
+
|
|
43
|
+
Args:
|
|
44
|
+
slug_id: The API slug identifier (e.g. 'karaoke-maker').
|
|
45
|
+
input: Input parameters for the API.
|
|
46
|
+
prefer_wait: If True, server will attempt synchronous execution.
|
|
47
|
+
|
|
48
|
+
Returns:
|
|
49
|
+
A TaskResponse with task_id and initial status.
|
|
50
|
+
"""
|
|
51
|
+
body: Dict[str, Any] = {"slug_id": slug_id, "input": input}
|
|
52
|
+
headers = {}
|
|
53
|
+
if prefer_wait:
|
|
54
|
+
headers["Prefer-Wait"] = "wait"
|
|
55
|
+
|
|
56
|
+
data = self._client._request("POST", "/call", json=body, headers=headers)
|
|
57
|
+
return TaskResponse.from_dict(data)
|
|
58
|
+
|
|
59
|
+
def get(self, task_id: str) -> TaskResponse:
|
|
60
|
+
"""Get the current status and details of a task.
|
|
61
|
+
|
|
62
|
+
Args:
|
|
63
|
+
task_id: The task ID returned from create().
|
|
64
|
+
|
|
65
|
+
Returns:
|
|
66
|
+
Current TaskResponse with status and optional output.
|
|
67
|
+
"""
|
|
68
|
+
data = self._client._request("GET", f"/call/{task_id}")
|
|
69
|
+
return TaskResponse.from_dict(data)
|
|
70
|
+
|
|
71
|
+
def cancel(self, task_id: str) -> None:
|
|
72
|
+
"""Request cancellation of a queued task.
|
|
73
|
+
|
|
74
|
+
Args:
|
|
75
|
+
task_id: The task ID to cancel.
|
|
76
|
+
|
|
77
|
+
Raises:
|
|
78
|
+
NotFoundError: If the task doesn't exist.
|
|
79
|
+
TaskError: If the task cannot be cancelled (already running/complete).
|
|
80
|
+
"""
|
|
81
|
+
self._client._request("POST", f"/call/{task_id}/cancel")
|
|
82
|
+
|
|
83
|
+
def create_and_wait(
|
|
84
|
+
self,
|
|
85
|
+
slug_id: str,
|
|
86
|
+
input: Dict[str, Any],
|
|
87
|
+
*,
|
|
88
|
+
timeout: float = DEFAULT_POLL_TIMEOUT,
|
|
89
|
+
poll_interval: float = DEFAULT_POLL_INTERVAL,
|
|
90
|
+
) -> TaskResponse:
|
|
91
|
+
"""Submit a task and block until it completes or fails.
|
|
92
|
+
|
|
93
|
+
This is a convenience method that:
|
|
94
|
+
1. Creates the task via create()
|
|
95
|
+
2. Polls get() until status is terminal
|
|
96
|
+
3. Returns the final result
|
|
97
|
+
|
|
98
|
+
Args:
|
|
99
|
+
slug_id: The API slug identifier.
|
|
100
|
+
input: Input parameters for the API.
|
|
101
|
+
timeout: Maximum total wait time in seconds.
|
|
102
|
+
poll_interval: Seconds between status checks.
|
|
103
|
+
|
|
104
|
+
Returns:
|
|
105
|
+
Final TaskResponse with output on success.
|
|
106
|
+
|
|
107
|
+
Raises:
|
|
108
|
+
TimeoutError: If the task doesn't complete within timeout.
|
|
109
|
+
"""
|
|
110
|
+
task = self.create(slug_id, input)
|
|
111
|
+
|
|
112
|
+
deadline = time.monotonic() + timeout
|
|
113
|
+
while time.monotonic() < deadline:
|
|
114
|
+
task = self.get(task.task_id)
|
|
115
|
+
|
|
116
|
+
if task.status and task.status.is_terminal:
|
|
117
|
+
return task
|
|
118
|
+
|
|
119
|
+
remaining = deadline - time.monotonic()
|
|
120
|
+
sleep = min(poll_interval, remaining)
|
|
121
|
+
if sleep <= 0:
|
|
122
|
+
break
|
|
123
|
+
time.sleep(sleep)
|
|
124
|
+
|
|
125
|
+
raise TimeoutError(
|
|
126
|
+
f"Task {task.task_id} did not complete within {timeout}s",
|
|
127
|
+
error_code="TIMEOUT",
|
|
128
|
+
details={
|
|
129
|
+
"task_id": task.task_id,
|
|
130
|
+
"last_status": task.status.value if task.status else "unknown",
|
|
131
|
+
},
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
class _TasksModule:
|
|
136
|
+
"""Task listing and querying for the authenticated user."""
|
|
137
|
+
|
|
138
|
+
def __init__(self, client: "TurboAPIClient"):
|
|
139
|
+
self._client = client
|
|
140
|
+
|
|
141
|
+
def list(
|
|
142
|
+
self,
|
|
143
|
+
*,
|
|
144
|
+
status: Optional[TaskStatus] = None,
|
|
145
|
+
api_slug: Optional[str] = None,
|
|
146
|
+
page: int = 1,
|
|
147
|
+
page_size: int = 20,
|
|
148
|
+
) -> TaskListResponse:
|
|
149
|
+
"""List tasks for the authenticated user.
|
|
150
|
+
|
|
151
|
+
Args:
|
|
152
|
+
status: Filter by task status.
|
|
153
|
+
api_slug: Filter by API slug.
|
|
154
|
+
page: Page number (1-indexed).
|
|
155
|
+
page_size: Items per page (max 100).
|
|
156
|
+
|
|
157
|
+
Returns:
|
|
158
|
+
A paginated list of TaskResponse items.
|
|
159
|
+
"""
|
|
160
|
+
params: Dict[str, Any] = {"page": page, "page_size": page_size}
|
|
161
|
+
if status:
|
|
162
|
+
params["status"] = status.value
|
|
163
|
+
if api_slug:
|
|
164
|
+
params["api_slug"] = api_slug
|
|
165
|
+
|
|
166
|
+
data = self._client._request("GET", "/tasks", params=params)
|
|
167
|
+
return TaskListResponse.from_dict(data)
|
|
168
|
+
|
|
169
|
+
def get(self, task_id: str) -> TaskResponse:
|
|
170
|
+
"""Get a specific task's detail.
|
|
171
|
+
|
|
172
|
+
Args:
|
|
173
|
+
task_id: The task ID.
|
|
174
|
+
|
|
175
|
+
Returns:
|
|
176
|
+
Full TaskResponse for the given task.
|
|
177
|
+
"""
|
|
178
|
+
data = self._client._request("GET", f"/tasks/{task_id}")
|
|
179
|
+
return TaskResponse.from_dict(data)
|
|
180
|
+
|
|
181
|
+
def logs(
|
|
182
|
+
self,
|
|
183
|
+
task_id: str,
|
|
184
|
+
*,
|
|
185
|
+
page: int = 1,
|
|
186
|
+
page_size: int = 50,
|
|
187
|
+
) -> Dict[str, Any]:
|
|
188
|
+
"""Get execution logs for a task.
|
|
189
|
+
|
|
190
|
+
Args:
|
|
191
|
+
task_id: The task ID.
|
|
192
|
+
page: Page number (1-indexed).
|
|
193
|
+
page_size: Items per page (max 100).
|
|
194
|
+
|
|
195
|
+
Returns:
|
|
196
|
+
Raw response data containing log items.
|
|
197
|
+
"""
|
|
198
|
+
params: Dict[str, Any] = {"page": page, "page_size": page_size}
|
|
199
|
+
return self._client._request("GET", f"/tasks/{task_id}/logs", params=params)
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
class _ApisModule:
|
|
203
|
+
"""API Market listing and discovery."""
|
|
204
|
+
|
|
205
|
+
def __init__(self, client: "TurboAPIClient"):
|
|
206
|
+
self._client = client
|
|
207
|
+
|
|
208
|
+
def list(
|
|
209
|
+
self,
|
|
210
|
+
*,
|
|
211
|
+
page: int = 1,
|
|
212
|
+
page_size: int = 20,
|
|
213
|
+
category: Optional[str] = None,
|
|
214
|
+
tags: Optional[List[str]] = None,
|
|
215
|
+
search: Optional[str] = None,
|
|
216
|
+
sort_by: str = "created_at",
|
|
217
|
+
sort_order: str = "desc",
|
|
218
|
+
) -> APIListData:
|
|
219
|
+
"""List available APIs in the marketplace.
|
|
220
|
+
|
|
221
|
+
Args:
|
|
222
|
+
page: Page number (1-indexed).
|
|
223
|
+
page_size: Items per page (max 100).
|
|
224
|
+
category: Filter by category slug.
|
|
225
|
+
tags: Filter by tag names.
|
|
226
|
+
search: Search in name and description.
|
|
227
|
+
sort_by: Sort field ('created_at', 'name', 'popularity').
|
|
228
|
+
sort_order: Sort order ('asc' or 'desc').
|
|
229
|
+
|
|
230
|
+
Returns:
|
|
231
|
+
A paginated list of APIResponse items.
|
|
232
|
+
"""
|
|
233
|
+
params: Dict[str, Any] = {
|
|
234
|
+
"page": page,
|
|
235
|
+
"page_size": page_size,
|
|
236
|
+
"sort_by": sort_by,
|
|
237
|
+
"sort_order": sort_order,
|
|
238
|
+
}
|
|
239
|
+
if category:
|
|
240
|
+
params["category"] = category
|
|
241
|
+
if tags:
|
|
242
|
+
params["tags"] = ",".join(tags)
|
|
243
|
+
if search:
|
|
244
|
+
params["search"] = search
|
|
245
|
+
|
|
246
|
+
data = self._client._request("GET", "/apis", params=params)
|
|
247
|
+
return APIListData.from_dict(data)
|
|
248
|
+
|
|
249
|
+
def get(self, slug: str) -> APIResponse:
|
|
250
|
+
"""Get details of a specific API by its slug.
|
|
251
|
+
|
|
252
|
+
Args:
|
|
253
|
+
slug: The API slug identifier (e.g. 'karaoke-maker').
|
|
254
|
+
|
|
255
|
+
Returns:
|
|
256
|
+
Full APIResponse with parameters and documentation.
|
|
257
|
+
"""
|
|
258
|
+
data = self._client._request("GET", f"/apis/{slug}")
|
|
259
|
+
return APIResponse.from_dict(data)
|
|
260
|
+
|
|
261
|
+
def categories(
|
|
262
|
+
self,
|
|
263
|
+
*,
|
|
264
|
+
page: int = 1,
|
|
265
|
+
page_size: int = 20,
|
|
266
|
+
) -> Dict[str, Any]:
|
|
267
|
+
"""List API categories.
|
|
268
|
+
|
|
269
|
+
Args:
|
|
270
|
+
page: Page number (1-indexed).
|
|
271
|
+
page_size: Items per page.
|
|
272
|
+
|
|
273
|
+
Returns:
|
|
274
|
+
Raw response data containing category items.
|
|
275
|
+
"""
|
|
276
|
+
params: Dict[str, Any] = {"page": page, "page_size": page_size}
|
|
277
|
+
return self._client._request("GET", "/apis/categories", params=params)
|
|
278
|
+
|
|
279
|
+
|
|
280
|
+
class TurboAPIClient:
|
|
281
|
+
"""TurboAPI client for calling AI services.
|
|
282
|
+
|
|
283
|
+
The client is configured once and provides access to the Call and Tasks APIs
|
|
284
|
+
via the `.call` and `.tasks` attributes.
|
|
285
|
+
|
|
286
|
+
Authentication is handled via an API key, passed as a Bearer token.
|
|
287
|
+
|
|
288
|
+
Usage:
|
|
289
|
+
client = TurboAPIClient(api_key="tbp_xxxxx")
|
|
290
|
+
|
|
291
|
+
# Quick call with blocking wait
|
|
292
|
+
result = client.call.create_and_wait("karaoke-maker", {
|
|
293
|
+
"audio_file": "https://...",
|
|
294
|
+
"task_key": "demo-001",
|
|
295
|
+
})
|
|
296
|
+
print(result.output)
|
|
297
|
+
|
|
298
|
+
# Or manual two-step
|
|
299
|
+
task = client.call.create("some-api", {"key": "value"})
|
|
300
|
+
status = client.call.get(task.task_id)
|
|
301
|
+
client.call.cancel(task.task_id)
|
|
302
|
+
"""
|
|
303
|
+
|
|
304
|
+
def __init__(
|
|
305
|
+
self,
|
|
306
|
+
api_key: Optional[str] = None,
|
|
307
|
+
*,
|
|
308
|
+
base_url: str = DEFAULT_BASE_URL,
|
|
309
|
+
timeout: float = DEFAULT_TIMEOUT,
|
|
310
|
+
):
|
|
311
|
+
self.base_url = base_url.rstrip("/")
|
|
312
|
+
self.timeout = timeout
|
|
313
|
+
|
|
314
|
+
self._headers: Dict[str, str] = {
|
|
315
|
+
"Content-Type": "application/json",
|
|
316
|
+
"Accept": "application/json",
|
|
317
|
+
}
|
|
318
|
+
if api_key:
|
|
319
|
+
self._headers["Authorization"] = f"Bearer {api_key}"
|
|
320
|
+
|
|
321
|
+
self.apis = _ApisModule(self)
|
|
322
|
+
self.call = _CallModule(self)
|
|
323
|
+
self.tasks = _TasksModule(self)
|
|
324
|
+
|
|
325
|
+
def _request(
|
|
326
|
+
self,
|
|
327
|
+
method: str,
|
|
328
|
+
path: str,
|
|
329
|
+
*,
|
|
330
|
+
json: Optional[Dict[str, Any]] = None,
|
|
331
|
+
params: Optional[Dict[str, Any]] = None,
|
|
332
|
+
headers: Optional[Dict[str, str]] = None,
|
|
333
|
+
) -> Any:
|
|
334
|
+
"""Make an HTTP request to the TurboAPI backend.
|
|
335
|
+
|
|
336
|
+
Args:
|
|
337
|
+
method: HTTP method.
|
|
338
|
+
path: URL path (appended to base_url).
|
|
339
|
+
json: JSON body.
|
|
340
|
+
params: Query parameters.
|
|
341
|
+
headers: Additional request headers.
|
|
342
|
+
|
|
343
|
+
Returns:
|
|
344
|
+
The 'data' field from the API response on success.
|
|
345
|
+
|
|
346
|
+
Raises:
|
|
347
|
+
TurboAPIError subclasses on failure.
|
|
348
|
+
"""
|
|
349
|
+
url = f"{self.base_url}{path}"
|
|
350
|
+
req_headers = {**self._headers, **(headers or {})}
|
|
351
|
+
|
|
352
|
+
try:
|
|
353
|
+
with httpx.Client(timeout=self.timeout) as http:
|
|
354
|
+
response = http.request(
|
|
355
|
+
method,
|
|
356
|
+
url,
|
|
357
|
+
json=json,
|
|
358
|
+
params=params,
|
|
359
|
+
headers=req_headers,
|
|
360
|
+
)
|
|
361
|
+
except httpx.TimeoutException as e:
|
|
362
|
+
raise TimeoutError(
|
|
363
|
+
f"Request timed out after {self.timeout}s",
|
|
364
|
+
error_code="TIMEOUT_ERROR",
|
|
365
|
+
) from e
|
|
366
|
+
except httpx.TransportError as e:
|
|
367
|
+
raise NetworkError(
|
|
368
|
+
f"Network error: {e}",
|
|
369
|
+
error_code="NETWORK_ERROR",
|
|
370
|
+
) from e
|
|
371
|
+
|
|
372
|
+
body = response.json()
|
|
373
|
+
|
|
374
|
+
if not body.get("success", False):
|
|
375
|
+
raise TurboAPIError.from_response(response.status_code, body)
|
|
376
|
+
|
|
377
|
+
return body.get("data")
|
turboapi/errors.py
ADDED
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Exception hierarchy for the TurboAPI SDK.
|
|
3
|
+
|
|
4
|
+
Maps backend error codes to typed Python exceptions.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from typing import Any, Dict, Optional
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class TurboAPIError(Exception):
|
|
11
|
+
"""Base exception for all TurboAPI SDK errors."""
|
|
12
|
+
|
|
13
|
+
def __init__(
|
|
14
|
+
self,
|
|
15
|
+
message: str,
|
|
16
|
+
status_code: Optional[int] = None,
|
|
17
|
+
error_code: Optional[str] = None,
|
|
18
|
+
details: Optional[Dict[str, Any]] = None,
|
|
19
|
+
request_id: Optional[str] = None,
|
|
20
|
+
):
|
|
21
|
+
self.status_code = status_code
|
|
22
|
+
self.error_code = error_code or "UNKNOWN"
|
|
23
|
+
self.details = details
|
|
24
|
+
self.request_id = request_id
|
|
25
|
+
super().__init__(message)
|
|
26
|
+
|
|
27
|
+
@property
|
|
28
|
+
def message(self) -> str:
|
|
29
|
+
return str(self.args[0]) if self.args else ""
|
|
30
|
+
|
|
31
|
+
@classmethod
|
|
32
|
+
def from_response(
|
|
33
|
+
cls,
|
|
34
|
+
status_code: int,
|
|
35
|
+
body: dict,
|
|
36
|
+
) -> "TurboAPIError":
|
|
37
|
+
"""Create an appropriate error from an API error response."""
|
|
38
|
+
error = body.get("error", {})
|
|
39
|
+
error_code = error.get("code", "UNKNOWN")
|
|
40
|
+
message = error.get("message", "Unknown error")
|
|
41
|
+
details = error.get("details")
|
|
42
|
+
meta = body.get("meta", {})
|
|
43
|
+
request_id = meta.get("request_id")
|
|
44
|
+
|
|
45
|
+
# Map error codes to typed exceptions
|
|
46
|
+
error_class = _ERROR_CODE_MAP.get(error_code, TurboAPIError)
|
|
47
|
+
|
|
48
|
+
return error_class(
|
|
49
|
+
message=message,
|
|
50
|
+
status_code=status_code,
|
|
51
|
+
error_code=error_code,
|
|
52
|
+
details=details,
|
|
53
|
+
request_id=request_id,
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
class AuthenticationError(TurboAPIError):
|
|
58
|
+
"""Invalid or missing API key / authentication."""
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
class RateLimitError(TurboAPIError):
|
|
62
|
+
"""Rate limit exceeded."""
|
|
63
|
+
|
|
64
|
+
@property
|
|
65
|
+
def retry_after(self) -> Optional[int]:
|
|
66
|
+
"""Get the retry-after duration in seconds, if provided."""
|
|
67
|
+
if self.details and "retry_after" in self.details:
|
|
68
|
+
return int(self.details["retry_after"])
|
|
69
|
+
return None
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
class NotFoundError(TurboAPIError):
|
|
73
|
+
"""Resource not found."""
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
class ValidationError(TurboAPIError):
|
|
77
|
+
"""Request validation failed."""
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
class TaskError(TurboAPIError):
|
|
81
|
+
"""Task execution failed."""
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
class ServerError(TurboAPIError):
|
|
85
|
+
"""Backend server error (5xx)."""
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
class NetworkError(TurboAPIError):
|
|
89
|
+
"""Network/connection error."""
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
class TimeoutError(TurboAPIError):
|
|
93
|
+
"""Request or task timeout."""
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
# Map backend error code prefixes to exception classes
|
|
97
|
+
_ERROR_CODE_MAP: Dict[str, type[TurboAPIError]] = {
|
|
98
|
+
# Auth errors
|
|
99
|
+
"AUTH_": AuthenticationError,
|
|
100
|
+
"UNAUTHORIZED": AuthenticationError,
|
|
101
|
+
"AUTH_TOKEN_EXPIRED": AuthenticationError,
|
|
102
|
+
"AUTH_INVALID_TOKEN": AuthenticationError,
|
|
103
|
+
# General errors
|
|
104
|
+
"NOT_FOUND": NotFoundError,
|
|
105
|
+
"API_NOT_FOUND": NotFoundError,
|
|
106
|
+
"USER_NOT_FOUND": NotFoundError,
|
|
107
|
+
"RATE_LIMITED": RateLimitError,
|
|
108
|
+
"RATE_LIMIT": RateLimitError,
|
|
109
|
+
"VALIDATION_ERROR": ValidationError,
|
|
110
|
+
"VALIDATION_": ValidationError,
|
|
111
|
+
"BAD_REQUEST": ValidationError,
|
|
112
|
+
"FORBIDDEN": AuthenticationError,
|
|
113
|
+
"CALL_": TaskError,
|
|
114
|
+
"CALL_FAILED": ServerError,
|
|
115
|
+
"INTERNAL_ERROR": ServerError,
|
|
116
|
+
"INTERNAL_": ServerError,
|
|
117
|
+
"POINTS_INSUFFICIENT": TaskError,
|
|
118
|
+
}
|
turboapi/models.py
ADDED
|
@@ -0,0 +1,273 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Data models for the TurboAPI SDK.
|
|
3
|
+
|
|
4
|
+
These mirror the backend schemas but are SDK-specific,
|
|
5
|
+
not tied to backend implementation details.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from dataclasses import dataclass, field
|
|
9
|
+
from datetime import datetime
|
|
10
|
+
from enum import Enum
|
|
11
|
+
from typing import Any, Dict, List, Optional
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class TaskStatus(str, Enum):
|
|
15
|
+
"""Task status enum matching backend TaskStatus."""
|
|
16
|
+
|
|
17
|
+
PENDING = "pending"
|
|
18
|
+
QUEUED = "queued"
|
|
19
|
+
STARTING = "starting"
|
|
20
|
+
PROCESSING = "processing"
|
|
21
|
+
SUCCEEDED = "succeeded"
|
|
22
|
+
FAILED = "failed"
|
|
23
|
+
CANCELLED = "cancelled"
|
|
24
|
+
TIMEOUT = "timeout"
|
|
25
|
+
|
|
26
|
+
@property
|
|
27
|
+
def is_terminal(self) -> bool:
|
|
28
|
+
"""Check if this is a terminal (non-transient) status."""
|
|
29
|
+
return self in {
|
|
30
|
+
TaskStatus.SUCCEEDED,
|
|
31
|
+
TaskStatus.FAILED,
|
|
32
|
+
TaskStatus.CANCELLED,
|
|
33
|
+
TaskStatus.TIMEOUT,
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
@property
|
|
37
|
+
def is_active(self) -> bool:
|
|
38
|
+
"""Check if the task is still being processed."""
|
|
39
|
+
return self in {
|
|
40
|
+
TaskStatus.QUEUED,
|
|
41
|
+
TaskStatus.STARTING,
|
|
42
|
+
TaskStatus.PROCESSING,
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class TaskPriority(str, Enum):
|
|
47
|
+
"""Task priority levels."""
|
|
48
|
+
|
|
49
|
+
LOW = "low"
|
|
50
|
+
NORMAL = "normal"
|
|
51
|
+
HIGH = "high"
|
|
52
|
+
CRITICAL = "critical"
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
@dataclass
|
|
56
|
+
class TaskLogItem:
|
|
57
|
+
"""A single log entry for a task."""
|
|
58
|
+
|
|
59
|
+
timestamp: str
|
|
60
|
+
level: str
|
|
61
|
+
message: str
|
|
62
|
+
details: Optional[Dict[str, Any]] = None
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
@dataclass
|
|
66
|
+
class TaskResponse:
|
|
67
|
+
"""Full task response with status and optional output."""
|
|
68
|
+
|
|
69
|
+
id: str
|
|
70
|
+
task_id: str
|
|
71
|
+
name: str
|
|
72
|
+
status: Optional[TaskStatus] = None
|
|
73
|
+
progress: int = 0
|
|
74
|
+
total_items: int = 0
|
|
75
|
+
completed_items: int = 0
|
|
76
|
+
failed_items: int = 0
|
|
77
|
+
message: Optional[str] = None
|
|
78
|
+
error_message: Optional[str] = None
|
|
79
|
+
output: Optional[Any] = None
|
|
80
|
+
api_slug: Optional[str] = None
|
|
81
|
+
priority: Optional[str] = None
|
|
82
|
+
prediction_id: Optional[str] = None
|
|
83
|
+
retry_count: int = 0
|
|
84
|
+
max_retries: int = 3
|
|
85
|
+
logs: Optional[List[TaskLogItem]] = None
|
|
86
|
+
created_at: Optional[datetime] = None
|
|
87
|
+
started_at: Optional[datetime] = None
|
|
88
|
+
completed_at: Optional[datetime] = None
|
|
89
|
+
expires_at: Optional[datetime] = None
|
|
90
|
+
|
|
91
|
+
@classmethod
|
|
92
|
+
def from_dict(cls, data: dict) -> "TaskResponse":
|
|
93
|
+
"""Create from API response dict (data field)."""
|
|
94
|
+
status_raw = data.get("status")
|
|
95
|
+
status = TaskStatus(status_raw) if status_raw else None
|
|
96
|
+
|
|
97
|
+
logs_raw = data.get("logs")
|
|
98
|
+
logs = None
|
|
99
|
+
if logs_raw:
|
|
100
|
+
logs = [
|
|
101
|
+
TaskLogItem(
|
|
102
|
+
timestamp=log["timestamp"],
|
|
103
|
+
level=log["level"],
|
|
104
|
+
message=log["message"],
|
|
105
|
+
details=log.get("details"),
|
|
106
|
+
)
|
|
107
|
+
for log in logs_raw
|
|
108
|
+
]
|
|
109
|
+
|
|
110
|
+
for dt_field in ("created_at", "started_at", "completed_at", "expires_at"):
|
|
111
|
+
val = data.get(dt_field)
|
|
112
|
+
if val and isinstance(val, str):
|
|
113
|
+
try:
|
|
114
|
+
data[dt_field] = datetime.fromisoformat(
|
|
115
|
+
val.replace("Z", "+00:00")
|
|
116
|
+
)
|
|
117
|
+
except (ValueError, TypeError):
|
|
118
|
+
data[dt_field] = val
|
|
119
|
+
|
|
120
|
+
return cls(
|
|
121
|
+
id=data.get("id") or data.get("task_id", ""),
|
|
122
|
+
task_id=data.get("task_id") or data.get("id", ""),
|
|
123
|
+
name=data.get("name", ""),
|
|
124
|
+
status=status,
|
|
125
|
+
progress=data.get("progress", 0),
|
|
126
|
+
total_items=data.get("total_items", 0),
|
|
127
|
+
completed_items=data.get("completed_items", 0),
|
|
128
|
+
failed_items=data.get("failed_items", 0),
|
|
129
|
+
message=data.get("message"),
|
|
130
|
+
error_message=data.get("error_message"),
|
|
131
|
+
output=data.get("output"),
|
|
132
|
+
api_slug=data.get("api_slug"),
|
|
133
|
+
priority=data.get("priority"),
|
|
134
|
+
prediction_id=data.get("prediction_id"),
|
|
135
|
+
retry_count=data.get("retry_count", 0),
|
|
136
|
+
max_retries=data.get("max_retries", 3),
|
|
137
|
+
logs=logs,
|
|
138
|
+
created_at=data.get("created_at"),
|
|
139
|
+
started_at=data.get("started_at"),
|
|
140
|
+
completed_at=data.get("completed_at"),
|
|
141
|
+
expires_at=data.get("expires_at"),
|
|
142
|
+
)
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
@dataclass
|
|
146
|
+
class TaskListResponse:
|
|
147
|
+
"""Paginated task list."""
|
|
148
|
+
|
|
149
|
+
items: List[TaskResponse]
|
|
150
|
+
total: int
|
|
151
|
+
page: int
|
|
152
|
+
page_size: int
|
|
153
|
+
total_pages: int
|
|
154
|
+
|
|
155
|
+
@classmethod
|
|
156
|
+
def from_dict(cls, data: dict) -> "TaskListResponse":
|
|
157
|
+
items_raw = data.get("items", [])
|
|
158
|
+
pagination = data.get("pagination", {})
|
|
159
|
+
items = [TaskResponse.from_dict(item) for item in items_raw]
|
|
160
|
+
return cls(
|
|
161
|
+
items=items,
|
|
162
|
+
total=pagination.get("total", 0),
|
|
163
|
+
page=pagination.get("page", 1),
|
|
164
|
+
page_size=pagination.get("page_size", 20),
|
|
165
|
+
total_pages=pagination.get("total_pages", 0),
|
|
166
|
+
)
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
@dataclass
|
|
170
|
+
class APIResponse:
|
|
171
|
+
id: str
|
|
172
|
+
name: str
|
|
173
|
+
slug: str
|
|
174
|
+
description: Optional[str] = None
|
|
175
|
+
category_id: Optional[int] = None
|
|
176
|
+
endpoint: Optional[str] = None
|
|
177
|
+
method: Optional[str] = None
|
|
178
|
+
documentation: Optional[Dict[str, Any]] = None
|
|
179
|
+
pricing: Optional[Dict[str, Any]] = None
|
|
180
|
+
status: str = "published"
|
|
181
|
+
is_official: bool = False
|
|
182
|
+
api_type: Optional[str] = None
|
|
183
|
+
handler_name: Optional[str] = None
|
|
184
|
+
upstream_platform_id: Optional[str] = None
|
|
185
|
+
parameters: Optional[Dict[str, Any]] = None
|
|
186
|
+
tags: List[str] = field(default_factory=list)
|
|
187
|
+
config_json: Optional[Dict[str, Any]] = None
|
|
188
|
+
show_detail: bool = True
|
|
189
|
+
created_at: Optional[datetime] = None
|
|
190
|
+
updated_at: Optional[datetime] = None
|
|
191
|
+
|
|
192
|
+
@classmethod
|
|
193
|
+
def from_dict(cls, data: dict) -> "APIResponse":
|
|
194
|
+
for dt_field in ("created_at", "updated_at"):
|
|
195
|
+
val = data.get(dt_field)
|
|
196
|
+
if val and isinstance(val, str):
|
|
197
|
+
try:
|
|
198
|
+
data[dt_field] = datetime.fromisoformat(val.replace("Z", "+00:00"))
|
|
199
|
+
except (ValueError, TypeError):
|
|
200
|
+
pass
|
|
201
|
+
return cls(
|
|
202
|
+
id=str(data.get("id", "")),
|
|
203
|
+
name=data.get("name", ""),
|
|
204
|
+
slug=data.get("slug", ""),
|
|
205
|
+
description=data.get("description"),
|
|
206
|
+
category_id=data.get("category_id"),
|
|
207
|
+
endpoint=data.get("endpoint"),
|
|
208
|
+
method=data.get("method"),
|
|
209
|
+
documentation=data.get("documentation"),
|
|
210
|
+
pricing=data.get("pricing"),
|
|
211
|
+
status=data.get("status", "published"),
|
|
212
|
+
is_official=data.get("is_official", False),
|
|
213
|
+
api_type=data.get("api_type"),
|
|
214
|
+
handler_name=data.get("handler_name"),
|
|
215
|
+
upstream_platform_id=data.get("upstream_platform_id"),
|
|
216
|
+
parameters=data.get("parameters"),
|
|
217
|
+
tags=data.get("tags", []),
|
|
218
|
+
config_json=data.get("config_json"),
|
|
219
|
+
show_detail=data.get("show_detail", True),
|
|
220
|
+
created_at=data.get("created_at"),
|
|
221
|
+
updated_at=data.get("updated_at"),
|
|
222
|
+
)
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
@dataclass
|
|
226
|
+
class CategoryResponse:
|
|
227
|
+
id: int
|
|
228
|
+
name: str
|
|
229
|
+
slug: str
|
|
230
|
+
description: Optional[str] = None
|
|
231
|
+
icon_url: Optional[str] = None
|
|
232
|
+
sort_order: int = 0
|
|
233
|
+
created_at: Optional[datetime] = None
|
|
234
|
+
|
|
235
|
+
@classmethod
|
|
236
|
+
def from_dict(cls, data: dict) -> "CategoryResponse":
|
|
237
|
+
val = data.get("created_at")
|
|
238
|
+
if val and isinstance(val, str):
|
|
239
|
+
try:
|
|
240
|
+
data["created_at"] = datetime.fromisoformat(val.replace("Z", "+00:00"))
|
|
241
|
+
except (ValueError, TypeError):
|
|
242
|
+
pass
|
|
243
|
+
return cls(
|
|
244
|
+
id=int(data.get("id", 0)),
|
|
245
|
+
name=data.get("name", ""),
|
|
246
|
+
slug=data.get("slug", ""),
|
|
247
|
+
description=data.get("description"),
|
|
248
|
+
icon_url=data.get("icon_url"),
|
|
249
|
+
sort_order=data.get("sort_order", 0),
|
|
250
|
+
created_at=data.get("created_at"),
|
|
251
|
+
)
|
|
252
|
+
|
|
253
|
+
|
|
254
|
+
@dataclass
|
|
255
|
+
class APIListData:
|
|
256
|
+
items: List[APIResponse]
|
|
257
|
+
total: int
|
|
258
|
+
page: int
|
|
259
|
+
page_size: int
|
|
260
|
+
total_pages: int
|
|
261
|
+
|
|
262
|
+
@classmethod
|
|
263
|
+
def from_dict(cls, data: dict) -> "APIListData":
|
|
264
|
+
items_raw = data.get("items", [])
|
|
265
|
+
pagination = data.get("pagination", {})
|
|
266
|
+
items = [APIResponse.from_dict(item) for item in items_raw]
|
|
267
|
+
return cls(
|
|
268
|
+
items=items,
|
|
269
|
+
total=pagination.get("total", 0),
|
|
270
|
+
page=pagination.get("page", 1),
|
|
271
|
+
page_size=pagination.get("page_size", 20),
|
|
272
|
+
total_pages=pagination.get("total_pages", 0),
|
|
273
|
+
)
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: turboapi-sdk
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: TurboAPI SDK - Call AI services through the TurboAPI platform
|
|
5
|
+
Project-URL: Homepage, https://turboapi.ai
|
|
6
|
+
Project-URL: Repository, https://github.com/turboapi/turboapi
|
|
7
|
+
Author: TurboAPI Team
|
|
8
|
+
License: MIT
|
|
9
|
+
Keywords: ai,api,sdk,turboapi
|
|
10
|
+
Classifier: Development Status :: 4 - Beta
|
|
11
|
+
Classifier: Intended Audience :: Developers
|
|
12
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
13
|
+
Classifier: Programming Language :: Python :: 3
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
17
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
18
|
+
Requires-Python: >=3.10
|
|
19
|
+
Requires-Dist: httpx<1,>=0.27
|
|
20
|
+
Requires-Dist: pydantic<3,>=2
|
|
21
|
+
Provides-Extra: dev
|
|
22
|
+
Requires-Dist: pytest-asyncio>=0.24; extra == 'dev'
|
|
23
|
+
Requires-Dist: pytest-httpx>=0.35; extra == 'dev'
|
|
24
|
+
Requires-Dist: pytest>=8; extra == 'dev'
|
|
25
|
+
Description-Content-Type: text/markdown
|
|
26
|
+
|
|
27
|
+
# TurboAPI SDK (Python)
|
|
28
|
+
|
|
29
|
+
Python client for calling AI services through [TurboAPI](https://turboapi.ai).
|
|
30
|
+
|
|
31
|
+
## Installation
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
pip install turboapi-sdk
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
## Quick Start
|
|
38
|
+
|
|
39
|
+
```python
|
|
40
|
+
from turboapi import TurboAPIClient
|
|
41
|
+
|
|
42
|
+
client = TurboAPIClient(api_key="tbp_your_api_key_here")
|
|
43
|
+
|
|
44
|
+
# Create a task and wait for result
|
|
45
|
+
result = client.call.create_and_wait(
|
|
46
|
+
slug_id="karaoke-maker",
|
|
47
|
+
input={
|
|
48
|
+
"audio_file": "https://example.com/song.mp3",
|
|
49
|
+
"task_key": "my-first-task",
|
|
50
|
+
},
|
|
51
|
+
timeout=300,
|
|
52
|
+
)
|
|
53
|
+
print(f"Task completed! Output: {result.output}")
|
|
54
|
+
|
|
55
|
+
# Or manage tasks manually
|
|
56
|
+
task = client.call.create("some-api", {"key": "value"})
|
|
57
|
+
print(f"Task ID: {task.task_id}, Status: {task.status}")
|
|
58
|
+
|
|
59
|
+
# Poll for updates
|
|
60
|
+
updated = client.call.get(task.task_id)
|
|
61
|
+
if updated.status.is_terminal:
|
|
62
|
+
print(f"Output: {updated.output}")
|
|
63
|
+
|
|
64
|
+
# Cancel a queued task
|
|
65
|
+
client.call.cancel(task.task_id)
|
|
66
|
+
|
|
67
|
+
# List your recent tasks
|
|
68
|
+
tasks = client.tasks.list(status="succeeded", page=1, page_size=10)
|
|
69
|
+
for t in tasks.items:
|
|
70
|
+
print(f"{t.task_id}: {t.name} - {t.status}")
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
## API Reference
|
|
74
|
+
|
|
75
|
+
### TurboAPIClient
|
|
76
|
+
|
|
77
|
+
```python
|
|
78
|
+
TurboAPIClient(
|
|
79
|
+
api_key: str | None = None,
|
|
80
|
+
*,
|
|
81
|
+
base_url: str = "https://api.turboapi.ai/api/v1",
|
|
82
|
+
timeout: float = 30.0,
|
|
83
|
+
)
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
### Call Module (`client.call`)
|
|
87
|
+
|
|
88
|
+
| Method | Description |
|
|
89
|
+
|--------|-------------|
|
|
90
|
+
| `create(slug_id, input, *, prefer_wait=False)` | Submit a task |
|
|
91
|
+
| `get(task_id)` | Get task status & result |
|
|
92
|
+
| `cancel(task_id)` | Cancel a queued task |
|
|
93
|
+
| `create_and_wait(slug_id, input, *, timeout=300, poll_interval=2)` | Submit & block until complete |
|
|
94
|
+
|
|
95
|
+
### Tasks Module (`client.tasks`)
|
|
96
|
+
|
|
97
|
+
| Method | Description |
|
|
98
|
+
|--------|-------------|
|
|
99
|
+
| `list(*, status, api_slug, page, page_size)` | List your tasks |
|
|
100
|
+
| `get(task_id)` | Get task detail |
|
|
101
|
+
| `logs(task_id, *, page, page_size)` | Get execution logs |
|
|
102
|
+
|
|
103
|
+
## Error Handling
|
|
104
|
+
|
|
105
|
+
```python
|
|
106
|
+
from turboapi import TurboAPIClient
|
|
107
|
+
from turboapi.errors import (
|
|
108
|
+
AuthenticationError,
|
|
109
|
+
RateLimitError,
|
|
110
|
+
NotFoundError,
|
|
111
|
+
TimeoutError,
|
|
112
|
+
)
|
|
113
|
+
|
|
114
|
+
client = TurboAPIClient(api_key="...")
|
|
115
|
+
|
|
116
|
+
try:
|
|
117
|
+
result = client.call.create_and_wait("some-api", {"key": "value"})
|
|
118
|
+
except AuthenticationError:
|
|
119
|
+
print("Check your API key")
|
|
120
|
+
except RateLimitError as e:
|
|
121
|
+
print(f"Slow down! Retry after {e.retry_after}s")
|
|
122
|
+
except NotFoundError:
|
|
123
|
+
print("Task or API not found")
|
|
124
|
+
except TimeoutError:
|
|
125
|
+
print("Task did not complete in time")
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
## Task Statuses
|
|
129
|
+
|
|
130
|
+
| Status | Terminal | Description |
|
|
131
|
+
|--------|----------|-------------|
|
|
132
|
+
| `pending` | No | Waiting to be queued |
|
|
133
|
+
| `queued` | No | In queue awaiting execution |
|
|
134
|
+
| `starting` | No | Worker starting up |
|
|
135
|
+
| `processing` | No | Execution in progress |
|
|
136
|
+
| `succeeded` | Yes | Completed successfully |
|
|
137
|
+
| `failed` | Yes | Execution failed |
|
|
138
|
+
| `cancelled` | Yes | Cancelled by user |
|
|
139
|
+
| `timeout` | Yes | Timed out |
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
turboapi/__init__.py,sha256=KzrIX_f9NhZK9C91rbiF-LLIha8htIy-JU3fFrC53k0,1113
|
|
2
|
+
turboapi/client.py,sha256=Pgjmrtr5WUca6ebWBvV-5JnNg7lv0TvCXSV9fOQvkJU,10921
|
|
3
|
+
turboapi/errors.py,sha256=fXaUXFT0IyRVEvMuY87RoiGbzUFrAwysHpidhgBb7VE,3202
|
|
4
|
+
turboapi/models.py,sha256=ENcavLaq8q3epsqWlXgPGSuyVf8iZYnfAwF7V-5g4x4,8489
|
|
5
|
+
turboapi_sdk-0.1.0.dist-info/METADATA,sha256=2kKUCzgkLsQjkj5NK0Luo-EhLv4UQAe8r2BiYFnZ_h0,3835
|
|
6
|
+
turboapi_sdk-0.1.0.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
|
|
7
|
+
turboapi_sdk-0.1.0.dist-info/RECORD,,
|