dazpy 2.9.0__tar.gz → 2.9.3__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.9.0 → dazpy-2.9.3}/PKG-INFO +63 -9
- {dazpy-2.9.0 → dazpy-2.9.3}/README.md +62 -8
- {dazpy-2.9.0 → dazpy-2.9.3}/dazpy/__init__.py +1 -1
- dazpy-2.9.3/dazpy/_batch.py +219 -0
- {dazpy-2.9.0 → dazpy-2.9.3}/dazpy/_client.py +57 -3
- {dazpy-2.9.0 → dazpy-2.9.3}/dazpy/_client_aio.py +37 -1
- {dazpy-2.9.0 → dazpy-2.9.3}/dazpy/_element.py +46 -2
- {dazpy-2.9.0 → dazpy-2.9.3}/dazpy/_node.py +40 -0
- {dazpy-2.9.0 → dazpy-2.9.3}/dazpy/_pose.py +30 -4
- {dazpy-2.9.0 → dazpy-2.9.3}/dazpy/_render.py +11 -2
- {dazpy-2.9.0 → dazpy-2.9.3}/dazpy/_scene.py +24 -3
- {dazpy-2.9.0 → dazpy-2.9.3}/dazpy/_scene_state.py +167 -10
- {dazpy-2.9.0 → dazpy-2.9.3}/dazpy/_skeleton.py +104 -0
- {dazpy-2.9.0 → dazpy-2.9.3}/dazpy/exceptions.py +10 -0
- {dazpy-2.9.0 → dazpy-2.9.3}/dazpy/poses.py +7 -7
- {dazpy-2.9.0 → dazpy-2.9.3}/dazpy.egg-info/PKG-INFO +63 -9
- {dazpy-2.9.0 → dazpy-2.9.3}/pyproject.toml +1 -1
- {dazpy-2.9.0 → dazpy-2.9.3}/tests/test_api.py +236 -1
- {dazpy-2.9.0 → dazpy-2.9.3}/tests/test_dazpy.py +694 -19
- {dazpy-2.9.0 → dazpy-2.9.3}/tests/test_dazpy_aio.py +61 -0
- dazpy-2.9.0/dazpy/_batch.py +0 -107
- {dazpy-2.9.0 → dazpy-2.9.3}/dazpy/_animation.py +0 -0
- {dazpy-2.9.0 → dazpy-2.9.3}/dazpy/_bone.py +0 -0
- {dazpy-2.9.0 → dazpy-2.9.3}/dazpy/_camera.py +0 -0
- {dazpy-2.9.0 → dazpy-2.9.3}/dazpy/_dforce.py +0 -0
- {dazpy-2.9.0 → dazpy-2.9.3}/dazpy/_geometry.py +0 -0
- {dazpy-2.9.0 → dazpy-2.9.3}/dazpy/_interaction.py +0 -0
- {dazpy-2.9.0 → dazpy-2.9.3}/dazpy/_light.py +0 -0
- {dazpy-2.9.0 → dazpy-2.9.3}/dazpy/_material.py +0 -0
- {dazpy-2.9.0 → dazpy-2.9.3}/dazpy/_modifier.py +0 -0
- {dazpy-2.9.0 → dazpy-2.9.3}/dazpy/_morph.py +0 -0
- {dazpy-2.9.0 → dazpy-2.9.3}/dazpy/_polling.py +0 -0
- {dazpy-2.9.0 → dazpy-2.9.3}/dazpy/_property.py +0 -0
- {dazpy-2.9.0 → dazpy-2.9.3}/dazpy/_render_api.py +0 -0
- {dazpy-2.9.0 → dazpy-2.9.3}/dazpy/_render_api_aio.py +0 -0
- {dazpy-2.9.0 → dazpy-2.9.3}/dazpy/_result.py +0 -0
- {dazpy-2.9.0 → dazpy-2.9.3}/dazpy/_scene_events.py +0 -0
- {dazpy-2.9.0 → dazpy-2.9.3}/dazpy/_script_builder.py +0 -0
- {dazpy-2.9.0 → dazpy-2.9.3}/dazpy/_shot_geometry.py +0 -0
- {dazpy-2.9.0 → dazpy-2.9.3}/dazpy/_timeline.py +0 -0
- {dazpy-2.9.0 → dazpy-2.9.3}/dazpy/_undo.py +0 -0
- {dazpy-2.9.0 → dazpy-2.9.3}/dazpy/_viewport.py +0 -0
- {dazpy-2.9.0 → dazpy-2.9.3}/dazpy/aio.py +0 -0
- {dazpy-2.9.0 → dazpy-2.9.3}/dazpy/cinematics.py +0 -0
- {dazpy-2.9.0 → dazpy-2.9.3}/dazpy/lighting.py +0 -0
- {dazpy-2.9.0 → dazpy-2.9.3}/dazpy/materials.py +0 -0
- {dazpy-2.9.0 → dazpy-2.9.3}/dazpy/math3.py +0 -0
- {dazpy-2.9.0 → dazpy-2.9.3}/dazpy/py.typed +0 -0
- {dazpy-2.9.0 → dazpy-2.9.3}/dazpy.egg-info/SOURCES.txt +0 -0
- {dazpy-2.9.0 → dazpy-2.9.3}/dazpy.egg-info/dependency_links.txt +0 -0
- {dazpy-2.9.0 → dazpy-2.9.3}/dazpy.egg-info/requires.txt +0 -0
- {dazpy-2.9.0 → dazpy-2.9.3}/dazpy.egg-info/top_level.txt +0 -0
- {dazpy-2.9.0 → dazpy-2.9.3}/setup.cfg +0 -0
- {dazpy-2.9.0 → dazpy-2.9.3}/tests/test_comfyui_client.py +0 -0
- {dazpy-2.9.0 → dazpy-2.9.3}/tests/test_comfyui_enhance_integration.py +0 -0
- {dazpy-2.9.0 → dazpy-2.9.3}/tests/test_dazpy_integration.py +0 -0
- {dazpy-2.9.0 → dazpy-2.9.3}/tests/test_dsp_runner.py +0 -0
- {dazpy-2.9.0 → dazpy-2.9.3}/tests/test_file_watcher.py +0 -0
- {dazpy-2.9.0 → dazpy-2.9.3}/tests/test_math3.py +0 -0
- {dazpy-2.9.0 → dazpy-2.9.3}/tests/test_performance.py +0 -0
- {dazpy-2.9.0 → dazpy-2.9.3}/tests/test_render_api.py +0 -0
- {dazpy-2.9.0 → dazpy-2.9.3}/tests/test_sprite_matrix_canvas_convert.py +0 -0
- {dazpy-2.9.0 → dazpy-2.9.3}/tests/test_sprite_matrix_integration.py +0 -0
- {dazpy-2.9.0 → dazpy-2.9.3}/tests/test_sprite_matrix_paths.py +0 -0
- {dazpy-2.9.0 → dazpy-2.9.3}/tests/test_sprite_matrix_presets.py +0 -0
- {dazpy-2.9.0 → dazpy-2.9.3}/tests/test_sprite_matrix_render_shot.py +0 -0
- {dazpy-2.9.0 → dazpy-2.9.3}/tests/test_sprite_matrix_render_util.py +0 -0
- {dazpy-2.9.0 → dazpy-2.9.3}/tests/test_sprite_matrix_schema.py +0 -0
- {dazpy-2.9.0 → dazpy-2.9.3}/tests/test_sprite_matrix_stylize_stage.py +0 -0
- {dazpy-2.9.0 → dazpy-2.9.3}/tests/test_sprite_matrix_workflow_builder.py +0 -0
- {dazpy-2.9.0 → dazpy-2.9.3}/tests/test_workflow_builder.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: dazpy
|
|
3
|
-
Version: 2.9.
|
|
3
|
+
Version: 2.9.3
|
|
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
|
|
@@ -17,7 +17,7 @@ Requires-Dist: httpx>=0.24; extra == "aio"
|
|
|
17
17
|
|
|
18
18
|
# DazScript Server
|
|
19
19
|
|
|
20
|
-
**Version 2.9.
|
|
20
|
+
**Version 2.9.3** | DAZ Studio 4.5+ | DAZ Studio 6.25+ | Windows & macOS
|
|
21
21
|
|
|
22
22
|
[](https://bluemoonfoundry.github.io/daz-script-server/)
|
|
23
23
|
[](https://bluemoonfoundry.github.io/daz-script-server/api-reference/)
|
|
@@ -80,6 +80,20 @@ print(response.json())
|
|
|
80
80
|
|
|
81
81
|
---
|
|
82
82
|
|
|
83
|
+
## Examples
|
|
84
|
+
|
|
85
|
+
**Complete example collection:** [daz-script-server-examples repository](https://github.com/bluemoonfoundry/daz-script-server-examples)
|
|
86
|
+
|
|
87
|
+
Production-ready examples ranging from beginner tutorials to advanced pipeline implementations:
|
|
88
|
+
- **48 examples** across 9 categories
|
|
89
|
+
- Individual READMEs with skill level classification
|
|
90
|
+
- Requirements.txt per example
|
|
91
|
+
- Comprehensive searchable table
|
|
92
|
+
|
|
93
|
+
Categories: fundamentals, character, animation, rendering, geometry, export, ml_data, ai_vision, bvh
|
|
94
|
+
|
|
95
|
+
---
|
|
96
|
+
|
|
83
97
|
## 📋 Table of Contents
|
|
84
98
|
|
|
85
99
|
### Getting Started
|
|
@@ -242,7 +256,7 @@ Async frameworks (FastAPI, FastMCP, ComfyUI custom nodes, asyncio/Temporal
|
|
|
242
256
|
workflows) previously had to wrap every `DazClient` call in
|
|
243
257
|
`asyncio.to_thread()` or hand-roll an `httpx.AsyncClient` wrapper.
|
|
244
258
|
`dazpy.aio.AsyncDazClient` mirrors `DazClient`'s full method surface —
|
|
245
|
-
`execute`, `execute_file`, async submit/status/result/list/cancel, render
|
|
259
|
+
`execute`, `execute_file`, inline/file async submit/status/result/list/cancel, render
|
|
246
260
|
submit/batch/animation, USD export, and server-health endpoints — as
|
|
247
261
|
native `async def` methods backed by `httpx.AsyncClient`, including the
|
|
248
262
|
same `retry_on_busy`/`max_wait` backoff semantics (via `asyncio.sleep`
|
|
@@ -253,8 +267,9 @@ from dazpy.aio import AsyncDazClient
|
|
|
253
267
|
|
|
254
268
|
async def main():
|
|
255
269
|
async with AsyncDazClient() as client:
|
|
256
|
-
|
|
257
|
-
|
|
270
|
+
request_id = await client.execute_file_async_submit("C:/jobs/pose-probe.dsa")
|
|
271
|
+
result = await client.get_request_result(request_id, wait=True)
|
|
272
|
+
print(result)
|
|
258
273
|
```
|
|
259
274
|
|
|
260
275
|
Requires the optional `httpx` dependency: `pip install dazpy[aio]`.
|
|
@@ -787,7 +802,7 @@ See the [full migration guide](MIGRATION.md) for upgrade steps and rollback inst
|
|
|
787
802
|
|
|
788
803
|
Long-running operations (renders, exports, batch jobs) no longer need to block the HTTP connection:
|
|
789
804
|
|
|
790
|
-
- **`POST /execute/async`** — Submit
|
|
805
|
+
- **`POST /execute/async`** — Submit inline `script` or host-side `scriptFile` work asynchronously; returns a `request_id` immediately
|
|
791
806
|
- **`POST /scripts/:id/async`** — Submit a registered script asynchronously
|
|
792
807
|
- **`GET /requests/:id/status`** — Poll for progress (`queued`, `running`, `completed`, `failed`, `cancelled`)
|
|
793
808
|
- **`GET /requests/:id/result`** — Fetch the final result; supports `?wait=true` to long-poll until complete
|
|
@@ -1771,10 +1786,36 @@ result = requests.get(f"{BASE}/requests/{req_id}/result?wait=true&timeout=300",
|
|
|
1771
1786
|
|
|
1772
1787
|
### `POST /execute/async`
|
|
1773
1788
|
|
|
1774
|
-
**Purpose:** Submit an inline script for asynchronous execution
|
|
1789
|
+
**Purpose:** Submit an inline script or host-side script file for asynchronous execution
|
|
1775
1790
|
**Authentication:** Required (if enabled)
|
|
1776
1791
|
|
|
1777
|
-
**Request Body:** Same as `POST /execute`
|
|
1792
|
+
**Request Body:** Same as `POST /execute`, plus optional `reportFile` for
|
|
1793
|
+
structured job observation.
|
|
1794
|
+
|
|
1795
|
+
`reportFile` is an absolute host-side path to a JSONL file owned by this job.
|
|
1796
|
+
The server truncates it when the request is accepted. The script may then write
|
|
1797
|
+
one JSON object per line. The path is injected into the script's argument object
|
|
1798
|
+
as reserved field `getArguments()[0].__dssReportFile`, so callers do not have to
|
|
1799
|
+
duplicate it inside `args`:
|
|
1800
|
+
|
|
1801
|
+
```jsonl
|
|
1802
|
+
{"type":"progress","value":0.25,"current":1,"total":4,"phase":"render","message":"Plate 1"}
|
|
1803
|
+
{"type":"log","level":"info","message":"Loaded scene"}
|
|
1804
|
+
{"type":"output","path":"C:/renders/run/plate-1.png","kind":"image","label":"plate"}
|
|
1805
|
+
```
|
|
1806
|
+
|
|
1807
|
+
Progress events require either `value` (0–1) or `current` plus `total`. Log
|
|
1808
|
+
events require `message`. Output events require `path`; repeating a path updates
|
|
1809
|
+
that manifest entry. A `manifest` event may provide an `outputs` array. Use a
|
|
1810
|
+
unique report file for every job.
|
|
1811
|
+
`reportFile` may not be the submitted `scriptFile`, because the server owns and
|
|
1812
|
+
truncates the former before the job is queued.
|
|
1813
|
+
|
|
1814
|
+
DAZ Studio 6 ingests report events from polling workers, so progress, logs, and
|
|
1815
|
+
outputs are visible while the main thread is occupied by the script. DAZ Studio
|
|
1816
|
+
4's JSON parser is main-thread-only; Studio 4 therefore ingests the final report
|
|
1817
|
+
after execution returns and does not expose report-driven progress while the job
|
|
1818
|
+
is still running.
|
|
1778
1819
|
|
|
1779
1820
|
**Response (immediate):**
|
|
1780
1821
|
```json
|
|
@@ -1808,7 +1849,15 @@ result = requests.get(f"{BASE}/requests/{req_id}/result?wait=true&timeout=300",
|
|
|
1808
1849
|
{
|
|
1809
1850
|
"request_id": "a3f2b891",
|
|
1810
1851
|
"status": "running",
|
|
1811
|
-
"progress": 0.
|
|
1852
|
+
"progress": 0.25,
|
|
1853
|
+
"observation": {
|
|
1854
|
+
"progress": {"value": 0.25, "current": 1, "total": 4, "phase": "render"},
|
|
1855
|
+
"log_tail": [{"level": "info", "source": "script", "message": "Loaded scene"}],
|
|
1856
|
+
"log_total": 1,
|
|
1857
|
+
"log_truncated": false,
|
|
1858
|
+
"output_manifest": {"count": 0, "outputs": []},
|
|
1859
|
+
"report_file": "C:/renders/run/job.jsonl"
|
|
1860
|
+
},
|
|
1812
1861
|
"elapsed_ms": 1240,
|
|
1813
1862
|
"queue_position": 0
|
|
1814
1863
|
}
|
|
@@ -1846,6 +1895,11 @@ result = requests.get(f"{BASE}/requests/{req_id}/result?wait=true&timeout=300",
|
|
|
1846
1895
|
}
|
|
1847
1896
|
```
|
|
1848
1897
|
|
|
1898
|
+
Completed, failed, and cancelled results also include `observation`. The log
|
|
1899
|
+
tail is capped at the newest 100 entries; `log_total` and `log_truncated` make
|
|
1900
|
+
the loss explicit. Ordinary captured `print()` output is folded into the same
|
|
1901
|
+
tail at completion while remaining available in the legacy `output` field.
|
|
1902
|
+
|
|
1849
1903
|
Returns HTTP 404 if the request ID is unknown or has been purged (TTL: 1 hour).
|
|
1850
1904
|
|
|
1851
1905
|
---
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# DazScript Server
|
|
2
2
|
|
|
3
|
-
**Version 2.9.
|
|
3
|
+
**Version 2.9.3** | DAZ Studio 4.5+ | DAZ Studio 6.25+ | Windows & macOS
|
|
4
4
|
|
|
5
5
|
[](https://bluemoonfoundry.github.io/daz-script-server/)
|
|
6
6
|
[](https://bluemoonfoundry.github.io/daz-script-server/api-reference/)
|
|
@@ -63,6 +63,20 @@ print(response.json())
|
|
|
63
63
|
|
|
64
64
|
---
|
|
65
65
|
|
|
66
|
+
## Examples
|
|
67
|
+
|
|
68
|
+
**Complete example collection:** [daz-script-server-examples repository](https://github.com/bluemoonfoundry/daz-script-server-examples)
|
|
69
|
+
|
|
70
|
+
Production-ready examples ranging from beginner tutorials to advanced pipeline implementations:
|
|
71
|
+
- **48 examples** across 9 categories
|
|
72
|
+
- Individual READMEs with skill level classification
|
|
73
|
+
- Requirements.txt per example
|
|
74
|
+
- Comprehensive searchable table
|
|
75
|
+
|
|
76
|
+
Categories: fundamentals, character, animation, rendering, geometry, export, ml_data, ai_vision, bvh
|
|
77
|
+
|
|
78
|
+
---
|
|
79
|
+
|
|
66
80
|
## 📋 Table of Contents
|
|
67
81
|
|
|
68
82
|
### Getting Started
|
|
@@ -225,7 +239,7 @@ Async frameworks (FastAPI, FastMCP, ComfyUI custom nodes, asyncio/Temporal
|
|
|
225
239
|
workflows) previously had to wrap every `DazClient` call in
|
|
226
240
|
`asyncio.to_thread()` or hand-roll an `httpx.AsyncClient` wrapper.
|
|
227
241
|
`dazpy.aio.AsyncDazClient` mirrors `DazClient`'s full method surface —
|
|
228
|
-
`execute`, `execute_file`, async submit/status/result/list/cancel, render
|
|
242
|
+
`execute`, `execute_file`, inline/file async submit/status/result/list/cancel, render
|
|
229
243
|
submit/batch/animation, USD export, and server-health endpoints — as
|
|
230
244
|
native `async def` methods backed by `httpx.AsyncClient`, including the
|
|
231
245
|
same `retry_on_busy`/`max_wait` backoff semantics (via `asyncio.sleep`
|
|
@@ -236,8 +250,9 @@ from dazpy.aio import AsyncDazClient
|
|
|
236
250
|
|
|
237
251
|
async def main():
|
|
238
252
|
async with AsyncDazClient() as client:
|
|
239
|
-
|
|
240
|
-
|
|
253
|
+
request_id = await client.execute_file_async_submit("C:/jobs/pose-probe.dsa")
|
|
254
|
+
result = await client.get_request_result(request_id, wait=True)
|
|
255
|
+
print(result)
|
|
241
256
|
```
|
|
242
257
|
|
|
243
258
|
Requires the optional `httpx` dependency: `pip install dazpy[aio]`.
|
|
@@ -770,7 +785,7 @@ See the [full migration guide](MIGRATION.md) for upgrade steps and rollback inst
|
|
|
770
785
|
|
|
771
786
|
Long-running operations (renders, exports, batch jobs) no longer need to block the HTTP connection:
|
|
772
787
|
|
|
773
|
-
- **`POST /execute/async`** — Submit
|
|
788
|
+
- **`POST /execute/async`** — Submit inline `script` or host-side `scriptFile` work asynchronously; returns a `request_id` immediately
|
|
774
789
|
- **`POST /scripts/:id/async`** — Submit a registered script asynchronously
|
|
775
790
|
- **`GET /requests/:id/status`** — Poll for progress (`queued`, `running`, `completed`, `failed`, `cancelled`)
|
|
776
791
|
- **`GET /requests/:id/result`** — Fetch the final result; supports `?wait=true` to long-poll until complete
|
|
@@ -1754,10 +1769,36 @@ result = requests.get(f"{BASE}/requests/{req_id}/result?wait=true&timeout=300",
|
|
|
1754
1769
|
|
|
1755
1770
|
### `POST /execute/async`
|
|
1756
1771
|
|
|
1757
|
-
**Purpose:** Submit an inline script for asynchronous execution
|
|
1772
|
+
**Purpose:** Submit an inline script or host-side script file for asynchronous execution
|
|
1758
1773
|
**Authentication:** Required (if enabled)
|
|
1759
1774
|
|
|
1760
|
-
**Request Body:** Same as `POST /execute`
|
|
1775
|
+
**Request Body:** Same as `POST /execute`, plus optional `reportFile` for
|
|
1776
|
+
structured job observation.
|
|
1777
|
+
|
|
1778
|
+
`reportFile` is an absolute host-side path to a JSONL file owned by this job.
|
|
1779
|
+
The server truncates it when the request is accepted. The script may then write
|
|
1780
|
+
one JSON object per line. The path is injected into the script's argument object
|
|
1781
|
+
as reserved field `getArguments()[0].__dssReportFile`, so callers do not have to
|
|
1782
|
+
duplicate it inside `args`:
|
|
1783
|
+
|
|
1784
|
+
```jsonl
|
|
1785
|
+
{"type":"progress","value":0.25,"current":1,"total":4,"phase":"render","message":"Plate 1"}
|
|
1786
|
+
{"type":"log","level":"info","message":"Loaded scene"}
|
|
1787
|
+
{"type":"output","path":"C:/renders/run/plate-1.png","kind":"image","label":"plate"}
|
|
1788
|
+
```
|
|
1789
|
+
|
|
1790
|
+
Progress events require either `value` (0–1) or `current` plus `total`. Log
|
|
1791
|
+
events require `message`. Output events require `path`; repeating a path updates
|
|
1792
|
+
that manifest entry. A `manifest` event may provide an `outputs` array. Use a
|
|
1793
|
+
unique report file for every job.
|
|
1794
|
+
`reportFile` may not be the submitted `scriptFile`, because the server owns and
|
|
1795
|
+
truncates the former before the job is queued.
|
|
1796
|
+
|
|
1797
|
+
DAZ Studio 6 ingests report events from polling workers, so progress, logs, and
|
|
1798
|
+
outputs are visible while the main thread is occupied by the script. DAZ Studio
|
|
1799
|
+
4's JSON parser is main-thread-only; Studio 4 therefore ingests the final report
|
|
1800
|
+
after execution returns and does not expose report-driven progress while the job
|
|
1801
|
+
is still running.
|
|
1761
1802
|
|
|
1762
1803
|
**Response (immediate):**
|
|
1763
1804
|
```json
|
|
@@ -1791,7 +1832,15 @@ result = requests.get(f"{BASE}/requests/{req_id}/result?wait=true&timeout=300",
|
|
|
1791
1832
|
{
|
|
1792
1833
|
"request_id": "a3f2b891",
|
|
1793
1834
|
"status": "running",
|
|
1794
|
-
"progress": 0.
|
|
1835
|
+
"progress": 0.25,
|
|
1836
|
+
"observation": {
|
|
1837
|
+
"progress": {"value": 0.25, "current": 1, "total": 4, "phase": "render"},
|
|
1838
|
+
"log_tail": [{"level": "info", "source": "script", "message": "Loaded scene"}],
|
|
1839
|
+
"log_total": 1,
|
|
1840
|
+
"log_truncated": false,
|
|
1841
|
+
"output_manifest": {"count": 0, "outputs": []},
|
|
1842
|
+
"report_file": "C:/renders/run/job.jsonl"
|
|
1843
|
+
},
|
|
1795
1844
|
"elapsed_ms": 1240,
|
|
1796
1845
|
"queue_position": 0
|
|
1797
1846
|
}
|
|
@@ -1829,6 +1878,11 @@ result = requests.get(f"{BASE}/requests/{req_id}/result?wait=true&timeout=300",
|
|
|
1829
1878
|
}
|
|
1830
1879
|
```
|
|
1831
1880
|
|
|
1881
|
+
Completed, failed, and cancelled results also include `observation`. The log
|
|
1882
|
+
tail is capped at the newest 100 entries; `log_total` and `log_truncated` make
|
|
1883
|
+
the loss explicit. Ordinary captured `print()` output is folded into the same
|
|
1884
|
+
tail at completion while remaining available in the legacy `output` field.
|
|
1885
|
+
|
|
1832
1886
|
Returns HTTP 404 if the request ID is unknown or has been purged (TTL: 1 hour).
|
|
1833
1887
|
|
|
1834
1888
|
---
|
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from ._client import DazClient
|
|
4
|
+
from .exceptions import BatchLimitExceededError
|
|
5
|
+
|
|
6
|
+
DEFAULT_MAX_OPERATIONS = 500
|
|
7
|
+
DEFAULT_MAX_SCRIPT_LENGTH = 900_000 # stays under the server's default 1MB script cap
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def build_operations_script(operations: list[tuple[list[str], str]]) -> str:
|
|
11
|
+
"""Build one IIFE script from a list of (body_lines, result_expression) pairs.
|
|
12
|
+
|
|
13
|
+
Shared by :meth:`Batch._build_script` and
|
|
14
|
+
:meth:`~dazpy.DazClient.execute_batch_async` so both produce scripts with
|
|
15
|
+
identical shape (keyed return object over ``_r0``, ``_r1``, ...).
|
|
16
|
+
"""
|
|
17
|
+
body_lines = []
|
|
18
|
+
return_parts = []
|
|
19
|
+
for i, (lines, result_expression) in enumerate(operations):
|
|
20
|
+
key = f"_r{i}"
|
|
21
|
+
body_lines.extend(lines)
|
|
22
|
+
body_lines.append(f"var {key} = {result_expression};")
|
|
23
|
+
return_parts.append(f'"{key}": {key}')
|
|
24
|
+
return_obj = "{" + ", ".join(return_parts) + "}"
|
|
25
|
+
body_lines.append(f"return {return_obj};")
|
|
26
|
+
body = "\n".join(body_lines)
|
|
27
|
+
return f"(function(){{\n{body}\n}})()"
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class BatchFuture:
|
|
31
|
+
"""Placeholder for a single result within a :class:`Batch` execution.
|
|
32
|
+
|
|
33
|
+
Created by :meth:`Batch.add` or :meth:`Batch.add_operation`; the
|
|
34
|
+
:attr:`value` property blocks until the batch has been executed.
|
|
35
|
+
"""
|
|
36
|
+
|
|
37
|
+
def __init__(self, key: str):
|
|
38
|
+
self._key = key
|
|
39
|
+
self._resolved = False
|
|
40
|
+
self._value = None
|
|
41
|
+
|
|
42
|
+
@property
|
|
43
|
+
def value(self) -> object:
|
|
44
|
+
"""The result value.
|
|
45
|
+
|
|
46
|
+
Raises:
|
|
47
|
+
RuntimeError: If :meth:`Batch.execute` has not been called yet.
|
|
48
|
+
"""
|
|
49
|
+
if not self._resolved:
|
|
50
|
+
raise RuntimeError("Batch has not been executed yet")
|
|
51
|
+
return self._value
|
|
52
|
+
|
|
53
|
+
def _resolve(self, value: object) -> None:
|
|
54
|
+
self._value = value
|
|
55
|
+
self._resolved = True
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
class Batch:
|
|
59
|
+
"""Collect multiple DazScript operations and execute them in a single HTTP round-trip.
|
|
60
|
+
|
|
61
|
+
Usage as a context manager (recommended)::
|
|
62
|
+
|
|
63
|
+
with Batch(client) as b:
|
|
64
|
+
pos_future = b.add(["var pos = Scene.findNode('Figure').getWSPos();",
|
|
65
|
+
"var pos = [pos.x, pos.y, pos.z];"])
|
|
66
|
+
name_future = b.add(["var name = Scene.findNode('Figure').getName();"])
|
|
67
|
+
# Both futures resolved after the `with` block
|
|
68
|
+
print(pos_future.value, name_future.value)
|
|
69
|
+
|
|
70
|
+
Or manually::
|
|
71
|
+
|
|
72
|
+
b = Batch(client)
|
|
73
|
+
f = b.add(["var x = 42;"])
|
|
74
|
+
b.execute()
|
|
75
|
+
print(f.value)
|
|
76
|
+
|
|
77
|
+
High-level helpers that generate operations programmatically should use
|
|
78
|
+
:meth:`add_operation` instead of :meth:`add` — it does not require the
|
|
79
|
+
caller to know the internally generated result-variable name, and
|
|
80
|
+
:meth:`add_prelude` lets multiple operations share one setup block (e.g.
|
|
81
|
+
a node lookup) emitted only once.
|
|
82
|
+
|
|
83
|
+
Args:
|
|
84
|
+
client: The :class:`~dazpy.DazClient` to use.
|
|
85
|
+
max_operations: Maximum number of queued operations before
|
|
86
|
+
:meth:`add_operation` raises :class:`~dazpy.exceptions.BatchLimitExceededError`.
|
|
87
|
+
max_script_length: Maximum generated script length (characters)
|
|
88
|
+
before :meth:`execute` raises :class:`~dazpy.exceptions.BatchLimitExceededError`.
|
|
89
|
+
"""
|
|
90
|
+
|
|
91
|
+
def __init__(
|
|
92
|
+
self,
|
|
93
|
+
client: DazClient,
|
|
94
|
+
max_operations: int = DEFAULT_MAX_OPERATIONS,
|
|
95
|
+
max_script_length: int = DEFAULT_MAX_SCRIPT_LENGTH,
|
|
96
|
+
):
|
|
97
|
+
self._client = client
|
|
98
|
+
self._ops: list[tuple[str, list[str], BatchFuture]] = []
|
|
99
|
+
self._preludes: dict[str, list[str]] = {}
|
|
100
|
+
self._prelude_order: list[str] = []
|
|
101
|
+
self._counter = 0
|
|
102
|
+
self._max_operations = max_operations
|
|
103
|
+
self._max_script_length = max_script_length
|
|
104
|
+
|
|
105
|
+
def add(self, lines: list[str]) -> BatchFuture:
|
|
106
|
+
"""Queue a list of DazScript lines to be included in the batch.
|
|
107
|
+
|
|
108
|
+
The last line in *lines* should assign the desired result to a
|
|
109
|
+
variable named after the internally generated key (``_r0``, ``_r1``,
|
|
110
|
+
... in call order) — inspect a prior :meth:`execute` call's generated
|
|
111
|
+
script if the exact naming matters, or prefer :meth:`add_operation`,
|
|
112
|
+
which does not require guessing the key name.
|
|
113
|
+
|
|
114
|
+
Args:
|
|
115
|
+
lines: DazScript source lines (no ``return`` needed).
|
|
116
|
+
|
|
117
|
+
Returns:
|
|
118
|
+
A :class:`BatchFuture` that resolves after :meth:`execute`.
|
|
119
|
+
"""
|
|
120
|
+
key = f"_r{self._counter}"
|
|
121
|
+
self._counter += 1
|
|
122
|
+
future = BatchFuture(key)
|
|
123
|
+
self._ops.append((key, lines, future))
|
|
124
|
+
return future
|
|
125
|
+
|
|
126
|
+
def add_prelude(self, prelude_key: str, lines: list[str]) -> None:
|
|
127
|
+
"""Register a shared setup block, emitted once per unique *prelude_key*.
|
|
128
|
+
|
|
129
|
+
Call this before :meth:`add_operation` calls whose bodies depend on
|
|
130
|
+
the prelude's bound variable(s) (e.g. a node lookup bound to
|
|
131
|
+
``_node_Fig``). Repeated calls with the same *prelude_key* are no-ops
|
|
132
|
+
after the first — use this instead of re-emitting an identical lookup
|
|
133
|
+
once per operation.
|
|
134
|
+
|
|
135
|
+
Args:
|
|
136
|
+
prelude_key: Stable identifier for this setup block (e.g.
|
|
137
|
+
``"node:Fig"``). Callers must pick keys that collide exactly
|
|
138
|
+
when — and only when — the generated lines are identical.
|
|
139
|
+
lines: DazScript source lines for the shared setup.
|
|
140
|
+
"""
|
|
141
|
+
if prelude_key not in self._preludes:
|
|
142
|
+
self._preludes[prelude_key] = list(lines)
|
|
143
|
+
self._prelude_order.append(prelude_key)
|
|
144
|
+
|
|
145
|
+
def add_operation(self, body_lines: list[str], result_expression: str) -> BatchFuture:
|
|
146
|
+
"""Queue an operation whose result the builder assigns internally.
|
|
147
|
+
|
|
148
|
+
Unlike :meth:`add`, the caller does not need to know the generated
|
|
149
|
+
key name — pass the JS expression that yields the result
|
|
150
|
+
(*result_expression*, e.g. a variable set inside *body_lines*, or a
|
|
151
|
+
literal expression), and the builder emits
|
|
152
|
+
``var _rN = <result_expression>;`` itself.
|
|
153
|
+
|
|
154
|
+
Args:
|
|
155
|
+
body_lines: DazScript source lines with no trailing result
|
|
156
|
+
assignment (side effects only, e.g. property writes).
|
|
157
|
+
result_expression: A JS expression evaluated once, immediately
|
|
158
|
+
after *body_lines* run, and used as this operation's result.
|
|
159
|
+
Mutation-only operations should pass ``"null"``.
|
|
160
|
+
|
|
161
|
+
Returns:
|
|
162
|
+
A :class:`BatchFuture` that resolves after :meth:`execute`.
|
|
163
|
+
|
|
164
|
+
Raises:
|
|
165
|
+
BatchLimitExceededError: If this call would exceed the batch's
|
|
166
|
+
configured ``max_operations``.
|
|
167
|
+
"""
|
|
168
|
+
if len(self._ops) >= self._max_operations:
|
|
169
|
+
raise BatchLimitExceededError(
|
|
170
|
+
f"Batch already has {len(self._ops)} operations "
|
|
171
|
+
f"(max_operations={self._max_operations})"
|
|
172
|
+
)
|
|
173
|
+
key = f"_r{self._counter}"
|
|
174
|
+
self._counter += 1
|
|
175
|
+
future = BatchFuture(key)
|
|
176
|
+
lines = list(body_lines) + [f"var {key} = {result_expression};"]
|
|
177
|
+
self._ops.append((key, lines, future))
|
|
178
|
+
return future
|
|
179
|
+
|
|
180
|
+
def _build_script(self) -> str:
|
|
181
|
+
body_lines = []
|
|
182
|
+
for prelude_key in self._prelude_order:
|
|
183
|
+
body_lines.extend(self._preludes[prelude_key])
|
|
184
|
+
return_parts = []
|
|
185
|
+
for key, lines, _ in self._ops:
|
|
186
|
+
body_lines.extend(lines)
|
|
187
|
+
return_parts.append(f'"{key}": {key}')
|
|
188
|
+
return_obj = "{" + ", ".join(return_parts) + "}"
|
|
189
|
+
body_lines.append(f"return {return_obj};")
|
|
190
|
+
body = "\n".join(body_lines)
|
|
191
|
+
return f"(function(){{\n{body}\n}})()"
|
|
192
|
+
|
|
193
|
+
def execute(self) -> None:
|
|
194
|
+
"""Execute all queued operations in a single HTTP request and resolve all futures.
|
|
195
|
+
|
|
196
|
+
Raises:
|
|
197
|
+
BatchLimitExceededError: If the generated script exceeds
|
|
198
|
+
``max_script_length``. Raised before any HTTP call.
|
|
199
|
+
"""
|
|
200
|
+
if not self._ops:
|
|
201
|
+
return
|
|
202
|
+
script = self._build_script()
|
|
203
|
+
if len(script) > self._max_script_length:
|
|
204
|
+
raise BatchLimitExceededError(
|
|
205
|
+
f"Generated batch script is {len(script)} characters "
|
|
206
|
+
f"(max_script_length={self._max_script_length})"
|
|
207
|
+
)
|
|
208
|
+
result = self._client.execute(script)
|
|
209
|
+
data = result.value or {}
|
|
210
|
+
for key, _, future in self._ops:
|
|
211
|
+
future._resolve(data.get(key))
|
|
212
|
+
|
|
213
|
+
def __enter__(self) -> "Batch":
|
|
214
|
+
return self
|
|
215
|
+
|
|
216
|
+
def __exit__(self, exc_type, exc_val, exc_tb) -> bool:
|
|
217
|
+
if exc_type is None:
|
|
218
|
+
self.execute()
|
|
219
|
+
return False
|
|
@@ -262,13 +262,16 @@ class DazClient:
|
|
|
262
262
|
return self._with_busy_retry(_do, retry_on_busy, max_wait)
|
|
263
263
|
|
|
264
264
|
def execute_async_submit(
|
|
265
|
-
self, script: str, args: object = None, *,
|
|
265
|
+
self, script: str, args: object = None, *, report_file: str | None = None,
|
|
266
|
+
retry_on_busy: bool = False, max_wait: float = 30.0
|
|
266
267
|
) -> str:
|
|
267
268
|
"""Submit a script for asynchronous execution and return immediately.
|
|
268
269
|
|
|
269
270
|
Args:
|
|
270
271
|
script: DazScript source code.
|
|
271
272
|
args: Optional argument for the script.
|
|
273
|
+
report_file: Optional host-side JSONL file used by the script to
|
|
274
|
+
report structured progress, logs, and output artefacts.
|
|
272
275
|
retry_on_busy: If ``True``, transparently retry with backoff when
|
|
273
276
|
the server reports ``StudioBusyError``/``ConcurrencyLimitError``,
|
|
274
277
|
instead of raising immediately.
|
|
@@ -292,6 +295,8 @@ class DazClient:
|
|
|
292
295
|
payload: dict = {"script": script}
|
|
293
296
|
if args is not None:
|
|
294
297
|
payload["args"] = args
|
|
298
|
+
if report_file is not None:
|
|
299
|
+
payload["reportFile"] = report_file
|
|
295
300
|
|
|
296
301
|
def _do():
|
|
297
302
|
resp = self._post("/execute/async", payload)
|
|
@@ -300,6 +305,53 @@ class DazClient:
|
|
|
300
305
|
|
|
301
306
|
return self._with_busy_retry(_do, retry_on_busy, max_wait)
|
|
302
307
|
|
|
308
|
+
def execute_file_async_submit(
|
|
309
|
+
self, script_file: str, args: object = None, *, report_file: str | None = None,
|
|
310
|
+
retry_on_busy: bool = False, max_wait: float = 30.0
|
|
311
|
+
) -> str:
|
|
312
|
+
"""Submit a host-side ``.dsa`` file for asynchronous execution.
|
|
313
|
+
|
|
314
|
+
The file is loaded by DAZ Studio when the queued job starts, preserving
|
|
315
|
+
its filename for ``getScriptFileName()`` and relative ``include()``
|
|
316
|
+
calls. Returns the server-assigned request id immediately; use the
|
|
317
|
+
request status/result/cancel methods to manage its lifecycle.
|
|
318
|
+
"""
|
|
319
|
+
payload: dict = {"scriptFile": script_file}
|
|
320
|
+
if args is not None:
|
|
321
|
+
payload["args"] = args
|
|
322
|
+
if report_file is not None:
|
|
323
|
+
payload["reportFile"] = report_file
|
|
324
|
+
|
|
325
|
+
def _do():
|
|
326
|
+
resp = self._post("/execute/async", payload)
|
|
327
|
+
_raise_for_error(resp)
|
|
328
|
+
return resp.json().get("request_id", "")
|
|
329
|
+
|
|
330
|
+
return self._with_busy_retry(_do, retry_on_busy, max_wait)
|
|
331
|
+
|
|
332
|
+
def execute_batch_async(
|
|
333
|
+
self, operations: list[dict], args: object = None, *, report_file: str | None = None
|
|
334
|
+
) -> str:
|
|
335
|
+
"""Submit multiple operations as one async request (one queue slot, one script).
|
|
336
|
+
|
|
337
|
+
Args:
|
|
338
|
+
operations: List of ``{"body_lines": [...], "result_expression": "..."}``
|
|
339
|
+
dicts — same shape as :meth:`~dazpy.Batch.add_operation`'s arguments.
|
|
340
|
+
args: Optional argument passed to the combined script.
|
|
341
|
+
report_file: Optional host-side JSONL file used for structured job
|
|
342
|
+
observation, as in :meth:`execute_async_submit`.
|
|
343
|
+
|
|
344
|
+
Returns:
|
|
345
|
+
The server-assigned ``request_id``. Poll it like any other async
|
|
346
|
+
request; the result's ``result`` field is a dict keyed ``"_r0"``,
|
|
347
|
+
``"_r1"``, ... in submission order.
|
|
348
|
+
"""
|
|
349
|
+
from ._batch import build_operations_script
|
|
350
|
+
|
|
351
|
+
pairs = [(op["body_lines"], op["result_expression"]) for op in operations]
|
|
352
|
+
script = build_operations_script(pairs)
|
|
353
|
+
return self.execute_async_submit(script, args=args, report_file=report_file)
|
|
354
|
+
|
|
303
355
|
def get_request_status(self, request_id: str) -> dict:
|
|
304
356
|
"""Return the current status of an async request.
|
|
305
357
|
|
|
@@ -307,7 +359,9 @@ class DazClient:
|
|
|
307
359
|
request_id: The ID returned by :meth:`execute_async_submit`.
|
|
308
360
|
|
|
309
361
|
Returns:
|
|
310
|
-
A dict with at least
|
|
362
|
+
A dict with at least ``"status"`` and ``"observation"`` keys.
|
|
363
|
+
Observation contains structured progress, the bounded log tail,
|
|
364
|
+
and the output manifest. Possible status values:
|
|
311
365
|
``"queued"``, ``"running"``, ``"completed"``, ``"failed"``,
|
|
312
366
|
``"cancelled"``, or ``"not_found"``.
|
|
313
367
|
"""
|
|
@@ -328,7 +382,7 @@ class DazClient:
|
|
|
328
382
|
|
|
329
383
|
Returns:
|
|
330
384
|
A dict containing ``success``, ``result``, ``output``, ``error``,
|
|
331
|
-
``duration_ms``, and ``
|
|
385
|
+
``duration_ms``, ``status``, and ``observation`` keys.
|
|
332
386
|
"""
|
|
333
387
|
params = {}
|
|
334
388
|
if wait:
|
|
@@ -127,12 +127,15 @@ class AsyncDazClient:
|
|
|
127
127
|
return await self._with_busy_retry(_do, retry_on_busy, max_wait)
|
|
128
128
|
|
|
129
129
|
async def execute_async_submit(
|
|
130
|
-
self, script: str, args: object = None, *,
|
|
130
|
+
self, script: str, args: object = None, *, report_file: str | None = None,
|
|
131
|
+
retry_on_busy: bool = False, max_wait: float = 30.0
|
|
131
132
|
) -> str:
|
|
132
133
|
"""Submit a script for async execution. See :meth:`dazpy.DazClient.execute_async_submit`."""
|
|
133
134
|
payload: dict = {"script": script}
|
|
134
135
|
if args is not None:
|
|
135
136
|
payload["args"] = args
|
|
137
|
+
if report_file is not None:
|
|
138
|
+
payload["reportFile"] = report_file
|
|
136
139
|
|
|
137
140
|
async def _do():
|
|
138
141
|
resp = await self._post("/execute/async", payload)
|
|
@@ -141,6 +144,39 @@ class AsyncDazClient:
|
|
|
141
144
|
|
|
142
145
|
return await self._with_busy_retry(_do, retry_on_busy, max_wait)
|
|
143
146
|
|
|
147
|
+
async def execute_file_async_submit(
|
|
148
|
+
self, script_file: str, args: object = None, *, report_file: str | None = None,
|
|
149
|
+
retry_on_busy: bool = False, max_wait: float = 30.0
|
|
150
|
+
) -> str:
|
|
151
|
+
"""Submit a host-side ``.dsa`` file asynchronously.
|
|
152
|
+
|
|
153
|
+
See :meth:`dazpy.DazClient.execute_file_async_submit`.
|
|
154
|
+
"""
|
|
155
|
+
payload: dict = {"scriptFile": script_file}
|
|
156
|
+
if args is not None:
|
|
157
|
+
payload["args"] = args
|
|
158
|
+
if report_file is not None:
|
|
159
|
+
payload["reportFile"] = report_file
|
|
160
|
+
|
|
161
|
+
async def _do():
|
|
162
|
+
resp = await self._post("/execute/async", payload)
|
|
163
|
+
_raise_for_error(resp)
|
|
164
|
+
return resp.json().get("request_id", "")
|
|
165
|
+
|
|
166
|
+
return await self._with_busy_retry(_do, retry_on_busy, max_wait)
|
|
167
|
+
|
|
168
|
+
async def execute_batch_async(
|
|
169
|
+
self, operations: list[dict], args: object = None, *, report_file: str | None = None
|
|
170
|
+
) -> str:
|
|
171
|
+
"""Submit multiple operations as one async request. See :meth:`dazpy.DazClient.execute_batch_async`."""
|
|
172
|
+
from ._batch import build_operations_script
|
|
173
|
+
|
|
174
|
+
pairs = [(op["body_lines"], op["result_expression"]) for op in operations]
|
|
175
|
+
script = build_operations_script(pairs)
|
|
176
|
+
return await self.execute_async_submit(
|
|
177
|
+
script, args=args, report_file=report_file
|
|
178
|
+
)
|
|
179
|
+
|
|
144
180
|
async def get_request_status(self, request_id: str) -> dict:
|
|
145
181
|
"""See :meth:`dazpy.DazClient.get_request_status`."""
|
|
146
182
|
resp = await self._get(f"/requests/{request_id}/status")
|