simplex 1.0.0__py3-none-any.whl → 1.0.6__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/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"]