docs
Python SDK

Getting Started

Write, deploy, and stream from your first uRun GPU function

This guide takes you from an empty file to a deployed GPU function you can invoke as a real-time session. You will write a Python backend, deploy it with the urun CLI, and see it appear in your console.

Install

Everything you need on the Python side lives in one package: urun-cli. It installs the urun command (urun deploy, urun list, …) and the importable urun compute library (from urun import App) you write your function against. Install it into the project environment where you author your app so both the command and the import are available together:

uv is a fast Python package manager. Install it with curl -LsSf https://astral.sh/uv/install.sh | sh, then add it to your project:

uv add urun-cli

To install the urun command globally on your PATH (handy if you only deploy and author the app elsewhere):

uv tool install urun-cli
pip install urun-cli

To run the command without installing it (CI or a throwaway shell):

uvx urun-cli --version

Verify it:

urun --version

One Python package, two faces

There is no separate urun package on PyPI — pip install urun-cli gives you both the urun command (deploy/list/scale) and the import urun compute library. The only other SDK is @urun-sh/core, the browser client covered in the TypeScript SDK.

Authenticate

uRun authenticates the CLI with an org-scoped deploy API key of the form urun_sk_ followed by 43 base64url characters. An operator vends this key for your org from the console.

urun login --api-key urun_sk_0a1B2c3D4e5F6g7H8i9J0k1L2m3N4o5P6q7R8s9T0uV

This verifies the key against your org config and stores it locally. For CI, set it in the environment instead:

export URUN_API_KEY=urun_sk_0a1B2c3D4e5F6g7H8i9J0k1L2m3N4o5P6q7R8s9T0uV

Deploy keys are not browser credentials

The urun_… key authorizes deploys from your machine or CI. Never ship it to a browser. Browser clients authenticate with WorkOS or a customer JWT — see TypeScript SDK auth.

Write a streaming function

Create app.py. This mirrors a real shipped app: one long-lived runtime owner on a single GPU that holds a model warm and produces a steered video stream.

import urun
from urun import App, Context
from urun.core import Dependencies, Credentials

app = App("hello-stream")

DEPS = Dependencies(python=["torch>=2.5.0", "diffusers", "transformers"])
CREDS = Credentials(optional_env=["HF_TOKEN"])

# The decorator kwargs are the function's config — GPU shape, deps, secrets.
# They are fixed at deploy time; session creation cannot override them.
@app.function(gpus="b200:1", deps=DEPS, credentials=CREDS)
async def generate(ctx: Context):
    # @store.cache keeps the model resident in GPU memory across sessions:
    # the first session loads it, every later session reuses it warm.
    pipe = load_pipeline(str(ctx.device))

    # A named video stream the browser subscribes to by name ("video").
    video = ctx.stream("video", codec="h264", fps=16)

    # The control doc syncs from the browser. Cheap config (the prompt) is a
    # live reactive read inline in the generator — no mapping, no polling loop.
    control = ctx.doc("control")

    def frames(*, stop_event, config):
        while not stop_event.is_set():
            prompt = control.desired.prompt.value or "a calm ocean at sunset"
            for frame in pipe(prompt=prompt, num_frames=17).frames[0]:
                yield frame

    pipeline = (
        ctx.source(frames, device="cuda")
        | ctx.pace(fps=16)
        | video
    )
    await pipeline.run()


@urun.store.cache(key="pipeline")   # gpu_pin=True is the default — the model stays in GPU memory
def load_pipeline(device: str):
    import torch
    from diffusers import DiffusionPipeline

    model_path = urun.store.path("hf://Wan-AI/Wan2.1-T2V-1.3B")
    pipe = DiffusionPipeline.from_pretrained(model_path, torch_dtype=torch.bfloat16)
    return pipe.to(device)

What the moving parts do:

  • @app.function(gpus="b200:1", …) — registers a deployed function. gpus="b200:1" requests one B200; the format is "{type}:{count}". This is config, fixed at deploy.
  • ctx: Context — uRun injects the session context: ctx.device, ctx.rank/ctx.world_size (for multi-GPU), ctx.session_id, ctx.stream(...), and ctx.doc(...).
  • @store.cache(gpu_pin=True) — loads the model once and keeps it in GPU memory across sessions on the same worker. See Store.
  • ctx.stream("video", codec="h264") — a named media sink. Passing codec/bitrate makes it a pipeline video sink that encodes frames automatically.
  • ctx.source | ctx.pace | video — the streaming pipeline: a generator head, frame-rate pacing, and the encoded video sink.
  • control.desired.prompt.value — a live reactive read of the browser-synced control doc, inline in the generator (cheap config); structural change uses doc.bind. See reactive control.
  • await pipeline.run() — drives the pipeline continuously until the session disconnects.

Deploy

urun deploy app.py

You will see uRun package the app directory, register a content-addressed manifest, upload any missing blobs, and poll the build to readiness:

Deploying hello-stream (fa61f31b0961)
Files: 1 app, deps: pip
Uploading 3 missing blob(s)
Deployment queued: 9c2f…
Waiting for build to complete (--no-wait to skip)

Once the build is ready and capacity is warm, urun app list shows the app as ready. (See the CLI docs for the full status table.)

Invoke it from the browser

The deployed function name (generate) is exactly what the browser calls. With the TypeScript SDK:

import { App } from '@urun-sh/core'

const app = App('hello-stream', { baseUrl: 'https://urun.sh', orgId: 'your-org-id' })
const session = app.generate()                 // returns a Session

const video = session.stream('video')
video.on('track', (track) => {
  videoEl.srcObject = new MediaStream([track])
})

// steer it live by writing the control doc:
session.doc('control').set({ prompt: { text: 'a forest at dawn' } })

The names line up: ctx.stream("video")session.stream("video"), ctx.doc("control")session.doc("control").

What just happened

  • urun deploy packaged your app and registered the function's config (GPU shape, deps, credentials) with the control plane.
  • The browser's app.generate() call allocated a session, warmed the GPU, and dialed the media transport.
  • @store.cache kept the model resident so the second session skips the load.
  • The control doc gave the browser a live steering surface with no REST endpoints — just named streams and a synced doc.

Next steps

  • App & functions — the decorator, local entrypoints, runtime overrides.
  • Compute — GPU/CPU specs and multi-GPU replica groups.
  • Streaming pipelines — the full ctx pipeline API.
  • Store — model downloads, versioned artifacts, and @store.cache.

On this page