wavespeed 2.0.3__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.3/src/wavespeed.egg-info → wavespeed-2.0.5}/PKG-INFO +10 -2
- {wavespeed-2.0.3 → wavespeed-2.0.5}/README.md +9 -1
- {wavespeed-2.0.3 → wavespeed-2.0.5}/src/wavespeed/_version.py +1 -1
- {wavespeed-2.0.3 → wavespeed-2.0.5}/src/wavespeed/api/__init__.py +3 -1
- {wavespeed-2.0.3 → wavespeed-2.0.5}/src/wavespeed/api/client.py +67 -50
- wavespeed-2.0.5/src/wavespeed/config.py +59 -0
- {wavespeed-2.0.3 → wavespeed-2.0.5/src/wavespeed.egg-info}/PKG-INFO +10 -2
- {wavespeed-2.0.3 → wavespeed-2.0.5}/src/wavespeed.egg-info/SOURCES.txt +2 -1
- {wavespeed-2.0.3 → wavespeed-2.0.5}/src/wavespeed.egg-info/scm_file_list.json +2 -1
- wavespeed-2.0.5/src/wavespeed.egg-info/scm_version.json +8 -0
- {wavespeed-2.0.3 → wavespeed-2.0.5}/tests/test_api.py +102 -2
- {wavespeed-2.0.3 → wavespeed-2.0.5}/tests/test_config.py +5 -1
- wavespeed-2.0.5/tests/test_timeouts.py +235 -0
- wavespeed-2.0.3/src/wavespeed/config.py +0 -42
- wavespeed-2.0.3/src/wavespeed.egg-info/scm_version.json +0 -8
- {wavespeed-2.0.3 → wavespeed-2.0.5}/.github/dependabot.yml +0 -0
- {wavespeed-2.0.3 → wavespeed-2.0.5}/.github/workflows/claude.yml +0 -0
- {wavespeed-2.0.3 → wavespeed-2.0.5}/.github/workflows/pre-commit.yml +0 -0
- {wavespeed-2.0.3 → wavespeed-2.0.5}/.github/workflows/python-packages.yml +0 -0
- {wavespeed-2.0.3 → wavespeed-2.0.5}/.github/workflows/python-publish.yml +0 -0
- {wavespeed-2.0.3 → wavespeed-2.0.5}/.github/workflows/tests.yml +0 -0
- {wavespeed-2.0.3 → wavespeed-2.0.5}/.gitignore +0 -0
- {wavespeed-2.0.3 → wavespeed-2.0.5}/.pre-commit-config.yaml +0 -0
- {wavespeed-2.0.3 → wavespeed-2.0.5}/CHANGELOG.md +0 -0
- {wavespeed-2.0.3 → wavespeed-2.0.5}/CLAUDE.md +0 -0
- {wavespeed-2.0.3 → wavespeed-2.0.5}/LICENSE +0 -0
- {wavespeed-2.0.3 → wavespeed-2.0.5}/MANIFEST.in +0 -0
- {wavespeed-2.0.3 → wavespeed-2.0.5}/VERSIONING.md +0 -0
- {wavespeed-2.0.3 → wavespeed-2.0.5}/assets/wavespeed-logo-dark.svg +0 -0
- {wavespeed-2.0.3 → wavespeed-2.0.5}/assets/wavespeed-logo-light.svg +0 -0
- {wavespeed-2.0.3 → wavespeed-2.0.5}/pyproject.toml +0 -0
- {wavespeed-2.0.3 → wavespeed-2.0.5}/pytest.ini +0 -0
- {wavespeed-2.0.3 → wavespeed-2.0.5}/setup.cfg +0 -0
- {wavespeed-2.0.3 → wavespeed-2.0.5}/src/wavespeed/__init__.py +0 -0
- {wavespeed-2.0.3 → wavespeed-2.0.5}/src/wavespeed/_config_module.py +0 -0
- {wavespeed-2.0.3 → wavespeed-2.0.5}/src/wavespeed.egg-info/dependency_links.txt +0 -0
- {wavespeed-2.0.3 → wavespeed-2.0.5}/src/wavespeed.egg-info/not-zip-safe +0 -0
- {wavespeed-2.0.3 → wavespeed-2.0.5}/src/wavespeed.egg-info/requires.txt +0 -0
- {wavespeed-2.0.3 → wavespeed-2.0.5}/src/wavespeed.egg-info/top_level.txt +0 -0
- {wavespeed-2.0.3 → wavespeed-2.0.5}/tests/__init__.py +0 -0
- {wavespeed-2.0.3 → 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.
|
|
@@ -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
|
|
|
@@ -129,6 +133,23 @@ class Client:
|
|
|
129
133
|
"X-Client-OS": _get_client_os(),
|
|
130
134
|
}
|
|
131
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
|
+
|
|
132
153
|
def _submit(
|
|
133
154
|
self,
|
|
134
155
|
model: str,
|
|
@@ -142,7 +163,9 @@ class Client:
|
|
|
142
163
|
model: Model identifier.
|
|
143
164
|
input: Input parameters.
|
|
144
165
|
enable_sync_mode: If True, wait for result in single request.
|
|
145
|
-
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).
|
|
146
169
|
|
|
147
170
|
Returns:
|
|
148
171
|
Tuple of (request_id, result). In async mode, result is None.
|
|
@@ -157,14 +180,14 @@ class Client:
|
|
|
157
180
|
if enable_sync_mode:
|
|
158
181
|
body["enable_sync_mode"] = True
|
|
159
182
|
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
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)
|
|
168
191
|
|
|
169
192
|
try:
|
|
170
193
|
response = requests.post(
|
|
@@ -231,13 +254,12 @@ class Client:
|
|
|
231
254
|
RuntimeError: If fetching result fails after retries.
|
|
232
255
|
"""
|
|
233
256
|
url = f"{self.base_url}/api/v3/predictions/{request_id}/result"
|
|
234
|
-
request_timeout =
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
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)
|
|
239
261
|
)
|
|
240
|
-
timeouts = (
|
|
262
|
+
timeouts = self._request_timeouts(request_timeout)
|
|
241
263
|
|
|
242
264
|
last_error: Exception | None = None
|
|
243
265
|
|
|
@@ -302,7 +324,8 @@ class Client:
|
|
|
302
324
|
|
|
303
325
|
Args:
|
|
304
326
|
request_id: The prediction request ID.
|
|
305
|
-
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).
|
|
306
329
|
poll_interval: Time between polls in seconds.
|
|
307
330
|
|
|
308
331
|
Returns:
|
|
@@ -313,14 +336,15 @@ class Client:
|
|
|
313
336
|
TimeoutError: If prediction times out.
|
|
314
337
|
"""
|
|
315
338
|
start_time = time.time()
|
|
339
|
+
overall_timeout = timeout if timeout is not None else api_config.timeout
|
|
316
340
|
|
|
317
341
|
while True:
|
|
318
342
|
# Check timeout
|
|
319
|
-
if
|
|
343
|
+
if overall_timeout is not None:
|
|
320
344
|
elapsed = time.time() - start_time
|
|
321
|
-
if elapsed >=
|
|
345
|
+
if elapsed >= overall_timeout:
|
|
322
346
|
raise TimeoutError(
|
|
323
|
-
f"Prediction timed out after {
|
|
347
|
+
f"Prediction timed out after {overall_timeout} seconds (task_id: {request_id})"
|
|
324
348
|
)
|
|
325
349
|
|
|
326
350
|
result = self._get_result(request_id, timeout=timeout)
|
|
@@ -332,14 +356,13 @@ class Client:
|
|
|
332
356
|
|
|
333
357
|
if status in ("failed", "cancelled", "timeout", "deleted"):
|
|
334
358
|
error = data.get("error") or "Unknown error"
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
)
|
|
359
|
+
error_cls = _TaskFailedError if status == "failed" else RuntimeError
|
|
360
|
+
raise error_cls(f"Prediction {status} (task_id: {request_id}): {error}")
|
|
338
361
|
|
|
339
362
|
time.sleep(poll_interval)
|
|
340
363
|
|
|
341
364
|
def _is_retryable_error(self, error: Exception) -> bool:
|
|
342
|
-
"""Determine if an error is worth
|
|
365
|
+
"""Determine if an error is worth re-running the task for.
|
|
343
366
|
|
|
344
367
|
Args:
|
|
345
368
|
error: The exception to check.
|
|
@@ -347,29 +370,17 @@ class Client:
|
|
|
347
370
|
Returns:
|
|
348
371
|
True if the error is retryable.
|
|
349
372
|
"""
|
|
350
|
-
#
|
|
351
|
-
#
|
|
352
|
-
|
|
373
|
+
# Only a failure reported by the task itself is re-run, and only when it
|
|
374
|
+
# was a transient upstream error (HTTP 5xx / 429), as before: a
|
|
375
|
+
# deterministic failure such as a content-policy rejection would fail
|
|
376
|
+
# again. Submission errors are ambiguous (the task may already exist),
|
|
377
|
+
# and poll-side HTTP errors, poll retry exhaustion and client-side wait
|
|
378
|
+
# timeouts all happen after the task was created and may be charged, so
|
|
379
|
+
# none of them may turn into another POST.
|
|
380
|
+
if not isinstance(error, _TaskFailedError):
|
|
353
381
|
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
|
|
382
|
+
error_str = str(error)
|
|
383
|
+
return "HTTP 5" in error_str or "HTTP 429" in error_str
|
|
373
384
|
|
|
374
385
|
def _format_sync_mode_error(self, data: dict[str, Any]) -> str:
|
|
375
386
|
"""Build an actionable error for a non-completed sync-mode response."""
|
|
@@ -464,8 +475,10 @@ class Client:
|
|
|
464
475
|
@staticmethod
|
|
465
476
|
def _task_id_from_error(error: Exception) -> str:
|
|
466
477
|
"""Extract a task ID from an error message, mirroring the JS SDK."""
|
|
467
|
-
match = re.search(r"task_id:\s*([^)]+)", str(error))
|
|
468
|
-
|
|
478
|
+
match = re.search(r"task_id:\s*([^)]+)|for task ([^\s:]+)", str(error))
|
|
479
|
+
if not match:
|
|
480
|
+
return "unknown"
|
|
481
|
+
return (match.group(1) or match.group(2)).strip()
|
|
469
482
|
|
|
470
483
|
def run_no_throw(
|
|
471
484
|
self,
|
|
@@ -581,7 +594,9 @@ class Client:
|
|
|
581
594
|
|
|
582
595
|
Args:
|
|
583
596
|
file: File path string or file-like object to upload.
|
|
584
|
-
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.
|
|
585
600
|
|
|
586
601
|
Returns:
|
|
587
602
|
URL of the uploaded file.
|
|
@@ -604,8 +619,10 @@ class Client:
|
|
|
604
619
|
|
|
605
620
|
ticket_url = f"{self.base_url}/api/v3/media/uploads"
|
|
606
621
|
headers = self._get_headers()
|
|
607
|
-
|
|
608
|
-
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
|
+
)
|
|
609
626
|
|
|
610
627
|
close_stream = False
|
|
611
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
|
|
@@ -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."""
|
|
@@ -286,7 +378,11 @@ class TestModuleLevelRun(unittest.TestCase):
|
|
|
286
378
|
mock_config.api_key = "config-key"
|
|
287
379
|
mock_config.base_url = "https://api.wavespeed.ai"
|
|
288
380
|
mock_config.connection_timeout = 10.0
|
|
289
|
-
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
|
|
290
386
|
mock_config.max_retries = 0
|
|
291
387
|
mock_config.max_connection_retries = 5
|
|
292
388
|
mock_config.retry_interval = 1.0
|
|
@@ -470,7 +566,11 @@ class TestRunNoThrow(unittest.TestCase):
|
|
|
470
566
|
mock_config.api_key = "config-key"
|
|
471
567
|
mock_config.base_url = "https://api.wavespeed.ai"
|
|
472
568
|
mock_config.connection_timeout = 10.0
|
|
473
|
-
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
|
|
474
574
|
mock_config.max_retries = 0
|
|
475
575
|
mock_config.max_connection_retries = 5
|
|
476
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
|