simplex 1.0.0__py3-none-any.whl → 1.0.8__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.
Potentially problematic release.
This version of simplex might be problematic. Click here for more details.
- simplex/__init__.py +34 -3
- simplex/client.py +355 -0
- simplex/errors.py +160 -0
- simplex/http_client.py +403 -0
- simplex/resources/__init__.py +10 -0
- simplex/resources/workflow.py +502 -0
- simplex/resources/workflow_session.py +333 -0
- simplex/types.py +276 -0
- simplex-1.0.8.dist-info/METADATA +408 -0
- simplex-1.0.8.dist-info/RECORD +13 -0
- {simplex-1.0.0.dist-info → simplex-1.0.8.dist-info}/WHEEL +1 -1
- {simplex-1.0.0.dist-info → simplex-1.0.8.dist-info/licenses}/LICENSE +2 -2
- simplex/constants.py +0 -1
- simplex/simplex.py +0 -187
- simplex/utils.py +0 -12
- simplex-1.0.0.dist-info/METADATA +0 -29
- simplex-1.0.0.dist-info/RECORD +0 -10
- simplex-1.0.0.dist-info/entry_points.txt +0 -2
- {simplex-1.0.0.dist-info → simplex-1.0.8.dist-info}/top_level.txt +0 -0
simplex/http_client.py
ADDED
|
@@ -0,0 +1,403 @@
|
|
|
1
|
+
"""
|
|
2
|
+
HTTP client for the Simplex SDK.
|
|
3
|
+
|
|
4
|
+
This module provides a robust HTTP client with automatic retry logic,
|
|
5
|
+
error handling, and support for various request types.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import time
|
|
9
|
+
from typing import Any, Dict, Optional
|
|
10
|
+
from urllib.parse import urlencode
|
|
11
|
+
|
|
12
|
+
import requests
|
|
13
|
+
from requests.adapters import HTTPAdapter
|
|
14
|
+
from urllib3.util.retry import Retry
|
|
15
|
+
|
|
16
|
+
from simplex.errors import (
|
|
17
|
+
AuthenticationError,
|
|
18
|
+
NetworkError,
|
|
19
|
+
RateLimitError,
|
|
20
|
+
SimplexError,
|
|
21
|
+
ValidationError,
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class HttpClient:
|
|
26
|
+
"""
|
|
27
|
+
HTTP client with retry logic and error handling.
|
|
28
|
+
|
|
29
|
+
This client handles all communication with the Simplex API, including:
|
|
30
|
+
- Automatic retry with exponential backoff
|
|
31
|
+
- Error mapping to custom exceptions
|
|
32
|
+
- Support for form-encoded and JSON requests
|
|
33
|
+
- File downloads
|
|
34
|
+
- Custom header management
|
|
35
|
+
|
|
36
|
+
Attributes:
|
|
37
|
+
base_url: Base URL for API requests
|
|
38
|
+
api_key: API key for authentication
|
|
39
|
+
timeout: Request timeout in seconds
|
|
40
|
+
retry_attempts: Number of retry attempts for failed requests
|
|
41
|
+
retry_delay: Base delay between retries in seconds
|
|
42
|
+
"""
|
|
43
|
+
|
|
44
|
+
def __init__(
|
|
45
|
+
self,
|
|
46
|
+
base_url: str,
|
|
47
|
+
api_key: str,
|
|
48
|
+
timeout: int = 30,
|
|
49
|
+
retry_attempts: int = 3,
|
|
50
|
+
retry_delay: int = 1,
|
|
51
|
+
headers: Optional[Dict[str, str]] = None,
|
|
52
|
+
):
|
|
53
|
+
"""
|
|
54
|
+
Initialize the HTTP client.
|
|
55
|
+
|
|
56
|
+
Args:
|
|
57
|
+
base_url: Base URL for the API (e.g., 'https://api.simplex.sh')
|
|
58
|
+
api_key: Your Simplex API key
|
|
59
|
+
timeout: Request timeout in seconds (default: 30)
|
|
60
|
+
retry_attempts: Maximum number of retry attempts (default: 3)
|
|
61
|
+
retry_delay: Base delay between retries in seconds (default: 1)
|
|
62
|
+
headers: Additional headers to include with all requests
|
|
63
|
+
"""
|
|
64
|
+
self.base_url = base_url.rstrip('/')
|
|
65
|
+
self.api_key = api_key
|
|
66
|
+
self.timeout = timeout
|
|
67
|
+
self.retry_attempts = retry_attempts
|
|
68
|
+
self.retry_delay = retry_delay
|
|
69
|
+
|
|
70
|
+
# Create a session with retry configuration
|
|
71
|
+
self.session = requests.Session()
|
|
72
|
+
|
|
73
|
+
# Configure default headers
|
|
74
|
+
self.session.headers.update({
|
|
75
|
+
'X-API-Key': api_key,
|
|
76
|
+
'User-Agent': 'Simplex-Python-SDK/1.0.0',
|
|
77
|
+
})
|
|
78
|
+
|
|
79
|
+
if headers:
|
|
80
|
+
self.session.headers.update(headers)
|
|
81
|
+
|
|
82
|
+
# Configure retry strategy for specific status codes
|
|
83
|
+
retry_strategy = Retry(
|
|
84
|
+
total=0, # We'll handle retries manually for more control
|
|
85
|
+
status_forcelist=[429, 500, 502, 503, 504],
|
|
86
|
+
backoff_factor=1,
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
adapter = HTTPAdapter(max_retries=retry_strategy)
|
|
90
|
+
self.session.mount("http://", adapter)
|
|
91
|
+
self.session.mount("https://", adapter)
|
|
92
|
+
|
|
93
|
+
def _should_retry(self, response: Optional[requests.Response]) -> bool:
|
|
94
|
+
"""
|
|
95
|
+
Determine if a request should be retried.
|
|
96
|
+
|
|
97
|
+
Args:
|
|
98
|
+
response: The response object (None for network errors)
|
|
99
|
+
|
|
100
|
+
Returns:
|
|
101
|
+
True if the request should be retried, False otherwise
|
|
102
|
+
"""
|
|
103
|
+
if response is None:
|
|
104
|
+
# Network error - should retry
|
|
105
|
+
return True
|
|
106
|
+
|
|
107
|
+
# Retry on rate limit, service unavailable, or server errors
|
|
108
|
+
return response.status_code in [429, 503] or response.status_code >= 500
|
|
109
|
+
|
|
110
|
+
def _handle_error(self, response: requests.Response) -> SimplexError:
|
|
111
|
+
"""
|
|
112
|
+
Convert HTTP errors to appropriate exception types.
|
|
113
|
+
|
|
114
|
+
Args:
|
|
115
|
+
response: The error response
|
|
116
|
+
|
|
117
|
+
Returns:
|
|
118
|
+
Appropriate SimplexError subclass for the error type
|
|
119
|
+
"""
|
|
120
|
+
status_code = response.status_code
|
|
121
|
+
|
|
122
|
+
# Try to extract error message from response
|
|
123
|
+
try:
|
|
124
|
+
data = response.json()
|
|
125
|
+
if isinstance(data, dict):
|
|
126
|
+
message = data.get('message') or data.get('error') or 'An error occurred'
|
|
127
|
+
else:
|
|
128
|
+
message = str(data)
|
|
129
|
+
except ValueError:
|
|
130
|
+
message = response.text or 'An error occurred'
|
|
131
|
+
|
|
132
|
+
# Map status codes to exception types
|
|
133
|
+
if status_code == 400:
|
|
134
|
+
return ValidationError(message, data=response.json() if response.text else None)
|
|
135
|
+
elif status_code in [401, 403]:
|
|
136
|
+
return AuthenticationError(message)
|
|
137
|
+
elif status_code == 429:
|
|
138
|
+
# Extract retry-after header if present
|
|
139
|
+
retry_after = response.headers.get('Retry-After')
|
|
140
|
+
retry_after_seconds = int(retry_after) if retry_after and retry_after.isdigit() else None
|
|
141
|
+
return RateLimitError(message, retry_after=retry_after_seconds)
|
|
142
|
+
else:
|
|
143
|
+
return SimplexError(message, status_code=status_code, data=response.json() if response.text else None)
|
|
144
|
+
|
|
145
|
+
def _make_request(
|
|
146
|
+
self,
|
|
147
|
+
method: str,
|
|
148
|
+
path: str,
|
|
149
|
+
data: Optional[Any] = None,
|
|
150
|
+
params: Optional[Dict[str, Any]] = None,
|
|
151
|
+
headers: Optional[Dict[str, str]] = None,
|
|
152
|
+
**kwargs: Any
|
|
153
|
+
) -> requests.Response:
|
|
154
|
+
"""
|
|
155
|
+
Make an HTTP request with retry logic.
|
|
156
|
+
|
|
157
|
+
Args:
|
|
158
|
+
method: HTTP method (GET, POST, etc.)
|
|
159
|
+
path: API endpoint path
|
|
160
|
+
data: Request body data
|
|
161
|
+
params: Query parameters
|
|
162
|
+
headers: Additional headers for this request
|
|
163
|
+
**kwargs: Additional arguments to pass to requests
|
|
164
|
+
|
|
165
|
+
Returns:
|
|
166
|
+
Response object
|
|
167
|
+
|
|
168
|
+
Raises:
|
|
169
|
+
SimplexError: If the request fails after all retries
|
|
170
|
+
"""
|
|
171
|
+
url = f"{self.base_url}{path}"
|
|
172
|
+
attempt = 0
|
|
173
|
+
last_exception = None
|
|
174
|
+
|
|
175
|
+
while attempt <= self.retry_attempts:
|
|
176
|
+
try:
|
|
177
|
+
response = self.session.request(
|
|
178
|
+
method=method,
|
|
179
|
+
url=url,
|
|
180
|
+
data=data,
|
|
181
|
+
params=params,
|
|
182
|
+
headers=headers,
|
|
183
|
+
timeout=self.timeout,
|
|
184
|
+
**kwargs
|
|
185
|
+
)
|
|
186
|
+
|
|
187
|
+
# Check for HTTP errors
|
|
188
|
+
if not response.ok:
|
|
189
|
+
error = self._handle_error(response)
|
|
190
|
+
|
|
191
|
+
# Retry if appropriate
|
|
192
|
+
if self._should_retry(response) and attempt < self.retry_attempts:
|
|
193
|
+
attempt += 1
|
|
194
|
+
time.sleep(self.retry_delay * attempt) # Exponential backoff
|
|
195
|
+
continue
|
|
196
|
+
|
|
197
|
+
raise error
|
|
198
|
+
|
|
199
|
+
return response
|
|
200
|
+
|
|
201
|
+
except requests.exceptions.RequestException as e:
|
|
202
|
+
last_exception = NetworkError(str(e))
|
|
203
|
+
|
|
204
|
+
# Retry network errors
|
|
205
|
+
if attempt < self.retry_attempts:
|
|
206
|
+
attempt += 1
|
|
207
|
+
time.sleep(self.retry_delay * attempt)
|
|
208
|
+
continue
|
|
209
|
+
|
|
210
|
+
raise last_exception
|
|
211
|
+
|
|
212
|
+
# If we get here, all retries failed
|
|
213
|
+
if last_exception:
|
|
214
|
+
raise last_exception
|
|
215
|
+
raise NetworkError("Request failed after all retries")
|
|
216
|
+
|
|
217
|
+
def get(self, path: str, params: Optional[Dict[str, Any]] = None) -> Any:
|
|
218
|
+
"""
|
|
219
|
+
Make a GET request.
|
|
220
|
+
|
|
221
|
+
Args:
|
|
222
|
+
path: API endpoint path
|
|
223
|
+
params: Query parameters
|
|
224
|
+
|
|
225
|
+
Returns:
|
|
226
|
+
Parsed JSON response
|
|
227
|
+
"""
|
|
228
|
+
response = self._make_request('GET', path, params=params)
|
|
229
|
+
return response.json()
|
|
230
|
+
|
|
231
|
+
def post(
|
|
232
|
+
self,
|
|
233
|
+
path: str,
|
|
234
|
+
data: Optional[Dict[str, Any]] = None,
|
|
235
|
+
headers: Optional[Dict[str, str]] = None
|
|
236
|
+
) -> Any:
|
|
237
|
+
"""
|
|
238
|
+
Make a POST request with form-encoded data.
|
|
239
|
+
|
|
240
|
+
This method sends data as application/x-www-form-urlencoded,
|
|
241
|
+
which is the default format for the Simplex API.
|
|
242
|
+
|
|
243
|
+
Args:
|
|
244
|
+
path: API endpoint path
|
|
245
|
+
data: Form data to send
|
|
246
|
+
headers: Additional headers
|
|
247
|
+
|
|
248
|
+
Returns:
|
|
249
|
+
Parsed JSON response
|
|
250
|
+
"""
|
|
251
|
+
# Convert data to form-encoded format
|
|
252
|
+
form_data = {}
|
|
253
|
+
if data:
|
|
254
|
+
for key, value in data.items():
|
|
255
|
+
if value is not None:
|
|
256
|
+
# Convert complex objects to JSON strings
|
|
257
|
+
if isinstance(value, (dict, list)):
|
|
258
|
+
import json
|
|
259
|
+
form_data[key] = json.dumps(value)
|
|
260
|
+
else:
|
|
261
|
+
form_data[key] = str(value)
|
|
262
|
+
|
|
263
|
+
request_headers = {'Content-Type': 'application/x-www-form-urlencoded'}
|
|
264
|
+
if headers:
|
|
265
|
+
request_headers.update(headers)
|
|
266
|
+
|
|
267
|
+
response = self._make_request(
|
|
268
|
+
'POST',
|
|
269
|
+
path,
|
|
270
|
+
data=urlencode(form_data) if form_data else None,
|
|
271
|
+
headers=request_headers
|
|
272
|
+
)
|
|
273
|
+
return response.json()
|
|
274
|
+
|
|
275
|
+
def post_json(
|
|
276
|
+
self,
|
|
277
|
+
path: str,
|
|
278
|
+
data: Optional[Any] = None,
|
|
279
|
+
headers: Optional[Dict[str, str]] = None
|
|
280
|
+
) -> Any:
|
|
281
|
+
"""
|
|
282
|
+
Make a POST request with JSON data.
|
|
283
|
+
|
|
284
|
+
Args:
|
|
285
|
+
path: API endpoint path
|
|
286
|
+
data: Data to send as JSON
|
|
287
|
+
headers: Additional headers
|
|
288
|
+
|
|
289
|
+
Returns:
|
|
290
|
+
Parsed JSON response
|
|
291
|
+
"""
|
|
292
|
+
request_headers = {'Content-Type': 'application/json'}
|
|
293
|
+
if headers:
|
|
294
|
+
request_headers.update(headers)
|
|
295
|
+
|
|
296
|
+
response = self._make_request(
|
|
297
|
+
'POST',
|
|
298
|
+
path,
|
|
299
|
+
json=data,
|
|
300
|
+
headers=request_headers
|
|
301
|
+
)
|
|
302
|
+
return response.json()
|
|
303
|
+
|
|
304
|
+
def put(
|
|
305
|
+
self,
|
|
306
|
+
path: str,
|
|
307
|
+
data: Optional[Any] = None,
|
|
308
|
+
headers: Optional[Dict[str, str]] = None
|
|
309
|
+
) -> Any:
|
|
310
|
+
"""
|
|
311
|
+
Make a PUT request.
|
|
312
|
+
|
|
313
|
+
Args:
|
|
314
|
+
path: API endpoint path
|
|
315
|
+
data: Data to send
|
|
316
|
+
headers: Additional headers
|
|
317
|
+
|
|
318
|
+
Returns:
|
|
319
|
+
Parsed JSON response
|
|
320
|
+
"""
|
|
321
|
+
response = self._make_request('PUT', path, json=data, headers=headers)
|
|
322
|
+
return response.json()
|
|
323
|
+
|
|
324
|
+
def patch(
|
|
325
|
+
self,
|
|
326
|
+
path: str,
|
|
327
|
+
data: Optional[Any] = None,
|
|
328
|
+
headers: Optional[Dict[str, str]] = None
|
|
329
|
+
) -> Any:
|
|
330
|
+
"""
|
|
331
|
+
Make a PATCH request.
|
|
332
|
+
|
|
333
|
+
Args:
|
|
334
|
+
path: API endpoint path
|
|
335
|
+
data: Data to send
|
|
336
|
+
headers: Additional headers
|
|
337
|
+
|
|
338
|
+
Returns:
|
|
339
|
+
Parsed JSON response
|
|
340
|
+
"""
|
|
341
|
+
response = self._make_request('PATCH', path, json=data, headers=headers)
|
|
342
|
+
return response.json()
|
|
343
|
+
|
|
344
|
+
def delete(self, path: str, headers: Optional[Dict[str, str]] = None) -> Any:
|
|
345
|
+
"""
|
|
346
|
+
Make a DELETE request.
|
|
347
|
+
|
|
348
|
+
Args:
|
|
349
|
+
path: API endpoint path
|
|
350
|
+
headers: Additional headers
|
|
351
|
+
|
|
352
|
+
Returns:
|
|
353
|
+
Parsed JSON response
|
|
354
|
+
"""
|
|
355
|
+
response = self._make_request('DELETE', path, headers=headers)
|
|
356
|
+
return response.json()
|
|
357
|
+
|
|
358
|
+
def download_file(
|
|
359
|
+
self,
|
|
360
|
+
path: str,
|
|
361
|
+
params: Optional[Dict[str, Any]] = None
|
|
362
|
+
) -> bytes:
|
|
363
|
+
"""
|
|
364
|
+
Download a file from the API.
|
|
365
|
+
|
|
366
|
+
Args:
|
|
367
|
+
path: API endpoint path
|
|
368
|
+
params: Query parameters
|
|
369
|
+
|
|
370
|
+
Returns:
|
|
371
|
+
File content as bytes
|
|
372
|
+
"""
|
|
373
|
+
response = self._make_request('GET', path, params=params)
|
|
374
|
+
return response.content
|
|
375
|
+
|
|
376
|
+
def set_header(self, key: str, value: str) -> None:
|
|
377
|
+
"""
|
|
378
|
+
Set a custom header for all requests.
|
|
379
|
+
|
|
380
|
+
Args:
|
|
381
|
+
key: Header name
|
|
382
|
+
value: Header value
|
|
383
|
+
"""
|
|
384
|
+
self.session.headers[key] = value
|
|
385
|
+
|
|
386
|
+
def remove_header(self, key: str) -> None:
|
|
387
|
+
"""
|
|
388
|
+
Remove a custom header.
|
|
389
|
+
|
|
390
|
+
Args:
|
|
391
|
+
key: Header name to remove
|
|
392
|
+
"""
|
|
393
|
+
self.session.headers.pop(key, None)
|
|
394
|
+
|
|
395
|
+
def update_api_key(self, api_key: str) -> None:
|
|
396
|
+
"""
|
|
397
|
+
Update the API key used for authentication.
|
|
398
|
+
|
|
399
|
+
Args:
|
|
400
|
+
api_key: New API key
|
|
401
|
+
"""
|
|
402
|
+
self.api_key = api_key
|
|
403
|
+
self.set_header('X-API-Key', api_key)
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Resources module for Simplex SDK.
|
|
3
|
+
|
|
4
|
+
Contains resource classes for interacting with different Simplex API endpoints.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from simplex.resources.workflow import Workflow
|
|
8
|
+
from simplex.resources.workflow_session import WorkflowSession
|
|
9
|
+
|
|
10
|
+
__all__ = ["Workflow", "WorkflowSession"]
|