Skip to main content

Run Python functions inside containers via a decorator.

Project description

yonder

yonder

Run Python functions on container-backed runners via a decorator. Functions and their arguments are serialized with cloudpickle, shipped to the runner, executed there, and the result is shipped back.

Install

# core package
pip install -e .

# with the bundled Docker runner
pip install -e ".[docker]"

# with the bundled Kubernetes runner
pip install -e ".[k8s]"

Quick start

import yonder
from yonder import DockerRunner

# Existing image. If the image doesn't already have cloudpickle, set
# inject_cloudpickle=True to derive an image that does (cached across
# runs, keyed by the base image). `when=` is an optional zero-arg
# predicate that makes the runner a no-op (calls run locally) when it
# returns False — useful for "use the container only if the library
# isn't already installed on the host".
runnerA = DockerRunner(
    image="url/to/image",
    env={"ENV_VARA": "value"},
    mounts={"/host/path": "/in/container"},
    inject_cloudpickle=True,
    when=lambda: liba_not_present(),
)

# Build from a Dockerfile (context defaults to the Dockerfile's directory).
runnerB = DockerRunner(
    dockerfile="./docker/myimg.Dockerfile",
    build_args={"VERSION": "1.2.3"},
)

# On-demand: fresh container per call instead of one long-lived container.
ephemeral = DockerRunner(image="url/to/image", on_demand=True)

# Attach to a container you manage outside yonder. yonder will start it
# if it's stopped, but never removes it on close.
external = DockerRunner(container="my-dev-container")

# Register them by name.
yonder.register({
    "runnerA": runnerA,
    "runnerB": runnerB,
    "ephemeral": ephemeral,
    "external": external,
})

# Decorate. Dispatch policy lives on the runner (via the runner's
# `when=`); the decorator just names the target.
@yonder.yonder(runner="runnerA")
def functionA(a, b):
    return a + b

@yonder.yonder(runner="runnerB")
def functionB(a, b):
    return a * b

Lazy start by default

A registered runner is not built/started until it’s actually needed. The first decorated call that targets a runner triggers its start() (image build/pull, persistent container creation). Runners that are never called incur no cost.

You can pre-warm explicitly when you’d rather pay startup cost up front:

runnerA.start()       # pre-warm a specific runner
yonder.wait()         # pre-warm every registered runner (parallel)

Both are opt-in. On-demand runners always behave the same way — they spin up a fresh container per call regardless of pre-warming.

Tear down

Tear down when done:

yonder.closeAll()
# or, per-runner:
runnerA.close()

DockerRunner is also a context manager:

with DockerRunner(image="python:3.11-slim") as r:
    yonder.register({"tmp": r})
    yonder.wait()
    ...
# container removed on exit

DockerRunner options

DockerRunner(
    # Exactly one of these three is required:
    image=None,          # use/pull an existing image; yonder creates the container
    dockerfile=None,     # build an image locally first; yonder creates the container
    container=None,      # attach to an existing container (managed outside yonder)

    build_context=None,  # override the build context directory (dockerfile mode)
    build_args=None,     # dict of Docker ARGs (dockerfile mode)

    env=None,            # environment variables (image/dockerfile mode only)
    mounts=None,         # {host_path: container_path} (image/dockerfile mode only)
    on_demand=False,     # one-shot container per call (image/dockerfile mode only)
    name=None,           # container/tag name (auto-generated if omitted)
    inject_cloudpickle=False,  # derive `FROM image + RUN pip install cloudpickle`
                               # (image mode only; tagged by base-image hash so the
                               # docker layer cache makes re-runs cheap)
    client=None,         # pre-built docker.DockerClient (else docker.from_env())
)

K8sRunner

Attach to an existing Kubernetes pod and exec into one of its containers. Pods are treated like externally-managed containers — yonder never creates or removes them.

from yonder import K8sRunner

# By pod name.
by_name = K8sRunner(
    pod="my-pod-abc123",
    namespace="default",
    container="app",  # optional; defaults to the pod's first container
)

# By label selector — first Running pod that matches is used.
by_selector = K8sRunner(
    selector="app=my-app,env=dev",
    namespace="default",
)
K8sRunner(
    # Exactly one of these two is required:
    pod=None,            # attach by pod name
    selector=None,       # attach by label selector

    namespace="default", # pod namespace
    container=None,      # container within the pod (default: first)
    kubeconfig=None,     # kubeconfig path (else KUBECONFIG/~/.kube/config,
                         # falling back to in-cluster config)
    context=None,        # kubeconfig context
    name=None,           # runner name (auto-derived if omitted)
    api_client=None,     # pre-built kubernetes.client.ApiClient
)

The target container must have python on PATH and cloudpickle installed (same Python minor version as the host is strongly recommended).

Architecture

  • ``Runner`` (yonder.Runner) — abstract base. Implementations hold their own config and expose run(payload) -> bytes, plus optional start() / close() lifecycle hooks.

  • ``DockerRunner`` — default implementation; uses the Docker SDK for Python. Supports existing images, Dockerfile builds, and attaching to externally-managed containers; persistent and on-demand modes.

  • ``K8sRunner`` — Kubernetes implementation; uses the Kubernetes Python client to exec into a pod selected by name or label selector.

  • ``YonderSession`` — process-wide name→runner registry. Populate with yonder.register({...}), bring everything online with yonder.wait(), tear everything down with yonder.closeAll().

  • Decorator — serializes (func, args, kwargs) with cloudpickle, calls the named runner’s run(payload), deserializes the envelope, and either returns the value or re-raises the exception.

Writing your own Runner

from yonder import Runner, register

class MyRunner(Runner):
    def __init__(self, **config):
        ...

    def start(self):
        # eager init (build/pull image, warm container, etc.)
        ...

    def run(self, payload: bytes) -> bytes:
        # send payload to your execution environment,
        # return the cloudpickled envelope
        ...

    def close(self):
        ...

register({"mine": MyRunner(...)})

Container requirements

Images used with DockerRunner must have:

  • python on PATH

  • cloudpickle installed (pip install cloudpickle). If your base image doesn’t have it, pass inject_cloudpickle=True and yonder will build a derived image that does (one-time, cached).

  • Ideally the same Python minor version as the host (cloudpickle pickles function bytecode, which is not portable across CPython minor versions).

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

run_yonder-0.1.0.tar.gz (22.8 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

run_yonder-0.1.0-py3-none-any.whl (21.8 kB view details)

Uploaded Python 3

File details

Details for the file run_yonder-0.1.0.tar.gz.

File metadata

  • Download URL: run_yonder-0.1.0.tar.gz
  • Upload date:
  • Size: 22.8 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.14.4

File hashes

Hashes for run_yonder-0.1.0.tar.gz
Algorithm Hash digest
SHA256 4b90508b44240afe1215af5ad4298cdc728b74fc3feb6cee71b32d5045c2d28b
MD5 22f8cfb15cf30eb1503aa2158bc11f43
BLAKE2b-256 1eac7c0acdd2afd3f3eb7b0b342ff1a871abd3e45ad8fdfe96fa1636ca68ae49

See more details on using hashes here.

File details

Details for the file run_yonder-0.1.0-py3-none-any.whl.

File metadata

  • Download URL: run_yonder-0.1.0-py3-none-any.whl
  • Upload date:
  • Size: 21.8 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.14.4

File hashes

Hashes for run_yonder-0.1.0-py3-none-any.whl
Algorithm Hash digest
SHA256 1aa937e6fd6a85dabdcb029d85940952af8c358793d126abd4dc53a999721c26
MD5 f84b71a752fd045e1363a86ee04e9845
BLAKE2b-256 6478cf0d04c0b127949526a6a9a277d108e92b9afeaa217cf7682f28f3724dfa

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page