synapsai-python 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.
- synapsai/__init__.py +16 -0
- synapsai/client.py +628 -0
- synapsai/exceptions.py +64 -0
- synapsai/logging.py +70 -0
- synapsai/processing.py +100 -0
- synapsai/resources/__init__.py +59 -0
- synapsai/resources/audio.py +495 -0
- synapsai/resources/chat.py +199 -0
- synapsai/resources/classifications.py +454 -0
- synapsai/resources/completions.py +162 -0
- synapsai/resources/embeddings.py +207 -0
- synapsai/resources/feature_extraction.py +80 -0
- synapsai/resources/fill_mask.py +88 -0
- synapsai/resources/images.py +560 -0
- synapsai/resources/models.py +80 -0
- synapsai/resources/question_answering.py +257 -0
- synapsai/resources/rerank.py +90 -0
- synapsai/resources/videos.py +303 -0
- synapsai/types/__init__.py +144 -0
- synapsai/types/audio.py +177 -0
- synapsai/types/classifications.py +228 -0
- synapsai/types/common.py +56 -0
- synapsai/types/completion.py +244 -0
- synapsai/types/embeddings.py +68 -0
- synapsai/types/feature_extraction.py +31 -0
- synapsai/types/fill_mask.py +38 -0
- synapsai/types/images.py +172 -0
- synapsai/types/models.py +28 -0
- synapsai/types/question_answering.py +127 -0
- synapsai/types/rerank.py +40 -0
- synapsai/types/videos.py +92 -0
- synapsai/utils.py +32 -0
- synapsai_python-0.1.0.dist-info/METADATA +312 -0
- synapsai_python-0.1.0.dist-info/RECORD +37 -0
- synapsai_python-0.1.0.dist-info/WHEEL +5 -0
- synapsai_python-0.1.0.dist-info/licenses/LICENSE +201 -0
- synapsai_python-0.1.0.dist-info/top_level.txt +1 -0
synapsai/__init__.py
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
"""
|
|
2
|
+
SynapsAI - OpenAI Compatible API Client Library
|
|
3
|
+
|
|
4
|
+
A scalable and maintainable Python client library for interacting with SynapsAI services,
|
|
5
|
+
compatible with OpenAI API patterns.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from .client import SynapsAI, AsyncSynapsAI
|
|
9
|
+
from .types import *
|
|
10
|
+
from .resources import *
|
|
11
|
+
|
|
12
|
+
__version__ = "0.1.0"
|
|
13
|
+
__all__ = [
|
|
14
|
+
"SynapsAI",
|
|
15
|
+
"AsyncSynapsAI",
|
|
16
|
+
]
|
synapsai/client.py
ADDED
|
@@ -0,0 +1,628 @@
|
|
|
1
|
+
# Copyright 2026 SynapsAI Technologies Inc.
|
|
2
|
+
#
|
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
# you may not use this file except in compliance with the License.
|
|
5
|
+
# You may obtain a copy of the License at
|
|
6
|
+
#
|
|
7
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
#
|
|
9
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
10
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
+
# See the License for the specific language governing permissions and
|
|
13
|
+
# limitations under the License.
|
|
14
|
+
|
|
15
|
+
"""
|
|
16
|
+
Main client classes for SynapsAI API
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
import httpx
|
|
20
|
+
import json
|
|
21
|
+
import os
|
|
22
|
+
import time
|
|
23
|
+
import random
|
|
24
|
+
import asyncio
|
|
25
|
+
from typing import Optional, Dict, Any, Union, AsyncIterator, Iterator
|
|
26
|
+
|
|
27
|
+
from .resources import (
|
|
28
|
+
ChatResource,
|
|
29
|
+
AsyncChatResource,
|
|
30
|
+
ImagesResource,
|
|
31
|
+
AsyncImagesResource,
|
|
32
|
+
VideosResource,
|
|
33
|
+
AsyncVideosResource,
|
|
34
|
+
EmbeddingsResource,
|
|
35
|
+
AsyncEmbeddingsResource,
|
|
36
|
+
AudioResource,
|
|
37
|
+
AsyncAudioResource,
|
|
38
|
+
CompletionsResource,
|
|
39
|
+
AsyncCompletionsResource,
|
|
40
|
+
ClassificationsResource,
|
|
41
|
+
AsyncClassificationsResource,
|
|
42
|
+
QuestionAnsweringResource,
|
|
43
|
+
AsyncQuestionAnsweringResource,
|
|
44
|
+
ModelsResource,
|
|
45
|
+
AsyncModelsResource,
|
|
46
|
+
FeatureExtractionResource,
|
|
47
|
+
AsyncFeatureExtractionResource,
|
|
48
|
+
FillMaskResource,
|
|
49
|
+
AsyncFillMaskResource,
|
|
50
|
+
RerankResource,
|
|
51
|
+
AsyncRerankResource,
|
|
52
|
+
)
|
|
53
|
+
from .exceptions import APIError, AuthenticationError
|
|
54
|
+
from .utils import build_url
|
|
55
|
+
from .logging import get_logger
|
|
56
|
+
|
|
57
|
+
logger = get_logger(__name__)
|
|
58
|
+
|
|
59
|
+
class BaseClient:
|
|
60
|
+
"""Base client with common functionality"""
|
|
61
|
+
|
|
62
|
+
def __init__(
|
|
63
|
+
self,
|
|
64
|
+
api_key: Optional[str] = None,
|
|
65
|
+
base_url: Optional[str] = None,
|
|
66
|
+
timeout: float = 300.0,
|
|
67
|
+
max_retries: int = 1,
|
|
68
|
+
headers: Optional[Dict[str, str]] = None,
|
|
69
|
+
httpx_client: Optional[httpx.Client] = None,
|
|
70
|
+
):
|
|
71
|
+
"""
|
|
72
|
+
Initialize the client with the provided arguments.
|
|
73
|
+
|
|
74
|
+
Args:
|
|
75
|
+
api_key: API key for authentication.
|
|
76
|
+
base_url: Base URL for the API.
|
|
77
|
+
timeout: Timeout for requests (seconds or httpx.Timeout).
|
|
78
|
+
max_retries: Maximum number of retries for requests (>=1).
|
|
79
|
+
headers: Additional headers to include in requests.
|
|
80
|
+
httpx_client: Custom HTTP client to use for requests.
|
|
81
|
+
"""
|
|
82
|
+
if api_key is None:
|
|
83
|
+
api_key = os.environ.get("SYNAPSAI_API_KEY")
|
|
84
|
+
|
|
85
|
+
if api_key is None:
|
|
86
|
+
raise AuthenticationError(
|
|
87
|
+
"No API key provided. You can set your API key in an environment variable `SYNAPSAI_API_KEY`, or you can pass it as an argument `SynapsAI(api_key=...)`."
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
if base_url is None:
|
|
91
|
+
base_url = os.environ.get("SYNAPSAI_API_BASE")
|
|
92
|
+
|
|
93
|
+
if base_url is None:
|
|
94
|
+
base_url = "https://api.synapsai.cloud/v1"
|
|
95
|
+
|
|
96
|
+
self.api_key = api_key
|
|
97
|
+
self.base_url = base_url.rstrip("/")
|
|
98
|
+
self.timeout = timeout
|
|
99
|
+
|
|
100
|
+
# ensure sensible value
|
|
101
|
+
self.max_retries = max(1, int(max_retries))
|
|
102
|
+
|
|
103
|
+
# Set up default headers
|
|
104
|
+
self._headers = {
|
|
105
|
+
"Content-Type": "application/json",
|
|
106
|
+
"User-Agent": "synapsai-python/1.0.0",
|
|
107
|
+
"Authorization": f"Bearer {api_key}",
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
if headers:
|
|
111
|
+
self._headers.update(headers)
|
|
112
|
+
|
|
113
|
+
self._client = httpx_client
|
|
114
|
+
|
|
115
|
+
def _build_request(
|
|
116
|
+
self,
|
|
117
|
+
**kwargs
|
|
118
|
+
) -> Dict[str, Any]:
|
|
119
|
+
"""Build request payload for SynapsAI API"""
|
|
120
|
+
# Filter out None values
|
|
121
|
+
filtered_kwargs = {k: v for k, v in kwargs.items() if v is not None}
|
|
122
|
+
|
|
123
|
+
return {
|
|
124
|
+
**filtered_kwargs
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
def _handle_error_response(self, response: httpx.Response) -> None:
|
|
128
|
+
# Ensure content is loaded for streamed responses
|
|
129
|
+
try:
|
|
130
|
+
# If _content is missing, this will populate it
|
|
131
|
+
if not hasattr(response, "_content"):
|
|
132
|
+
response.read()
|
|
133
|
+
except Exception:
|
|
134
|
+
# If this fails, we still fall back to a generic message below
|
|
135
|
+
pass
|
|
136
|
+
|
|
137
|
+
try:
|
|
138
|
+
error_data = response.json()
|
|
139
|
+
# Support the infra server structured error format:
|
|
140
|
+
if isinstance(error_data, dict) and "error" in error_data:
|
|
141
|
+
err = error_data["error"]
|
|
142
|
+
message = err.get("message") or err.get("error") or str(err)
|
|
143
|
+
|
|
144
|
+
# keep original status semantics
|
|
145
|
+
raise APIError(message, status_code=response.status_code)
|
|
146
|
+
else:
|
|
147
|
+
# try to extract message from common patterns
|
|
148
|
+
if isinstance(error_data, dict):
|
|
149
|
+
message = error_data.get("error", {}).get("message") or error_data.get("message") or str(error_data)
|
|
150
|
+
else:
|
|
151
|
+
message = str(error_data)
|
|
152
|
+
except Exception:
|
|
153
|
+
message = f"HTTP {response.status_code}: {response.text}"
|
|
154
|
+
raise APIError(message, status_code=response.status_code)
|
|
155
|
+
|
|
156
|
+
def _should_retry(self, method: str, response: Optional[httpx.Response], exc: Optional[BaseException], attempt: int) -> bool:
|
|
157
|
+
"""
|
|
158
|
+
Decide whether to retry a request.
|
|
159
|
+
|
|
160
|
+
Retry on:
|
|
161
|
+
- network/transport errors (httpx.RequestError)
|
|
162
|
+
- timeout errors (httpx.TimeoutException)
|
|
163
|
+
- HTTP 429 (rate limit) and 5xx server errors
|
|
164
|
+
|
|
165
|
+
For safety, we limit number of retries via self.max_retries externally.
|
|
166
|
+
"""
|
|
167
|
+
# If exception is present and is a network/timeout error -> retry
|
|
168
|
+
if exc is not None:
|
|
169
|
+
if isinstance(exc, (httpx.RequestError, httpx.TimeoutException)):
|
|
170
|
+
return True
|
|
171
|
+
return False
|
|
172
|
+
|
|
173
|
+
if response is not None:
|
|
174
|
+
# Retry on 429 or 5xx
|
|
175
|
+
if response.status_code == 429:
|
|
176
|
+
return True
|
|
177
|
+
if 500 <= response.status_code < 600:
|
|
178
|
+
return True
|
|
179
|
+
|
|
180
|
+
return False
|
|
181
|
+
|
|
182
|
+
def _backoff_delay(self, attempt: int) -> float:
|
|
183
|
+
"""
|
|
184
|
+
Exponential backoff with jitter.
|
|
185
|
+
|
|
186
|
+
attempt is 0-based. We use base 0.5s.
|
|
187
|
+
"""
|
|
188
|
+
base = 0.5
|
|
189
|
+
delay = base * (2 ** attempt)
|
|
190
|
+
# add jitter up to 0.5s
|
|
191
|
+
delay += random.uniform(0, 0.5)
|
|
192
|
+
# Cap delay to a sensible maximum (e.g., 30s)
|
|
193
|
+
return min(delay, 30.0)
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
class SynapsAI(BaseClient):
|
|
197
|
+
"""Synchronous SynapsAI client"""
|
|
198
|
+
|
|
199
|
+
chat: ChatResource
|
|
200
|
+
images: ImagesResource
|
|
201
|
+
videos: VideosResource
|
|
202
|
+
embeddings: EmbeddingsResource
|
|
203
|
+
audio: AudioResource
|
|
204
|
+
completions: CompletionsResource
|
|
205
|
+
classifications: ClassificationsResource
|
|
206
|
+
question_answering: QuestionAnsweringResource
|
|
207
|
+
models: ModelsResource
|
|
208
|
+
feature_extraction: FeatureExtractionResource
|
|
209
|
+
fill_mask: FillMaskResource
|
|
210
|
+
rerank: RerankResource
|
|
211
|
+
|
|
212
|
+
def __init__(self, api_key: Optional[str] = None, **kwargs):
|
|
213
|
+
super().__init__(api_key=api_key, **kwargs)
|
|
214
|
+
if self._client is None:
|
|
215
|
+
self._client = httpx.Client(
|
|
216
|
+
timeout=self.timeout,
|
|
217
|
+
headers=self._headers,
|
|
218
|
+
http2=True,
|
|
219
|
+
verify=False,
|
|
220
|
+
)
|
|
221
|
+
|
|
222
|
+
# Initialize resource handlers
|
|
223
|
+
self.chat = ChatResource(self)
|
|
224
|
+
self.images = ImagesResource(self)
|
|
225
|
+
self.videos = VideosResource(self)
|
|
226
|
+
self.embeddings = EmbeddingsResource(self)
|
|
227
|
+
self.audio = AudioResource(self)
|
|
228
|
+
self.completions = CompletionsResource(self)
|
|
229
|
+
self.classifications = ClassificationsResource(self)
|
|
230
|
+
self.question_answering = QuestionAnsweringResource(self)
|
|
231
|
+
self.models = ModelsResource(self)
|
|
232
|
+
self.feature_extraction = FeatureExtractionResource(self)
|
|
233
|
+
self.fill_mask = FillMaskResource(self)
|
|
234
|
+
self.rerank = RerankResource(self)
|
|
235
|
+
|
|
236
|
+
def __enter__(self):
|
|
237
|
+
return self
|
|
238
|
+
|
|
239
|
+
def __exit__(self, exc_type, exc_val, exc_tb):
|
|
240
|
+
self.close()
|
|
241
|
+
|
|
242
|
+
def close(self):
|
|
243
|
+
"""Close the HTTP client"""
|
|
244
|
+
self._client.close()
|
|
245
|
+
|
|
246
|
+
def _request(
|
|
247
|
+
self,
|
|
248
|
+
method: str,
|
|
249
|
+
endpoint: str,
|
|
250
|
+
json_data: Optional[Dict[str, Any]] = None,
|
|
251
|
+
data: Optional[Dict[str, Any]] = None,
|
|
252
|
+
files: Optional[Dict[str, Any]] = None,
|
|
253
|
+
stream: bool = False,
|
|
254
|
+
) -> httpx.Response:
|
|
255
|
+
"""
|
|
256
|
+
Make a request to the API with retries.
|
|
257
|
+
|
|
258
|
+
Retries on network errors, timeouts, and server-side errors (429, 5xx).
|
|
259
|
+
"""
|
|
260
|
+
url = build_url(self.base_url, endpoint)
|
|
261
|
+
|
|
262
|
+
# prepare kwargs used for every attempt
|
|
263
|
+
base_kwargs = {
|
|
264
|
+
"method": method,
|
|
265
|
+
"url": url,
|
|
266
|
+
"timeout": self.timeout,
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
if json_data:
|
|
270
|
+
base_kwargs["json"] = json_data
|
|
271
|
+
if data:
|
|
272
|
+
base_kwargs["data"] = data
|
|
273
|
+
if files:
|
|
274
|
+
base_kwargs["files"] = files
|
|
275
|
+
if stream:
|
|
276
|
+
# note: for streaming we still attempt to (re)establish the stream on failures opening it
|
|
277
|
+
base_kwargs["stream"] = True
|
|
278
|
+
|
|
279
|
+
attempt = 0
|
|
280
|
+
last_exc: Optional[BaseException] = None
|
|
281
|
+
last_response: Optional[httpx.Response] = None
|
|
282
|
+
|
|
283
|
+
while attempt < self.max_retries:
|
|
284
|
+
try:
|
|
285
|
+
response = self._client.request(**base_kwargs)
|
|
286
|
+
last_response = response
|
|
287
|
+
|
|
288
|
+
# If server-side error or rate-limit, decide if retry
|
|
289
|
+
if response.status_code >= 400:
|
|
290
|
+
if self._should_retry(method, response, None, attempt) and attempt < (self.max_retries - 1):
|
|
291
|
+
# close body and wait before retrying
|
|
292
|
+
try:
|
|
293
|
+
response.read()
|
|
294
|
+
except Exception:
|
|
295
|
+
pass
|
|
296
|
+
delay = self._backoff_delay(attempt)
|
|
297
|
+
time.sleep(delay)
|
|
298
|
+
attempt += 1
|
|
299
|
+
continue
|
|
300
|
+
else:
|
|
301
|
+
# Non-retryable or out of retries -> raise
|
|
302
|
+
self._handle_error_response(response)
|
|
303
|
+
# success
|
|
304
|
+
return response
|
|
305
|
+
|
|
306
|
+
except (httpx.RequestError, httpx.TimeoutException) as e:
|
|
307
|
+
last_exc = e
|
|
308
|
+
# decide if we should retry
|
|
309
|
+
if self._should_retry(method, None, e, attempt) and attempt < (self.max_retries - 1):
|
|
310
|
+
delay = self._backoff_delay(attempt)
|
|
311
|
+
time.sleep(delay)
|
|
312
|
+
attempt += 1
|
|
313
|
+
continue
|
|
314
|
+
# no more retries
|
|
315
|
+
raise APIError(str(e))
|
|
316
|
+
|
|
317
|
+
# If we exit loop without returning, raise last known problem
|
|
318
|
+
if last_response is not None:
|
|
319
|
+
self._handle_error_response(last_response)
|
|
320
|
+
if last_exc is not None:
|
|
321
|
+
raise APIError(str(last_exc))
|
|
322
|
+
raise APIError("Unknown error during request")
|
|
323
|
+
|
|
324
|
+
def _post(
|
|
325
|
+
self,
|
|
326
|
+
endpoint: str,
|
|
327
|
+
json_data: Optional[Dict[str, Any]] = None,
|
|
328
|
+
data: Optional[Dict[str, Any]] = None,
|
|
329
|
+
files: Optional[Dict[str, Any]] = None,
|
|
330
|
+
) -> httpx.Response:
|
|
331
|
+
"""Make a POST request"""
|
|
332
|
+
return self._request("POST", endpoint, json_data, data, files)
|
|
333
|
+
|
|
334
|
+
def _get(self, endpoint: str) -> httpx.Response:
|
|
335
|
+
"""Make a GET request"""
|
|
336
|
+
return self._request("GET", endpoint)
|
|
337
|
+
|
|
338
|
+
def _delete(self, endpoint: str) -> httpx.Response:
|
|
339
|
+
"""Make a DELETE request"""
|
|
340
|
+
return self._request("DELETE", endpoint)
|
|
341
|
+
|
|
342
|
+
def _stream_response(
|
|
343
|
+
self,
|
|
344
|
+
endpoint: str,
|
|
345
|
+
json_data: Optional[Dict[str, Any]] = None,
|
|
346
|
+
data: Optional[Dict[str, Any]] = None,
|
|
347
|
+
files: Optional[Dict[str, Any]] = None,
|
|
348
|
+
) -> Iterator[Dict[str, Any]]:
|
|
349
|
+
"""
|
|
350
|
+
Stream response data from a POST endpoint.
|
|
351
|
+
|
|
352
|
+
The attempt to establish the stream will be retried using the same backoff rules.
|
|
353
|
+
Once the stream is established, streaming errors are raised as-is.
|
|
354
|
+
"""
|
|
355
|
+
url = build_url(self.base_url, endpoint)
|
|
356
|
+
base_kwargs = {
|
|
357
|
+
"method": "POST",
|
|
358
|
+
"url": url,
|
|
359
|
+
"timeout": self.timeout,
|
|
360
|
+
"json": json_data,
|
|
361
|
+
"data": data,
|
|
362
|
+
"files": files,
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
attempt = 0
|
|
366
|
+
while attempt < self.max_retries:
|
|
367
|
+
try:
|
|
368
|
+
with self._client.stream(**base_kwargs) as response:
|
|
369
|
+
if response.status_code >= 400:
|
|
370
|
+
# decide whether to retry establishing stream
|
|
371
|
+
if self._should_retry("POST", response, None, attempt) and attempt < (self.max_retries - 1):
|
|
372
|
+
try:
|
|
373
|
+
response.read()
|
|
374
|
+
except Exception:
|
|
375
|
+
pass
|
|
376
|
+
delay = self._backoff_delay(attempt)
|
|
377
|
+
time.sleep(delay)
|
|
378
|
+
attempt += 1
|
|
379
|
+
continue
|
|
380
|
+
else:
|
|
381
|
+
self._handle_error_response(response)
|
|
382
|
+
|
|
383
|
+
# stream established, iterate lines and yield
|
|
384
|
+
for line in response.iter_lines():
|
|
385
|
+
line = line.strip()
|
|
386
|
+
if not line:
|
|
387
|
+
continue
|
|
388
|
+
if line.startswith("data: "):
|
|
389
|
+
data_line = line[6:] # Remove "data: " prefix
|
|
390
|
+
if data_line == "[DONE]":
|
|
391
|
+
return
|
|
392
|
+
try:
|
|
393
|
+
yield json.loads(data_line)
|
|
394
|
+
except json.JSONDecodeError as e:
|
|
395
|
+
# Log the malformed data for debugging
|
|
396
|
+
logger.warning(f"Received malformed JSON data: {data_line[:100]}...")
|
|
397
|
+
logger.warning(f"JSON decode error: {e}")
|
|
398
|
+
continue
|
|
399
|
+
# If stream ends naturally, return
|
|
400
|
+
return
|
|
401
|
+
|
|
402
|
+
except (httpx.RequestError, httpx.TimeoutException) as e:
|
|
403
|
+
# network issue while establishing stream, maybe retry
|
|
404
|
+
if attempt < (self.max_retries - 1):
|
|
405
|
+
delay = self._backoff_delay(attempt)
|
|
406
|
+
time.sleep(delay)
|
|
407
|
+
attempt += 1
|
|
408
|
+
continue
|
|
409
|
+
raise APIError(str(e))
|
|
410
|
+
|
|
411
|
+
raise APIError("Failed to establish stream after retries")
|
|
412
|
+
|
|
413
|
+
def _get_stream(self, endpoint: str) -> httpx.Response:
|
|
414
|
+
"""Make a streaming GET request for binary content (e.g. video download)."""
|
|
415
|
+
url = build_url(self.base_url, endpoint)
|
|
416
|
+
request = self._client.build_request("GET", url, timeout=self.timeout)
|
|
417
|
+
response = self._client.send(request, stream=True)
|
|
418
|
+
if response.status_code >= 400:
|
|
419
|
+
response.read()
|
|
420
|
+
self._handle_error_response(response)
|
|
421
|
+
return response
|
|
422
|
+
|
|
423
|
+
|
|
424
|
+
class AsyncSynapsAI(BaseClient):
|
|
425
|
+
"""Asynchronous SynapsAI client"""
|
|
426
|
+
chat: AsyncChatResource
|
|
427
|
+
images: AsyncImagesResource
|
|
428
|
+
videos: AsyncVideosResource
|
|
429
|
+
embeddings: AsyncEmbeddingsResource
|
|
430
|
+
audio: AsyncAudioResource
|
|
431
|
+
completions: AsyncCompletionsResource
|
|
432
|
+
classifications: AsyncClassificationsResource
|
|
433
|
+
question_answering: AsyncQuestionAnsweringResource
|
|
434
|
+
models: AsyncModelsResource
|
|
435
|
+
feature_extraction: AsyncFeatureExtractionResource
|
|
436
|
+
fill_mask: AsyncFillMaskResource
|
|
437
|
+
rerank: AsyncRerankResource
|
|
438
|
+
|
|
439
|
+
def __init__(self, api_key: Optional[str] = None, **kwargs):
|
|
440
|
+
super().__init__(api_key=api_key, **kwargs)
|
|
441
|
+
if self._client is None:
|
|
442
|
+
self._client = httpx.AsyncClient(
|
|
443
|
+
timeout=self.timeout,
|
|
444
|
+
headers=self._headers,
|
|
445
|
+
http2=True,
|
|
446
|
+
)
|
|
447
|
+
|
|
448
|
+
# Initialize async resource handlers
|
|
449
|
+
self.chat = AsyncChatResource(self)
|
|
450
|
+
self.images = AsyncImagesResource(self)
|
|
451
|
+
self.videos = AsyncVideosResource(self)
|
|
452
|
+
self.embeddings = AsyncEmbeddingsResource(self)
|
|
453
|
+
self.audio = AsyncAudioResource(self)
|
|
454
|
+
self.completions = AsyncCompletionsResource(self)
|
|
455
|
+
self.classifications = AsyncClassificationsResource(self)
|
|
456
|
+
self.question_answering = AsyncQuestionAnsweringResource(self)
|
|
457
|
+
self.models = AsyncModelsResource(self)
|
|
458
|
+
self.feature_extraction = AsyncFeatureExtractionResource(self)
|
|
459
|
+
self.fill_mask = AsyncFillMaskResource(self)
|
|
460
|
+
self.rerank = AsyncRerankResource(self)
|
|
461
|
+
|
|
462
|
+
async def __aenter__(self):
|
|
463
|
+
return self
|
|
464
|
+
|
|
465
|
+
async def __aexit__(self, exc_type, exc_val, exc_tb):
|
|
466
|
+
await self.close()
|
|
467
|
+
|
|
468
|
+
async def close(self):
|
|
469
|
+
"""Close the HTTP client"""
|
|
470
|
+
await self._client.aclose()
|
|
471
|
+
|
|
472
|
+
async def _request(
|
|
473
|
+
self,
|
|
474
|
+
method: str,
|
|
475
|
+
endpoint: str,
|
|
476
|
+
json_data: Optional[Dict[str, Any]] = None,
|
|
477
|
+
data: Optional[Dict[str, Any]] = None,
|
|
478
|
+
files: Optional[Dict[str, Any]] = None,
|
|
479
|
+
) -> httpx.Response:
|
|
480
|
+
"""Make an async request to the API with retries."""
|
|
481
|
+
url = build_url(self.base_url, endpoint)
|
|
482
|
+
|
|
483
|
+
base_kwargs: Dict[str, Any] = {
|
|
484
|
+
"method": method,
|
|
485
|
+
"url": url,
|
|
486
|
+
"timeout": self.timeout,
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
if json_data:
|
|
490
|
+
base_kwargs["json"] = json_data
|
|
491
|
+
if data:
|
|
492
|
+
base_kwargs["data"] = data
|
|
493
|
+
if files:
|
|
494
|
+
base_kwargs["files"] = files
|
|
495
|
+
|
|
496
|
+
attempt = 0
|
|
497
|
+
last_exc: Optional[BaseException] = None
|
|
498
|
+
last_response: Optional[httpx.Response] = None
|
|
499
|
+
|
|
500
|
+
while attempt < self.max_retries:
|
|
501
|
+
try:
|
|
502
|
+
response = await self._client.request(**base_kwargs)
|
|
503
|
+
last_response = response
|
|
504
|
+
|
|
505
|
+
if response.status_code >= 400:
|
|
506
|
+
if self._should_retry(method, response, None, attempt) and attempt < (self.max_retries - 1):
|
|
507
|
+
try:
|
|
508
|
+
await response.aread()
|
|
509
|
+
except Exception:
|
|
510
|
+
pass
|
|
511
|
+
delay = self._backoff_delay(attempt)
|
|
512
|
+
await asyncio.sleep(delay)
|
|
513
|
+
attempt += 1
|
|
514
|
+
continue
|
|
515
|
+
else:
|
|
516
|
+
self._handle_error_response(response)
|
|
517
|
+
return response
|
|
518
|
+
|
|
519
|
+
except (httpx.RequestError, httpx.TimeoutException) as e:
|
|
520
|
+
last_exc = e
|
|
521
|
+
if self._should_retry(method, None, e, attempt) and attempt < (self.max_retries - 1):
|
|
522
|
+
delay = self._backoff_delay(attempt)
|
|
523
|
+
await asyncio.sleep(delay)
|
|
524
|
+
attempt += 1
|
|
525
|
+
continue
|
|
526
|
+
raise APIError(str(e))
|
|
527
|
+
|
|
528
|
+
# If we exit loop without returning, raise last known problem
|
|
529
|
+
if last_response is not None:
|
|
530
|
+
self._handle_error_response(last_response)
|
|
531
|
+
if last_exc is not None:
|
|
532
|
+
raise APIError(str(last_exc))
|
|
533
|
+
raise APIError("Unknown error during request")
|
|
534
|
+
|
|
535
|
+
async def _post(
|
|
536
|
+
self,
|
|
537
|
+
endpoint: str,
|
|
538
|
+
json_data: Optional[Dict[str, Any]] = None,
|
|
539
|
+
data: Optional[Dict[str, Any]] = None,
|
|
540
|
+
files: Optional[Dict[str, Any]] = None,
|
|
541
|
+
) -> httpx.Response:
|
|
542
|
+
"""Make an async POST request"""
|
|
543
|
+
return await self._request("POST", endpoint, json_data, data, files)
|
|
544
|
+
|
|
545
|
+
async def _get(self, endpoint: str) -> httpx.Response:
|
|
546
|
+
"""Make a GET request"""
|
|
547
|
+
return await self._request("GET", endpoint=endpoint)
|
|
548
|
+
|
|
549
|
+
async def _delete(self, endpoint: str) -> httpx.Response:
|
|
550
|
+
"""Make a DELETE request"""
|
|
551
|
+
return await self._request("DELETE", endpoint=endpoint)
|
|
552
|
+
|
|
553
|
+
async def _stream_response(
|
|
554
|
+
self,
|
|
555
|
+
endpoint: str,
|
|
556
|
+
json_data: Optional[Dict[str, Any]] = None,
|
|
557
|
+
data: Optional[Dict[str, Any]] = None,
|
|
558
|
+
files: Optional[Dict[str, Any]] = None,
|
|
559
|
+
) -> AsyncIterator[Dict[str, Any]]:
|
|
560
|
+
"""
|
|
561
|
+
Stream response data from a POST endpoint (async).
|
|
562
|
+
|
|
563
|
+
The attempt to establish the stream will be retried using the same backoff rules.
|
|
564
|
+
Once the stream is established, streaming errors are raised as-is.
|
|
565
|
+
"""
|
|
566
|
+
url = build_url(self.base_url, endpoint)
|
|
567
|
+
|
|
568
|
+
base_kwargs = {
|
|
569
|
+
"method": "POST",
|
|
570
|
+
"url": url,
|
|
571
|
+
"timeout": self.timeout,
|
|
572
|
+
"json": json_data,
|
|
573
|
+
"data": data,
|
|
574
|
+
"files": files,
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
attempt = 0
|
|
578
|
+
while attempt < self.max_retries:
|
|
579
|
+
try:
|
|
580
|
+
async with self._client.stream(**base_kwargs) as response:
|
|
581
|
+
if response.status_code >= 400:
|
|
582
|
+
if self._should_retry("POST", response, None, attempt) and attempt < (self.max_retries - 1):
|
|
583
|
+
try:
|
|
584
|
+
await response.aread()
|
|
585
|
+
except Exception:
|
|
586
|
+
pass
|
|
587
|
+
delay = self._backoff_delay(attempt)
|
|
588
|
+
await asyncio.sleep(delay)
|
|
589
|
+
attempt += 1
|
|
590
|
+
continue
|
|
591
|
+
else:
|
|
592
|
+
self._handle_error_response(response)
|
|
593
|
+
|
|
594
|
+
async for raw_line in response.aiter_lines():
|
|
595
|
+
line = raw_line.strip()
|
|
596
|
+
if not line:
|
|
597
|
+
continue
|
|
598
|
+
if line.startswith("data: "):
|
|
599
|
+
data_line = line[6:] # Remove "data: " prefix
|
|
600
|
+
if data_line == "[DONE]":
|
|
601
|
+
return
|
|
602
|
+
try:
|
|
603
|
+
yield json.loads(data_line)
|
|
604
|
+
except json.JSONDecodeError:
|
|
605
|
+
# skip malformed line
|
|
606
|
+
continue
|
|
607
|
+
# stream ended normally
|
|
608
|
+
return
|
|
609
|
+
|
|
610
|
+
except (httpx.RequestError, httpx.TimeoutException) as e:
|
|
611
|
+
if attempt < (self.max_retries - 1):
|
|
612
|
+
delay = self._backoff_delay(attempt)
|
|
613
|
+
await asyncio.sleep(delay)
|
|
614
|
+
attempt += 1
|
|
615
|
+
continue
|
|
616
|
+
raise APIError(str(e))
|
|
617
|
+
|
|
618
|
+
raise APIError("Failed to establish stream after retries")
|
|
619
|
+
|
|
620
|
+
async def _get_stream(self, endpoint: str) -> httpx.Response:
|
|
621
|
+
"""Make an async streaming GET request for binary content (e.g. video download)."""
|
|
622
|
+
url = build_url(self.base_url, endpoint)
|
|
623
|
+
request = self._client.build_request("GET", url, timeout=self.timeout)
|
|
624
|
+
response = await self._client.send(request, stream=True)
|
|
625
|
+
if response.status_code >= 400:
|
|
626
|
+
await response.aread()
|
|
627
|
+
self._handle_error_response(response)
|
|
628
|
+
return response
|
synapsai/exceptions.py
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
# Copyright 2026 SynapsAI Technologies Inc.
|
|
2
|
+
#
|
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
# you may not use this file except in compliance with the License.
|
|
5
|
+
# You may obtain a copy of the License at
|
|
6
|
+
#
|
|
7
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
#
|
|
9
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
10
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
+
# See the License for the specific language governing permissions and
|
|
13
|
+
# limitations under the License.
|
|
14
|
+
|
|
15
|
+
"""
|
|
16
|
+
Exception classes for SynapsAI client library
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
from typing import Optional, Dict, Any
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class SynapsAIError(Exception):
|
|
23
|
+
"""Base exception class for SynapsAI errors"""
|
|
24
|
+
pass
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class APIError(SynapsAIError):
|
|
28
|
+
"""Exception raised for API errors"""
|
|
29
|
+
|
|
30
|
+
def __init__(
|
|
31
|
+
self,
|
|
32
|
+
message: str,
|
|
33
|
+
status_code: Optional[int] = None,
|
|
34
|
+
response_data: Optional[Dict[str, Any]] = None,
|
|
35
|
+
):
|
|
36
|
+
super().__init__(message)
|
|
37
|
+
self.message = message
|
|
38
|
+
self.status_code = status_code
|
|
39
|
+
self.response_data = response_data
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class AuthenticationError(APIError):
|
|
43
|
+
"""Exception raised for authentication errors"""
|
|
44
|
+
pass
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
class RateLimitError(APIError):
|
|
48
|
+
"""Exception raised when rate limit is exceeded"""
|
|
49
|
+
pass
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
class ValidationError(SynapsAIError):
|
|
53
|
+
"""Exception raised for validation errors"""
|
|
54
|
+
pass
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
class TimeoutError(SynapsAIError):
|
|
58
|
+
"""Exception raised for timeout errors"""
|
|
59
|
+
pass
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
class ConnectionError(SynapsAIError):
|
|
63
|
+
"""Exception raised for connection errors"""
|
|
64
|
+
pass
|