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.
Files changed (40) hide show
  1. {wavespeed-2.0.4/src/wavespeed.egg-info → wavespeed-2.0.5}/PKG-INFO +10 -2
  2. {wavespeed-2.0.4 → wavespeed-2.0.5}/README.md +9 -1
  3. {wavespeed-2.0.4 → wavespeed-2.0.5}/src/wavespeed/_version.py +1 -1
  4. {wavespeed-2.0.4 → wavespeed-2.0.5}/src/wavespeed/api/__init__.py +3 -1
  5. {wavespeed-2.0.4 → wavespeed-2.0.5}/src/wavespeed/api/client.py +46 -22
  6. wavespeed-2.0.5/src/wavespeed/config.py +59 -0
  7. {wavespeed-2.0.4 → wavespeed-2.0.5/src/wavespeed.egg-info}/PKG-INFO +10 -2
  8. {wavespeed-2.0.4 → wavespeed-2.0.5}/src/wavespeed.egg-info/SOURCES.txt +2 -1
  9. {wavespeed-2.0.4 → wavespeed-2.0.5}/src/wavespeed.egg-info/scm_file_list.json +2 -1
  10. {wavespeed-2.0.4 → wavespeed-2.0.5}/src/wavespeed.egg-info/scm_version.json +2 -2
  11. {wavespeed-2.0.4 → wavespeed-2.0.5}/tests/test_api.py +10 -2
  12. {wavespeed-2.0.4 → wavespeed-2.0.5}/tests/test_config.py +5 -1
  13. wavespeed-2.0.5/tests/test_timeouts.py +235 -0
  14. wavespeed-2.0.4/src/wavespeed/config.py +0 -42
  15. {wavespeed-2.0.4 → wavespeed-2.0.5}/.github/dependabot.yml +0 -0
  16. {wavespeed-2.0.4 → wavespeed-2.0.5}/.github/workflows/claude.yml +0 -0
  17. {wavespeed-2.0.4 → wavespeed-2.0.5}/.github/workflows/pre-commit.yml +0 -0
  18. {wavespeed-2.0.4 → wavespeed-2.0.5}/.github/workflows/python-packages.yml +0 -0
  19. {wavespeed-2.0.4 → wavespeed-2.0.5}/.github/workflows/python-publish.yml +0 -0
  20. {wavespeed-2.0.4 → wavespeed-2.0.5}/.github/workflows/tests.yml +0 -0
  21. {wavespeed-2.0.4 → wavespeed-2.0.5}/.gitignore +0 -0
  22. {wavespeed-2.0.4 → wavespeed-2.0.5}/.pre-commit-config.yaml +0 -0
  23. {wavespeed-2.0.4 → wavespeed-2.0.5}/CHANGELOG.md +0 -0
  24. {wavespeed-2.0.4 → wavespeed-2.0.5}/CLAUDE.md +0 -0
  25. {wavespeed-2.0.4 → wavespeed-2.0.5}/LICENSE +0 -0
  26. {wavespeed-2.0.4 → wavespeed-2.0.5}/MANIFEST.in +0 -0
  27. {wavespeed-2.0.4 → wavespeed-2.0.5}/VERSIONING.md +0 -0
  28. {wavespeed-2.0.4 → wavespeed-2.0.5}/assets/wavespeed-logo-dark.svg +0 -0
  29. {wavespeed-2.0.4 → wavespeed-2.0.5}/assets/wavespeed-logo-light.svg +0 -0
  30. {wavespeed-2.0.4 → wavespeed-2.0.5}/pyproject.toml +0 -0
  31. {wavespeed-2.0.4 → wavespeed-2.0.5}/pytest.ini +0 -0
  32. {wavespeed-2.0.4 → wavespeed-2.0.5}/setup.cfg +0 -0
  33. {wavespeed-2.0.4 → wavespeed-2.0.5}/src/wavespeed/__init__.py +0 -0
  34. {wavespeed-2.0.4 → wavespeed-2.0.5}/src/wavespeed/_config_module.py +0 -0
  35. {wavespeed-2.0.4 → wavespeed-2.0.5}/src/wavespeed.egg-info/dependency_links.txt +0 -0
  36. {wavespeed-2.0.4 → wavespeed-2.0.5}/src/wavespeed.egg-info/not-zip-safe +0 -0
  37. {wavespeed-2.0.4 → wavespeed-2.0.5}/src/wavespeed.egg-info/requires.txt +0 -0
  38. {wavespeed-2.0.4 → wavespeed-2.0.5}/src/wavespeed.egg-info/top_level.txt +0 -0
  39. {wavespeed-2.0.4 → wavespeed-2.0.5}/tests/__init__.py +0 -0
  40. {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.4
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=36000.0, # Max wait time in seconds (default: 36000.0)
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=36000.0, # Max wait time in seconds (default: 36000.0)
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.4"
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: Total API call timeout in seconds.
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
- request_timeout = timeout if timeout is not None else api_config.timeout
165
- # Use connection timeout for connect, request_timeout for read
166
- connect_timeout = (
167
- min(self.connection_timeout, request_timeout)
168
- if request_timeout
169
- else self.connection_timeout
170
- )
171
- timeouts = (connect_timeout, request_timeout)
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 = timeout if timeout is not None else api_config.timeout
239
- connect_timeout = (
240
- min(self.connection_timeout, request_timeout)
241
- if request_timeout
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 = (connect_timeout, request_timeout)
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 (None = no timeout).
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 timeout is not None:
343
+ if overall_timeout is not None:
324
344
  elapsed = time.time() - start_time
325
- if elapsed >= timeout:
345
+ if elapsed >= overall_timeout:
326
346
  raise TimeoutError(
327
- f"Prediction timed out after {timeout} seconds (task_id: {request_id})"
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: Total API call timeout in seconds.
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
- timeout = timeout or api_config.timeout
601
- request_timeout = (min(self.connection_timeout, timeout), 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.4
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=36000.0, # Max wait time in seconds (default: 36000.0)
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
@@ -34,4 +34,5 @@ src/wavespeed/api/client.py
34
34
  tests/__init__.py
35
35
  tests/conftest.py
36
36
  tests/test_api.py
37
- tests/test_config.py
37
+ tests/test_config.py
38
+ tests/test_timeouts.py
@@ -27,6 +27,7 @@
27
27
  "tests/__init__.py",
28
28
  "tests/conftest.py",
29
29
  "tests/test_api.py",
30
- "tests/test_config.py"
30
+ "tests/test_config.py",
31
+ "tests/test_timeouts.py"
31
32
  ]
32
33
  }
@@ -1,7 +1,7 @@
1
1
  {
2
- "tag": "2.0.4",
2
+ "tag": "2.0.5",
3
3
  "distance": 0,
4
- "node": "g1bf08ac65f5019f75a190187231450423b203238",
4
+ "node": "g7bcfc516a9110256323601831fd5f5bab8b923c1",
5
5
  "dirty": false,
6
6
  "branch": "HEAD",
7
7
  "node_date": "2026-10-17"
@@ -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 = 36000.0
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 = 36000.0
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.assertEqual(api.timeout, 36000.0)
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