pulze-renderflow 1.2.7__tar.gz → 2.0.7__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.
- {pulze_renderflow-1.2.7 → pulze_renderflow-2.0.7}/.gitignore +25 -24
- pulze_renderflow-2.0.7/CLAUDE.md +30 -0
- {pulze_renderflow-1.2.7 → pulze_renderflow-2.0.7}/MANIFEST.in +5 -5
- pulze_renderflow-2.0.7/PKG-INFO +122 -0
- pulze_renderflow-2.0.7/README.md +102 -0
- {pulze_renderflow-1.2.7 → pulze_renderflow-2.0.7}/pyproject.toml +58 -57
- pulze_renderflow-2.0.7/renderflow/__init__.py +176 -0
- pulze_renderflow-2.0.7/renderflow/__init__.pyi +113 -0
- pulze_renderflow-2.0.7/renderflow/farm.py +125 -0
- pulze_renderflow-2.0.7/renderflow/models.py +39 -0
- pulze_renderflow-2.0.7/renderflow/models.pyi +2722 -0
- pulze_renderflow-2.0.7/renderflow/openapi.json +20939 -0
- pulze_renderflow-2.0.7/renderflow/py.typed +0 -0
- pulze_renderflow-2.0.7/renderflow/runtime.py +757 -0
- pulze_renderflow-2.0.7/renderflow/wire.py +248 -0
- pulze_renderflow-2.0.7/scripts/build.py +63 -0
- pulze_renderflow-2.0.7/scripts/generate.py +68 -0
- pulze_renderflow-2.0.7/test/test_client.py +136 -0
- pulze_renderflow-2.0.7/test/test_runtime.py +678 -0
- pulze_renderflow-1.2.7/PKG-INFO +0 -190
- pulze_renderflow-1.2.7/README.md +0 -168
- pulze_renderflow-1.2.7/renderflow/__init__.py +0 -14
- pulze_renderflow-1.2.7/renderflow/client.py +0 -299
- pulze_renderflow-1.2.7/renderflow/models.py +0 -800
- {pulze_renderflow-1.2.7 → pulze_renderflow-2.0.7}/LICENSE +0 -0
|
@@ -1,24 +1,25 @@
|
|
|
1
|
-
__pycache__/
|
|
2
|
-
*.py[cod]
|
|
3
|
-
*$py.class
|
|
4
|
-
*.so
|
|
5
|
-
.Python
|
|
6
|
-
build/
|
|
7
|
-
develop-eggs/
|
|
8
|
-
dist/
|
|
9
|
-
downloads/
|
|
10
|
-
eggs/
|
|
11
|
-
.eggs/
|
|
12
|
-
lib/
|
|
13
|
-
lib64/
|
|
14
|
-
parts/
|
|
15
|
-
sdist/
|
|
16
|
-
var/
|
|
17
|
-
wheels/
|
|
18
|
-
*.egg-info/
|
|
19
|
-
.installed.cfg
|
|
20
|
-
*.egg
|
|
21
|
-
.env
|
|
22
|
-
.venv
|
|
23
|
-
venv/
|
|
24
|
-
ENV/
|
|
1
|
+
__pycache__/
|
|
2
|
+
*.py[cod]
|
|
3
|
+
*$py.class
|
|
4
|
+
*.so
|
|
5
|
+
.Python
|
|
6
|
+
build/
|
|
7
|
+
develop-eggs/
|
|
8
|
+
dist/
|
|
9
|
+
downloads/
|
|
10
|
+
eggs/
|
|
11
|
+
.eggs/
|
|
12
|
+
lib/
|
|
13
|
+
lib64/
|
|
14
|
+
parts/
|
|
15
|
+
sdist/
|
|
16
|
+
var/
|
|
17
|
+
wheels/
|
|
18
|
+
*.egg-info/
|
|
19
|
+
.installed.cfg
|
|
20
|
+
*.egg
|
|
21
|
+
.env
|
|
22
|
+
.venv
|
|
23
|
+
venv/
|
|
24
|
+
ENV/
|
|
25
|
+
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# packages/sdks/python — CLAUDE.md
|
|
2
|
+
|
|
3
|
+
The published Python SDK. **Standard library only — no runtime dependencies, ever.** The client
|
|
4
|
+
imports inside a DCC's bundled Python with nothing installed; adding a dependency breaks every
|
|
5
|
+
plugin that embeds it.
|
|
6
|
+
|
|
7
|
+
## How it works
|
|
8
|
+
|
|
9
|
+
- `renderflow/wire.py` binds operations off the OpenAPI document **at runtime** — there is no
|
|
10
|
+
generated per-operation code. `farm.jobs.get_jobs()` exists because the document says so.
|
|
11
|
+
- `renderflow/openapi.json` is a **committed copy** of the service's `openapi.public.json`;
|
|
12
|
+
`sdk-surface.test.ts` (service) holds it byte-equal. A route change that skips regeneration fails
|
|
13
|
+
the suite.
|
|
14
|
+
- `renderflow/models.pyi` = TypedDicts for editors/mypy, generated by `datamodel-code-generator`
|
|
15
|
+
(dev-only tool); `models.py` is a tiny runtime shim. A stub file never executes, so it may use
|
|
16
|
+
3.11 typing on a 3.10 interpreter.
|
|
17
|
+
|
|
18
|
+
## Commands
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
python scripts/generate.py # refresh openapi.json + models.pyi after a public route change
|
|
22
|
+
python test/test_runtime.py # stdlib unittest, no pytest
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
Both generated files are **committed** — review them like code. Publishing is manual
|
|
26
|
+
(`python -m build` + `twine upload`), on its own cadence; this package's version field is real.
|
|
27
|
+
|
|
28
|
+
`examples/` holds the example **custom plugins** (staged into installs as `sdk/examples`,
|
|
29
|
+
excluded from the wheel — `packages = ["renderflow"]`). If a work-list marker or progress format
|
|
30
|
+
changes, `job-progress` in the service reads recorded runtime output — keep them in step.
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
include README.md
|
|
2
|
-
include LICENSE
|
|
3
|
-
recursive-include renderflow *.py
|
|
4
|
-
recursive-exclude scripts *
|
|
5
|
-
recursive-exclude examples *
|
|
1
|
+
include README.md
|
|
2
|
+
include LICENSE
|
|
3
|
+
recursive-include renderflow *.py *.pyi openapi.json
|
|
4
|
+
recursive-exclude scripts *
|
|
5
|
+
recursive-exclude examples *
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: pulze-renderflow
|
|
3
|
+
Version: 2.0.7
|
|
4
|
+
Summary: Official RenderFlow API client for Python
|
|
5
|
+
Project-URL: Homepage, https://pulze.io/products/renderflow
|
|
6
|
+
Author-email: Pulze <support@pulze.io>
|
|
7
|
+
License-Expression: MIT
|
|
8
|
+
License-File: LICENSE
|
|
9
|
+
Keywords: 3dsmax,arnold,blender,cinema4d,corona,fusion,houdini,maya,network-rendering,nuke,pulze,redshift,render-farm,renderflow,vray
|
|
10
|
+
Classifier: Development Status :: 4 - Beta
|
|
11
|
+
Classifier: Intended Audience :: Developers
|
|
12
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
13
|
+
Classifier: Programming Language :: Python :: 3
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
18
|
+
Requires-Python: >=3.10
|
|
19
|
+
Description-Content-Type: text/markdown
|
|
20
|
+
|
|
21
|
+
# renderflow
|
|
22
|
+
|
|
23
|
+
Official Python client for the [RenderFlow](https://pulze.io/products/renderflow) API.
|
|
24
|
+
|
|
25
|
+
## Installation
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
pip install pulze-renderflow
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
The package has **no dependencies** — the whole client is standard library. That is a feature,
|
|
32
|
+
not an accident: drop the folder on `sys.path` and it works inside a DCC's bundled Python (Maya,
|
|
33
|
+
Houdini, Nuke, VRED), where installing anything is a fight.
|
|
34
|
+
|
|
35
|
+
## Quick start
|
|
36
|
+
|
|
37
|
+
```python
|
|
38
|
+
import renderflow as rf
|
|
39
|
+
|
|
40
|
+
farm = rf.connect() # the local service on 127.0.0.1:44442
|
|
41
|
+
# farm = rf.connect("https://rf.studio.lan:44442", api_key="rf_...")
|
|
42
|
+
|
|
43
|
+
for job in farm.jobs.get_jobs(status="working").data:
|
|
44
|
+
print(job.name, job.status)
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
`connect()` falls back to the environment for each argument it isn't given: `RENDERFLOW_URL`
|
|
48
|
+
for the address, `RENDERFLOW_API_KEY` for the key (the same variable `rfcli` reads). With
|
|
49
|
+
neither, it talks to the service on this machine — which needs no credential, and is how a DCC
|
|
50
|
+
plugin on a workstation submits.
|
|
51
|
+
|
|
52
|
+
## Finding the operation you want
|
|
53
|
+
|
|
54
|
+
Operations are grouped by tag and named after the API document's `operationId`, so a route
|
|
55
|
+
reads the same here, in the TypeScript SDK, in `rfcli` and in the MCP tools:
|
|
56
|
+
|
|
57
|
+
```python
|
|
58
|
+
farm.tags() # every group: jobs, nodes, pools, schedule, ...
|
|
59
|
+
dir(farm.jobs) # every operation in one: get_jobs, create_job, stop_job, ...
|
|
60
|
+
help(farm.jobs.get_jobs) # the operation's own documentation, generated from the document
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
| you want | you write |
|
|
64
|
+
| --- | --- |
|
|
65
|
+
| `GET /jobs` | `farm.jobs.get_jobs()` |
|
|
66
|
+
| `GET /jobs/{id}` | `farm.jobs.get_job(id)` |
|
|
67
|
+
| `POST /jobs` | `farm.jobs.create_job(body=...)` |
|
|
68
|
+
| `POST /jobs/{id}/stop` | `farm.jobs.stop_job(id)` |
|
|
69
|
+
| `GET /nodes` | `farm.nodes.get_nodes()` |
|
|
70
|
+
|
|
71
|
+
## Submitting a job
|
|
72
|
+
|
|
73
|
+
A recipe — `type` plus a file and the flat per-DCC fields — is the short way in; the service
|
|
74
|
+
expands it into the step tree:
|
|
75
|
+
|
|
76
|
+
```python
|
|
77
|
+
job = farm.jobs.create_job(body={
|
|
78
|
+
"type": "blender.render",
|
|
79
|
+
"file": "//share/scenes/shot_010.blend",
|
|
80
|
+
"frame": "1-100",
|
|
81
|
+
"pool_id": "...",
|
|
82
|
+
})
|
|
83
|
+
|
|
84
|
+
print(job.id, job.status)
|
|
85
|
+
farm.jobs.stop_job(job.id)
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
Bodies are plain dicts; answers read with dot access (`job.name`, `page.data`) and hand back the
|
|
89
|
+
raw payload via `.to_dict()`.
|
|
90
|
+
|
|
91
|
+
## Models
|
|
92
|
+
|
|
93
|
+
Typed names for every request and response schema live in `renderflow.models`, and your editor
|
|
94
|
+
reads them from `models.pyi` — autocomplete and type checking with nothing to install:
|
|
95
|
+
|
|
96
|
+
```python
|
|
97
|
+
from renderflow.models import MayaRenderRecipe
|
|
98
|
+
|
|
99
|
+
recipe: MayaRenderRecipe = {"type": "maya.render", "file": "//share/scenes/hero.mb", "host": {"name": "maya"}}
|
|
100
|
+
farm.jobs.create_job(body=recipe)
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
At runtime a model is a dict — the wire is JSON and the service validates.
|
|
104
|
+
|
|
105
|
+
## Errors
|
|
106
|
+
|
|
107
|
+
A refused call raises `renderflow.wire.ApiError`, carrying the status and the service's own
|
|
108
|
+
message — `str(error)` is the sentence an operator should read.
|
|
109
|
+
|
|
110
|
+
## How this package is built
|
|
111
|
+
|
|
112
|
+
There is no generated client. `renderflow/wire.py` binds operations off the OpenAPI document at
|
|
113
|
+
runtime, and the document ships inside the package as `renderflow/openapi.json` — a committed
|
|
114
|
+
copy of the service's `openapi.public.json`. An operation added to the API arrives here the
|
|
115
|
+
moment that copy is refreshed:
|
|
116
|
+
|
|
117
|
+
```bash
|
|
118
|
+
python scripts/generate.py # copies ../../service/openapi.public.json + rewrites models.pyi
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
`models.pyi` is written by [datamodel-code-generator](https://github.com/koxudaxi/datamodel-code-generator),
|
|
122
|
+
a dev-only tool — never a dependency of the package.
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
# renderflow
|
|
2
|
+
|
|
3
|
+
Official Python client for the [RenderFlow](https://pulze.io/products/renderflow) API.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
pip install pulze-renderflow
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
The package has **no dependencies** — the whole client is standard library. That is a feature,
|
|
12
|
+
not an accident: drop the folder on `sys.path` and it works inside a DCC's bundled Python (Maya,
|
|
13
|
+
Houdini, Nuke, VRED), where installing anything is a fight.
|
|
14
|
+
|
|
15
|
+
## Quick start
|
|
16
|
+
|
|
17
|
+
```python
|
|
18
|
+
import renderflow as rf
|
|
19
|
+
|
|
20
|
+
farm = rf.connect() # the local service on 127.0.0.1:44442
|
|
21
|
+
# farm = rf.connect("https://rf.studio.lan:44442", api_key="rf_...")
|
|
22
|
+
|
|
23
|
+
for job in farm.jobs.get_jobs(status="working").data:
|
|
24
|
+
print(job.name, job.status)
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
`connect()` falls back to the environment for each argument it isn't given: `RENDERFLOW_URL`
|
|
28
|
+
for the address, `RENDERFLOW_API_KEY` for the key (the same variable `rfcli` reads). With
|
|
29
|
+
neither, it talks to the service on this machine — which needs no credential, and is how a DCC
|
|
30
|
+
plugin on a workstation submits.
|
|
31
|
+
|
|
32
|
+
## Finding the operation you want
|
|
33
|
+
|
|
34
|
+
Operations are grouped by tag and named after the API document's `operationId`, so a route
|
|
35
|
+
reads the same here, in the TypeScript SDK, in `rfcli` and in the MCP tools:
|
|
36
|
+
|
|
37
|
+
```python
|
|
38
|
+
farm.tags() # every group: jobs, nodes, pools, schedule, ...
|
|
39
|
+
dir(farm.jobs) # every operation in one: get_jobs, create_job, stop_job, ...
|
|
40
|
+
help(farm.jobs.get_jobs) # the operation's own documentation, generated from the document
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
| you want | you write |
|
|
44
|
+
| --- | --- |
|
|
45
|
+
| `GET /jobs` | `farm.jobs.get_jobs()` |
|
|
46
|
+
| `GET /jobs/{id}` | `farm.jobs.get_job(id)` |
|
|
47
|
+
| `POST /jobs` | `farm.jobs.create_job(body=...)` |
|
|
48
|
+
| `POST /jobs/{id}/stop` | `farm.jobs.stop_job(id)` |
|
|
49
|
+
| `GET /nodes` | `farm.nodes.get_nodes()` |
|
|
50
|
+
|
|
51
|
+
## Submitting a job
|
|
52
|
+
|
|
53
|
+
A recipe — `type` plus a file and the flat per-DCC fields — is the short way in; the service
|
|
54
|
+
expands it into the step tree:
|
|
55
|
+
|
|
56
|
+
```python
|
|
57
|
+
job = farm.jobs.create_job(body={
|
|
58
|
+
"type": "blender.render",
|
|
59
|
+
"file": "//share/scenes/shot_010.blend",
|
|
60
|
+
"frame": "1-100",
|
|
61
|
+
"pool_id": "...",
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
print(job.id, job.status)
|
|
65
|
+
farm.jobs.stop_job(job.id)
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
Bodies are plain dicts; answers read with dot access (`job.name`, `page.data`) and hand back the
|
|
69
|
+
raw payload via `.to_dict()`.
|
|
70
|
+
|
|
71
|
+
## Models
|
|
72
|
+
|
|
73
|
+
Typed names for every request and response schema live in `renderflow.models`, and your editor
|
|
74
|
+
reads them from `models.pyi` — autocomplete and type checking with nothing to install:
|
|
75
|
+
|
|
76
|
+
```python
|
|
77
|
+
from renderflow.models import MayaRenderRecipe
|
|
78
|
+
|
|
79
|
+
recipe: MayaRenderRecipe = {"type": "maya.render", "file": "//share/scenes/hero.mb", "host": {"name": "maya"}}
|
|
80
|
+
farm.jobs.create_job(body=recipe)
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
At runtime a model is a dict — the wire is JSON and the service validates.
|
|
84
|
+
|
|
85
|
+
## Errors
|
|
86
|
+
|
|
87
|
+
A refused call raises `renderflow.wire.ApiError`, carrying the status and the service's own
|
|
88
|
+
message — `str(error)` is the sentence an operator should read.
|
|
89
|
+
|
|
90
|
+
## How this package is built
|
|
91
|
+
|
|
92
|
+
There is no generated client. `renderflow/wire.py` binds operations off the OpenAPI document at
|
|
93
|
+
runtime, and the document ships inside the package as `renderflow/openapi.json` — a committed
|
|
94
|
+
copy of the service's `openapi.public.json`. An operation added to the API arrives here the
|
|
95
|
+
moment that copy is refreshed:
|
|
96
|
+
|
|
97
|
+
```bash
|
|
98
|
+
python scripts/generate.py # copies ../../service/openapi.public.json + rewrites models.pyi
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
`models.pyi` is written by [datamodel-code-generator](https://github.com/koxudaxi/datamodel-code-generator),
|
|
102
|
+
a dev-only tool — never a dependency of the package.
|
|
@@ -1,57 +1,58 @@
|
|
|
1
|
-
[build-system]
|
|
2
|
-
requires = ["hatchling"]
|
|
3
|
-
build-backend = "hatchling.build"
|
|
4
|
-
|
|
5
|
-
[project]
|
|
6
|
-
name = "pulze-renderflow"
|
|
7
|
-
version = "
|
|
8
|
-
description = "Official RenderFlow API client for Python"
|
|
9
|
-
readme = "README.md"
|
|
10
|
-
license = "MIT"
|
|
11
|
-
requires-python = ">=3.10"
|
|
12
|
-
authors = [
|
|
13
|
-
{ name = "Pulze", email = "support@pulze.io" }
|
|
14
|
-
]
|
|
15
|
-
keywords = [
|
|
16
|
-
"3dsmax",
|
|
17
|
-
"arnold",
|
|
18
|
-
"blender",
|
|
19
|
-
"cinema4d",
|
|
20
|
-
"corona",
|
|
21
|
-
"fusion",
|
|
22
|
-
"houdini",
|
|
23
|
-
"maya",
|
|
24
|
-
"nuke",
|
|
25
|
-
"redshift",
|
|
26
|
-
"vray",
|
|
27
|
-
"network-rendering",
|
|
28
|
-
"pulze",
|
|
29
|
-
"render-farm",
|
|
30
|
-
"renderflow",
|
|
31
|
-
]
|
|
32
|
-
classifiers = [
|
|
33
|
-
"Development Status :: 4 - Beta",
|
|
34
|
-
"Intended Audience :: Developers",
|
|
35
|
-
"License :: OSI Approved :: MIT License",
|
|
36
|
-
"Programming Language :: Python :: 3",
|
|
37
|
-
"Programming Language :: Python :: 3.10",
|
|
38
|
-
"Programming Language :: Python :: 3.11",
|
|
39
|
-
"Programming Language :: Python :: 3.12",
|
|
40
|
-
"Programming Language :: Python :: 3.13",
|
|
41
|
-
]
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
[
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
[
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "pulze-renderflow"
|
|
7
|
+
version = "2.0.7"
|
|
8
|
+
description = "Official RenderFlow API client for Python"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
license = "MIT"
|
|
11
|
+
requires-python = ">=3.10"
|
|
12
|
+
authors = [
|
|
13
|
+
{ name = "Pulze", email = "support@pulze.io" }
|
|
14
|
+
]
|
|
15
|
+
keywords = [
|
|
16
|
+
"3dsmax",
|
|
17
|
+
"arnold",
|
|
18
|
+
"blender",
|
|
19
|
+
"cinema4d",
|
|
20
|
+
"corona",
|
|
21
|
+
"fusion",
|
|
22
|
+
"houdini",
|
|
23
|
+
"maya",
|
|
24
|
+
"nuke",
|
|
25
|
+
"redshift",
|
|
26
|
+
"vray",
|
|
27
|
+
"network-rendering",
|
|
28
|
+
"pulze",
|
|
29
|
+
"render-farm",
|
|
30
|
+
"renderflow",
|
|
31
|
+
]
|
|
32
|
+
classifiers = [
|
|
33
|
+
"Development Status :: 4 - Beta",
|
|
34
|
+
"Intended Audience :: Developers",
|
|
35
|
+
"License :: OSI Approved :: MIT License",
|
|
36
|
+
"Programming Language :: Python :: 3",
|
|
37
|
+
"Programming Language :: Python :: 3.10",
|
|
38
|
+
"Programming Language :: Python :: 3.11",
|
|
39
|
+
"Programming Language :: Python :: 3.12",
|
|
40
|
+
"Programming Language :: Python :: 3.13",
|
|
41
|
+
]
|
|
42
|
+
# None, deliberately. The client binds to the OpenAPI document shipped inside the package
|
|
43
|
+
# (`renderflow/wire.py` + `renderflow/openapi.json`), so the whole SDK is standard library —
|
|
44
|
+
# it imports inside a DCC's bundled Python with nothing installed. The typed models are
|
|
45
|
+
# `models.pyi`, which only editors read. datamodel-code-generator is a dev-only tool used by
|
|
46
|
+
# `scripts/generate.py`, never a requirement of the package.
|
|
47
|
+
dependencies = []
|
|
48
|
+
|
|
49
|
+
[project.urls]
|
|
50
|
+
Homepage = "https://pulze.io/products/renderflow"
|
|
51
|
+
|
|
52
|
+
[tool.hatch.build.targets.wheel]
|
|
53
|
+
packages = ["renderflow"]
|
|
54
|
+
|
|
55
|
+
[tool.hatch.build.targets.sdist]
|
|
56
|
+
exclude = [
|
|
57
|
+
"examples/",
|
|
58
|
+
]
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
"""RenderFlow — write a job, run it on the farm.
|
|
2
|
+
|
|
3
|
+
Two halves, one import:
|
|
4
|
+
|
|
5
|
+
import renderflow as rf
|
|
6
|
+
|
|
7
|
+
plates = rf.files("plates", pattern="*.exr") # declarations become UI fields and CLI flags
|
|
8
|
+
|
|
9
|
+
@rf.execute
|
|
10
|
+
def convert(plate): # runs on a render node, once per item
|
|
11
|
+
...
|
|
12
|
+
|
|
13
|
+
farm = rf.connect() # and the REST API, when you want it
|
|
14
|
+
farm.jobs.get_jobs(status="working")
|
|
15
|
+
|
|
16
|
+
**Importing this package touches nothing but the standard library.** That is a requirement, not a
|
|
17
|
+
nicety: the runtime half runs on render nodes, under whatever Python the machine happens to have,
|
|
18
|
+
and a node has no site-packages of ours. `connect()` is where httpx and the generated client load,
|
|
19
|
+
which is why it is a function and not a class you construct at import time.
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
from __future__ import annotations
|
|
23
|
+
|
|
24
|
+
import atexit
|
|
25
|
+
import os
|
|
26
|
+
import sys
|
|
27
|
+
from typing import TYPE_CHECKING, Any
|
|
28
|
+
|
|
29
|
+
from . import runtime
|
|
30
|
+
from .runtime import (
|
|
31
|
+
AppNotFound,
|
|
32
|
+
Fatal,
|
|
33
|
+
WorkItem,
|
|
34
|
+
app,
|
|
35
|
+
args,
|
|
36
|
+
bool_,
|
|
37
|
+
choice,
|
|
38
|
+
cleanup,
|
|
39
|
+
done,
|
|
40
|
+
env,
|
|
41
|
+
execute,
|
|
42
|
+
file,
|
|
43
|
+
files,
|
|
44
|
+
float_,
|
|
45
|
+
folder,
|
|
46
|
+
frames,
|
|
47
|
+
int_,
|
|
48
|
+
job,
|
|
49
|
+
log,
|
|
50
|
+
node,
|
|
51
|
+
output,
|
|
52
|
+
pairs,
|
|
53
|
+
path,
|
|
54
|
+
paths,
|
|
55
|
+
prepare,
|
|
56
|
+
progress,
|
|
57
|
+
resolution,
|
|
58
|
+
step,
|
|
59
|
+
submit,
|
|
60
|
+
task,
|
|
61
|
+
text,
|
|
62
|
+
tool,
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
# `int`, `float`, `bool` and `file` are builtins; inside this module the declarations are spelled
|
|
66
|
+
# with a trailing underscore so they do not shadow them, and exported under the names the docs use.
|
|
67
|
+
# A caller writes `rf.int("samples")` — `rf` is a namespace, so nothing is shadowed there.
|
|
68
|
+
globals()["int"] = int_
|
|
69
|
+
globals()["float"] = float_
|
|
70
|
+
globals()["bool"] = bool_
|
|
71
|
+
|
|
72
|
+
if TYPE_CHECKING: # for editors only — importing this at runtime would pull in httpx
|
|
73
|
+
from .farm import Farm
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
_INSPECT_FLAG = "--rf-inspect"
|
|
77
|
+
|
|
78
|
+
if _INSPECT_FLAG in sys.argv:
|
|
79
|
+
# Inspecting means importing the script: its declarations only exist once its module body
|
|
80
|
+
# has run, so the schema is printed at interpreter exit rather than from here.
|
|
81
|
+
#
|
|
82
|
+
# The script's own prints go to stderr while that happens. They are still worth seeing when
|
|
83
|
+
# a schema comes out wrong, and they are not part of the answer.
|
|
84
|
+
sys.argv = [argument for argument in sys.argv if argument != _INSPECT_FLAG]
|
|
85
|
+
runtime._ctx.inspecting = True
|
|
86
|
+
_real_stdout = sys.stdout
|
|
87
|
+
sys.stdout = sys.stderr
|
|
88
|
+
|
|
89
|
+
@atexit.register
|
|
90
|
+
def _print_schema() -> None:
|
|
91
|
+
sys.stdout = _real_stdout
|
|
92
|
+
runtime._inspect()
|
|
93
|
+
|
|
94
|
+
else:
|
|
95
|
+
# A bare `python script.py --plates ./x` runs the whole list here, at exit — the declarations
|
|
96
|
+
# only exist once the module body has run. A bootstrap binds values before the script imports
|
|
97
|
+
# us, which sets `driven` and keeps this out of its way.
|
|
98
|
+
runtime._read_argv(sys.argv[1:])
|
|
99
|
+
|
|
100
|
+
_previous_excepthook = sys.excepthook
|
|
101
|
+
|
|
102
|
+
def _mark_crashed(kind, value, trace) -> None:
|
|
103
|
+
runtime._ctx.crashed = True
|
|
104
|
+
_previous_excepthook(kind, value, trace)
|
|
105
|
+
|
|
106
|
+
sys.excepthook = _mark_crashed
|
|
107
|
+
|
|
108
|
+
@atexit.register
|
|
109
|
+
def _run_bare() -> None:
|
|
110
|
+
if runtime._ctx.driven or runtime._ctx.crashed or runtime._ctx.hooks["execute"] is None:
|
|
111
|
+
return
|
|
112
|
+
|
|
113
|
+
code = runtime._local(sys.argv[1:])
|
|
114
|
+
if code:
|
|
115
|
+
sys.stdout.flush()
|
|
116
|
+
sys.stderr.flush()
|
|
117
|
+
os._exit(code) # atexit cannot set the exit code any other way
|
|
118
|
+
|
|
119
|
+
__all__ = [
|
|
120
|
+
# connecting
|
|
121
|
+
"connect", "Farm",
|
|
122
|
+
# declarations
|
|
123
|
+
"text", "int", "float", "bool", "choice", "file", "folder", "pairs", "resolution", "output",
|
|
124
|
+
"frames", "files", "args",
|
|
125
|
+
# lifecycle
|
|
126
|
+
"execute", "submit", "prepare", "cleanup",
|
|
127
|
+
# context
|
|
128
|
+
"job", "step", "task", "node", "env", "paths", "path", "app", "tool",
|
|
129
|
+
# reporting
|
|
130
|
+
"progress", "log", "fatal", "done",
|
|
131
|
+
# exceptions
|
|
132
|
+
"Fatal", "AppNotFound", "WorkItem",
|
|
133
|
+
]
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def connect(url: str | None = None, api_key: str | None = None, *, timeout: float = 60.0) -> "Farm":
|
|
137
|
+
"""Connect to a farm.
|
|
138
|
+
|
|
139
|
+
Each argument falls back to the environment and then to the local service: ``url`` to
|
|
140
|
+
``RENDERFLOW_URL`` then ``http://127.0.0.1:44442``, ``api_key`` to ``RENDERFLOW_API_KEY`` then
|
|
141
|
+
to no credential at all — which is what a caller on the machine itself needs.
|
|
142
|
+
|
|
143
|
+
The REST client loads here rather than at import, so a render node running a script never pays
|
|
144
|
+
for it and never needs it installed.
|
|
145
|
+
"""
|
|
146
|
+
from .farm import Farm
|
|
147
|
+
|
|
148
|
+
return Farm(url, api_key, timeout=timeout)
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
def fatal(message: str) -> None:
|
|
152
|
+
"""End this task with a named reason. Re-exported from the runtime for `rf.fatal(...)`."""
|
|
153
|
+
from .runtime import fatal as _fatal
|
|
154
|
+
|
|
155
|
+
_fatal(message)
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def __getattr__(name: str) -> Any:
|
|
159
|
+
"""`Farm` on demand — see the note at the top about staying stdlib-only at import."""
|
|
160
|
+
if name == "Farm":
|
|
161
|
+
from .farm import Farm
|
|
162
|
+
|
|
163
|
+
return Farm
|
|
164
|
+
|
|
165
|
+
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
from importlib.metadata import PackageNotFoundError, version as _version
|
|
169
|
+
|
|
170
|
+
# Read from the installed distribution rather than written here: the two spellings had already
|
|
171
|
+
# drifted (this said 2.0.0 while the package built 1.3.0), and a version a caller reads should be
|
|
172
|
+
# the version they installed. `pyproject.toml` is the one place it is set.
|
|
173
|
+
try:
|
|
174
|
+
__version__ = _version("pulze-renderflow")
|
|
175
|
+
except PackageNotFoundError: # a source checkout that was never installed
|
|
176
|
+
__version__ = "0.0.0.dev0"
|