pulze-renderflow 2.0.8__tar.gz → 2.0.16__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (24) hide show
  1. {pulze_renderflow-2.0.8 → pulze_renderflow-2.0.16}/CLAUDE.md +19 -1
  2. {pulze_renderflow-2.0.8 → pulze_renderflow-2.0.16}/PKG-INFO +60 -4
  3. {pulze_renderflow-2.0.8 → pulze_renderflow-2.0.16}/README.md +59 -3
  4. {pulze_renderflow-2.0.8 → pulze_renderflow-2.0.16}/pyproject.toml +58 -58
  5. {pulze_renderflow-2.0.8 → pulze_renderflow-2.0.16}/renderflow/__init__.py +14 -31
  6. {pulze_renderflow-2.0.8 → pulze_renderflow-2.0.16}/renderflow/__init__.pyi +132 -113
  7. pulze_renderflow-2.0.16/renderflow/farm.py +243 -0
  8. pulze_renderflow-2.0.16/renderflow/live.py +533 -0
  9. {pulze_renderflow-2.0.8 → pulze_renderflow-2.0.16}/renderflow/models.pyi +3280 -2722
  10. {pulze_renderflow-2.0.8 → pulze_renderflow-2.0.16}/renderflow/openapi.json +4357 -2568
  11. {pulze_renderflow-2.0.8 → pulze_renderflow-2.0.16}/renderflow/runtime.py +884 -757
  12. {pulze_renderflow-2.0.8 → pulze_renderflow-2.0.16}/renderflow/wire.py +10 -2
  13. {pulze_renderflow-2.0.8 → pulze_renderflow-2.0.16}/scripts/build.py +60 -63
  14. pulze_renderflow-2.0.16/scripts/test.mjs +48 -0
  15. {pulze_renderflow-2.0.8 → pulze_renderflow-2.0.16}/test/test_client.py +242 -136
  16. pulze_renderflow-2.0.16/test/test_live.py +404 -0
  17. {pulze_renderflow-2.0.8 → pulze_renderflow-2.0.16}/test/test_runtime.py +994 -678
  18. pulze_renderflow-2.0.8/renderflow/farm.py +0 -125
  19. {pulze_renderflow-2.0.8 → pulze_renderflow-2.0.16}/.gitignore +0 -0
  20. {pulze_renderflow-2.0.8 → pulze_renderflow-2.0.16}/LICENSE +0 -0
  21. {pulze_renderflow-2.0.8 → pulze_renderflow-2.0.16}/MANIFEST.in +0 -0
  22. {pulze_renderflow-2.0.8 → pulze_renderflow-2.0.16}/renderflow/models.py +0 -0
  23. {pulze_renderflow-2.0.8 → pulze_renderflow-2.0.16}/renderflow/py.typed +0 -0
  24. {pulze_renderflow-2.0.8 → pulze_renderflow-2.0.16}/scripts/generate.py +0 -0
@@ -19,9 +19,27 @@ plugin that embeds it.
19
19
 
20
20
  ```bash
21
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
22
+ python test/test_runtime.py # the plugin runtime; stdlib unittest, no pytest
23
+ python test/test_client.py # the wire client
24
+ python test/test_live.py # the hand-rolled WebSocket
25
+ npm run test-python # from the repo root: all three, and what the gate runs
23
26
  ```
24
27
 
28
+ All three suites are in `test-suites` through `scripts/test.mjs`, which the root's `test-python`
29
+ calls — this package is not an npm workspace, so there is no `npm test --workspace` that reaches it.
30
+ A missing interpreter **fails** that script rather than skipping: `renderflow/runtime.py` is the
31
+ plugin API a node imports, and no type-checker anywhere can see it.
32
+
33
+ `farm.py` holds what is not an operation of the document: `JobsGroup.settled`, the wait, and
34
+ `LiveGroup.on`, the subscription. `GROUPS` is how one tag gets more than the document gives it, and
35
+ `LIVE_ENTITIES` is the rest — group name to socket entity, so a group whose collection the farm
36
+ pushes gets `on` and one that has no live collection does not. A group that adds something overrides
37
+ `__dir__` too, because `dir(farm.jobs)` is how this client is meant to be explored.
38
+
39
+ `live.py` is the client half of RFC 6455, hand-rolled on `socket`, `ssl` and `struct`, because the
40
+ standard library has no WebSocket client and this package is allowed no dependency. It speaks the
41
+ same protocol as `sdks/typescript/src/live.ts`, frame for frame.
42
+
25
43
  Both generated files are **committed** — review them like code. Publishing is manual
26
44
  (`python -m build` + `twine upload`), on its own cadence; this package's version field is real.
27
45
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: pulze-renderflow
3
- Version: 2.0.8
3
+ Version: 2.0.16
4
4
  Summary: Official RenderFlow API client for Python
5
5
  Project-URL: Homepage, https://pulze.io/products/renderflow
6
6
  Author-email: Pulze <support@pulze.io>
@@ -38,7 +38,7 @@ Houdini, Nuke, VRED), where installing anything is a fight.
38
38
  import renderflow as rf
39
39
 
40
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_...")
41
+ # farm = rf.connect("https://rf.studio.lan:44445", api_key="rf_...")
42
42
 
43
43
  for job in farm.jobs.get_jobs(status="working").data:
44
44
  print(job.name, job.status)
@@ -55,7 +55,7 @@ Operations are grouped by tag and named after the API document's `operationId`,
55
55
  reads the same here, in the TypeScript SDK, in `rfcli` and in the MCP tools:
56
56
 
57
57
  ```python
58
- farm.tags() # every group: jobs, nodes, pools, schedule, render_licenses, ...
58
+ farm.tags() # every group: jobs, nodes, pools, schedule, licenses, ...
59
59
  dir(farm.jobs) # every operation in one: get_jobs, create_job, stop_job, ...
60
60
  help(farm.jobs.get_jobs) # the operation's own documentation, generated from the document
61
61
  ```
@@ -90,7 +90,63 @@ farm.jobs.stop_job(job.id)
90
90
  ```
91
91
 
92
92
  Bodies are plain dicts; answers read with dot access (`job.name`, `page.data`) and hand back the
93
- raw payload via `.to_dict()`.
93
+ raw payload via `.to_dict()`. An answer the service does not send as JSON comes back as **bytes** —
94
+ `farm.tasks.get_task_preview(id)` is a rendered frame, ready to write to a file.
95
+
96
+ ## Waiting for a job
97
+
98
+ ```python
99
+ job = farm.jobs.settled(job.id) # answers on completed or failed
100
+ job = farm.jobs.settled(job.id, interval=30) # a render measured in hours
101
+ ```
102
+
103
+ `None` means the job is no longer there. Archiving and deleting look the same from out here, and
104
+ neither is going to finish.
105
+
106
+ A stop is **not** an end, because suspending a job is resumable — pass
107
+ `statuses=["completed", "failed", "suspended"]` if a stop is an answer for your caller. `timeout` is
108
+ in seconds and there is none by default, since a long render legitimately takes days; it raises
109
+ `TimeoutError` rather than handing back a job that has not finished.
110
+
111
+ This polls rather than watching, so a wait needs no socket, no thread and no reconnect behaviour,
112
+ and it works where a proxy passes HTTP and refuses an upgrade. `interval` is the cost: two seconds is
113
+ right for a test render and wrong for an overnight one. When the moment a job finishes is what
114
+ matters, watch it instead.
115
+
116
+ ## Live updates
117
+
118
+ One socket, shared by every subscription, opened on the first and closed with the last. No polling.
119
+
120
+ ```python
121
+ def draw(event):
122
+ if event.op == "resync":
123
+ return refetch() # events were missed, read again
124
+ print(event.op, event.id, event.patch)
125
+
126
+ stop = farm.jobs.on(draw, {"id": job.id})
127
+ stop()
128
+ ```
129
+
130
+ `on` is on every group whose collection the farm pushes: `jobs`, `tasks`, `nodes`, `pools`, `users`,
131
+ `user_groups`, `errors`, `schedule` and `licenses`. The second argument narrows it — `id` on
132
+ any of them, `step_id` on tasks, `node_id` on the node joins — and a list is one subscription over
133
+ several ids rather than several subscriptions.
134
+
135
+ An insert carries the whole row on `event.doc`. An update carries `event.patch`, **keyed by dotted
136
+ path**, so a job's progress arrives as `event.patch["progress.percent"]`. A delete carries the id.
137
+
138
+ **A `resync` event is not a change.** It means events were missed and the collection has to be read
139
+ again, because the socket has no replay: a reconnect, a dropped frame, or a client the farm outran
140
+ all mean the same thing. It arrives through the same handler precisely so there is nothing to forget
141
+ to pass.
142
+
143
+ `farm.live.close()` drops everything, and `farm.live.on_error` reports a refused credential; it
144
+ writes to stderr until you replace it. The socket runs on a daemon thread, so a script that forgot to
145
+ unsubscribe still exits.
146
+
147
+ There is still **no dependency**: `renderflow/live.py` is the client half of RFC 6455 hand-rolled on
148
+ `socket`, `ssl` and `struct`, because installing `websocket-client` into Maya is the fight this
149
+ package exists to avoid.
94
150
 
95
151
  ## Models
96
152
 
@@ -18,7 +18,7 @@ Houdini, Nuke, VRED), where installing anything is a fight.
18
18
  import renderflow as rf
19
19
 
20
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_...")
21
+ # farm = rf.connect("https://rf.studio.lan:44445", api_key="rf_...")
22
22
 
23
23
  for job in farm.jobs.get_jobs(status="working").data:
24
24
  print(job.name, job.status)
@@ -35,7 +35,7 @@ Operations are grouped by tag and named after the API document's `operationId`,
35
35
  reads the same here, in the TypeScript SDK, in `rfcli` and in the MCP tools:
36
36
 
37
37
  ```python
38
- farm.tags() # every group: jobs, nodes, pools, schedule, render_licenses, ...
38
+ farm.tags() # every group: jobs, nodes, pools, schedule, licenses, ...
39
39
  dir(farm.jobs) # every operation in one: get_jobs, create_job, stop_job, ...
40
40
  help(farm.jobs.get_jobs) # the operation's own documentation, generated from the document
41
41
  ```
@@ -70,7 +70,63 @@ farm.jobs.stop_job(job.id)
70
70
  ```
71
71
 
72
72
  Bodies are plain dicts; answers read with dot access (`job.name`, `page.data`) and hand back the
73
- raw payload via `.to_dict()`.
73
+ raw payload via `.to_dict()`. An answer the service does not send as JSON comes back as **bytes** —
74
+ `farm.tasks.get_task_preview(id)` is a rendered frame, ready to write to a file.
75
+
76
+ ## Waiting for a job
77
+
78
+ ```python
79
+ job = farm.jobs.settled(job.id) # answers on completed or failed
80
+ job = farm.jobs.settled(job.id, interval=30) # a render measured in hours
81
+ ```
82
+
83
+ `None` means the job is no longer there. Archiving and deleting look the same from out here, and
84
+ neither is going to finish.
85
+
86
+ A stop is **not** an end, because suspending a job is resumable — pass
87
+ `statuses=["completed", "failed", "suspended"]` if a stop is an answer for your caller. `timeout` is
88
+ in seconds and there is none by default, since a long render legitimately takes days; it raises
89
+ `TimeoutError` rather than handing back a job that has not finished.
90
+
91
+ This polls rather than watching, so a wait needs no socket, no thread and no reconnect behaviour,
92
+ and it works where a proxy passes HTTP and refuses an upgrade. `interval` is the cost: two seconds is
93
+ right for a test render and wrong for an overnight one. When the moment a job finishes is what
94
+ matters, watch it instead.
95
+
96
+ ## Live updates
97
+
98
+ One socket, shared by every subscription, opened on the first and closed with the last. No polling.
99
+
100
+ ```python
101
+ def draw(event):
102
+ if event.op == "resync":
103
+ return refetch() # events were missed, read again
104
+ print(event.op, event.id, event.patch)
105
+
106
+ stop = farm.jobs.on(draw, {"id": job.id})
107
+ stop()
108
+ ```
109
+
110
+ `on` is on every group whose collection the farm pushes: `jobs`, `tasks`, `nodes`, `pools`, `users`,
111
+ `user_groups`, `errors`, `schedule` and `licenses`. The second argument narrows it — `id` on
112
+ any of them, `step_id` on tasks, `node_id` on the node joins — and a list is one subscription over
113
+ several ids rather than several subscriptions.
114
+
115
+ An insert carries the whole row on `event.doc`. An update carries `event.patch`, **keyed by dotted
116
+ path**, so a job's progress arrives as `event.patch["progress.percent"]`. A delete carries the id.
117
+
118
+ **A `resync` event is not a change.** It means events were missed and the collection has to be read
119
+ again, because the socket has no replay: a reconnect, a dropped frame, or a client the farm outran
120
+ all mean the same thing. It arrives through the same handler precisely so there is nothing to forget
121
+ to pass.
122
+
123
+ `farm.live.close()` drops everything, and `farm.live.on_error` reports a refused credential; it
124
+ writes to stderr until you replace it. The socket runs on a daemon thread, so a script that forgot to
125
+ unsubscribe still exits.
126
+
127
+ There is still **no dependency**: `renderflow/live.py` is the client half of RFC 6455 hand-rolled on
128
+ `socket`, `ssl` and `struct`, because installing `websocket-client` into Maya is the fight this
129
+ package exists to avoid.
74
130
 
75
131
  ## Models
76
132
 
@@ -1,58 +1,58 @@
1
- [build-system]
2
- requires = ["hatchling"]
3
- build-backend = "hatchling.build"
4
-
5
- [project]
6
- name = "pulze-renderflow"
7
- version = "2.0.8"
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
- ]
1
+ [build-system]
2
+ requires = ["hatchling"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "pulze-renderflow"
7
+ version = "2.0.16"
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
+ ]
@@ -4,7 +4,7 @@ Two halves, one import:
4
4
 
5
5
  import renderflow as rf
6
6
 
7
- plates = rf.files("plates", pattern="*.exr") # declarations become UI fields and CLI flags
7
+ plates = rf.folder("plates") # a declaration is the field and the value
8
8
 
9
9
  @rf.execute
10
10
  def convert(plate): # runs on a render node, once per item
@@ -22,7 +22,6 @@ which is why it is a function and not a class you construct at import time.
22
22
  from __future__ import annotations
23
23
 
24
24
  import atexit
25
- import os
26
25
  import sys
27
26
  from typing import TYPE_CHECKING, Any
28
27
 
@@ -39,14 +38,15 @@ from .runtime import (
39
38
  done,
40
39
  env,
41
40
  execute,
41
+ expand,
42
42
  file,
43
- files,
44
43
  float_,
45
44
  folder,
46
45
  frames,
47
46
  int_,
48
47
  job,
49
48
  log,
49
+ list_,
50
50
  node,
51
51
  output,
52
52
  pairs,
@@ -54,20 +54,24 @@ from .runtime import (
54
54
  paths,
55
55
  prepare,
56
56
  progress,
57
+ read,
57
58
  resolution,
59
+ state,
58
60
  step,
59
61
  submit,
60
62
  task,
63
+ tasks,
61
64
  text,
62
65
  tool,
63
66
  )
64
67
 
65
- # `int`, `float`, `bool` and `file` are builtins; inside this module the declarations are spelled
68
+ # `int`, `float`, `bool` and `list` are builtins; inside this module they are spelled
66
69
  # with a trailing underscore so they do not shadow them, and exported under the names the docs use.
67
70
  # A caller writes `rf.int("samples")` — `rf` is a namespace, so nothing is shadowed there.
68
71
  globals()["int"] = int_
69
72
  globals()["float"] = float_
70
73
  globals()["bool"] = bool_
74
+ globals()["list"] = list_
71
75
 
72
76
  if TYPE_CHECKING: # for editors only — importing this at runtime would pull in httpx
73
77
  from .farm import Farm
@@ -91,41 +95,20 @@ if _INSPECT_FLAG in sys.argv:
91
95
  sys.stdout = _real_stdout
92
96
  runtime._inspect()
93
97
 
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
98
+ # Nothing happens otherwise. Importing this package does not make a script runnable as a program:
99
+ # a plugin runs as a task, where a bootstrap binds its values, its node and the farm's path mapping
100
+ # first, and none of that exists in a terminal.
118
101
 
119
102
  __all__ = [
120
103
  # connecting
121
104
  "connect", "Farm",
122
105
  # declarations
123
106
  "text", "int", "float", "bool", "choice", "file", "folder", "pairs", "resolution", "output",
124
- "frames", "files", "args",
107
+ "frames", "args", "expand", "list",
125
108
  # lifecycle
126
- "execute", "submit", "prepare", "cleanup",
109
+ "execute", "submit", "tasks", "read", "prepare", "cleanup",
127
110
  # context
128
- "job", "step", "task", "node", "env", "paths", "path", "app", "tool",
111
+ "job", "step", "task", "node", "env", "paths", "path", "app", "tool", "state",
129
112
  # reporting
130
113
  "progress", "log", "fatal", "done",
131
114
  # exceptions