wavespeed 1.0.8__tar.gz → 1.0.9__tar.gz

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.
Files changed (69) hide show
  1. {wavespeed-1.0.8 → wavespeed-1.0.9}/PKG-INFO +5 -3
  2. {wavespeed-1.0.8 → wavespeed-1.0.9}/README.md +4 -2
  3. {wavespeed-1.0.8 → wavespeed-1.0.9}/src/wavespeed/_version.py +1 -1
  4. {wavespeed-1.0.8 → wavespeed-1.0.9}/src/wavespeed/api/__init__.py +3 -1
  5. {wavespeed-1.0.8 → wavespeed-1.0.9}/src/wavespeed/api/client.py +87 -8
  6. {wavespeed-1.0.8 → wavespeed-1.0.9}/src/wavespeed/config.py +0 -1
  7. {wavespeed-1.0.8 → wavespeed-1.0.9}/src/wavespeed/serverless/modules/health.py +3 -1
  8. {wavespeed-1.0.8 → wavespeed-1.0.9}/src/wavespeed.egg-info/PKG-INFO +5 -3
  9. {wavespeed-1.0.8 → wavespeed-1.0.9}/tests/test_api.py +37 -0
  10. {wavespeed-1.0.8 → wavespeed-1.0.9}/tests/test_config.py +1 -3
  11. {wavespeed-1.0.8 → wavespeed-1.0.9}/.github/workflows/claude-code-review.yml +0 -0
  12. {wavespeed-1.0.8 → wavespeed-1.0.9}/.github/workflows/claude.yml +0 -0
  13. {wavespeed-1.0.8 → wavespeed-1.0.9}/.github/workflows/pre-commit.yml +0 -0
  14. {wavespeed-1.0.8 → wavespeed-1.0.9}/.github/workflows/python-packages.yml +0 -0
  15. {wavespeed-1.0.8 → wavespeed-1.0.9}/.github/workflows/python-publish.yml +0 -0
  16. {wavespeed-1.0.8 → wavespeed-1.0.9}/.gitignore +0 -0
  17. {wavespeed-1.0.8 → wavespeed-1.0.9}/.pre-commit-config.yaml +0 -0
  18. {wavespeed-1.0.8 → wavespeed-1.0.9}/CLAUDE.md +0 -0
  19. {wavespeed-1.0.8 → wavespeed-1.0.9}/LICENSE +0 -0
  20. {wavespeed-1.0.8 → wavespeed-1.0.9}/MANIFEST.in +0 -0
  21. {wavespeed-1.0.8 → wavespeed-1.0.9}/VERSIONING.md +0 -0
  22. {wavespeed-1.0.8 → wavespeed-1.0.9}/images/build_and_push.sh +0 -0
  23. {wavespeed-1.0.8 → wavespeed-1.0.9}/images/test_worker/Dockerfile +0 -0
  24. {wavespeed-1.0.8 → wavespeed-1.0.9}/images/test_worker/build_and_test.sh +0 -0
  25. {wavespeed-1.0.8 → wavespeed-1.0.9}/images/test_worker/handler.py +0 -0
  26. {wavespeed-1.0.8 → wavespeed-1.0.9}/images/test_worker/requirements.txt +0 -0
  27. {wavespeed-1.0.8 → wavespeed-1.0.9}/images/test_worker/test_input.json +0 -0
  28. {wavespeed-1.0.8 → wavespeed-1.0.9}/pyproject.toml +0 -0
  29. {wavespeed-1.0.8 → wavespeed-1.0.9}/pytest.ini +0 -0
  30. {wavespeed-1.0.8 → wavespeed-1.0.9}/setup.cfg +0 -0
  31. {wavespeed-1.0.8 → wavespeed-1.0.9}/src/wavespeed/__init__.py +0 -0
  32. {wavespeed-1.0.8 → wavespeed-1.0.9}/src/wavespeed/_config_module.py +0 -0
  33. {wavespeed-1.0.8 → wavespeed-1.0.9}/src/wavespeed/serverless/__init__.py +0 -0
  34. {wavespeed-1.0.8 → wavespeed-1.0.9}/src/wavespeed/serverless/modules/__init__.py +0 -0
  35. {wavespeed-1.0.8 → wavespeed-1.0.9}/src/wavespeed/serverless/modules/fastapi.py +0 -0
  36. {wavespeed-1.0.8 → wavespeed-1.0.9}/src/wavespeed/serverless/modules/handler.py +0 -0
  37. {wavespeed-1.0.8 → wavespeed-1.0.9}/src/wavespeed/serverless/modules/heartbeat.py +0 -0
  38. {wavespeed-1.0.8 → wavespeed-1.0.9}/src/wavespeed/serverless/modules/http.py +0 -0
  39. {wavespeed-1.0.8 → wavespeed-1.0.9}/src/wavespeed/serverless/modules/job.py +0 -0
  40. {wavespeed-1.0.8 → wavespeed-1.0.9}/src/wavespeed/serverless/modules/local.py +0 -0
  41. {wavespeed-1.0.8 → wavespeed-1.0.9}/src/wavespeed/serverless/modules/logger.py +0 -0
  42. {wavespeed-1.0.8 → wavespeed-1.0.9}/src/wavespeed/serverless/modules/progress.py +0 -0
  43. {wavespeed-1.0.8 → wavespeed-1.0.9}/src/wavespeed/serverless/modules/scaler.py +0 -0
  44. {wavespeed-1.0.8 → wavespeed-1.0.9}/src/wavespeed/serverless/modules/state.py +0 -0
  45. {wavespeed-1.0.8 → wavespeed-1.0.9}/src/wavespeed/serverless/utils/__init__.py +0 -0
  46. {wavespeed-1.0.8 → wavespeed-1.0.9}/src/wavespeed/serverless/utils/boto3_utils.py +0 -0
  47. {wavespeed-1.0.8 → wavespeed-1.0.9}/src/wavespeed/serverless/utils/validator.py +0 -0
  48. {wavespeed-1.0.8 → wavespeed-1.0.9}/src/wavespeed/serverless/worker.py +0 -0
  49. {wavespeed-1.0.8 → wavespeed-1.0.9}/src/wavespeed.egg-info/SOURCES.txt +0 -0
  50. {wavespeed-1.0.8 → wavespeed-1.0.9}/src/wavespeed.egg-info/dependency_links.txt +0 -0
  51. {wavespeed-1.0.8 → wavespeed-1.0.9}/src/wavespeed.egg-info/not-zip-safe +0 -0
  52. {wavespeed-1.0.8 → wavespeed-1.0.9}/src/wavespeed.egg-info/requires.txt +0 -0
  53. {wavespeed-1.0.8 → wavespeed-1.0.9}/src/wavespeed.egg-info/top_level.txt +0 -0
  54. {wavespeed-1.0.8 → wavespeed-1.0.9}/tests/__init__.py +0 -0
  55. {wavespeed-1.0.8 → wavespeed-1.0.9}/tests/conftest.py +0 -0
  56. {wavespeed-1.0.8 → wavespeed-1.0.9}/tests/test_serverless/__init__.py +0 -0
  57. {wavespeed-1.0.8 → wavespeed-1.0.9}/tests/test_serverless/test_modules/__init__.py +0 -0
  58. {wavespeed-1.0.8 → wavespeed-1.0.9}/tests/test_serverless/test_modules/test_fastapi.py +0 -0
  59. {wavespeed-1.0.8 → wavespeed-1.0.9}/tests/test_serverless/test_modules/test_handler.py +0 -0
  60. {wavespeed-1.0.8 → wavespeed-1.0.9}/tests/test_serverless/test_modules/test_http.py +0 -0
  61. {wavespeed-1.0.8 → wavespeed-1.0.9}/tests/test_serverless/test_modules/test_job.py +0 -0
  62. {wavespeed-1.0.8 → wavespeed-1.0.9}/tests/test_serverless/test_modules/test_local.py +0 -0
  63. {wavespeed-1.0.8 → wavespeed-1.0.9}/tests/test_serverless/test_modules/test_logger.py +0 -0
  64. {wavespeed-1.0.8 → wavespeed-1.0.9}/tests/test_serverless/test_modules/test_progress.py +0 -0
  65. {wavespeed-1.0.8 → wavespeed-1.0.9}/tests/test_serverless/test_modules/test_scaler.py +0 -0
  66. {wavespeed-1.0.8 → wavespeed-1.0.9}/tests/test_serverless/test_modules/test_state.py +0 -0
  67. {wavespeed-1.0.8 → wavespeed-1.0.9}/tests/test_serverless/test_utils/__init__.py +0 -0
  68. {wavespeed-1.0.8 → wavespeed-1.0.9}/tests/test_serverless/test_utils/test_validator.py +0 -0
  69. {wavespeed-1.0.8 → wavespeed-1.0.9}/tests/test_serverless/test_worker.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: wavespeed
3
- Version: 1.0.8
3
+ Version: 1.0.9
4
4
  Summary: WaveSpeedAI Python Client — Official Python SDK for WaveSpeedAI inference platform
5
5
  Author-email: WaveSpeedAI <support@wavespeed.ai>
6
6
  License: MIT
@@ -93,13 +93,15 @@ output = wavespeed.run(
93
93
  {"prompt": "Cat"},
94
94
  timeout=36000.0, # Max wait time in seconds (default: 36000.0)
95
95
  poll_interval=1.0, # Status check interval (default: 1.0)
96
- enable_sync_mode=False, # Single request mode, no polling (default: False)
96
+ enable_sync_mode=False, # Best-effort sync result attempt (default: False)
97
97
  )
98
98
  ```
99
99
 
100
100
  ### Sync Mode
101
101
 
102
- Use `enable_sync_mode=True` for a single request that waits for the result (no polling).
102
+ Use `enable_sync_mode=True` to ask the API to wait for the result in the initial
103
+ request. If the server-side sync wait times out, the SDK raises an error with
104
+ the task ID/result URL; the task continues processing and can be queried later.
103
105
 
104
106
  > **Note:** Not all models support sync mode. Check the model documentation for availability.
105
107
 
@@ -64,13 +64,15 @@ output = wavespeed.run(
64
64
  {"prompt": "Cat"},
65
65
  timeout=36000.0, # Max wait time in seconds (default: 36000.0)
66
66
  poll_interval=1.0, # Status check interval (default: 1.0)
67
- enable_sync_mode=False, # Single request mode, no polling (default: False)
67
+ enable_sync_mode=False, # Best-effort sync result attempt (default: False)
68
68
  )
69
69
  ```
70
70
 
71
71
  ### Sync Mode
72
72
 
73
- Use `enable_sync_mode=True` for a single request that waits for the result (no polling).
73
+ Use `enable_sync_mode=True` to ask the API to wait for the result in the initial
74
+ request. If the server-side sync wait times out, the SDK raises an error with
75
+ the task ID/result URL; the task continues processing and can be queried later.
74
76
 
75
77
  > **Note:** Not all models support sync mode. Check the model documentation for availability.
76
78
 
@@ -1,2 +1,2 @@
1
1
  # This file is automatically generated by setuptools_scm
2
- __version__ = "1.0.8"
2
+ __version__ = "1.0.9"
@@ -51,7 +51,9 @@ def run(
51
51
  input: Input parameters for the model.
52
52
  timeout: Maximum time to wait for completion (None = no timeout).
53
53
  poll_interval: Interval between status checks in seconds.
54
- enable_sync_mode: If True, use synchronous mode (single request).
54
+ enable_sync_mode: If True, use synchronous mode (best-effort single
55
+ request). If the server-side sync wait times out, an error is
56
+ raised with the task ID so the result can be queried later.
55
57
  max_retries: Maximum retries for this request (overrides default setting).
56
58
 
57
59
  Returns:
@@ -9,6 +9,9 @@ import requests
9
9
 
10
10
  from wavespeed.config import api as api_config
11
11
 
12
+ # HTTP status codes that are safe to retry
13
+ _RETRYABLE_STATUS_CODES = {429, 500, 502, 503, 504}
14
+
12
15
 
13
16
  class Client:
14
17
  """WaveSpeed API client.
@@ -25,7 +28,7 @@ class Client:
25
28
  client = Client(api_key="your-api-key")
26
29
  output = client.run("wavespeed-ai/z-image/turbo", {"prompt": "Cat"})
27
30
 
28
- # With sync mode (single request, waits for result)
31
+ # With sync mode (best-effort single request, waits for result)
29
32
  output = client.run("wavespeed-ai/z-image/turbo", {"prompt": "Cat"}, enable_sync_mode=True)
30
33
 
31
34
  # With retry
@@ -57,6 +60,18 @@ class Client:
57
60
  retry_interval if retry_interval is not None else api_config.retry_interval
58
61
  )
59
62
 
63
+ @staticmethod
64
+ def _is_retryable_status(status_code: int) -> bool:
65
+ """Check if an HTTP status code is retryable.
66
+
67
+ Args:
68
+ status_code: HTTP response status code.
69
+
70
+ Returns:
71
+ True if the status code indicates a transient error worth retrying.
72
+ """
73
+ return status_code in _RETRYABLE_STATUS_CODES
74
+
60
75
  def _get_headers(self) -> dict[str, str]:
61
76
  """Get request headers with authentication."""
62
77
  if not self.api_key:
@@ -106,6 +121,8 @@ class Client:
106
121
  )
107
122
  timeouts = (connect_timeout, request_timeout)
108
123
 
124
+ last_error: Exception | None = None
125
+
109
126
  for retry in range(self.max_connection_retries + 1):
110
127
  try:
111
128
  response = requests.post(
@@ -113,6 +130,24 @@ class Client:
113
130
  )
114
131
 
115
132
  if response.status_code != 200:
133
+ # Retry on transient server errors (5xx) and rate limiting (429)
134
+ if self._is_retryable_status(response.status_code):
135
+ last_error = RuntimeError(
136
+ f"Failed to submit prediction: HTTP {response.status_code}: "
137
+ f"{response.text}"
138
+ )
139
+ if retry < self.max_connection_retries:
140
+ delay = self.retry_interval * (retry + 1)
141
+ print(
142
+ f"Server error (HTTP {response.status_code}) on attempt "
143
+ f"{retry + 1}/{self.max_connection_retries + 1}, "
144
+ f"retrying in {delay} seconds..."
145
+ )
146
+ time.sleep(delay)
147
+ continue
148
+ raise last_error
149
+
150
+ # Non-retryable HTTP errors (4xx etc.) fail immediately
116
151
  raise RuntimeError(
117
152
  f"Failed to submit prediction: HTTP {response.status_code}: "
118
153
  f"{response.text}"
@@ -133,6 +168,7 @@ class Client:
133
168
  requests.exceptions.ConnectionError,
134
169
  requests.exceptions.Timeout,
135
170
  ) as e:
171
+ last_error = e
136
172
  print(
137
173
  f"Connection error on attempt {retry + 1}/{self.max_connection_retries + 1}:"
138
174
  )
@@ -171,6 +207,8 @@ class Client:
171
207
  )
172
208
  timeouts = (connect_timeout, request_timeout)
173
209
 
210
+ last_error: Exception | None = None
211
+
174
212
  for retry in range(self.max_connection_retries + 1):
175
213
  try:
176
214
  response = requests.get(
@@ -178,6 +216,23 @@ class Client:
178
216
  )
179
217
 
180
218
  if response.status_code != 200:
219
+ # Retry on transient server errors (5xx) and rate limiting (429)
220
+ if self._is_retryable_status(response.status_code):
221
+ last_error = RuntimeError(
222
+ f"Failed to get result for task {request_id}: "
223
+ f"HTTP {response.status_code}: {response.text}"
224
+ )
225
+ if retry < self.max_connection_retries:
226
+ delay = self.retry_interval * (retry + 1)
227
+ print(
228
+ f"Server error (HTTP {response.status_code}) getting result "
229
+ f"on attempt {retry + 1}/{self.max_connection_retries + 1}, "
230
+ f"retrying in {delay} seconds..."
231
+ )
232
+ time.sleep(delay)
233
+ continue
234
+ raise last_error
235
+
181
236
  raise RuntimeError(
182
237
  f"Failed to get result for task {request_id}: "
183
238
  f"HTTP {response.status_code}: {response.text}"
@@ -189,6 +244,7 @@ class Client:
189
244
  requests.exceptions.ConnectionError,
190
245
  requests.exceptions.Timeout,
191
246
  ) as e:
247
+ last_error = e
192
248
  print(
193
249
  f"Connection error getting result on attempt {retry + 1}/{self.max_connection_retries + 1}:"
194
250
  )
@@ -261,7 +317,12 @@ class Client:
261
317
  """
262
318
  # Always retry timeout and connection errors
263
319
  if isinstance(
264
- error, (requests.exceptions.Timeout, requests.exceptions.ConnectionError)
320
+ error,
321
+ (
322
+ requests.exceptions.Timeout,
323
+ requests.exceptions.ConnectionError,
324
+ TimeoutError,
325
+ ),
265
326
  ):
266
327
  return True
267
328
 
@@ -273,6 +334,25 @@ class Client:
273
334
 
274
335
  return False
275
336
 
337
+ @staticmethod
338
+ def _format_sync_mode_error(data: dict[str, Any]) -> str:
339
+ """Build an actionable error for a non-completed sync-mode response."""
340
+ request_id = data.get("id") or "unknown"
341
+ error = data.get("error") or "Unknown error"
342
+ urls = data.get("urls") or {}
343
+ result_url = urls.get("get") if isinstance(urls, dict) else None
344
+
345
+ is_sync_timeout = data.get("code") == 5004 or (
346
+ data.get("status") == "processing" and "Sync mode timed out" in error
347
+ )
348
+ if is_sync_timeout:
349
+ message = f"Sync mode timed out (task_id: {request_id}): {error}"
350
+ if result_url and result_url not in message:
351
+ message += f" Query the result later at: {result_url}"
352
+ return message
353
+
354
+ return f"Prediction failed (task_id: {request_id}): {error}"
355
+
276
356
  def run(
277
357
  self,
278
358
  model: str,
@@ -290,7 +370,9 @@ class Client:
290
370
  input: Input parameters for the model.
291
371
  timeout: Maximum time to wait for completion (None = no timeout).
292
372
  poll_interval: Interval between status checks in seconds.
293
- enable_sync_mode: If True, use synchronous mode (single request).
373
+ enable_sync_mode: If True, use synchronous mode (best-effort single
374
+ request). If the server-side sync wait times out, the SDK raises
375
+ an error with the task ID so the result can be queried later.
294
376
  max_retries: Maximum task-level retries (overrides client setting).
295
377
 
296
378
  Returns:
@@ -314,12 +396,8 @@ class Client:
314
396
  # In sync mode, extract outputs from the result
315
397
  status = sync_result.get("data", {}).get("status")
316
398
  if status != "completed":
317
- error = (
318
- sync_result.get("data", {}).get("error") or "Unknown error"
319
- )
320
- request_id = sync_result.get("data", {}).get("id", "unknown")
321
399
  raise RuntimeError(
322
- f"Prediction failed (task_id: {request_id}): {error}"
400
+ self._format_sync_mode_error(sync_result.get("data", {}))
323
401
  )
324
402
  data = sync_result.get("data", {})
325
403
  return {"outputs": data.get("outputs", [])}
@@ -328,6 +406,7 @@ class Client:
328
406
 
329
407
  except Exception as e:
330
408
  last_error = e
409
+
331
410
  is_retryable = self._is_retryable_error(e)
332
411
 
333
412
  if not is_retryable or attempt >= task_retries:
@@ -84,7 +84,6 @@ def _detect_serverless_env() -> Optional[str]:
84
84
  The serverless environment type ("runpod", "waverless") or None
85
85
  if not running in a known serverless environment.
86
86
  """
87
-
88
87
  # Check for native Waverless environment
89
88
  if os.environ.get("WAVERLESS_ENDPOINT_ID"):
90
89
  return "waverless"
@@ -45,7 +45,9 @@ class HealthServer:
45
45
  """Start the health server in a background thread."""
46
46
  try:
47
47
  self._server = HTTPServer((self.host, self.port), HealthHandler)
48
- self._thread = threading.Thread(target=self._server.serve_forever, daemon=True)
48
+ self._thread = threading.Thread(
49
+ target=self._server.serve_forever, daemon=True
50
+ )
49
51
  self._thread.start()
50
52
  log.info(f"Health server started at http://{self.host}:{self.port}/health")
51
53
  except Exception as e:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: wavespeed
3
- Version: 1.0.8
3
+ Version: 1.0.9
4
4
  Summary: WaveSpeedAI Python Client — Official Python SDK for WaveSpeedAI inference platform
5
5
  Author-email: WaveSpeedAI <support@wavespeed.ai>
6
6
  License: MIT
@@ -93,13 +93,15 @@ output = wavespeed.run(
93
93
  {"prompt": "Cat"},
94
94
  timeout=36000.0, # Max wait time in seconds (default: 36000.0)
95
95
  poll_interval=1.0, # Status check interval (default: 1.0)
96
- enable_sync_mode=False, # Single request mode, no polling (default: False)
96
+ enable_sync_mode=False, # Best-effort sync result attempt (default: False)
97
97
  )
98
98
  ```
99
99
 
100
100
  ### Sync Mode
101
101
 
102
- Use `enable_sync_mode=True` for a single request that waits for the result (no polling).
102
+ Use `enable_sync_mode=True` to ask the API to wait for the result in the initial
103
+ request. If the server-side sync wait times out, the SDK raises an error with
104
+ the task ID/result URL; the task continues processing and can be queried later.
103
105
 
104
106
  > **Note:** Not all models support sync mode. Check the model documentation for availability.
105
107
 
@@ -171,6 +171,43 @@ class TestClient(unittest.TestCase):
171
171
  client.run("wavespeed-ai/z-image/turbo", {"prompt": "test"}, timeout=10)
172
172
  self.assertIn("timed out", str(ctx.exception))
173
173
 
174
+ @patch("wavespeed.api.client.requests.get")
175
+ @patch("wavespeed.api.client.requests.post")
176
+ def test_run_sync_mode_timeout_raises_without_fallback(self, mock_post, mock_get):
177
+ """Test sync-mode timeout keeps the task queryable without async fallback."""
178
+ result_url = "https://api.wavespeed.ai/api/v3/predictions/req-timeout/result"
179
+ mock_post_response = MagicMock()
180
+ mock_post_response.status_code = 200
181
+ mock_post_response.json.return_value = {
182
+ "data": {
183
+ "id": "req-timeout",
184
+ "status": "processing",
185
+ "code": 5004,
186
+ "error": (
187
+ "Sync mode timed out after 90 seconds. The prediction is "
188
+ "still processing asynchronously."
189
+ ),
190
+ "urls": {"get": result_url},
191
+ }
192
+ }
193
+ mock_post.return_value = mock_post_response
194
+
195
+ client = Client(api_key="test-key")
196
+ with self.assertRaises(RuntimeError) as ctx:
197
+ client.run(
198
+ "wavespeed-ai/z-image/turbo",
199
+ {"prompt": "test"},
200
+ enable_sync_mode=True,
201
+ max_retries=1,
202
+ )
203
+
204
+ error = str(ctx.exception)
205
+ self.assertIn("Sync mode timed out", error)
206
+ self.assertIn("req-timeout", error)
207
+ self.assertIn(result_url, error)
208
+ mock_post.assert_called_once()
209
+ mock_get.assert_not_called()
210
+
174
211
 
175
212
  class TestModuleLevelRun(unittest.TestCase):
176
213
  """Tests for the module-level run() function."""
@@ -43,9 +43,7 @@ class TestResolveWaverlessUrl(unittest.TestCase):
43
43
  """Test that $WAVERLESS_POD_ID is replaced with pod_id."""
44
44
  template = "https://api.wavespeed.ai/v2/test/job-take/$WAVERLESS_POD_ID"
45
45
  result = _resolve_waverless_url(template, "my-pod-123")
46
- self.assertEqual(
47
- result, "https://api.wavespeed.ai/v2/test/job-take/my-pod-123"
48
- )
46
+ self.assertEqual(result, "https://api.wavespeed.ai/v2/test/job-take/my-pod-123")
49
47
 
50
48
  def test_preserves_id_placeholder(self):
51
49
  """Test that $ID is NOT replaced - it's for job/worker ID at runtime."""
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes