wavespeed 2.0.4__tar.gz → 2.0.5__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.4/src/wavespeed.egg-info → wavespeed-2.0.5}/PKG-INFO +10 -2
- {wavespeed-2.0.4 → wavespeed-2.0.5}/README.md +9 -1
- {wavespeed-2.0.4 → wavespeed-2.0.5}/src/wavespeed/_version.py +1 -1
- {wavespeed-2.0.4 → wavespeed-2.0.5}/src/wavespeed/api/__init__.py +3 -1
- {wavespeed-2.0.4 → wavespeed-2.0.5}/src/wavespeed/api/client.py +46 -22
- wavespeed-2.0.5/src/wavespeed/config.py +59 -0
- {wavespeed-2.0.4 → wavespeed-2.0.5/src/wavespeed.egg-info}/PKG-INFO +10 -2
- {wavespeed-2.0.4 → wavespeed-2.0.5}/src/wavespeed.egg-info/SOURCES.txt +2 -1
- {wavespeed-2.0.4 → wavespeed-2.0.5}/src/wavespeed.egg-info/scm_file_list.json +2 -1
- {wavespeed-2.0.4 → wavespeed-2.0.5}/src/wavespeed.egg-info/scm_version.json +2 -2
- {wavespeed-2.0.4 → wavespeed-2.0.5}/tests/test_api.py +10 -2
- {wavespeed-2.0.4 → wavespeed-2.0.5}/tests/test_config.py +5 -1
- wavespeed-2.0.5/tests/test_timeouts.py +235 -0
- wavespeed-2.0.4/src/wavespeed/config.py +0 -42
- {wavespeed-2.0.4 → wavespeed-2.0.5}/.github/dependabot.yml +0 -0
- {wavespeed-2.0.4 → wavespeed-2.0.5}/.github/workflows/claude.yml +0 -0
- {wavespeed-2.0.4 → wavespeed-2.0.5}/.github/workflows/pre-commit.yml +0 -0
- {wavespeed-2.0.4 → wavespeed-2.0.5}/.github/workflows/python-packages.yml +0 -0
- {wavespeed-2.0.4 → wavespeed-2.0.5}/.github/workflows/python-publish.yml +0 -0
- {wavespeed-2.0.4 → wavespeed-2.0.5}/.github/workflows/tests.yml +0 -0
- {wavespeed-2.0.4 → wavespeed-2.0.5}/.gitignore +0 -0
- {wavespeed-2.0.4 → wavespeed-2.0.5}/.pre-commit-config.yaml +0 -0
- {wavespeed-2.0.4 → wavespeed-2.0.5}/CHANGELOG.md +0 -0
- {wavespeed-2.0.4 → wavespeed-2.0.5}/CLAUDE.md +0 -0
- {wavespeed-2.0.4 → wavespeed-2.0.5}/LICENSE +0 -0
- {wavespeed-2.0.4 → wavespeed-2.0.5}/MANIFEST.in +0 -0
- {wavespeed-2.0.4 → wavespeed-2.0.5}/VERSIONING.md +0 -0
- {wavespeed-2.0.4 → wavespeed-2.0.5}/assets/wavespeed-logo-dark.svg +0 -0
- {wavespeed-2.0.4 → wavespeed-2.0.5}/assets/wavespeed-logo-light.svg +0 -0
- {wavespeed-2.0.4 → wavespeed-2.0.5}/pyproject.toml +0 -0
- {wavespeed-2.0.4 → wavespeed-2.0.5}/pytest.ini +0 -0
- {wavespeed-2.0.4 → wavespeed-2.0.5}/setup.cfg +0 -0
- {wavespeed-2.0.4 → wavespeed-2.0.5}/src/wavespeed/__init__.py +0 -0
- {wavespeed-2.0.4 → wavespeed-2.0.5}/src/wavespeed/_config_module.py +0 -0
- {wavespeed-2.0.4 → wavespeed-2.0.5}/src/wavespeed.egg-info/dependency_links.txt +0 -0
- {wavespeed-2.0.4 → wavespeed-2.0.5}/src/wavespeed.egg-info/not-zip-safe +0 -0
- {wavespeed-2.0.4 → wavespeed-2.0.5}/src/wavespeed.egg-info/requires.txt +0 -0
- {wavespeed-2.0.4 → wavespeed-2.0.5}/src/wavespeed.egg-info/top_level.txt +0 -0
- {wavespeed-2.0.4 → wavespeed-2.0.5}/tests/__init__.py +0 -0
- {wavespeed-2.0.4 → wavespeed-2.0.5}/tests/conftest.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: wavespeed
|
|
3
|
-
Version: 2.0.
|
|
3
|
+
Version: 2.0.5
|
|
4
4
|
Summary: WaveSpeed Python Client — Official Python SDK for the WaveSpeed inference platform
|
|
5
5
|
Author-email: WaveSpeedAI <support@wavespeed.ai>
|
|
6
6
|
License: MIT
|
|
@@ -94,12 +94,20 @@ output = client.run("wavespeed-ai/z-image/turbo", {"prompt": "Cat"})
|
|
|
94
94
|
output = wavespeed.run(
|
|
95
95
|
"wavespeed-ai/z-image/turbo",
|
|
96
96
|
{"prompt": "Cat"},
|
|
97
|
-
timeout=
|
|
97
|
+
timeout=None, # Max wait time in seconds (default: None = wait until the task finishes)
|
|
98
98
|
poll_interval=1.0, # Status check interval (default: 1.0)
|
|
99
99
|
enable_sync_mode=False, # Best-effort sync result attempt (default: False)
|
|
100
100
|
)
|
|
101
101
|
```
|
|
102
102
|
|
|
103
|
+
### Timeouts
|
|
104
|
+
|
|
105
|
+
`timeout` is the overall wait for a task and defaults to no limit. Each HTTP
|
|
106
|
+
request also has its own read timeout (time waiting for the server, not a
|
|
107
|
+
total-transfer deadline): 30s for result queries, 60s for async submit and
|
|
108
|
+
120s for uploads. Sync mode has no default read timeout. Set
|
|
109
|
+
`wavespeed.config.api.read_timeout` to override all of these at once.
|
|
110
|
+
|
|
103
111
|
### Sync Mode
|
|
104
112
|
|
|
105
113
|
Use `enable_sync_mode=True` to ask the API to wait for the result in the initial
|
|
@@ -65,12 +65,20 @@ output = client.run("wavespeed-ai/z-image/turbo", {"prompt": "Cat"})
|
|
|
65
65
|
output = wavespeed.run(
|
|
66
66
|
"wavespeed-ai/z-image/turbo",
|
|
67
67
|
{"prompt": "Cat"},
|
|
68
|
-
timeout=
|
|
68
|
+
timeout=None, # Max wait time in seconds (default: None = wait until the task finishes)
|
|
69
69
|
poll_interval=1.0, # Status check interval (default: 1.0)
|
|
70
70
|
enable_sync_mode=False, # Best-effort sync result attempt (default: False)
|
|
71
71
|
)
|
|
72
72
|
```
|
|
73
73
|
|
|
74
|
+
### Timeouts
|
|
75
|
+
|
|
76
|
+
`timeout` is the overall wait for a task and defaults to no limit. Each HTTP
|
|
77
|
+
request also has its own read timeout (time waiting for the server, not a
|
|
78
|
+
total-transfer deadline): 30s for result queries, 60s for async submit and
|
|
79
|
+
120s for uploads. Sync mode has no default read timeout. Set
|
|
80
|
+
`wavespeed.config.api.read_timeout` to override all of these at once.
|
|
81
|
+
|
|
74
82
|
### Sync Mode
|
|
75
83
|
|
|
76
84
|
Use `enable_sync_mode=True` to ask the API to wait for the result in the initial
|
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
# This file is automatically generated by setuptools_scm
|
|
2
|
-
__version__ = "2.0.
|
|
2
|
+
__version__ = "2.0.5"
|
|
@@ -173,7 +173,9 @@ def upload(file: str | BinaryIO, *, timeout: float | None = None) -> str:
|
|
|
173
173
|
|
|
174
174
|
Args:
|
|
175
175
|
file: File path string or file-like object to upload.
|
|
176
|
-
timeout:
|
|
176
|
+
timeout: Per-request read timeout in seconds (default:
|
|
177
|
+
``api_config.read_timeout_upload``;
|
|
178
|
+
``api_config.read_timeout`` overrides). Not a total-transfer deadline.
|
|
177
179
|
|
|
178
180
|
Returns:
|
|
179
181
|
URL of the uploaded file.
|
|
@@ -133,6 +133,23 @@ class Client:
|
|
|
133
133
|
"X-Client-OS": _get_client_os(),
|
|
134
134
|
}
|
|
135
135
|
|
|
136
|
+
@staticmethod
|
|
137
|
+
def _default_read_timeout(per_type_default: float) -> float:
|
|
138
|
+
"""Return ``api_config.read_timeout`` if set, else the per-type default."""
|
|
139
|
+
override = api_config.read_timeout
|
|
140
|
+
return override if override is not None else per_type_default
|
|
141
|
+
|
|
142
|
+
def _request_timeouts(
|
|
143
|
+
self, read_timeout: float | None
|
|
144
|
+
) -> tuple[float, float | None]:
|
|
145
|
+
"""Build a (connect, read) timeouts tuple; read None means no limit."""
|
|
146
|
+
connect_timeout = (
|
|
147
|
+
min(self.connection_timeout, read_timeout)
|
|
148
|
+
if read_timeout
|
|
149
|
+
else self.connection_timeout
|
|
150
|
+
)
|
|
151
|
+
return (connect_timeout, read_timeout)
|
|
152
|
+
|
|
136
153
|
def _submit(
|
|
137
154
|
self,
|
|
138
155
|
model: str,
|
|
@@ -146,7 +163,9 @@ class Client:
|
|
|
146
163
|
model: Model identifier.
|
|
147
164
|
input: Input parameters.
|
|
148
165
|
enable_sync_mode: If True, wait for result in single request.
|
|
149
|
-
timeout: Request timeout in seconds.
|
|
166
|
+
timeout: Request read timeout in seconds. Default:
|
|
167
|
+
``api_config.read_timeout_submit`` (async; ``api_config.read_timeout``
|
|
168
|
+
overrides) or ``api_config.timeout`` (sync mode; None = no limit).
|
|
150
169
|
|
|
151
170
|
Returns:
|
|
152
171
|
Tuple of (request_id, result). In async mode, result is None.
|
|
@@ -161,14 +180,14 @@ class Client:
|
|
|
161
180
|
if enable_sync_mode:
|
|
162
181
|
body["enable_sync_mode"] = True
|
|
163
182
|
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
timeouts = (
|
|
183
|
+
if timeout is not None:
|
|
184
|
+
request_timeout = timeout
|
|
185
|
+
elif enable_sync_mode:
|
|
186
|
+
# The sync-mode POST stays open until the task finishes.
|
|
187
|
+
request_timeout = api_config.timeout
|
|
188
|
+
else:
|
|
189
|
+
request_timeout = self._default_read_timeout(api_config.read_timeout_submit)
|
|
190
|
+
timeouts = self._request_timeouts(request_timeout)
|
|
172
191
|
|
|
173
192
|
try:
|
|
174
193
|
response = requests.post(
|
|
@@ -235,13 +254,12 @@ class Client:
|
|
|
235
254
|
RuntimeError: If fetching result fails after retries.
|
|
236
255
|
"""
|
|
237
256
|
url = f"{self.base_url}/api/v3/predictions/{request_id}/result"
|
|
238
|
-
request_timeout =
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
else self.connection_timeout
|
|
257
|
+
request_timeout = (
|
|
258
|
+
timeout
|
|
259
|
+
if timeout is not None
|
|
260
|
+
else self._default_read_timeout(api_config.read_timeout_get)
|
|
243
261
|
)
|
|
244
|
-
timeouts = (
|
|
262
|
+
timeouts = self._request_timeouts(request_timeout)
|
|
245
263
|
|
|
246
264
|
last_error: Exception | None = None
|
|
247
265
|
|
|
@@ -306,7 +324,8 @@ class Client:
|
|
|
306
324
|
|
|
307
325
|
Args:
|
|
308
326
|
request_id: The prediction request ID.
|
|
309
|
-
timeout: Maximum wait time in seconds
|
|
327
|
+
timeout: Maximum wait time in seconds. None falls back to
|
|
328
|
+
``api_config.timeout``, which defaults to None (no limit).
|
|
310
329
|
poll_interval: Time between polls in seconds.
|
|
311
330
|
|
|
312
331
|
Returns:
|
|
@@ -317,14 +336,15 @@ class Client:
|
|
|
317
336
|
TimeoutError: If prediction times out.
|
|
318
337
|
"""
|
|
319
338
|
start_time = time.time()
|
|
339
|
+
overall_timeout = timeout if timeout is not None else api_config.timeout
|
|
320
340
|
|
|
321
341
|
while True:
|
|
322
342
|
# Check timeout
|
|
323
|
-
if
|
|
343
|
+
if overall_timeout is not None:
|
|
324
344
|
elapsed = time.time() - start_time
|
|
325
|
-
if elapsed >=
|
|
345
|
+
if elapsed >= overall_timeout:
|
|
326
346
|
raise TimeoutError(
|
|
327
|
-
f"Prediction timed out after {
|
|
347
|
+
f"Prediction timed out after {overall_timeout} seconds (task_id: {request_id})"
|
|
328
348
|
)
|
|
329
349
|
|
|
330
350
|
result = self._get_result(request_id, timeout=timeout)
|
|
@@ -574,7 +594,9 @@ class Client:
|
|
|
574
594
|
|
|
575
595
|
Args:
|
|
576
596
|
file: File path string or file-like object to upload.
|
|
577
|
-
timeout:
|
|
597
|
+
timeout: Per-request read timeout in seconds (default:
|
|
598
|
+
``api_config.read_timeout_upload``;
|
|
599
|
+
``api_config.read_timeout`` overrides). Not a total-transfer deadline.
|
|
578
600
|
|
|
579
601
|
Returns:
|
|
580
602
|
URL of the uploaded file.
|
|
@@ -597,8 +619,10 @@ class Client:
|
|
|
597
619
|
|
|
598
620
|
ticket_url = f"{self.base_url}/api/v3/media/uploads"
|
|
599
621
|
headers = self._get_headers()
|
|
600
|
-
|
|
601
|
-
request_timeout =
|
|
622
|
+
# Per-request read timeout (socket inactivity), not a transfer deadline.
|
|
623
|
+
request_timeout = self._request_timeouts(
|
|
624
|
+
timeout or self._default_read_timeout(api_config.read_timeout_upload)
|
|
625
|
+
)
|
|
602
626
|
|
|
603
627
|
close_stream = False
|
|
604
628
|
if isinstance(file, str):
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
"""Configuration module for WaveSpeed SDK."""
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
import sys
|
|
5
|
+
from typing import Optional
|
|
6
|
+
|
|
7
|
+
from ._config_module import install_config_module
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
_save_config_ignore = {
|
|
11
|
+
# workaround: "Can't pickle <function ...>"
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class api:
|
|
16
|
+
"""API client configuration options."""
|
|
17
|
+
|
|
18
|
+
# Authentication
|
|
19
|
+
api_key: Optional[str] = os.environ.get("WAVESPEED_API_KEY")
|
|
20
|
+
|
|
21
|
+
# API base URL
|
|
22
|
+
base_url: str = "https://api.wavespeed.ai"
|
|
23
|
+
|
|
24
|
+
# Connection timeout in seconds
|
|
25
|
+
connection_timeout: float = 10.0
|
|
26
|
+
|
|
27
|
+
# Overall wait cap in seconds for a task. None (default) means no cap: the
|
|
28
|
+
# SDK waits until the task reaches a terminal status. An explicit
|
|
29
|
+
# ``timeout=`` argument takes priority over this value.
|
|
30
|
+
timeout: Optional[float] = None
|
|
31
|
+
|
|
32
|
+
# Per-request read timeout in seconds (time waiting for the server, not a
|
|
33
|
+
# total-transfer deadline). Bounds a half-open connection to one request
|
|
34
|
+
# instead of the whole task. Defaults by request type:
|
|
35
|
+
# result queries and other read-only GETs
|
|
36
|
+
read_timeout_get: float = 30.0
|
|
37
|
+
# async submit POST (a timeout here is an ambiguous outcome, so more headroom)
|
|
38
|
+
read_timeout_submit: float = 60.0
|
|
39
|
+
# upload ticket POST and file PUT
|
|
40
|
+
read_timeout_upload: float = 120.0
|
|
41
|
+
# Sync-mode submit stays open until the task finishes: no default limit.
|
|
42
|
+
|
|
43
|
+
# Optional single override applied to all of the above (not sync-mode
|
|
44
|
+
# submit). None keeps the per-type defaults.
|
|
45
|
+
read_timeout: Optional[float] = None
|
|
46
|
+
|
|
47
|
+
# Maximum number of retries for the entire operation (task-level retries)
|
|
48
|
+
max_retries: int = 0
|
|
49
|
+
|
|
50
|
+
# Maximum retries for idempotent result-query GET requests. Submission
|
|
51
|
+
# POSTs are intentionally sent at most once.
|
|
52
|
+
max_connection_retries: int = 5
|
|
53
|
+
|
|
54
|
+
# Base interval between retries in seconds (actual delay = retry_interval * attempt)
|
|
55
|
+
retry_interval: float = 1.0
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
# adds patch, save_config, etc
|
|
59
|
+
install_config_module(sys.modules[__name__])
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: wavespeed
|
|
3
|
-
Version: 2.0.
|
|
3
|
+
Version: 2.0.5
|
|
4
4
|
Summary: WaveSpeed Python Client — Official Python SDK for the WaveSpeed inference platform
|
|
5
5
|
Author-email: WaveSpeedAI <support@wavespeed.ai>
|
|
6
6
|
License: MIT
|
|
@@ -94,12 +94,20 @@ output = client.run("wavespeed-ai/z-image/turbo", {"prompt": "Cat"})
|
|
|
94
94
|
output = wavespeed.run(
|
|
95
95
|
"wavespeed-ai/z-image/turbo",
|
|
96
96
|
{"prompt": "Cat"},
|
|
97
|
-
timeout=
|
|
97
|
+
timeout=None, # Max wait time in seconds (default: None = wait until the task finishes)
|
|
98
98
|
poll_interval=1.0, # Status check interval (default: 1.0)
|
|
99
99
|
enable_sync_mode=False, # Best-effort sync result attempt (default: False)
|
|
100
100
|
)
|
|
101
101
|
```
|
|
102
102
|
|
|
103
|
+
### Timeouts
|
|
104
|
+
|
|
105
|
+
`timeout` is the overall wait for a task and defaults to no limit. Each HTTP
|
|
106
|
+
request also has its own read timeout (time waiting for the server, not a
|
|
107
|
+
total-transfer deadline): 30s for result queries, 60s for async submit and
|
|
108
|
+
120s for uploads. Sync mode has no default read timeout. Set
|
|
109
|
+
`wavespeed.config.api.read_timeout` to override all of these at once.
|
|
110
|
+
|
|
103
111
|
### Sync Mode
|
|
104
112
|
|
|
105
113
|
Use `enable_sync_mode=True` to ask the API to wait for the result in the initial
|
|
@@ -378,7 +378,11 @@ class TestModuleLevelRun(unittest.TestCase):
|
|
|
378
378
|
mock_config.api_key = "config-key"
|
|
379
379
|
mock_config.base_url = "https://api.wavespeed.ai"
|
|
380
380
|
mock_config.connection_timeout = 10.0
|
|
381
|
-
mock_config.timeout =
|
|
381
|
+
mock_config.timeout = None
|
|
382
|
+
mock_config.read_timeout = None
|
|
383
|
+
mock_config.read_timeout_get = 30.0
|
|
384
|
+
mock_config.read_timeout_submit = 60.0
|
|
385
|
+
mock_config.read_timeout_upload = 120.0
|
|
382
386
|
mock_config.max_retries = 0
|
|
383
387
|
mock_config.max_connection_retries = 5
|
|
384
388
|
mock_config.retry_interval = 1.0
|
|
@@ -562,7 +566,11 @@ class TestRunNoThrow(unittest.TestCase):
|
|
|
562
566
|
mock_config.api_key = "config-key"
|
|
563
567
|
mock_config.base_url = "https://api.wavespeed.ai"
|
|
564
568
|
mock_config.connection_timeout = 10.0
|
|
565
|
-
mock_config.timeout =
|
|
569
|
+
mock_config.timeout = None
|
|
570
|
+
mock_config.read_timeout = None
|
|
571
|
+
mock_config.read_timeout_get = 30.0
|
|
572
|
+
mock_config.read_timeout_submit = 60.0
|
|
573
|
+
mock_config.read_timeout_upload = 120.0
|
|
566
574
|
mock_config.max_retries = 0
|
|
567
575
|
mock_config.max_connection_retries = 5
|
|
568
576
|
mock_config.retry_interval = 1.0
|
|
@@ -26,7 +26,11 @@ class TestApiConfig(unittest.TestCase):
|
|
|
26
26
|
"""Test the shipped default values."""
|
|
27
27
|
self.assertEqual(api.base_url, "https://api.wavespeed.ai")
|
|
28
28
|
self.assertEqual(api.connection_timeout, 10.0)
|
|
29
|
-
self.
|
|
29
|
+
self.assertIsNone(api.timeout)
|
|
30
|
+
self.assertIsNone(api.read_timeout)
|
|
31
|
+
self.assertEqual(api.read_timeout_get, 30.0)
|
|
32
|
+
self.assertEqual(api.read_timeout_submit, 60.0)
|
|
33
|
+
self.assertEqual(api.read_timeout_upload, 120.0)
|
|
30
34
|
self.assertEqual(api.max_retries, 0)
|
|
31
35
|
self.assertEqual(api.max_connection_retries, 5)
|
|
32
36
|
self.assertEqual(api.retry_interval, 1.0)
|
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
"""Tests for the overall-wait cap vs. per-request read timeout split."""
|
|
2
|
+
|
|
3
|
+
import http.server
|
|
4
|
+
import io
|
|
5
|
+
import threading
|
|
6
|
+
import time
|
|
7
|
+
import unittest
|
|
8
|
+
from unittest.mock import MagicMock, patch
|
|
9
|
+
|
|
10
|
+
import requests
|
|
11
|
+
|
|
12
|
+
from wavespeed.api import Client
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def _resp(status=200, payload=None):
|
|
16
|
+
r = MagicMock()
|
|
17
|
+
r.status_code = status
|
|
18
|
+
r.text = ""
|
|
19
|
+
r.json.return_value = payload if payload is not None else {}
|
|
20
|
+
return r
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def _submit_ok():
|
|
24
|
+
return _resp(200, {"data": {"id": "t1"}})
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def _status(status, outputs=None):
|
|
28
|
+
return _resp(200, {"data": {"status": status, "outputs": outputs or []}})
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class FakeClock:
|
|
32
|
+
"""time.time()/sleep() replacement: sleep advances the clock instantly."""
|
|
33
|
+
|
|
34
|
+
def __init__(self):
|
|
35
|
+
"""Start at t=0."""
|
|
36
|
+
self.now = 0.0
|
|
37
|
+
|
|
38
|
+
def time(self):
|
|
39
|
+
"""Return the fake now."""
|
|
40
|
+
return self.now
|
|
41
|
+
|
|
42
|
+
def sleep(self, seconds):
|
|
43
|
+
"""Advance the fake clock."""
|
|
44
|
+
self.now += seconds
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
@patch("wavespeed.api.client.time")
|
|
48
|
+
@patch("wavespeed.api.client.requests.get")
|
|
49
|
+
@patch("wavespeed.api.client.requests.post")
|
|
50
|
+
class TestTimeouts(unittest.TestCase):
|
|
51
|
+
"""Overall cap and per-request read timeouts."""
|
|
52
|
+
|
|
53
|
+
def setUp(self):
|
|
54
|
+
"""Create a client."""
|
|
55
|
+
self.client = Client(api_key="k")
|
|
56
|
+
|
|
57
|
+
def _clock(self, mock_time):
|
|
58
|
+
clock = FakeClock()
|
|
59
|
+
mock_time.time.side_effect = clock.time
|
|
60
|
+
mock_time.sleep.side_effect = clock.sleep
|
|
61
|
+
return clock
|
|
62
|
+
|
|
63
|
+
def test_default_has_no_overall_cap(self, post, get, mock_time):
|
|
64
|
+
"""Test default has no overall cap."""
|
|
65
|
+
clock = self._clock(mock_time)
|
|
66
|
+
post.return_value = _submit_ok()
|
|
67
|
+
# Polls at 1000s intervals: stays processing past 36000s, then completes.
|
|
68
|
+
get.side_effect = [_status("processing")] * 40 + [_status("completed", ["u"])]
|
|
69
|
+
out = self.client.run("m/x", {}, poll_interval=1000.0)
|
|
70
|
+
self.assertEqual(out, {"outputs": ["u"]})
|
|
71
|
+
self.assertEqual(get.call_count, 41)
|
|
72
|
+
self.assertGreater(clock.now, 36000.0)
|
|
73
|
+
|
|
74
|
+
def test_explicit_timeout_still_raises(self, post, get, mock_time):
|
|
75
|
+
"""Test explicit timeout still raises."""
|
|
76
|
+
self._clock(mock_time)
|
|
77
|
+
post.return_value = _submit_ok()
|
|
78
|
+
get.return_value = _status("processing")
|
|
79
|
+
with self.assertRaises(TimeoutError):
|
|
80
|
+
self.client.run("m/x", {}, timeout=50.0, poll_interval=10.0)
|
|
81
|
+
|
|
82
|
+
def test_config_timeout_still_caps(self, post, get, mock_time):
|
|
83
|
+
"""Test config timeout still caps."""
|
|
84
|
+
self._clock(mock_time)
|
|
85
|
+
post.return_value = _submit_ok()
|
|
86
|
+
get.return_value = _status("processing")
|
|
87
|
+
with patch("wavespeed.api.client.api_config.timeout", 50.0):
|
|
88
|
+
with self.assertRaises(TimeoutError):
|
|
89
|
+
self.client.run("m/x", {}, poll_interval=10.0)
|
|
90
|
+
|
|
91
|
+
def test_async_submit_and_get_use_read_timeout(self, post, get, mock_time):
|
|
92
|
+
"""Test async submit and get use read timeout."""
|
|
93
|
+
self._clock(mock_time)
|
|
94
|
+
post.return_value = _submit_ok()
|
|
95
|
+
get.return_value = _status("completed")
|
|
96
|
+
self.client.run("m/x", {})
|
|
97
|
+
self.assertEqual(post.call_args.kwargs["timeout"], (10.0, 60.0))
|
|
98
|
+
self.assertEqual(get.call_args.kwargs["timeout"], (10.0, 30.0))
|
|
99
|
+
|
|
100
|
+
def test_override_applies_to_get_and_submit_not_sync(self, post, get, mock_time):
|
|
101
|
+
"""Single read_timeout override covers GET and async submit, not sync."""
|
|
102
|
+
self._clock(mock_time)
|
|
103
|
+
post.return_value = _submit_ok()
|
|
104
|
+
get.return_value = _status("completed")
|
|
105
|
+
with patch("wavespeed.api.client.api_config.read_timeout", 7.0):
|
|
106
|
+
self.client.run("m/x", {})
|
|
107
|
+
self.assertEqual(post.call_args.kwargs["timeout"], (7.0, 7.0))
|
|
108
|
+
self.assertEqual(get.call_args.kwargs["timeout"], (7.0, 7.0))
|
|
109
|
+
post.return_value = _resp(200, {"data": {"status": "completed"}})
|
|
110
|
+
self.client.run("m/x", {}, enable_sync_mode=True)
|
|
111
|
+
self.assertEqual(post.call_args.kwargs["timeout"], (10.0, None))
|
|
112
|
+
|
|
113
|
+
def test_sync_submit_no_read_timeout_by_default(self, post, get, mock_time):
|
|
114
|
+
"""Test sync submit no read timeout by default."""
|
|
115
|
+
post.return_value = _resp(200, {"data": {"status": "completed"}})
|
|
116
|
+
self.client.run("m/x", {}, enable_sync_mode=True)
|
|
117
|
+
self.assertEqual(post.call_args.kwargs["timeout"], (10.0, None))
|
|
118
|
+
|
|
119
|
+
def test_sync_submit_explicit_timeout(self, post, get, mock_time):
|
|
120
|
+
"""Test sync submit explicit timeout."""
|
|
121
|
+
post.return_value = _resp(200, {"data": {"status": "completed"}})
|
|
122
|
+
self.client.run("m/x", {}, enable_sync_mode=True, timeout=300.0)
|
|
123
|
+
self.assertEqual(post.call_args.kwargs["timeout"], (10.0, 300.0))
|
|
124
|
+
|
|
125
|
+
def test_explicit_timeout_applies_to_requests(self, post, get, mock_time):
|
|
126
|
+
"""Test explicit timeout applies to requests."""
|
|
127
|
+
self._clock(mock_time)
|
|
128
|
+
post.return_value = _submit_ok()
|
|
129
|
+
get.return_value = _status("completed")
|
|
130
|
+
self.client.run("m/x", {}, timeout=300.0)
|
|
131
|
+
self.assertEqual(post.call_args.kwargs["timeout"], (10.0, 300.0))
|
|
132
|
+
self.assertEqual(get.call_args.kwargs["timeout"], (10.0, 300.0))
|
|
133
|
+
|
|
134
|
+
def test_poll_retries_502_then_succeeds(self, post, get, mock_time):
|
|
135
|
+
"""Test poll retries 502 then succeeds."""
|
|
136
|
+
self._clock(mock_time)
|
|
137
|
+
post.return_value = _submit_ok()
|
|
138
|
+
get.side_effect = [_resp(502), _status("completed", ["u"])]
|
|
139
|
+
out = self.client.run("m/x", {})
|
|
140
|
+
self.assertEqual(out, {"outputs": ["u"]})
|
|
141
|
+
self.assertEqual(get.call_count, 2)
|
|
142
|
+
|
|
143
|
+
def test_submit_timeout_is_not_retried(self, post, get, mock_time):
|
|
144
|
+
"""Test submit timeout is not retried."""
|
|
145
|
+
post.side_effect = requests.exceptions.ReadTimeout()
|
|
146
|
+
with self.assertRaises(RuntimeError):
|
|
147
|
+
self.client.run("m/x", {})
|
|
148
|
+
self.assertEqual(post.call_count, 1)
|
|
149
|
+
get.assert_not_called()
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
if __name__ == "__main__":
|
|
153
|
+
unittest.main()
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def _ticket(url):
|
|
157
|
+
return _resp(
|
|
158
|
+
200,
|
|
159
|
+
{
|
|
160
|
+
"code": 200,
|
|
161
|
+
"data": {
|
|
162
|
+
"download_url": "https://example.com/f.bin",
|
|
163
|
+
"upload": {"method": "PUT", "url": url, "headers": {}},
|
|
164
|
+
},
|
|
165
|
+
},
|
|
166
|
+
)
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
@patch("wavespeed.api.client.requests.put")
|
|
170
|
+
@patch("wavespeed.api.client.requests.post")
|
|
171
|
+
class TestUploadTimeouts(unittest.TestCase):
|
|
172
|
+
"""upload() uses the per-request read timeout for every request."""
|
|
173
|
+
|
|
174
|
+
def test_default_read_timeout(self, post, put):
|
|
175
|
+
"""Ticket POST and file PUT carry (connect, 120.0); override applies."""
|
|
176
|
+
post.return_value = _ticket("https://s.example.com/u")
|
|
177
|
+
put.return_value = _resp(200)
|
|
178
|
+
Client(api_key="k").upload(io.BytesIO(b"abc"))
|
|
179
|
+
self.assertEqual(post.call_args.kwargs["timeout"], (10.0, 120.0))
|
|
180
|
+
self.assertEqual(put.call_args.kwargs["timeout"], (10.0, 120.0))
|
|
181
|
+
with patch("wavespeed.api.client.api_config.read_timeout", 7.0):
|
|
182
|
+
Client(api_key="k").upload(io.BytesIO(b"abc"))
|
|
183
|
+
self.assertEqual(post.call_args.kwargs["timeout"], (7.0, 7.0))
|
|
184
|
+
self.assertEqual(put.call_args.kwargs["timeout"], (7.0, 7.0))
|
|
185
|
+
|
|
186
|
+
def test_explicit_timeout(self, post, put):
|
|
187
|
+
"""An explicit timeout applies to both requests."""
|
|
188
|
+
post.return_value = _ticket("https://s.example.com/u")
|
|
189
|
+
put.return_value = _resp(200)
|
|
190
|
+
Client(api_key="k").upload(io.BytesIO(b"abc"), timeout=300.0)
|
|
191
|
+
self.assertEqual(post.call_args.kwargs["timeout"], (10.0, 300.0))
|
|
192
|
+
self.assertEqual(put.call_args.kwargs["timeout"], (10.0, 300.0))
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
class TestSlowProgressingPut(unittest.TestCase):
|
|
196
|
+
"""A slow but progressing PUT is not aborted by the read timeout."""
|
|
197
|
+
|
|
198
|
+
def test_slow_progressing_put_not_aborted(self):
|
|
199
|
+
"""Body takes longer than read_timeout in total, gaps stay below it."""
|
|
200
|
+
received = []
|
|
201
|
+
|
|
202
|
+
class Handler(http.server.BaseHTTPRequestHandler):
|
|
203
|
+
def do_PUT(self):
|
|
204
|
+
remaining = int(self.headers["Content-Length"])
|
|
205
|
+
got = 0
|
|
206
|
+
while remaining:
|
|
207
|
+
chunk = self.rfile.read(min(65536, remaining))
|
|
208
|
+
got += len(chunk)
|
|
209
|
+
remaining -= len(chunk)
|
|
210
|
+
time.sleep(0.01) # slow consumer, keeps making progress
|
|
211
|
+
received.append(got)
|
|
212
|
+
time.sleep(0.5) # response headers arrive late but < timeout
|
|
213
|
+
self.send_response(200)
|
|
214
|
+
self.send_header("Content-Length", "0")
|
|
215
|
+
self.end_headers()
|
|
216
|
+
|
|
217
|
+
def log_message(self, *args):
|
|
218
|
+
pass
|
|
219
|
+
|
|
220
|
+
server = http.server.ThreadingHTTPServer(("127.0.0.1", 0), Handler)
|
|
221
|
+
threading.Thread(target=server.serve_forever, daemon=True).start()
|
|
222
|
+
url = f"http://127.0.0.1:{server.server_address[1]}/u"
|
|
223
|
+
size = 24 * 1024 * 1024
|
|
224
|
+
try:
|
|
225
|
+
with patch("wavespeed.api.client.requests.post") as post:
|
|
226
|
+
post.return_value = _ticket(url)
|
|
227
|
+
with patch("wavespeed.api.client.api_config.read_timeout_upload", 2.0):
|
|
228
|
+
start = time.time()
|
|
229
|
+
out = Client(api_key="k").upload(io.BytesIO(b"x" * size))
|
|
230
|
+
elapsed = time.time() - start
|
|
231
|
+
finally:
|
|
232
|
+
server.shutdown()
|
|
233
|
+
self.assertEqual(out, "https://example.com/f.bin")
|
|
234
|
+
self.assertEqual(received, [size])
|
|
235
|
+
self.assertGreater(elapsed, 2.0) # total transfer exceeded read_timeout
|
|
@@ -1,42 +0,0 @@
|
|
|
1
|
-
"""Configuration module for WaveSpeed SDK."""
|
|
2
|
-
|
|
3
|
-
import os
|
|
4
|
-
import sys
|
|
5
|
-
from typing import Optional
|
|
6
|
-
|
|
7
|
-
from ._config_module import install_config_module
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
_save_config_ignore = {
|
|
11
|
-
# workaround: "Can't pickle <function ...>"
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
class api:
|
|
16
|
-
"""API client configuration options."""
|
|
17
|
-
|
|
18
|
-
# Authentication
|
|
19
|
-
api_key: Optional[str] = os.environ.get("WAVESPEED_API_KEY")
|
|
20
|
-
|
|
21
|
-
# API base URL
|
|
22
|
-
base_url: str = "https://api.wavespeed.ai"
|
|
23
|
-
|
|
24
|
-
# Connection timeout in seconds
|
|
25
|
-
connection_timeout: float = 10.0
|
|
26
|
-
|
|
27
|
-
# Total API call timeout in seconds
|
|
28
|
-
timeout: float = 36000.0
|
|
29
|
-
|
|
30
|
-
# Maximum number of retries for the entire operation (task-level retries)
|
|
31
|
-
max_retries: int = 0
|
|
32
|
-
|
|
33
|
-
# Maximum retries for idempotent result-query GET requests. Submission
|
|
34
|
-
# POSTs are intentionally sent at most once.
|
|
35
|
-
max_connection_retries: int = 5
|
|
36
|
-
|
|
37
|
-
# Base interval between retries in seconds (actual delay = retry_interval * attempt)
|
|
38
|
-
retry_interval: float = 1.0
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
# adds patch, save_config, etc
|
|
42
|
-
install_config_module(sys.modules[__name__])
|
|
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
|