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 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,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.29.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any