Overview
splime is a private Python control plane for reusing trusted code. The framework serializes trusted Python functions and Pipeline graphs into SPL/YAML. The local daemon stores those object versions, creates per-spec virtual environments, and runs workers. The central server coordinates libraries, machines, access, remote runs, events, and artifacts. The server does not execute user code.
`spl.core`
Python object model, Pipeline builder, adapters & artifacts, SPL/YAML import/export, and `SPLClient`. Ships in the `splime` package.
`spl-daemon`
Local registry, object versions, worker subprocesses, environment cache, server sync, and run history. The daemon command, also in `splime`.
`spl-server`
Central registry, libraries, machines, tokens, grants, remote run queue, artifacts, and console API.
Installation
splime ships as a single package - the framework and the local daemon together. It requires Python 3.13 or newer. The distribution is named `splime`; the import package is `spl`, and the daemon command is `spl-daemon`.
Install from PyPI
Use the current SPLime 0.4.9 release on PyPI.
python3.13 -m pip install "splime==0.4.9"
# isolated CLI for a worker:
pipx install "splime==0.4.9" # or: uv tool install "splime==0.4.9"
spl-daemon --version
Upgrade from PyPI
python3.13 -m pip install --upgrade "splime==0.4.9"
spl-daemon --version
Editable contributor checkout
Use an editable install only when developing SPLime itself; it is not an alternative release-install path.
git clone https://github.com/yastrebovks/splime
cd splime
python -m pip install -e ".[test]"
Package names
| Project | Package | Console command | Role |
|---|---|---|---|
| `splime` | `spl` | `spl-daemon` | Python framework, SDK, and local daemon runtime (one package). |
| `spl-server` | `spl_server` | `spl-server` | Central control plane server. |
Quickstart
This is the smallest useful local workflow: start the daemon, register the current Python interpreter as an environment, publish a function, inspect its signature, and call it.
Start the local daemon
python -m spl.daemon serve --host 127.0.0.1 --port 8765 --home .\.spl-daemon
Use the framework from Python
from spl import SPLClient
def add(a: int, b: int, scale: int = 1) -> dict:
value = (a + b) * scale
return {
"a": a,
"b": b,
"scale": scale,
"value": value,
"formula": f"({a} + {b}) * {scale} = {value}",
}
client = SPLClient()
client.health()
client.register_env("default")
published = client.publish(add, name="demo_add", env="default")
print(published.name, published.entrypoint)
print(client.describe("demo_add"))
result = client.call("demo_add", kwargs={"a": 2, "b": 3, "scale": 10})
print(result.mode) # local
print(result.output) # 50 — the unwrapped value
Expected result shape
| Object kind | Selector | How to read |
|---|---|---|
| Function | No `output` needed | `result.output` (same as `result.value` — functions return a plain value) |
| Pipeline with alias | `output="alias"` | `result.output` unwraps the port dict; `result.value` keeps it raw |
| Pipeline without selector | No `output` | Result is a map keyed by output aliases or node ports. |
Core concepts
Object
An SPL object is a versioned callable unit. It is either a Function or a Pipeline. The object name is the stable lookup key in a local daemon or server library. `display_name` is the human-friendly label shown in descriptions and the console.
Function
A Function wraps one Python callable. The framework extracts its signature, annotations, default values, source body, and distribution dependencies when exporting to SPL/YAML.
Pipeline
A Pipeline is a graph of nodes. Nodes can be local Python functions (`NodeFunction`), remote SPL objects (`NodeRemote`), or scalar values linked into node inputs. Pipeline aliases are named outputs that users can select with `output="alias"`.
Environment
A daemon environment is a named base Python executable. For each unique dependency spec, the daemon creates a separate cached venv under `environment-builds/<spec-hash>/venv`. The hash is based on the base Python executable and exact `package==version` dependencies captured in the object metadata.
Library
A server library is an owner-scoped namespace and access boundary. Objects are unique inside the owner/library namespace. The default library is `default`; custom libraries can define execution policy and a default machine.
Owners, libraries, and handles
A library is always the pair (owner, slug), so your default and a
teammate's shared default are two different libraries. Every user has a unique
@handle — a human reference you can pass anywhere an owner id is accepted;
canonical ids stay the stored identity, handles resolve on the server.
When you pass library= without an owner, splime looks in your own namespace
first. If the object is not there and exactly one accessible library of the same slug has it,
the call resolves through that library and the receipt says so. Several candidates fail
loudly with a copy-pasteable list. Writes never auto-resolve. Since 0.4.4.
client.whoami() # who am I, on which server
client.users() # id / @handle directory (no emails)
client.libraries() # rows carry owner + owned
client.library.get("default", owner="@alice") # someone else's library, explicitly
client.call("report", library="default") # own first; unique shared match auto-resolves
Run & manifest
Since 0.4.0 every run can be retained as data. `keep="on_failure"` is the default for local runs: a failed run keeps a versioned JSON manifest (per-node statuses, fingerprints, applied adapters and runtimes with their resolution source) plus completed artifacts, with a 7-day retention TTL. Retained runs are listed, inspected, and pruned with `run-list` / `run-show` / `run-prune`, and resumed from a recalculation set: the selected nodes and their descendants recompute, everything else is frozen and digest-validated. Each resume creates a new run that records its `parent_run_id` lineage.
In the 0.4 series, connected daemons can also mirror visible server objects into the local cache with `pull()` or a dry-run-first `pull_all()`, so signatures and normal calls keep working after the server connection goes away.
Node runtimes
Runtime is a per-node property. Pipelines can tag individual nodes with `native` (in the conductor process, the default), `venv-subprocess` (an isolated process over the stdlib-only SPL-free runner), or `docker` (0.4 series: the node runs in a container built from the object environment spec, or from an explicit `runtime_config.docker.image`). Resolution mirrors the adapter hierarchy — runtime config, node tag, then run-level override — and the selected runtime with its source is recorded in the run manifest. Subprocess and Docker nodes accept JSON-native inputs and honor `node_timeout_seconds`.
Offline cache & identity
The local daemon is a working offline cache, not just a proxy. In the 0.4 series the daemon remembers the enrolled owner identity across restarts, offline periods, and server restarts: publishing and bare-name lookups keep resolving against your own objects with no network. Local operations never wait on the central server — when it is unreachable, server-side listings simply come back empty and lookup misses explain the remediation (`client.connect_server(...)` to re-enroll, `client.pull(...)` to mirror an object for offline use). Identity diagnostics and stored-connection hygiene live in `spl-daemon doctor`, `connections-list`, and `connections-prune`.
Framework API
`SPLClient`
`SPLClient` is the main user API. It talks to the local daemon, even when the daemon later talks to the central server.
from spl import SPLClient
client = SPLClient()
client = SPLClient(daemon_port=8766)
client = SPLClient(base_url="http://127.0.0.1:8766")
| Method | Purpose |
|---|---|
| `health()` | Checks the local daemon endpoint. |
| `register_env(name="default", python=None)` | Registers a Python executable. `python=None` means the current interpreter. |
| `publish(obj, name=None, env="default")` | Serializes a live function or Pipeline and stores a new daemon object version. |
| `publish_yaml(yaml, name, entrypoint, env)` | Registers an already exported SPL/YAML bundle. |
| `objects(scope="auto" | "local" | "server" | "all")` | Lists the connected server catalog by default, local daemon objects, or both. |
| `signature()`, `inputs()`, `outputs()`, `describe()` | Reads callable metadata for objects and Pipeline child functions. |
| `submit()`, `call()` | Starts local execution or a server-coordinated run on an allowed worker daemon. `submit()` returns a run handle immediately; `call()` waits for the result. |
| `pull(name, owner=None, library=None, version=None)` | Mirrors one visible server object into the local cache for offline use. Repeat pulls are idempotent by content hash. |
| `pull_all(owner=None, library=None, dry_run=False)` | Mirrors the visible server catalog in a batch. `dry_run=True` plans without writing; per-object failures are collected in the receipt. |
Import and export helpers
from pathlib import Path
from spl.core import spl_export_to_file, spl_import_from_file
spl_export_to_file(Path("bundle.yaml"), [my_pipeline])
namespace = {}
spl_import_from_file(Path("bundle.yaml"), namespace)
restored = namespace["my_pipeline"]
Functions and pipelines
Use `lift()` to turn a function or node into a Pipeline builder. Use `bind()` to link inputs to constants or outputs from other builders. Use `alias()` to name the node result, and `render(name)` to produce a publishable Pipeline.
from spl.core.common import Deployment, lift
def happiness(a: int):
return "Happy" if a == 300 else "Saaad :c"
def traktorist(a: int, b: int, scale: int = 1, happiness_val=None) -> dict:
value = (a + b) * scale
return {
"a": a,
"b": b,
"scale": scale,
"value": value,
"formula": f"({a} + {b}) * {scale} = {value}",
"feeling": happiness_val,
}
pipeline = (
lift(traktorist)
.bind(happiness_val=lift(happiness))
.alias("result")
.render("demo_traktorist_pipeline")
)
deployment = Deployment(pipeline)
result_node = pipeline.get_node_by_alias("result")
run = deployment.run(a=300, b=0, scale=1)
print(run[result_node]["default"]["feeling"])
`Deployment(pipeline)` is supported for compatibility. Use `Deployment(client, pipeline)` when the graph contains `NodeRemote`, because remote node execution needs the local daemon and server connection.
Publish and call the Pipeline
client.register_env("spl_core")
client.publish(pipeline, name="demo_traktorist_pipeline", env="spl_core")
print(client.describe("demo_traktorist_pipeline"))
result = client.call(
"demo_traktorist_pipeline",
kwargs={"a": 300, "b": 0, "scale": 1},
output="result",
)
print(result.output)
Calling objects
Local call
result = client.call("demo_add", kwargs={"a": 2, "b": 3})
assert result.mode == "local"
print(result.output)
Server-coordinated remote call through the local daemon
result = client.call(
"risk_report",
owner="alice",
library="risk",
target_machine="alice-gpu-01",
kwargs={"customer_id": 42},
output="report",
timeout_seconds=60,
)
assert result.mode == "server"
print(result.output)
Call a Function inside a Pipeline
result = client.call(
"demo_traktorist_pipeline",
function="happiness",
kwargs={"a": 300},
)
print(result.output)
The same function can also be referenced with the compact name `demo_traktorist_pipeline::happiness` where the API accepts an object reference.
Inspect before calling
print(client.describe("demo_traktorist_pipeline"))
print(client.describe("demo_traktorist_pipeline", function="happiness"))
display(client.signature("demo_traktorist_pipeline", function="happiness"))
display(client.inputs("demo_traktorist_pipeline", function="happiness"))
display(client.outputs("demo_traktorist_pipeline", function="happiness"))
Server catalog versus local catalog
client.objects(compact=True) # connected server catalog, local if offline
client.objects(scope="local", compact=True) # local daemon registry
client.objects(scope="server", compact=True) # global/server-visible catalog
client.objects(scope="all", compact=True) # both surfaces
client.objects(scope="server", owner="alice", library="risk")
NodeRemote
`NodeRemote` represents a remote SPL object inside a Pipeline. When `inputs` and `outputs` are omitted, it asks the local daemon to resolve the signature through the active server connection. The default version is `latest`.
Create by full remote name
from spl.core.entities.node_remote import NodeRemote
node = NodeRemote(
name="demo_traktorist_pipeline::happiness",
)
pipeline = lift(node).bind(a=300).alias("feeling").render("remote_feeling")
Create by Pipeline and Function
node = NodeRemote(
pipeline="demo_traktorist_pipeline",
function="happiness",
)
pipeline = lift(node).bind(a=300).alias("feeling").render("remote_feeling")
Run a Pipeline with remote nodes
deployment = Deployment(client, pipeline)
print(deployment.run(a=300, output="feeling")) # plain value
Run a single node
# Canonical: wire the node into a (one-node) pipeline
from spl import Deployment, lift
value = Deployment(client, lift(node).alias("out").render("one_node")).run(
a=300, output="out",
)
# client.run_node(...) / client.run_node_result(...) warned through 0.1.x
# and were removed in 0.2.0 - Deployment(...).run(...) is the canonical path.
Adapters & artifacts
When one node's output flows into the next - or is returned as a file - splime can materialize it as an artifact instead of passing an in-memory value. An adapter decides how a Python value of a given `(type, format)` is written to and read back from that artifact. Artifacts are content-addressed: every `ArtifactRef` records a `sha256` and `size` that are verified on load.
Pick a wire format on an edge
Call `as_format(...)` on a producer to serialize that edge with a named format:
from spl.core.common import lift
producer = lift(make_frame)
pipeline = (
lift(consume)
.bind(value=producer.as_format("csv")) # this edge crosses as a CSV artifact
.alias("report")
.render("daily_report")
)
Register a custom adapter
Built-in formats cover common cases; for your own type, register `save`/`load` functions with `add_adapter(py_type, format, save=..., load=...)`. It returns a new pipeline (pipelines are immutable), keyed by `make_key(py_type, format)`.
def save_box(path: str, value: Box) -> None:
with open(path, "wb") as f:
f.write(value.to_bytes())
def load_box(path: str) -> Box:
with open(path, "rb") as f:
return Box.from_bytes(f.read())
pipeline = pipeline.add_adapter(Box, "bytes", save=save_box, load=load_box)
Libraries & access
Libraries group versioned objects and control who can see and run them. Creating and curating libraries needs a server-connected client - pass `user_token` and `machine_token`, or connect the daemon to splime first.
Create and publish into a library
client = SPLClient(user_token="...", machine_token="...")
client.library.create(
"risk",
display_name="Risk",
description="Production scoring functions",
visibility="private",
)
client.publish(risk_score, name="risk_score", library="risk")
Reference or copy across libraries
`add_reference` adds a live link that follows the source (use `version="latest"`); `copy_object` takes an owned snapshot and keeps provenance (`source_object_id`).
client.library.add_reference(
"risk", "shared_score",
owner="admin2", from_library="source",
version="latest", alias="partner_score",
)
client.library.copy_object(
"shared_score",
into_library="risk",
from_owner="admin2", from_library="source",
version=3, new_name="partner_score_v3",
)
client.library.remove_entry("risk", "partner_score")
Grant and revoke scoped access
client.library.grant(
"risk", "analyst1",
scopes=["metadata:read", "objects:read", "execute"],
)
client.library.revoke("risk", "analyst1")
client.libraries(include_accessible=True) # libraries you own or can use
Local daemon
The local daemon is the private runtime. It should run close to the Python environment and data it needs. It stores object versions in SQLite and launches each run in a worker subprocess using a cached virtual environment.
Start and health check
python -m spl.daemon serve --host 127.0.0.1 --port 8765 --home .\.spl-daemon
python -m spl.daemon health
Data layout
| Path | Meaning |
|---|---|
| `daemon.sqlite3` | Local registry, versions, runs, envs, server connections, and sync queue. |
| `objects/<name>/versions/<n>.yaml` | Human-readable YAML cache for diagnostics. |
| `environment-builds/<spec-hash>/venv` | Cached worker venv for one dependency specification. |
| `runs/<run-id>/` | Inputs, materialized object YAML, stdout, stderr, result, and artifacts. |
| `daemon-endpoint.json` | Current daemon URL used by `SPLClient()` auto-discovery. |
Environment commands
python -m spl.daemon env-add spl_core C:\Python313\python.exe
python -m spl.daemon env-list
python -m spl.daemon env-build-list
python -m spl.daemon env-build-show <spec_hash>
python -m spl.daemon env-build-rebuild <spec_hash> --wait
Connect daemon to the server
python -m spl.daemon server-connect ^
--server-url https://splime.io/api ^
--machine-token <machine-token> ^
--user-token <user-token> ^
--machine-id <machine-id> ^
--display-name "Kirill laptop"
You can also connect from Python by passing tokens to `SPLClient` or by calling `client.connect_server(...)`.
client = SPLClient(
server_url="https://splime.io/api",
machine_token="<machine-token>",
user_token="<user-token>",
machine_id="machine-123",
display_name="Kirill laptop",
)
Server
`spl-server` is the central coordinator. It stores users, tokens, teams, libraries, machines, object versions, machine snapshots, remote runs, events, artifacts, settings, and audit activity. It coordinates execution but does not run user code.
Run locally
cd spl-server
python -m pip install -e ".[test]"
python -m daemon_server serve --host 127.0.0.1 --port 9876 --home .\.spl-daemon-server
Run with systemd on Ubuntu
sudo cp /opt/spl-server/deploy/systemd/spl-server.service /etc/systemd/system/spl-server.service
sudo systemctl daemon-reload
sudo systemctl enable --now spl-server
systemctl status spl-server
journalctl -u spl-server -f
To inspect the effective `ExecStart`, use `systemctl cat spl-server.service` or `systemctl show spl-server.service -p ExecStart`. After editing a unit file, run `sudo systemctl daemon-reload` and then `sudo systemctl restart spl-server`.
Direct server client
from spl.server_client import SPLServerClient
server = SPLServerClient(
token="<user-or-service-token>",
base_url="https://splime.io/api",
)
print(server.signature("risk_report", owner="alice", library="risk"))
result = server.call(
"risk_report",
owner="alice",
library="risk",
target_machine="alice-gpu-01",
kwargs={"customer_id": 42},
output="report",
wait_timeout_seconds=60,
)
print(result.output)
External execution token client
from spl.server_client import SPLServerClient
external = SPLServerClient.external_token(
token="<library-execution-token>",
base_url="https://splime.io/api",
)
print(external.signature("risk_report", library="risk"))
result = external.call(
"risk_report",
library="risk",
kwargs={"customer_id": 42},
output="report",
wait_timeout_seconds=60,
)
External token clients intentionally expose only callable metadata, run launch/read, events, and artifact download. They cannot manage machines, tokens, grants, settings, cancel/retry, broad object lists, or raw YAML.
Security and access model
splime assumes trusted published code and explicit execution boundaries. Access is enforced through token scopes, ownership, library grants, machine grants, delegated machine subtokens, and external execution tokens.
| Credential | Typical use | Limits |
|---|---|---|
| User token | Console, SDK owner actions, library and token management. | Limited by scopes and owner context. |
| Machine token | Daemon heartbeat, sync, job claim, artifact upload. | No broad admin or token issuance scopes. |
| Machine subtoken | Delegated launch onto another user's machine. | Bound to one machine and allowed users. |
| Library execution token | External service integration for one library or callable. | No broad listing, raw YAML, cancel/retry, admin, grants, or token management. |
Raw token values are one-time response fields. Store them immediately. List/detail APIs return hints, hashes, and metadata instead of the secret value.
Recipes
See the server library instead of only local objects
server_objects = client.objects(compact=True)
all_objects = client.objects(scope="all", compact=True)
Get source/YAML for an object version
from spl.server_client import SPLServerClient
server = SPLServerClient("<user-token>")
obj = server.get_object(
"demo_traktorist_pipeline",
library="default",
include_yaml=True,
)
print(obj["yaml"])
Raw YAML is available only to authorized user or machine contexts. Library execution tokens cannot request `include_yaml=1`.
Queue a run for an offline machine
run = client.submit(
"risk_report",
target_machine="alice-gpu-01",
owner="alice",
library="risk",
kwargs={"customer_id": 42},
output="report",
offline_policy="queue",
)
print(run.id, run.status)
Download artifacts
result = client.call(
"risk_report",
kwargs={"customer_id": 42},
output="report",
artifacts_dir="artifacts/risk_report",
)
print(result.downloaded_artifacts)
Refresh an editable development checkout
# Contributor workflow: an editable install picks up source edits immediately
python -m pip install -e ".[test]"
# Restart the daemon so workers pick up the new code
systemctl --user restart spl-daemon # or relaunch: spl-daemon serve
Troubleshooting
`SPLClient.call()` says the local daemon is not reachable
`SPLClient` is trying to connect to the local daemon URL, usually `http://127.0.0.1:8765` or the endpoint saved in `daemon-endpoint.json`. Start the daemon in the same machine, WSL, Docker container, or remote kernel where the notebook runs, or pass `daemon_port` or `base_url` explicitly.
`spl-daemon: command not found` after install
The `spl-daemon` command ships inside the `splime` package. Make sure the install's scripts directory is on your `PATH`, or install the CLI in its own environment with `pipx install "splime==0.4.9"` (or `uv tool install "splime==0.4.9"`).
Python version error during install
The packages require Python 3.13+. Create the venv with Python 3.13 or newer. Python 3.12 environments will fail the package metadata check.
Worker environment does not match SPL metadata
The object metadata pins exact package versions. The daemon creates a venv per unique dependency spec. If a run uses a different version, inspect `env-build-list`, review the exact package versions captured during publish, and rebuild the matching spec hash.
Server object requires local environments that are not registered
A mirrored server object references an environment name, for example `spl_core`. Register a base Python executable under the same name with `client.register_env("spl_core")` or `python -m spl.daemon env-add spl_core C:\Python313\python.exe`.
`NodeRemote` cannot resolve inputs and outputs
The constructor omitted explicit ports, so it asked the local daemon to resolve the remote signature. Connect the daemon to splime, confirm `client.current_server_connection()`, or pass explicit `url`, `inputs`, and `outputs`.
Reference
Important local daemon CLI commands
python -m spl.daemon serve
python -m spl.daemon health
python -m spl.daemon env-add <name> <python.exe>
python -m spl.daemon env-list
python -m spl.daemon env-build-list
python -m spl.daemon object-list --compact
python -m spl.daemon object-show <name-or-id>
python -m spl.daemon object-signature <name-or-id>
python -m spl.daemon object-versions <name-or-id>
python -m spl.daemon run <object> --kwargs "{\"seed\": 42}" --wait
python -m spl.daemon server-connect --machine-token <token> --user-token <token>
python -m spl.daemon pull <name> # mirror one server object locally
python -m spl.daemon pull --all --dry-run # plan a batch mirror of the visible catalog
python -m spl.daemon doctor # setup checklist + enrolled identity line
python -m spl.daemon connections-list
python -m spl.daemon connections-prune --dry-run
Important server API surfaces
| Endpoint | Purpose |
|---|---|
| `GET /health` | Server health check. |
| `GET /console/overview` | Single console startup snapshot. |
| `POST /daemon-enrollment` | Create machine credentials for daemon pairing. |
| `POST /connections/connect` and `POST /sync` | Daemon lease, heartbeat, object sync, job polling, and run updates. |
| `GET /libraries`, `GET /objects`, `GET /owners/<owner>/libraries/<library>/objects` | Library and object catalog reads. |
| `GET /objects/<name>/signature?function=<function>` | Callable metadata for objects and Pipeline child functions. |
| `POST /remote-runs` | Create a remote run. |
| `GET /remote-runs/<id>/detail` | Run state, timeline, result, and artifacts. |
Testing the workspace
cd spl-core
python -m pytest
cd ../spl-daemon
python -m pytest
cd ../spl-server
python -m pytest
cd ../spl-frontend
npm run check