wavespeed 2.0.3__tar.gz → 2.0.4__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.
- {wavespeed-2.0.3 → wavespeed-2.0.4}/PKG-INFO +1 -1
- {wavespeed-2.0.3 → wavespeed-2.0.4}/src/wavespeed/_version.py +1 -1
- {wavespeed-2.0.3 → wavespeed-2.0.4}/src/wavespeed/api/client.py +21 -28
- {wavespeed-2.0.3 → wavespeed-2.0.4}/src/wavespeed.egg-info/PKG-INFO +1 -1
- wavespeed-2.0.4/src/wavespeed.egg-info/scm_version.json +8 -0
- {wavespeed-2.0.3 → wavespeed-2.0.4}/tests/test_api.py +92 -0
- wavespeed-2.0.3/src/wavespeed.egg-info/scm_version.json +0 -8
- {wavespeed-2.0.3 → wavespeed-2.0.4}/.github/dependabot.yml +0 -0
- {wavespeed-2.0.3 → wavespeed-2.0.4}/.github/workflows/claude.yml +0 -0
- {wavespeed-2.0.3 → wavespeed-2.0.4}/.github/workflows/pre-commit.yml +0 -0
- {wavespeed-2.0.3 → wavespeed-2.0.4}/.github/workflows/python-packages.yml +0 -0
- {wavespeed-2.0.3 → wavespeed-2.0.4}/.github/workflows/python-publish.yml +0 -0
- {wavespeed-2.0.3 → wavespeed-2.0.4}/.github/workflows/tests.yml +0 -0
- {wavespeed-2.0.3 → wavespeed-2.0.4}/.gitignore +0 -0
- {wavespeed-2.0.3 → wavespeed-2.0.4}/.pre-commit-config.yaml +0 -0
- {wavespeed-2.0.3 → wavespeed-2.0.4}/CHANGELOG.md +0 -0
- {wavespeed-2.0.3 → wavespeed-2.0.4}/CLAUDE.md +0 -0
- {wavespeed-2.0.3 → wavespeed-2.0.4}/LICENSE +0 -0
- {wavespeed-2.0.3 → wavespeed-2.0.4}/MANIFEST.in +0 -0
- {wavespeed-2.0.3 → wavespeed-2.0.4}/README.md +0 -0
- {wavespeed-2.0.3 → wavespeed-2.0.4}/VERSIONING.md +0 -0
- {wavespeed-2.0.3 → wavespeed-2.0.4}/assets/wavespeed-logo-dark.svg +0 -0
- {wavespeed-2.0.3 → wavespeed-2.0.4}/assets/wavespeed-logo-light.svg +0 -0
- {wavespeed-2.0.3 → wavespeed-2.0.4}/pyproject.toml +0 -0
- {wavespeed-2.0.3 → wavespeed-2.0.4}/pytest.ini +0 -0
- {wavespeed-2.0.3 → wavespeed-2.0.4}/setup.cfg +0 -0
- {wavespeed-2.0.3 → wavespeed-2.0.4}/src/wavespeed/__init__.py +0 -0
- {wavespeed-2.0.3 → wavespeed-2.0.4}/src/wavespeed/_config_module.py +0 -0
- {wavespeed-2.0.3 → wavespeed-2.0.4}/src/wavespeed/api/__init__.py +0 -0
- {wavespeed-2.0.3 → wavespeed-2.0.4}/src/wavespeed/config.py +0 -0
- {wavespeed-2.0.3 → wavespeed-2.0.4}/src/wavespeed.egg-info/SOURCES.txt +0 -0
- {wavespeed-2.0.3 → wavespeed-2.0.4}/src/wavespeed.egg-info/dependency_links.txt +0 -0
- {wavespeed-2.0.3 → wavespeed-2.0.4}/src/wavespeed.egg-info/not-zip-safe +0 -0
- {wavespeed-2.0.3 → wavespeed-2.0.4}/src/wavespeed.egg-info/requires.txt +0 -0
- {wavespeed-2.0.3 → wavespeed-2.0.4}/src/wavespeed.egg-info/scm_file_list.json +0 -0
- {wavespeed-2.0.3 → wavespeed-2.0.4}/src/wavespeed.egg-info/top_level.txt +0 -0
- {wavespeed-2.0.3 → wavespeed-2.0.4}/tests/__init__.py +0 -0
- {wavespeed-2.0.3 → wavespeed-2.0.4}/tests/conftest.py +0 -0
- {wavespeed-2.0.3 → wavespeed-2.0.4}/tests/test_config.py +0 -0
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
# This file is automatically generated by setuptools_scm
|
|
2
|
-
__version__ = "2.0.
|
|
2
|
+
__version__ = "2.0.4"
|
|
@@ -44,6 +44,10 @@ class _SubmissionError(RuntimeError):
|
|
|
44
44
|
"""A task submission failed and must not be retried automatically."""
|
|
45
45
|
|
|
46
46
|
|
|
47
|
+
class _TaskFailedError(RuntimeError):
|
|
48
|
+
"""The task itself reported status ``failed``; a task-level re-run is allowed."""
|
|
49
|
+
|
|
50
|
+
|
|
47
51
|
class Client:
|
|
48
52
|
"""WaveSpeed API client.
|
|
49
53
|
|
|
@@ -332,14 +336,13 @@ class Client:
|
|
|
332
336
|
|
|
333
337
|
if status in ("failed", "cancelled", "timeout", "deleted"):
|
|
334
338
|
error = data.get("error") or "Unknown error"
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
)
|
|
339
|
+
error_cls = _TaskFailedError if status == "failed" else RuntimeError
|
|
340
|
+
raise error_cls(f"Prediction {status} (task_id: {request_id}): {error}")
|
|
338
341
|
|
|
339
342
|
time.sleep(poll_interval)
|
|
340
343
|
|
|
341
344
|
def _is_retryable_error(self, error: Exception) -> bool:
|
|
342
|
-
"""Determine if an error is worth
|
|
345
|
+
"""Determine if an error is worth re-running the task for.
|
|
343
346
|
|
|
344
347
|
Args:
|
|
345
348
|
error: The exception to check.
|
|
@@ -347,29 +350,17 @@ class Client:
|
|
|
347
350
|
Returns:
|
|
348
351
|
True if the error is retryable.
|
|
349
352
|
"""
|
|
350
|
-
#
|
|
351
|
-
#
|
|
352
|
-
|
|
353
|
+
# Only a failure reported by the task itself is re-run, and only when it
|
|
354
|
+
# was a transient upstream error (HTTP 5xx / 429), as before: a
|
|
355
|
+
# deterministic failure such as a content-policy rejection would fail
|
|
356
|
+
# again. Submission errors are ambiguous (the task may already exist),
|
|
357
|
+
# and poll-side HTTP errors, poll retry exhaustion and client-side wait
|
|
358
|
+
# timeouts all happen after the task was created and may be charged, so
|
|
359
|
+
# none of them may turn into another POST.
|
|
360
|
+
if not isinstance(error, _TaskFailedError):
|
|
353
361
|
return False
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
if isinstance(
|
|
357
|
-
error,
|
|
358
|
-
(
|
|
359
|
-
requests.exceptions.Timeout,
|
|
360
|
-
requests.exceptions.ConnectionError,
|
|
361
|
-
TimeoutError,
|
|
362
|
-
),
|
|
363
|
-
):
|
|
364
|
-
return True
|
|
365
|
-
|
|
366
|
-
# Retry server errors (5xx) and rate limiting (429)
|
|
367
|
-
if isinstance(error, RuntimeError):
|
|
368
|
-
error_str = str(error)
|
|
369
|
-
if "HTTP 5" in error_str or "HTTP 429" in error_str:
|
|
370
|
-
return True
|
|
371
|
-
|
|
372
|
-
return False
|
|
362
|
+
error_str = str(error)
|
|
363
|
+
return "HTTP 5" in error_str or "HTTP 429" in error_str
|
|
373
364
|
|
|
374
365
|
def _format_sync_mode_error(self, data: dict[str, Any]) -> str:
|
|
375
366
|
"""Build an actionable error for a non-completed sync-mode response."""
|
|
@@ -464,8 +455,10 @@ class Client:
|
|
|
464
455
|
@staticmethod
|
|
465
456
|
def _task_id_from_error(error: Exception) -> str:
|
|
466
457
|
"""Extract a task ID from an error message, mirroring the JS SDK."""
|
|
467
|
-
match = re.search(r"task_id:\s*([^)]+)", str(error))
|
|
468
|
-
|
|
458
|
+
match = re.search(r"task_id:\s*([^)]+)|for task ([^\s:]+)", str(error))
|
|
459
|
+
if not match:
|
|
460
|
+
return "unknown"
|
|
461
|
+
return (match.group(1) or match.group(2)).strip()
|
|
469
462
|
|
|
470
463
|
def run_no_throw(
|
|
471
464
|
self,
|
|
@@ -149,6 +149,98 @@ class TestClient(unittest.TestCase):
|
|
|
149
149
|
|
|
150
150
|
mock_post.assert_called_once()
|
|
151
151
|
|
|
152
|
+
@staticmethod
|
|
153
|
+
def _response(status_code, payload=None, text=""):
|
|
154
|
+
response = MagicMock()
|
|
155
|
+
response.status_code = status_code
|
|
156
|
+
response.text = text
|
|
157
|
+
response.json.return_value = payload
|
|
158
|
+
return response
|
|
159
|
+
|
|
160
|
+
@patch("wavespeed.api.client.time.sleep")
|
|
161
|
+
@patch("wavespeed.api.client.requests.get")
|
|
162
|
+
@patch("wavespeed.api.client.requests.post")
|
|
163
|
+
def test_task_retries_do_not_repost_after_poll_retry_exhaustion(
|
|
164
|
+
self, mock_post, mock_get, _mock_sleep
|
|
165
|
+
):
|
|
166
|
+
"""A poll that exhausts its GET retries must not create a second task."""
|
|
167
|
+
mock_post.return_value = self._response(200, {"data": {"id": "req-1"}})
|
|
168
|
+
mock_get.return_value = self._response(502, text="Bad Gateway")
|
|
169
|
+
|
|
170
|
+
client = Client(api_key="test-key", max_retries=2, max_connection_retries=1)
|
|
171
|
+
with self.assertRaises(RuntimeError) as ctx:
|
|
172
|
+
client.run("wavespeed-ai/z-image/turbo", {"prompt": "test"})
|
|
173
|
+
|
|
174
|
+
self.assertIn("req-1", str(ctx.exception))
|
|
175
|
+
self.assertIn("HTTP 502", str(ctx.exception))
|
|
176
|
+
self.assertEqual(mock_post.call_count, 1)
|
|
177
|
+
self.assertEqual(mock_get.call_count, 2)
|
|
178
|
+
|
|
179
|
+
result = client.run_no_throw("wavespeed-ai/z-image/turbo", {"prompt": "test"})
|
|
180
|
+
self.assertEqual(result["task_id"], "req-1")
|
|
181
|
+
self.assertEqual(mock_post.call_count, 2) # one POST per run, never per poll
|
|
182
|
+
|
|
183
|
+
@patch("wavespeed.api.client.time.sleep")
|
|
184
|
+
@patch("wavespeed.api.client.requests.get")
|
|
185
|
+
@patch("wavespeed.api.client.requests.post")
|
|
186
|
+
def test_task_retries_do_not_repost_after_wait_timeout(
|
|
187
|
+
self, mock_post, mock_get, _mock_sleep
|
|
188
|
+
):
|
|
189
|
+
"""A client-side wait timeout leaves the task alive and must not re-POST."""
|
|
190
|
+
mock_post.return_value = self._response(200, {"data": {"id": "req-1"}})
|
|
191
|
+
mock_get.return_value = self._response(200, {"data": {"status": "processing"}})
|
|
192
|
+
|
|
193
|
+
client = Client(api_key="test-key", max_retries=2)
|
|
194
|
+
with self.assertRaises(TimeoutError) as ctx:
|
|
195
|
+
client.run("wavespeed-ai/z-image/turbo", {"prompt": "test"}, timeout=0)
|
|
196
|
+
|
|
197
|
+
self.assertIn("req-1", str(ctx.exception))
|
|
198
|
+
self.assertEqual(mock_post.call_count, 1)
|
|
199
|
+
|
|
200
|
+
@patch("wavespeed.api.client.time.sleep")
|
|
201
|
+
@patch("wavespeed.api.client.requests.get")
|
|
202
|
+
@patch("wavespeed.api.client.requests.post")
|
|
203
|
+
def test_task_retries_rerun_when_task_reports_failed(
|
|
204
|
+
self, mock_post, mock_get, _mock_sleep
|
|
205
|
+
):
|
|
206
|
+
"""A task that fails with a transient upstream error is re-run when max_retries > 0."""
|
|
207
|
+
mock_post.side_effect = [
|
|
208
|
+
self._response(200, {"data": {"id": "req-1"}}),
|
|
209
|
+
self._response(200, {"data": {"id": "req-2"}}),
|
|
210
|
+
]
|
|
211
|
+
mock_get.side_effect = [
|
|
212
|
+
self._response(
|
|
213
|
+
200, {"data": {"status": "failed", "error": "HTTP 500 upstream error"}}
|
|
214
|
+
),
|
|
215
|
+
self._response(200, {"data": {"status": "completed", "outputs": ["url"]}}),
|
|
216
|
+
]
|
|
217
|
+
|
|
218
|
+
client = Client(api_key="test-key", max_retries=1)
|
|
219
|
+
result = client.run("wavespeed-ai/z-image/turbo", {"prompt": "test"})
|
|
220
|
+
|
|
221
|
+
self.assertEqual(result, {"outputs": ["url"]})
|
|
222
|
+
self.assertEqual(mock_post.call_count, 2)
|
|
223
|
+
|
|
224
|
+
@patch("wavespeed.api.client.time.sleep")
|
|
225
|
+
@patch("wavespeed.api.client.requests.get")
|
|
226
|
+
@patch("wavespeed.api.client.requests.post")
|
|
227
|
+
def test_task_retries_do_not_rerun_deterministic_failure(
|
|
228
|
+
self, mock_post, mock_get, _mock_sleep
|
|
229
|
+
):
|
|
230
|
+
"""A task that fails for a non-transient reason is not re-submitted."""
|
|
231
|
+
mock_post.side_effect = [self._response(200, {"data": {"id": "req-1"}})]
|
|
232
|
+
mock_get.side_effect = [
|
|
233
|
+
self._response(
|
|
234
|
+
200, {"data": {"status": "failed", "error": "content policy violation"}}
|
|
235
|
+
),
|
|
236
|
+
]
|
|
237
|
+
|
|
238
|
+
client = Client(api_key="test-key", max_retries=2)
|
|
239
|
+
with self.assertRaises(RuntimeError):
|
|
240
|
+
client.run("wavespeed-ai/z-image/turbo", {"prompt": "test"})
|
|
241
|
+
|
|
242
|
+
self.assertEqual(mock_post.call_count, 1)
|
|
243
|
+
|
|
152
244
|
@patch("wavespeed.api.client.requests.get")
|
|
153
245
|
def test_get_result_success(self, mock_get):
|
|
154
246
|
"""Test successful result retrieval."""
|
|
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
|
|
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
|
|
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
|
|
File without changes
|
|
File without changes
|