Skip to main content

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.9

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for pulze-renderflow 2.0.9
File Size Uploaded
pulze_renderflow-2.0.9.tar.gz 94.2 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for pulze-renderflow 2.0.9
File Interpreter ABI Platform
pulze_renderflow-2.0.9-py3-none-any.whl Python 3 none any Details

Total release size: 178.4 kB

Release files / pulze_renderflow-2.0.9.tar.gz

Download URL pulze_renderflow-2.0.9.tar.gz
Size 94.2 kB
Tags Source
SHA-256 checksum
How to use checksums
7df42c042b5259ffc4151b3dfe52c0fa95efe356758f45388fe94438306d5823
BLAKE2b-256 checksum
How to use checksums
15490d8676443f6a41a1b997f07f63fe39fb41d74ad3aad745ce319debc05eec
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.10.8

Release files / pulze_renderflow-2.0.9-py3-none-any.whl

Download URL pulze_renderflow-2.0.9-py3-none-any.whl
Size 84.2 kB
Tags Python 3
SHA-256 checksum
How to use checksums
9b53e5db0ec75c6912fabb5545341a56283a5a7ad88ae35e45c12b53f522df0d
BLAKE2b-256 checksum
How to use checksums
8a8051ce910a82c4316298a7d895b3183a7575d90de712e462b54918236206dd
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.10.8

Release history Release notifications | RSS feed

2.0.16

2 release files

This release

2.0.9 This release

2 release files

2.0.8

2 release files

2.0.7

2 release files

1.3.0

2 release files

1.2.7

2 release files

1.2.5

2 release files

1.2.4

2 release files

1.2.2

2 release files

1.2.1

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page