pulze-renderflow
Official Python client for the RenderFlow API.
Installation
pip install pulze-renderflow
The package has no dependencies — the whole client is standard library. That is a feature,
not an accident: drop the folder on sys.path and it works inside a DCC's bundled Python (Maya,
Houdini, Nuke, VRED), where installing anything is a fight.
Quick start
import renderflow as rf
farm = rf.connect() # the local service on 127.0.0.1:44442
# farm = rf.connect("https://rf.studio.lan:44445", api_key="rf_...")
for job in farm.jobs.get_jobs(status="working").data:
print(job.name, job.status)
connect() falls back to the environment for each argument it isn't given: RENDERFLOW_URL
for the address, RENDERFLOW_API_KEY for the key (the same variable rfcli reads). With
neither, it talks to the service on this machine — which needs no credential, and is how a DCC
plugin on a workstation submits.
Finding the operation you want
Operations are grouped by tag and named after the API document's operationId, so a route
reads the same here, in the TypeScript SDK, in rfcli and in the MCP tools:
farm.tags() # every group: jobs, nodes, pools, schedule, licenses, ...
dir(farm.jobs) # every operation in one: get_jobs, create_job, stop_job, ...
help(farm.jobs.get_jobs) # the operation's own documentation, generated from the document
| you want | you write |
|---|---|
GET /jobs |
farm.jobs.get_jobs() |
GET /jobs/{id} |
farm.jobs.get_job(id) |
POST /jobs |
farm.jobs.create_job(body=...) |
POST /jobs/{id}/stop |
farm.jobs.stop_job(id) |
GET /nodes |
farm.nodes.get_nodes() |
Submitting a job
A recipe — type plus a file and the flat per-DCC fields — is the short way in; the service
expands it into the step tree:
Every recipe requires type, file and host — host is which application renders it, and only
its id is mandatory:
job = farm.jobs.create_job(body={
"type": "blender.render",
"file": "//share/scenes/shot_010.blend",
"host": {"id": "blender", "version": "4.5"},
"frame": "1-100",
"pool_id": "...",
})
print(job.id, job.status)
farm.jobs.stop_job(job.id)
Bodies are plain dicts; answers read with dot access (job.name, page.data) and hand back the
raw payload via .to_dict(). An answer the service does not send as JSON comes back as bytes —
farm.tasks.get_task_preview(id) is a rendered frame, ready to write to a file.
Waiting for a job
job = farm.jobs.settled(job.id) # answers on completed or failed
job = farm.jobs.settled(job.id, interval=30) # a render measured in hours
None means the job is no longer there. Archiving and deleting look the same from out here, and
neither is going to finish.
A stop is not an end, because suspending a job is resumable — pass
statuses=["completed", "failed", "suspended"] if a stop is an answer for your caller. timeout is
in seconds and there is none by default, since a long render legitimately takes days; it raises
TimeoutError rather than handing back a job that has not finished.
This polls rather than watching, so a wait needs no socket, no thread and no reconnect behaviour,
and it works where a proxy passes HTTP and refuses an upgrade. interval is the cost: two seconds is
right for a test render and wrong for an overnight one. When the moment a job finishes is what
matters, watch it instead.
Live updates
One socket, shared by every subscription, opened on the first and closed with the last. No polling.
def draw(event):
if event.op == "resync":
return refetch() # events were missed, read again
print(event.op, event.id, event.patch)
stop = farm.jobs.on(draw, {"id": job.id})
stop()
on is on every group whose collection the farm pushes: jobs, tasks, nodes, pools, users,
user_groups, errors, schedule and licenses. The second argument narrows it — id on
any of them, step_id on tasks, node_id on the node joins — and a list is one subscription over
several ids rather than several subscriptions.
An insert carries the whole row on event.doc. An update carries event.patch, keyed by dotted
path, so a job's progress arrives as event.patch["progress.percent"]. A delete carries the id.
A resync event is not a change. It means events were missed and the collection has to be read
again, because the socket has no replay: a reconnect, a dropped frame, or a client the farm outran
all mean the same thing. It arrives through the same handler precisely so there is nothing to forget
to pass.
farm.live.close() drops everything, and farm.live.on_error reports a refused credential; it
writes to stderr until you replace it. The socket runs on a daemon thread, so a script that forgot to
unsubscribe still exits.
There is still no dependency: renderflow/live.py is the client half of RFC 6455 hand-rolled on
socket, ssl and struct, because installing websocket-client into Maya is the fight this
package exists to avoid.
Models
Typed names for every request and response schema live in renderflow.models, and your editor
reads them from models.pyi — autocomplete and type checking with nothing to install:
from renderflow.models import MayaRenderRecipe
recipe: MayaRenderRecipe = {"type": "maya.render", "file": "//share/scenes/hero.mb", "host": {"id": "maya", "version": "2026"}}
farm.jobs.create_job(body=recipe)
At runtime a model is a dict — the wire is JSON and the service validates.
Errors
A refused call raises renderflow.wire.ApiError, carrying the status and the service's own
message — str(error) is the sentence an operator should read.
How this package is built
There is no generated client. renderflow/wire.py binds operations off the OpenAPI document at
runtime, and the document ships inside the package as renderflow/openapi.json — a committed
copy of the service's openapi.public.json. An operation added to the API arrives here the
moment that copy is refreshed:
python scripts/generate.py # copies ../../service/openapi.public.json + rewrites models.pyi
models.pyi is written by datamodel-code-generator,
a dev-only tool — never a dependency of the package.
Metadata
Release files for pulze-renderflow 2.0.16
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| pulze_renderflow-2.0.16.tar.gz | 98.7 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| pulze_renderflow-2.0.16-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 186.6 kB
Release files / pulze_renderflow-2.0.16.tar.gz
| Download URL | pulze_renderflow-2.0.16.tar.gz |
|---|---|
| Size | 98.7 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
d71a1cf91ee6e0375109ad41a2929d058cf2541bfcfabc8c3febc5c761b48601
|
|
BLAKE2b-256 checksum How to use checksums |
f53de349a4232fa578a55643be49c2b216be74f8ae5abb0fe863005f0613dc84
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.11.4
|
Release files / pulze_renderflow-2.0.16-py3-none-any.whl
| Download URL | pulze_renderflow-2.0.16-py3-none-any.whl |
|---|---|
| Size | 87.8 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
44305900479b002b4fafabf32a64a0460ff4edd830883e31f8d3d061a3dc7ba9
|
|
BLAKE2b-256 checksum How to use checksums |
878532c6553055a8e11aa0e54dec08a75fdda05a1af8429d6281077765314dee
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.11.4
|