dazpy 2.7.2__tar.gz → 2.8.0__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.
- {dazpy-2.7.2/dazpy.egg-info → dazpy-2.8.0}/PKG-INFO +48 -3
- dazpy-2.7.2/PKG-INFO → dazpy-2.8.0/README.md +47 -17
- {dazpy-2.7.2 → dazpy-2.8.0}/dazpy/__init__.py +1 -1
- {dazpy-2.7.2 → dazpy-2.8.0}/dazpy/_client.py +165 -25
- {dazpy-2.7.2 → dazpy-2.8.0}/dazpy/exceptions.py +24 -0
- dazpy-2.7.2/README.md → dazpy-2.8.0/dazpy.egg-info/PKG-INFO +62 -2
- {dazpy-2.7.2 → dazpy-2.8.0}/pyproject.toml +1 -1
- {dazpy-2.7.2 → dazpy-2.8.0}/tests/test_dazpy.py +103 -0
- {dazpy-2.7.2 → dazpy-2.8.0}/dazpy/_animation.py +0 -0
- {dazpy-2.7.2 → dazpy-2.8.0}/dazpy/_batch.py +0 -0
- {dazpy-2.7.2 → dazpy-2.8.0}/dazpy/_bone.py +0 -0
- {dazpy-2.7.2 → dazpy-2.8.0}/dazpy/_camera.py +0 -0
- {dazpy-2.7.2 → dazpy-2.8.0}/dazpy/_dforce.py +0 -0
- {dazpy-2.7.2 → dazpy-2.8.0}/dazpy/_element.py +0 -0
- {dazpy-2.7.2 → dazpy-2.8.0}/dazpy/_geometry.py +0 -0
- {dazpy-2.7.2 → dazpy-2.8.0}/dazpy/_interaction.py +0 -0
- {dazpy-2.7.2 → dazpy-2.8.0}/dazpy/_light.py +0 -0
- {dazpy-2.7.2 → dazpy-2.8.0}/dazpy/_material.py +0 -0
- {dazpy-2.7.2 → dazpy-2.8.0}/dazpy/_modifier.py +0 -0
- {dazpy-2.7.2 → dazpy-2.8.0}/dazpy/_morph.py +0 -0
- {dazpy-2.7.2 → dazpy-2.8.0}/dazpy/_node.py +0 -0
- {dazpy-2.7.2 → dazpy-2.8.0}/dazpy/_polling.py +0 -0
- {dazpy-2.7.2 → dazpy-2.8.0}/dazpy/_pose.py +0 -0
- {dazpy-2.7.2 → dazpy-2.8.0}/dazpy/_property.py +0 -0
- {dazpy-2.7.2 → dazpy-2.8.0}/dazpy/_render.py +0 -0
- {dazpy-2.7.2 → dazpy-2.8.0}/dazpy/_render_api.py +0 -0
- {dazpy-2.7.2 → dazpy-2.8.0}/dazpy/_result.py +0 -0
- {dazpy-2.7.2 → dazpy-2.8.0}/dazpy/_scene.py +0 -0
- {dazpy-2.7.2 → dazpy-2.8.0}/dazpy/_scene_events.py +0 -0
- {dazpy-2.7.2 → dazpy-2.8.0}/dazpy/_scene_state.py +0 -0
- {dazpy-2.7.2 → dazpy-2.8.0}/dazpy/_script_builder.py +0 -0
- {dazpy-2.7.2 → dazpy-2.8.0}/dazpy/_skeleton.py +0 -0
- {dazpy-2.7.2 → dazpy-2.8.0}/dazpy/_timeline.py +0 -0
- {dazpy-2.7.2 → dazpy-2.8.0}/dazpy/_undo.py +0 -0
- {dazpy-2.7.2 → dazpy-2.8.0}/dazpy/_viewport.py +0 -0
- {dazpy-2.7.2 → dazpy-2.8.0}/dazpy/math3.py +0 -0
- {dazpy-2.7.2 → dazpy-2.8.0}/dazpy/py.typed +0 -0
- {dazpy-2.7.2 → dazpy-2.8.0}/dazpy.egg-info/SOURCES.txt +0 -0
- {dazpy-2.7.2 → dazpy-2.8.0}/dazpy.egg-info/dependency_links.txt +0 -0
- {dazpy-2.7.2 → dazpy-2.8.0}/dazpy.egg-info/requires.txt +0 -0
- {dazpy-2.7.2 → dazpy-2.8.0}/dazpy.egg-info/top_level.txt +0 -0
- {dazpy-2.7.2 → dazpy-2.8.0}/setup.cfg +0 -0
- {dazpy-2.7.2 → dazpy-2.8.0}/tests/test_api.py +0 -0
- {dazpy-2.7.2 → dazpy-2.8.0}/tests/test_comfyui_client.py +0 -0
- {dazpy-2.7.2 → dazpy-2.8.0}/tests/test_comfyui_enhance_integration.py +0 -0
- {dazpy-2.7.2 → dazpy-2.8.0}/tests/test_dazpy_integration.py +0 -0
- {dazpy-2.7.2 → dazpy-2.8.0}/tests/test_dsp_runner.py +0 -0
- {dazpy-2.7.2 → dazpy-2.8.0}/tests/test_file_watcher.py +0 -0
- {dazpy-2.7.2 → dazpy-2.8.0}/tests/test_performance.py +0 -0
- {dazpy-2.7.2 → dazpy-2.8.0}/tests/test_workflow_builder.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: dazpy
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.8.0
|
|
4
4
|
Summary: Python SDK for the DAZ Studio Script Server
|
|
5
5
|
License: MIT
|
|
6
6
|
Project-URL: Homepage, https://github.com/bluemoonfoundry/daz-script-server
|
|
@@ -15,7 +15,7 @@ Requires-Dist: sphinx-rtd-theme>=2.0; extra == "docs"
|
|
|
15
15
|
|
|
16
16
|
# DazScript Server
|
|
17
17
|
|
|
18
|
-
**Version 2.
|
|
18
|
+
**Version 2.8.0** | DAZ Studio 4.5+ | DAZ Studio 6.25+ | Windows & macOS
|
|
19
19
|
|
|
20
20
|
[](https://bluemoonfoundry.github.io/daz-script-server/)
|
|
21
21
|
[](https://bluemoonfoundry.github.io/daz-script-server/api-reference/)
|
|
@@ -83,6 +83,7 @@ print(response.json())
|
|
|
83
83
|
### Getting Started
|
|
84
84
|
- [Quick Start](#-quick-start)
|
|
85
85
|
- [Why This Exists](#why-this-exists)
|
|
86
|
+
- [What's New in v2.8.0](#whats-new-in-v280)
|
|
86
87
|
- [What's New in v2.7.2](#whats-new-in-v272)
|
|
87
88
|
- [What's New in v2.7.1](#whats-new-in-v271)
|
|
88
89
|
- [What's New in v2.7.0](#whats-new-in-v270)
|
|
@@ -169,6 +170,50 @@ DAZ Studio is powerful for 3D content creation, but automation is limited to man
|
|
|
169
170
|
|
|
170
171
|
---
|
|
171
172
|
|
|
173
|
+
## What's New in v2.8.0
|
|
174
|
+
|
|
175
|
+
### 🚦 Fail Fast When DAZ Studio Is Busy — `503 STUDIO_BUSY`
|
|
176
|
+
|
|
177
|
+
DAZ Studio's Qt main thread is single-threaded: if a user loads a scene,
|
|
178
|
+
saves, clears the scene, or renders through the DAZ Studio UI while the
|
|
179
|
+
server is also being driven remotely, requests that need the main thread
|
|
180
|
+
used to block indefinitely with no signal to the caller. `/execute`,
|
|
181
|
+
`/scripts/:id/execute`, the async-enqueue endpoints, and the render-submit
|
|
182
|
+
endpoints now check DAZ Studio's busy state first and return `503` with
|
|
183
|
+
`error_code: "STUDIO_BUSY"` (plus a `Retry-After` header) immediately
|
|
184
|
+
instead of hanging.
|
|
185
|
+
|
|
186
|
+
`dazpy` surfaces this as a new exception hierarchy — `DazBusyError`, with
|
|
187
|
+
`StudioBusyError` (503) and `ConcurrencyLimitError` (429) subclasses, each
|
|
188
|
+
carrying `reason` and `retry_after`. This also fixes a pre-existing bug
|
|
189
|
+
where `429 CONCURRENT_LIMIT_EXCEEDED` responses were misreported as
|
|
190
|
+
`ScriptRuntimeError`, as if the script itself had failed.
|
|
191
|
+
|
|
192
|
+
```python
|
|
193
|
+
from dazpy import DazClient
|
|
194
|
+
from dazpy.exceptions import DazBusyError
|
|
195
|
+
|
|
196
|
+
client = DazClient()
|
|
197
|
+
try:
|
|
198
|
+
result = client.execute("1+1;")
|
|
199
|
+
except DazBusyError as e:
|
|
200
|
+
print(f"DAZ Studio is busy: {e.reason} — retry after {e.retry_after}s")
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
Opt in to automatic retry with backoff instead of handling the exception
|
|
204
|
+
yourself:
|
|
205
|
+
|
|
206
|
+
```python
|
|
207
|
+
result = client.execute("1+1;", retry_on_busy=True, max_wait=30.0)
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
`retry_on_busy`/`max_wait` are available on `execute`, `execute_file`,
|
|
211
|
+
`execute_async_submit`, `render_submit`, `render_batch_submit`, and
|
|
212
|
+
`render_animation_submit`. Retry is opt-in and off by default — callers
|
|
213
|
+
(and any LLM driving them) stay informed rather than silently blocked.
|
|
214
|
+
|
|
215
|
+
---
|
|
216
|
+
|
|
172
217
|
## What's New in v2.7.2
|
|
173
218
|
|
|
174
219
|
No functional changes to `dazpy`'s behavior. `2.7.1`'s version bump missed
|
|
@@ -667,7 +712,7 @@ automation code without authoring DazScript by hand.
|
|
|
667
712
|
Download the `.whl` file from the [latest release](https://github.com/bluemoonfoundry/daz-script-server/releases/latest) and install it:
|
|
668
713
|
|
|
669
714
|
```bash
|
|
670
|
-
pip install dazpy-2.
|
|
715
|
+
pip install dazpy-2.8.0-py3-none-any.whl
|
|
671
716
|
```
|
|
672
717
|
|
|
673
718
|
Or install directly from the repo for development:
|
|
@@ -1,21 +1,6 @@
|
|
|
1
|
-
Metadata-Version: 2.4
|
|
2
|
-
Name: dazpy
|
|
3
|
-
Version: 2.7.2
|
|
4
|
-
Summary: Python SDK for the DAZ Studio Script Server
|
|
5
|
-
License: MIT
|
|
6
|
-
Project-URL: Homepage, https://github.com/bluemoonfoundry/daz-script-server
|
|
7
|
-
Project-URL: Repository, https://github.com/bluemoonfoundry/daz-script-server
|
|
8
|
-
Project-URL: Bug Tracker, https://github.com/bluemoonfoundry/daz-script-server/issues
|
|
9
|
-
Requires-Python: >=3.10
|
|
10
|
-
Description-Content-Type: text/markdown
|
|
11
|
-
Requires-Dist: requests>=2.28
|
|
12
|
-
Provides-Extra: docs
|
|
13
|
-
Requires-Dist: sphinx>=7.0; extra == "docs"
|
|
14
|
-
Requires-Dist: sphinx-rtd-theme>=2.0; extra == "docs"
|
|
15
|
-
|
|
16
1
|
# DazScript Server
|
|
17
2
|
|
|
18
|
-
**Version 2.
|
|
3
|
+
**Version 2.8.0** | DAZ Studio 4.5+ | DAZ Studio 6.25+ | Windows & macOS
|
|
19
4
|
|
|
20
5
|
[](https://bluemoonfoundry.github.io/daz-script-server/)
|
|
21
6
|
[](https://bluemoonfoundry.github.io/daz-script-server/api-reference/)
|
|
@@ -83,6 +68,7 @@ print(response.json())
|
|
|
83
68
|
### Getting Started
|
|
84
69
|
- [Quick Start](#-quick-start)
|
|
85
70
|
- [Why This Exists](#why-this-exists)
|
|
71
|
+
- [What's New in v2.8.0](#whats-new-in-v280)
|
|
86
72
|
- [What's New in v2.7.2](#whats-new-in-v272)
|
|
87
73
|
- [What's New in v2.7.1](#whats-new-in-v271)
|
|
88
74
|
- [What's New in v2.7.0](#whats-new-in-v270)
|
|
@@ -169,6 +155,50 @@ DAZ Studio is powerful for 3D content creation, but automation is limited to man
|
|
|
169
155
|
|
|
170
156
|
---
|
|
171
157
|
|
|
158
|
+
## What's New in v2.8.0
|
|
159
|
+
|
|
160
|
+
### 🚦 Fail Fast When DAZ Studio Is Busy — `503 STUDIO_BUSY`
|
|
161
|
+
|
|
162
|
+
DAZ Studio's Qt main thread is single-threaded: if a user loads a scene,
|
|
163
|
+
saves, clears the scene, or renders through the DAZ Studio UI while the
|
|
164
|
+
server is also being driven remotely, requests that need the main thread
|
|
165
|
+
used to block indefinitely with no signal to the caller. `/execute`,
|
|
166
|
+
`/scripts/:id/execute`, the async-enqueue endpoints, and the render-submit
|
|
167
|
+
endpoints now check DAZ Studio's busy state first and return `503` with
|
|
168
|
+
`error_code: "STUDIO_BUSY"` (plus a `Retry-After` header) immediately
|
|
169
|
+
instead of hanging.
|
|
170
|
+
|
|
171
|
+
`dazpy` surfaces this as a new exception hierarchy — `DazBusyError`, with
|
|
172
|
+
`StudioBusyError` (503) and `ConcurrencyLimitError` (429) subclasses, each
|
|
173
|
+
carrying `reason` and `retry_after`. This also fixes a pre-existing bug
|
|
174
|
+
where `429 CONCURRENT_LIMIT_EXCEEDED` responses were misreported as
|
|
175
|
+
`ScriptRuntimeError`, as if the script itself had failed.
|
|
176
|
+
|
|
177
|
+
```python
|
|
178
|
+
from dazpy import DazClient
|
|
179
|
+
from dazpy.exceptions import DazBusyError
|
|
180
|
+
|
|
181
|
+
client = DazClient()
|
|
182
|
+
try:
|
|
183
|
+
result = client.execute("1+1;")
|
|
184
|
+
except DazBusyError as e:
|
|
185
|
+
print(f"DAZ Studio is busy: {e.reason} — retry after {e.retry_after}s")
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
Opt in to automatic retry with backoff instead of handling the exception
|
|
189
|
+
yourself:
|
|
190
|
+
|
|
191
|
+
```python
|
|
192
|
+
result = client.execute("1+1;", retry_on_busy=True, max_wait=30.0)
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
`retry_on_busy`/`max_wait` are available on `execute`, `execute_file`,
|
|
196
|
+
`execute_async_submit`, `render_submit`, `render_batch_submit`, and
|
|
197
|
+
`render_animation_submit`. Retry is opt-in and off by default — callers
|
|
198
|
+
(and any LLM driving them) stay informed rather than silently blocked.
|
|
199
|
+
|
|
200
|
+
---
|
|
201
|
+
|
|
172
202
|
## What's New in v2.7.2
|
|
173
203
|
|
|
174
204
|
No functional changes to `dazpy`'s behavior. `2.7.1`'s version bump missed
|
|
@@ -667,7 +697,7 @@ automation code without authoring DazScript by hand.
|
|
|
667
697
|
Download the `.whl` file from the [latest release](https://github.com/bluemoonfoundry/daz-script-server/releases/latest) and install it:
|
|
668
698
|
|
|
669
699
|
```bash
|
|
670
|
-
pip install dazpy-2.
|
|
700
|
+
pip install dazpy-2.8.0-py3-none-any.whl
|
|
671
701
|
```
|
|
672
702
|
|
|
673
703
|
Or install directly from the repo for development:
|
|
@@ -2,14 +2,18 @@ from __future__ import annotations
|
|
|
2
2
|
|
|
3
3
|
import os
|
|
4
4
|
import re
|
|
5
|
+
import time
|
|
5
6
|
|
|
6
7
|
import requests as _requests
|
|
7
8
|
|
|
8
9
|
from .exceptions import (
|
|
9
10
|
AuthenticationError,
|
|
11
|
+
ConcurrencyLimitError,
|
|
10
12
|
ConnectionError,
|
|
13
|
+
DazBusyError,
|
|
11
14
|
ScriptRuntimeError,
|
|
12
15
|
ScriptSyntaxError,
|
|
16
|
+
StudioBusyError,
|
|
13
17
|
TimeoutError,
|
|
14
18
|
)
|
|
15
19
|
from ._result import ExecutionResult
|
|
@@ -27,10 +31,37 @@ def _load_token() -> str:
|
|
|
27
31
|
return ""
|
|
28
32
|
|
|
29
33
|
|
|
30
|
-
def
|
|
34
|
+
def _parse_retry_after(resp: _requests.Response) -> float:
|
|
35
|
+
raw = resp.headers.get("Retry-After", "")
|
|
36
|
+
try:
|
|
37
|
+
return float(raw)
|
|
38
|
+
except (TypeError, ValueError):
|
|
39
|
+
return 2.0
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def _raise_for_error(resp: _requests.Response) -> None:
|
|
43
|
+
"""Raise a typed exception for authentication or server-busy responses.
|
|
44
|
+
|
|
45
|
+
Leaves 2xx responses (and DazScript's own success:false runtime/syntax
|
|
46
|
+
errors, which use HTTP 200) for the caller to handle.
|
|
47
|
+
"""
|
|
31
48
|
status = resp.status_code
|
|
32
49
|
if status == 401 or status == 403:
|
|
33
50
|
raise AuthenticationError(f"HTTP {status}: {resp.text[:200]}")
|
|
51
|
+
if status < 400:
|
|
52
|
+
return
|
|
53
|
+
data = resp.json()
|
|
54
|
+
error_code = data.get("error_code", "")
|
|
55
|
+
error_msg = data.get("error") or f"HTTP {status}"
|
|
56
|
+
retry_after = _parse_retry_after(resp)
|
|
57
|
+
if error_code == "STUDIO_BUSY":
|
|
58
|
+
raise StudioBusyError(error_msg, reason=data.get("detail", error_msg), retry_after=retry_after)
|
|
59
|
+
if error_code == "CONCURRENT_LIMIT_EXCEEDED":
|
|
60
|
+
raise ConcurrencyLimitError(error_msg, reason=error_msg, retry_after=retry_after)
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def _map_response(resp: _requests.Response, script: str = "") -> ExecutionResult:
|
|
64
|
+
_raise_for_error(resp)
|
|
34
65
|
|
|
35
66
|
data = resp.json()
|
|
36
67
|
request_id = data.get("request_id", "")
|
|
@@ -118,13 +149,37 @@ class DazClient:
|
|
|
118
149
|
except _requests.exceptions.Timeout as e:
|
|
119
150
|
raise TimeoutError(f"Request timed out after {self._timeout}s") from e
|
|
120
151
|
|
|
121
|
-
def
|
|
152
|
+
def _with_busy_retry(self, fn, retry_on_busy: bool, max_wait: float):
|
|
153
|
+
"""Run *fn* (a zero-arg callable), retrying on DazBusyError while
|
|
154
|
+
*retry_on_busy* is true, up to *max_wait* seconds total."""
|
|
155
|
+
if not retry_on_busy:
|
|
156
|
+
return fn()
|
|
157
|
+
deadline = time.monotonic() + max_wait
|
|
158
|
+
backoff = 1.0
|
|
159
|
+
while True:
|
|
160
|
+
try:
|
|
161
|
+
return fn()
|
|
162
|
+
except DazBusyError:
|
|
163
|
+
remaining = deadline - time.monotonic()
|
|
164
|
+
if remaining <= 0:
|
|
165
|
+
raise
|
|
166
|
+
time.sleep(min(backoff, remaining))
|
|
167
|
+
backoff = min(backoff + 1.0, 5.0)
|
|
168
|
+
|
|
169
|
+
def execute(
|
|
170
|
+
self, script: str, args: object = None, *, retry_on_busy: bool = False, max_wait: float = 30.0
|
|
171
|
+
) -> ExecutionResult:
|
|
122
172
|
"""Execute a DazScript string synchronously.
|
|
123
173
|
|
|
124
174
|
Args:
|
|
125
175
|
script: DazScript source code to execute.
|
|
126
176
|
args: Optional value passed into the script as ``getArguments()[0]``.
|
|
127
177
|
Must be JSON-serialisable.
|
|
178
|
+
retry_on_busy: If ``True``, transparently retry with backoff when
|
|
179
|
+
the server reports ``StudioBusyError``/``ConcurrencyLimitError``,
|
|
180
|
+
instead of raising immediately.
|
|
181
|
+
max_wait: Maximum total seconds to retry when *retry_on_busy* is
|
|
182
|
+
``True``, before re-raising the busy error.
|
|
128
183
|
|
|
129
184
|
Returns:
|
|
130
185
|
The execution result containing the script return value and any
|
|
@@ -135,20 +190,36 @@ class DazClient:
|
|
|
135
190
|
AuthenticationError: If the token is invalid or the IP is blocked.
|
|
136
191
|
ScriptSyntaxError: If the script contains a parse error.
|
|
137
192
|
ScriptRuntimeError: If the script raises a runtime exception.
|
|
193
|
+
StudioBusyError: If DAZ Studio's main thread is busy and
|
|
194
|
+
*retry_on_busy* is ``False`` or *max_wait* is exceeded.
|
|
195
|
+
ConcurrencyLimitError: If too many concurrent requests are in
|
|
196
|
+
flight and *retry_on_busy* is ``False`` or *max_wait* is
|
|
197
|
+
exceeded.
|
|
138
198
|
TimeoutError: If the request exceeds *timeout* seconds.
|
|
139
199
|
"""
|
|
140
200
|
payload: dict = {"script": script}
|
|
141
201
|
if args is not None:
|
|
142
202
|
payload["args"] = args
|
|
143
|
-
resp = self._post("/execute", payload)
|
|
144
|
-
return _map_response(resp, script=script)
|
|
145
203
|
|
|
146
|
-
|
|
204
|
+
def _do():
|
|
205
|
+
resp = self._post("/execute", payload)
|
|
206
|
+
return _map_response(resp, script=script)
|
|
207
|
+
|
|
208
|
+
return self._with_busy_retry(_do, retry_on_busy, max_wait)
|
|
209
|
+
|
|
210
|
+
def execute_file(
|
|
211
|
+
self, script_file: str, args: object = None, *, retry_on_busy: bool = False, max_wait: float = 30.0
|
|
212
|
+
) -> ExecutionResult:
|
|
147
213
|
"""Execute a ``.dsa`` script file that resides on the DAZ Studio host.
|
|
148
214
|
|
|
149
215
|
Args:
|
|
150
216
|
script_file: Absolute path to the ``.dsa`` file on the server host.
|
|
151
217
|
args: Optional argument passed to the script.
|
|
218
|
+
retry_on_busy: If ``True``, transparently retry with backoff when
|
|
219
|
+
the server reports ``StudioBusyError``/``ConcurrencyLimitError``,
|
|
220
|
+
instead of raising immediately.
|
|
221
|
+
max_wait: Maximum total seconds to retry when *retry_on_busy* is
|
|
222
|
+
``True``, before re-raising the busy error.
|
|
152
223
|
|
|
153
224
|
Returns:
|
|
154
225
|
The execution result.
|
|
@@ -158,20 +229,36 @@ class DazClient:
|
|
|
158
229
|
AuthenticationError: On auth failure.
|
|
159
230
|
ScriptSyntaxError: On parse error.
|
|
160
231
|
ScriptRuntimeError: On runtime error.
|
|
232
|
+
StudioBusyError: If DAZ Studio's main thread is busy and
|
|
233
|
+
*retry_on_busy* is ``False`` or *max_wait* is exceeded.
|
|
234
|
+
ConcurrencyLimitError: If too many concurrent requests are in
|
|
235
|
+
flight and *retry_on_busy* is ``False`` or *max_wait* is
|
|
236
|
+
exceeded.
|
|
161
237
|
TimeoutError: On HTTP timeout.
|
|
162
238
|
"""
|
|
163
239
|
payload: dict = {"scriptFile": script_file}
|
|
164
240
|
if args is not None:
|
|
165
241
|
payload["args"] = args
|
|
166
|
-
resp = self._post("/execute", payload)
|
|
167
|
-
return _map_response(resp)
|
|
168
242
|
|
|
169
|
-
|
|
243
|
+
def _do():
|
|
244
|
+
resp = self._post("/execute", payload)
|
|
245
|
+
return _map_response(resp)
|
|
246
|
+
|
|
247
|
+
return self._with_busy_retry(_do, retry_on_busy, max_wait)
|
|
248
|
+
|
|
249
|
+
def execute_async_submit(
|
|
250
|
+
self, script: str, args: object = None, *, retry_on_busy: bool = False, max_wait: float = 30.0
|
|
251
|
+
) -> str:
|
|
170
252
|
"""Submit a script for asynchronous execution and return immediately.
|
|
171
253
|
|
|
172
254
|
Args:
|
|
173
255
|
script: DazScript source code.
|
|
174
256
|
args: Optional argument for the script.
|
|
257
|
+
retry_on_busy: If ``True``, transparently retry with backoff when
|
|
258
|
+
the server reports ``StudioBusyError``/``ConcurrencyLimitError``,
|
|
259
|
+
instead of raising immediately.
|
|
260
|
+
max_wait: Maximum total seconds to retry when *retry_on_busy* is
|
|
261
|
+
``True``, before re-raising the busy error.
|
|
175
262
|
|
|
176
263
|
Returns:
|
|
177
264
|
The server-assigned ``request_id`` string. Use it with
|
|
@@ -181,14 +268,22 @@ class DazClient:
|
|
|
181
268
|
Raises:
|
|
182
269
|
ConnectionError: If the server cannot be reached.
|
|
183
270
|
AuthenticationError: On auth failure.
|
|
271
|
+
StudioBusyError: If DAZ Studio's main thread is busy and
|
|
272
|
+
*retry_on_busy* is ``False`` or *max_wait* is exceeded.
|
|
273
|
+
ConcurrencyLimitError: If too many concurrent requests are in
|
|
274
|
+
flight and *retry_on_busy* is ``False`` or *max_wait* is
|
|
275
|
+
exceeded.
|
|
184
276
|
"""
|
|
185
277
|
payload: dict = {"script": script}
|
|
186
278
|
if args is not None:
|
|
187
279
|
payload["args"] = args
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
280
|
+
|
|
281
|
+
def _do():
|
|
282
|
+
resp = self._post("/execute/async", payload)
|
|
283
|
+
_raise_for_error(resp)
|
|
284
|
+
return resp.json().get("request_id", "")
|
|
285
|
+
|
|
286
|
+
return self._with_busy_retry(_do, retry_on_busy, max_wait)
|
|
192
287
|
|
|
193
288
|
def get_request_status(self, request_id: str) -> dict:
|
|
194
289
|
"""Return the current status of an async request.
|
|
@@ -282,6 +377,8 @@ class DazClient:
|
|
|
282
377
|
engine: str = "",
|
|
283
378
|
iray_samples: int = 0,
|
|
284
379
|
reset_morphs: bool = False,
|
|
380
|
+
retry_on_busy: bool = False,
|
|
381
|
+
max_wait: float = 30.0,
|
|
285
382
|
) -> dict:
|
|
286
383
|
"""Submit a render job and return immediately.
|
|
287
384
|
|
|
@@ -296,6 +393,11 @@ class DazClient:
|
|
|
296
393
|
engine: Render engine (``"iray"``, ``"filament"``).
|
|
297
394
|
iray_samples: iRay sample count (0 = use scene default).
|
|
298
395
|
reset_morphs: If ``True``, reset all morphs to defaults before applying.
|
|
396
|
+
retry_on_busy: If ``True``, transparently retry with backoff when
|
|
397
|
+
the server reports ``StudioBusyError``/``ConcurrencyLimitError``,
|
|
398
|
+
instead of raising immediately.
|
|
399
|
+
max_wait: Maximum total seconds to retry when *retry_on_busy* is
|
|
400
|
+
``True``, before re-raising the busy error.
|
|
299
401
|
|
|
300
402
|
Returns:
|
|
301
403
|
A dict with ``request_id``, ``status`` (``"queued"``), and
|
|
@@ -304,6 +406,11 @@ class DazClient:
|
|
|
304
406
|
Raises:
|
|
305
407
|
ConnectionError: If the server cannot be reached.
|
|
306
408
|
AuthenticationError: On HTTP 401/403.
|
|
409
|
+
StudioBusyError: If DAZ Studio's main thread is busy and
|
|
410
|
+
*retry_on_busy* is ``False`` or *max_wait* is exceeded.
|
|
411
|
+
ConcurrencyLimitError: If too many concurrent requests are in
|
|
412
|
+
flight and *retry_on_busy* is ``False`` or *max_wait* is
|
|
413
|
+
exceeded.
|
|
307
414
|
"""
|
|
308
415
|
payload: dict = {"output_path": output_path}
|
|
309
416
|
if width and height:
|
|
@@ -323,18 +430,28 @@ class DazClient:
|
|
|
323
430
|
payload["figure"] = figure
|
|
324
431
|
if morphs:
|
|
325
432
|
payload["morphs"] = morphs
|
|
326
|
-
resp = self._post("/render", payload)
|
|
327
|
-
if resp.status_code in (401, 403):
|
|
328
|
-
raise AuthenticationError(f"HTTP {resp.status_code}: {resp.text[:200]}")
|
|
329
|
-
return resp.json()
|
|
330
433
|
|
|
331
|
-
|
|
434
|
+
def _do():
|
|
435
|
+
resp = self._post("/render", payload)
|
|
436
|
+
_raise_for_error(resp)
|
|
437
|
+
return resp.json()
|
|
438
|
+
|
|
439
|
+
return self._with_busy_retry(_do, retry_on_busy, max_wait)
|
|
440
|
+
|
|
441
|
+
def render_batch_submit(
|
|
442
|
+
self, variants: list, base: dict | None = None, *, retry_on_busy: bool = False, max_wait: float = 30.0
|
|
443
|
+
) -> dict:
|
|
332
444
|
"""Submit a batch render job and return immediately.
|
|
333
445
|
|
|
334
446
|
Args:
|
|
335
447
|
variants: List of variant dicts, each with at least ``output_path``.
|
|
336
448
|
Supported keys mirror :meth:`render_submit` optional fields.
|
|
337
449
|
base: Optional shared defaults applied to all variants.
|
|
450
|
+
retry_on_busy: If ``True``, transparently retry with backoff when
|
|
451
|
+
the server reports ``StudioBusyError``/``ConcurrencyLimitError``,
|
|
452
|
+
instead of raising immediately.
|
|
453
|
+
max_wait: Maximum total seconds to retry when *retry_on_busy* is
|
|
454
|
+
``True``, before re-raising the busy error.
|
|
338
455
|
|
|
339
456
|
Returns:
|
|
340
457
|
A dict with ``batch_id``, ``request_ids`` (list), and ``total`` keys.
|
|
@@ -342,14 +459,22 @@ class DazClient:
|
|
|
342
459
|
Raises:
|
|
343
460
|
ConnectionError: If the server cannot be reached.
|
|
344
461
|
AuthenticationError: On HTTP 401/403.
|
|
462
|
+
StudioBusyError: If DAZ Studio's main thread is busy and
|
|
463
|
+
*retry_on_busy* is ``False`` or *max_wait* is exceeded.
|
|
464
|
+
ConcurrencyLimitError: If too many concurrent requests are in
|
|
465
|
+
flight and *retry_on_busy* is ``False`` or *max_wait* is
|
|
466
|
+
exceeded.
|
|
345
467
|
"""
|
|
346
468
|
payload: dict = {"variants": variants}
|
|
347
469
|
if base:
|
|
348
470
|
payload["base"] = base
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
471
|
+
|
|
472
|
+
def _do():
|
|
473
|
+
resp = self._post("/render/batch", payload)
|
|
474
|
+
_raise_for_error(resp)
|
|
475
|
+
return resp.json()
|
|
476
|
+
|
|
477
|
+
return self._with_busy_retry(_do, retry_on_busy, max_wait)
|
|
353
478
|
|
|
354
479
|
def render_animation_submit(
|
|
355
480
|
self,
|
|
@@ -362,6 +487,8 @@ class DazClient:
|
|
|
362
487
|
height: int = 0,
|
|
363
488
|
camera: str = "",
|
|
364
489
|
engine: str = "",
|
|
490
|
+
retry_on_busy: bool = False,
|
|
491
|
+
max_wait: float = 30.0,
|
|
365
492
|
) -> dict:
|
|
366
493
|
"""Submit an animation render job spanning a frame range and return immediately.
|
|
367
494
|
|
|
@@ -381,6 +508,11 @@ class DazClient:
|
|
|
381
508
|
height: Image height in pixels (must be paired with *width*).
|
|
382
509
|
camera: Camera label to render from.
|
|
383
510
|
engine: Render engine (``"iray"``, ``"filament"``).
|
|
511
|
+
retry_on_busy: If ``True``, transparently retry with backoff when
|
|
512
|
+
the server reports ``StudioBusyError``/``ConcurrencyLimitError``,
|
|
513
|
+
instead of raising immediately.
|
|
514
|
+
max_wait: Maximum total seconds to retry when *retry_on_busy* is
|
|
515
|
+
``True``, before re-raising the busy error.
|
|
384
516
|
|
|
385
517
|
Returns:
|
|
386
518
|
A dict with ``request_id``, ``status`` (``"queued"``), and
|
|
@@ -389,6 +521,11 @@ class DazClient:
|
|
|
389
521
|
Raises:
|
|
390
522
|
ConnectionError: If the server cannot be reached.
|
|
391
523
|
AuthenticationError: On HTTP 401/403.
|
|
524
|
+
StudioBusyError: If DAZ Studio's main thread is busy and
|
|
525
|
+
*retry_on_busy* is ``False`` or *max_wait* is exceeded.
|
|
526
|
+
ConcurrencyLimitError: If too many concurrent requests are in
|
|
527
|
+
flight and *retry_on_busy* is ``False`` or *max_wait* is
|
|
528
|
+
exceeded.
|
|
392
529
|
"""
|
|
393
530
|
payload: dict = {
|
|
394
531
|
"output_path": output_path,
|
|
@@ -403,10 +540,13 @@ class DazClient:
|
|
|
403
540
|
payload["camera"] = camera
|
|
404
541
|
if engine:
|
|
405
542
|
payload["engine"] = engine
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
543
|
+
|
|
544
|
+
def _do():
|
|
545
|
+
resp = self._post("/render/animation", payload)
|
|
546
|
+
_raise_for_error(resp)
|
|
547
|
+
return resp.json()
|
|
548
|
+
|
|
549
|
+
return self._with_busy_retry(_do, retry_on_busy, max_wait)
|
|
410
550
|
|
|
411
551
|
def cancel_render(self, request_id: str) -> bool:
|
|
412
552
|
"""Cancel a queued or running render job.
|
|
@@ -10,6 +10,30 @@ class AuthenticationError(DazError):
|
|
|
10
10
|
"""Raised on HTTP 401 or 403 (bad or missing API token, or IP blocked)."""
|
|
11
11
|
|
|
12
12
|
|
|
13
|
+
class DazBusyError(DazError):
|
|
14
|
+
"""Base class for transient "DAZ Studio is busy, please retry" conditions.
|
|
15
|
+
|
|
16
|
+
Attributes:
|
|
17
|
+
reason: Human-readable explanation of why the server is busy.
|
|
18
|
+
retry_after: Server-suggested seconds to wait before retrying.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
def __init__(self, message: str, reason: str = "", retry_after: float = 2.0):
|
|
22
|
+
super().__init__(message)
|
|
23
|
+
self.reason = reason
|
|
24
|
+
self.retry_after = retry_after
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class StudioBusyError(DazBusyError):
|
|
28
|
+
"""Raised on HTTP 503 STUDIO_BUSY: DAZ Studio's main thread is occupied
|
|
29
|
+
with a scene load, save, clear, or render and cannot service the request."""
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class ConcurrencyLimitError(DazBusyError):
|
|
33
|
+
"""Raised on HTTP 429 CONCURRENT_LIMIT_EXCEEDED: too many requests are
|
|
34
|
+
already in flight against the server."""
|
|
35
|
+
|
|
36
|
+
|
|
13
37
|
class ScriptError(DazError):
|
|
14
38
|
"""Base class for errors that originate inside a DazScript execution.
|
|
15
39
|
|
|
@@ -1,6 +1,21 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: dazpy
|
|
3
|
+
Version: 2.8.0
|
|
4
|
+
Summary: Python SDK for the DAZ Studio Script Server
|
|
5
|
+
License: MIT
|
|
6
|
+
Project-URL: Homepage, https://github.com/bluemoonfoundry/daz-script-server
|
|
7
|
+
Project-URL: Repository, https://github.com/bluemoonfoundry/daz-script-server
|
|
8
|
+
Project-URL: Bug Tracker, https://github.com/bluemoonfoundry/daz-script-server/issues
|
|
9
|
+
Requires-Python: >=3.10
|
|
10
|
+
Description-Content-Type: text/markdown
|
|
11
|
+
Requires-Dist: requests>=2.28
|
|
12
|
+
Provides-Extra: docs
|
|
13
|
+
Requires-Dist: sphinx>=7.0; extra == "docs"
|
|
14
|
+
Requires-Dist: sphinx-rtd-theme>=2.0; extra == "docs"
|
|
15
|
+
|
|
1
16
|
# DazScript Server
|
|
2
17
|
|
|
3
|
-
**Version 2.
|
|
18
|
+
**Version 2.8.0** | DAZ Studio 4.5+ | DAZ Studio 6.25+ | Windows & macOS
|
|
4
19
|
|
|
5
20
|
[](https://bluemoonfoundry.github.io/daz-script-server/)
|
|
6
21
|
[](https://bluemoonfoundry.github.io/daz-script-server/api-reference/)
|
|
@@ -68,6 +83,7 @@ print(response.json())
|
|
|
68
83
|
### Getting Started
|
|
69
84
|
- [Quick Start](#-quick-start)
|
|
70
85
|
- [Why This Exists](#why-this-exists)
|
|
86
|
+
- [What's New in v2.8.0](#whats-new-in-v280)
|
|
71
87
|
- [What's New in v2.7.2](#whats-new-in-v272)
|
|
72
88
|
- [What's New in v2.7.1](#whats-new-in-v271)
|
|
73
89
|
- [What's New in v2.7.0](#whats-new-in-v270)
|
|
@@ -154,6 +170,50 @@ DAZ Studio is powerful for 3D content creation, but automation is limited to man
|
|
|
154
170
|
|
|
155
171
|
---
|
|
156
172
|
|
|
173
|
+
## What's New in v2.8.0
|
|
174
|
+
|
|
175
|
+
### 🚦 Fail Fast When DAZ Studio Is Busy — `503 STUDIO_BUSY`
|
|
176
|
+
|
|
177
|
+
DAZ Studio's Qt main thread is single-threaded: if a user loads a scene,
|
|
178
|
+
saves, clears the scene, or renders through the DAZ Studio UI while the
|
|
179
|
+
server is also being driven remotely, requests that need the main thread
|
|
180
|
+
used to block indefinitely with no signal to the caller. `/execute`,
|
|
181
|
+
`/scripts/:id/execute`, the async-enqueue endpoints, and the render-submit
|
|
182
|
+
endpoints now check DAZ Studio's busy state first and return `503` with
|
|
183
|
+
`error_code: "STUDIO_BUSY"` (plus a `Retry-After` header) immediately
|
|
184
|
+
instead of hanging.
|
|
185
|
+
|
|
186
|
+
`dazpy` surfaces this as a new exception hierarchy — `DazBusyError`, with
|
|
187
|
+
`StudioBusyError` (503) and `ConcurrencyLimitError` (429) subclasses, each
|
|
188
|
+
carrying `reason` and `retry_after`. This also fixes a pre-existing bug
|
|
189
|
+
where `429 CONCURRENT_LIMIT_EXCEEDED` responses were misreported as
|
|
190
|
+
`ScriptRuntimeError`, as if the script itself had failed.
|
|
191
|
+
|
|
192
|
+
```python
|
|
193
|
+
from dazpy import DazClient
|
|
194
|
+
from dazpy.exceptions import DazBusyError
|
|
195
|
+
|
|
196
|
+
client = DazClient()
|
|
197
|
+
try:
|
|
198
|
+
result = client.execute("1+1;")
|
|
199
|
+
except DazBusyError as e:
|
|
200
|
+
print(f"DAZ Studio is busy: {e.reason} — retry after {e.retry_after}s")
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
Opt in to automatic retry with backoff instead of handling the exception
|
|
204
|
+
yourself:
|
|
205
|
+
|
|
206
|
+
```python
|
|
207
|
+
result = client.execute("1+1;", retry_on_busy=True, max_wait=30.0)
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
`retry_on_busy`/`max_wait` are available on `execute`, `execute_file`,
|
|
211
|
+
`execute_async_submit`, `render_submit`, `render_batch_submit`, and
|
|
212
|
+
`render_animation_submit`. Retry is opt-in and off by default — callers
|
|
213
|
+
(and any LLM driving them) stay informed rather than silently blocked.
|
|
214
|
+
|
|
215
|
+
---
|
|
216
|
+
|
|
157
217
|
## What's New in v2.7.2
|
|
158
218
|
|
|
159
219
|
No functional changes to `dazpy`'s behavior. `2.7.1`'s version bump missed
|
|
@@ -652,7 +712,7 @@ automation code without authoring DazScript by hand.
|
|
|
652
712
|
Download the `.whl` file from the [latest release](https://github.com/bluemoonfoundry/daz-script-server/releases/latest) and install it:
|
|
653
713
|
|
|
654
714
|
```bash
|
|
655
|
-
pip install dazpy-2.
|
|
715
|
+
pip install dazpy-2.8.0-py3-none-any.whl
|
|
656
716
|
```
|
|
657
717
|
|
|
658
718
|
Or install directly from the repo for development:
|
|
@@ -935,6 +935,109 @@ class TestErrorMapping(unittest.TestCase):
|
|
|
935
935
|
with self.assertRaises(exceptions.ScriptSyntaxError):
|
|
936
936
|
client.execute("{bad syntax")
|
|
937
937
|
|
|
938
|
+
def test_studio_busy_error_503(self):
|
|
939
|
+
import requests as req
|
|
940
|
+
resp = MagicMock(spec=req.Response)
|
|
941
|
+
resp.status_code = 503
|
|
942
|
+
resp.json.return_value = {
|
|
943
|
+
"success": False,
|
|
944
|
+
"error_code": "STUDIO_BUSY",
|
|
945
|
+
"error": "DAZ Studio's main thread is busy; please retry shortly",
|
|
946
|
+
"detail": "DAZ Studio is currently loading a scene",
|
|
947
|
+
}
|
|
948
|
+
resp.headers = {"Retry-After": "2"}
|
|
949
|
+
resp.text = ""
|
|
950
|
+
client = DazClient.__new__(DazClient)
|
|
951
|
+
object.__setattr__(client, "_base", "http://127.0.0.1:18811")
|
|
952
|
+
object.__setattr__(client, "_token", "")
|
|
953
|
+
object.__setattr__(client, "_timeout", 30.0)
|
|
954
|
+
with patch("dazpy._client._requests.post", return_value=resp):
|
|
955
|
+
with self.assertRaises(exceptions.StudioBusyError) as ctx:
|
|
956
|
+
client.execute("1+1;")
|
|
957
|
+
self.assertIsInstance(ctx.exception, exceptions.DazBusyError)
|
|
958
|
+
self.assertEqual(ctx.exception.retry_after, 2.0)
|
|
959
|
+
self.assertIn("loading a scene", ctx.exception.reason)
|
|
960
|
+
|
|
961
|
+
def test_concurrency_limit_error_429(self):
|
|
962
|
+
import requests as req
|
|
963
|
+
resp = MagicMock(spec=req.Response)
|
|
964
|
+
resp.status_code = 429
|
|
965
|
+
resp.json.return_value = {
|
|
966
|
+
"success": False,
|
|
967
|
+
"error_code": "CONCURRENT_LIMIT_EXCEEDED",
|
|
968
|
+
"error": "Server busy: maximum concurrent requests reached, please retry",
|
|
969
|
+
}
|
|
970
|
+
resp.headers = {}
|
|
971
|
+
resp.text = ""
|
|
972
|
+
client = DazClient.__new__(DazClient)
|
|
973
|
+
object.__setattr__(client, "_base", "http://127.0.0.1:18811")
|
|
974
|
+
object.__setattr__(client, "_token", "")
|
|
975
|
+
object.__setattr__(client, "_timeout", 30.0)
|
|
976
|
+
with patch("dazpy._client._requests.post", return_value=resp):
|
|
977
|
+
with self.assertRaises(exceptions.ConcurrencyLimitError) as ctx:
|
|
978
|
+
client.execute("1+1;")
|
|
979
|
+
self.assertIsInstance(ctx.exception, exceptions.DazBusyError)
|
|
980
|
+
self.assertNotIsInstance(ctx.exception, exceptions.ScriptRuntimeError)
|
|
981
|
+
self.assertEqual(ctx.exception.retry_after, 2.0)
|
|
982
|
+
|
|
983
|
+
def _busy_resp(self):
|
|
984
|
+
import requests as req
|
|
985
|
+
resp = MagicMock(spec=req.Response)
|
|
986
|
+
resp.status_code = 503
|
|
987
|
+
resp.json.return_value = {
|
|
988
|
+
"success": False,
|
|
989
|
+
"error_code": "STUDIO_BUSY",
|
|
990
|
+
"error": "DAZ Studio's main thread is busy; please retry shortly",
|
|
991
|
+
"detail": "DAZ Studio is currently rendering",
|
|
992
|
+
}
|
|
993
|
+
resp.headers = {"Retry-After": "1"}
|
|
994
|
+
resp.text = ""
|
|
995
|
+
return resp
|
|
996
|
+
|
|
997
|
+
def _success_resp(self):
|
|
998
|
+
import requests as req
|
|
999
|
+
resp = MagicMock(spec=req.Response)
|
|
1000
|
+
resp.status_code = 200
|
|
1001
|
+
resp.json.return_value = {"success": True, "result": 2, "output": [], "request_id": "ok1"}
|
|
1002
|
+
resp.headers = {}
|
|
1003
|
+
resp.text = ""
|
|
1004
|
+
return resp
|
|
1005
|
+
|
|
1006
|
+
def _busy_client(self):
|
|
1007
|
+
client = DazClient.__new__(DazClient)
|
|
1008
|
+
object.__setattr__(client, "_base", "http://127.0.0.1:18811")
|
|
1009
|
+
object.__setattr__(client, "_token", "")
|
|
1010
|
+
object.__setattr__(client, "_timeout", 30.0)
|
|
1011
|
+
return client
|
|
1012
|
+
|
|
1013
|
+
def test_execute_without_retry_on_busy_raises_immediately(self):
|
|
1014
|
+
client = self._busy_client()
|
|
1015
|
+
with patch("dazpy._client._requests.post", return_value=self._busy_resp()) as post:
|
|
1016
|
+
with self.assertRaises(exceptions.StudioBusyError):
|
|
1017
|
+
client.execute("1+1;")
|
|
1018
|
+
self.assertEqual(post.call_count, 1)
|
|
1019
|
+
|
|
1020
|
+
def test_execute_retry_on_busy_succeeds_after_retries(self):
|
|
1021
|
+
client = self._busy_client()
|
|
1022
|
+
responses = [self._busy_resp(), self._busy_resp(), self._success_resp()]
|
|
1023
|
+
with patch("dazpy._client._requests.post", side_effect=responses) as post:
|
|
1024
|
+
with patch("dazpy._client.time.sleep") as sleep:
|
|
1025
|
+
result = client.execute("1+1;", retry_on_busy=True, max_wait=10.0)
|
|
1026
|
+
self.assertEqual(result.value, 2)
|
|
1027
|
+
self.assertEqual(post.call_count, 3)
|
|
1028
|
+
self.assertEqual(sleep.call_count, 2)
|
|
1029
|
+
|
|
1030
|
+
def test_execute_retry_on_busy_gives_up_after_max_wait(self):
|
|
1031
|
+
client = self._busy_client()
|
|
1032
|
+
with patch("dazpy._client._requests.post", return_value=self._busy_resp()):
|
|
1033
|
+
with patch("dazpy._client.time.sleep"):
|
|
1034
|
+
with patch(
|
|
1035
|
+
"dazpy._client.time.monotonic",
|
|
1036
|
+
side_effect=[0.0, 0.0, 5.0, 11.0, 20.0, 20.0],
|
|
1037
|
+
):
|
|
1038
|
+
with self.assertRaises(exceptions.StudioBusyError):
|
|
1039
|
+
client.execute("1+1;", retry_on_busy=True, max_wait=10.0)
|
|
1040
|
+
|
|
938
1041
|
def test_connection_error(self):
|
|
939
1042
|
import requests as req
|
|
940
1043
|
client = DazClient.__new__(DazClient)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
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
|