This logo was generated with AI and heavily inspired by the Elysia logo.
Runpy connects a Rust control plane to Python worker processes over Unix domain
sockets. The Rust runpy crate owns process lifecycle, health monitoring,
message routing, and attributed output. The Python runpyrs SDK turns a PEP
723 script into a managed worker. uv supplies
the script's declared Python version and dependencies.
Both packages are currently installed from this Git repository; they are not published to crates.io or PyPI.
Use Rust for orchestration and Python for task-specific code such as scraping, data processing, or model inference. Runpy provides:
- Managed lifecycle — start, monitor, message, and terminate complete worker process groups.
- PEP 723 execution — each script declares its own Python and dependencies; adjacent uv lockfiles are supported.
- Typed envelope APIs — exchange application-owned JSON
metaanddatawhile Runpy protects itsx_routing namespace. - Bidirectional messaging — global and per-worker handlers, targeted sends, replies, and broadcasts.
- Watchdog reporting — inspect registered process state and available resource data.
- Structured logging — route Python
Worker.logenvelopes through the application. - Attributed process output — observe bounded, nonblocking worker stdout and stderr records with trusted worker IDs.
cargo add runpy --git https://github.com/dark1zinn/runpy
cargo add tokio --features full
cargo add serde_json
mkdir -p worker
uv init --script worker/my_worker.py --python 3.10
uv add --script worker/my_worker.py \
"runpyrs @ git+https://github.com/dark1zinn/runpy#subdirectory=worker"Create worker/my_worker.py:
# /// script
# requires-python = ">=3.10"
# dependencies = [
# "runpyrs @ git+https://github.com/dark1zinn/runpy#subdirectory=worker",
# ]
# ///
from runpyrs import RunScript, Worker
class MyWorker(Worker):
def execute(self, data: dict) -> dict:
return {"status": "ok", "input": data}
if __name__ == "__main__":
RunScript(MyWorker)Start it from Rust:
use runpy::{Data, Envelope, Manager};
use serde_json::{json, Value};
use std::time::Duration;
fn object(value: Value) -> Data {
value.as_object().cloned().expect("JSON object")
}
#[tokio::main]
async fn main() {
let mut manager = Manager::new("worker");
let (finished_tx, mut finished_rx) = tokio::sync::mpsc::unbounded_channel();
manager.on_message(move |inbound| {
let operation = inbound
.envelope
.meta()
.get("x_op")
.and_then(Value::as_str);
match operation {
Some("ready") => inbound.reply(Envelope::execute(object(
json!({"task": "example"}),
))),
Some("done") => {
println!(
"result: {}",
Value::Object(inbound.envelope.data().clone())
);
let _ = finished_tx.send(());
}
Some("error") => {
eprintln!(
"worker error: {}",
Value::Object(inbound.envelope.data().clone())
);
let _ = finished_tx.send(());
}
_ => {}
}
});
let mut worker = manager.worker("my_worker");
let worker_id = worker.spawn().await.expect("worker should start");
println!("spawned {worker_id}");
tokio::time::timeout(Duration::from_secs(30), finished_rx.recv())
.await
.expect("worker response timed out")
.expect("worker response channel closed");
worker.terminate().await.expect("worker should stop");
}Runpy starts:
uv run --no-project [--locked] --script <worker.py> <socket-path> <worker-id>
The worker sends ready, Rust replies with execute, and the Python return
value arrives as done. Runpy owns trusted worker/socket metadata and cleans up
the process group and socket when the worker terminates or the Manager drops.
Process output is separate from structured socket envelopes:
use runpy::{Manager, WorkerOutputStream};
let mut manager = Manager::new("worker");
manager.on_worker_output(|output| match output.stream {
WorkerOutputStream::Stdout => {
println!("{}: {}", output.worker_id, output.line);
}
WorkerOutputStream::Stderr => {
eprintln!("{}: {}", output.worker_id, output.line);
}
});The callback runs on Runpy's dedicated output-dispatcher thread and should return promptly. Capture is bounded and best effort. Runpy never treats stderr or output text as an automatic termination signal; applications own that policy.
- Project overview — architecture, lifecycle, protocol, output guarantees, Python execution model, and operations.
- Installation and worker setup — Git dependencies, PEP 723 scripts, uv lockfiles, and Manager configuration.
- Python SDK guide —
runpyrssetup and API. - Playground — executable Rust/Python integration.
- Rust API reference — generate locally with
cargo doc -p runpy --no-deps --open.
The Manager-owned Scribbler reads logging settings at construction:
| Variable | Values | Description |
|---|---|---|
ENVIRONMENT |
development, dev |
Enables maximum verbosity. |
LOG |
0–5, off, error, warning, info, debug, verbose |
Selects the maximum visible level. |
NO_COLOR |
any value | Disables ANSI color output. |
Managed workers always receive PYTHONUNBUFFERED=1 so captured Python output
is prompt.
Enable the pre-commit hook once per clone:
git config --local core.hooksPath .githooksRun Rust checks:
cargo fmt --all -- --check
cargo clippy --workspace --all-targets --all-features --locked -- -D warnings
cargo test --workspace --all-features --locked -- --test-threads=1
cargo test --doc -p runpy --lockedRun Python checks:
uv run --frozen --python 3.10 ruff format --check worker/src/runpyrs worker/tests examples/playground/worker/my_script.py
uv run --frozen --python 3.10 ruff check worker/src/runpyrs worker/tests examples/playground/worker/my_script.py
uv run --frozen --python 3.10 --package runpyrs --extra dev pytest worker/testsRun the integration playground:
cargo run -p playgroundOpen an issue with reproduction steps, operating system, architecture, and Rust/Python/uv versions. Pull requests that improve stability, reliability, documentation, and useful behavior coverage are welcome.
Apache-2.0 — see LICENSE.
With ❤️ @dark1zinn
