Overview
SPLime is a private Python control plane. 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-framework`
Python object model, Pipeline builder, SPL/YAML import/export, `SPLClient`, and direct server clients.
`spl-daemon`
Local registry, object versions, worker subprocesses, environment cache, server sync, and local run history.
`spl-server`
Central registry, libraries, machines, tokens, grants, remote run queue, artifacts, and console API.
Installation
All Python packages currently require Python 3.13 or newer. Install the framework into the notebook or script environment, and install the daemon into the environment that will run the local daemon service.
Editable development install
cd spl-core
python -m pip install -e ".[test]"
cd ../spl-daemon
python -m pip install -e ".[test]"
Build and install wheels
cd spl-core
.\tools\build-package.ps1 -OutDir ..\dist-packages
cd ..\spl-daemon
.\tools\build-package.ps1 -OutDir ..\dist-packages
cd ..
python -m pip install --find-links .\dist-packages --force-reinstall spl-framework spl-daemon
Use `--find-links` when installing `spl-daemon` from local wheels. The daemon depends on `spl-framework`, and pip must be able to find the locally built framework wheel.
Package names
| Project | Package | Console command | Role |
|---|---|---|---|
| `spl-core` | `spl-framework` | None | Python framework and SDK clients. |
| `spl-daemon` | `spl-daemon` | `spl-daemon` | Local daemon runtime and worker execution. |
| `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.client 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.value) # function value
Expected result shape
| Object kind | Selector | How to read |
|---|---|---|
| Function | No `output` needed | `result.value` |
| Pipeline with alias | `output="alias"` | `result.value["default"]` for a single-output node |
| 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.
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.client 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="local" | "server" | "all")` | Lists local daemon objects, server-visible objects, or both. |
| `signature()`, `inputs()`, `outputs()`, `describe()` | Reads callable metadata for objects and Pipeline child functions. |
| `start()`, `queue()`, `call()` | Starts local or server-side execution. |
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.value["default"])
Calling Objects
Local call
result = client.call("demo_add", kwargs={"a": 2, "b": 3})
assert result.mode == "local"
print(result.value)
Server-side 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.value)
Call a Function inside a Pipeline
result = client.call(
"demo_traktorist_pipeline",
function="happiness",
kwargs={"a": 300},
)
print(result.value)
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) # 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)
result_node = pipeline.get_node_by_alias("feeling")
run = deployment.run(a=300)
print(run[result_node]["default"])
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.value)
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.
Examples Cookbook
These examples show the practical composition patterns SPLime supports today. Local `NodeFunction` steps run in the current Pipeline process. `NodeRemote` steps call the local daemon, which creates a server-side remote run and waits for the selected target daemon machine to return the value.
Mixed local and remote Pipeline
In this pattern the preparation and final formatting functions run locally, while `demo_traktorist_pipeline::happiness` runs on the machine selected by the remote object's library execution settings.
from spl.core.common import Deployment, lift
from spl.core.entities.node_remote import NodeRemote
def prepare_score(a: int) -> int:
return a
def format_feeling(feeling: str) -> dict:
return {
"feeling": feeling,
"source": "local formatter",
}
remote_happiness = NodeRemote(
pipeline="demo_traktorist_pipeline",
function="happiness",
)
mixed_pipeline = (
lift(format_feeling)
.bind(feeling=lift(remote_happiness).bind(a=lift(prepare_score)))
.alias("result")
.render("mixed_local_remote_pipeline")
)
deployment = Deployment(client, mixed_pipeline)
result_node = mixed_pipeline.get_node_by_alias("result")
run = deployment.run(a=300)
print(run[result_node]["default"])
In-memory remote node pinned to a machine
Use this only for local notebook experiments with `Deployment(client, pipeline)`. It works because `SPLClient.run_node()` forwards a `target_machine` attribute when one exists on the live node object. It is not yet a stable publishable Pipeline feature.
def pin_remote_node(node, target_machine: str):
# Current workaround: works for this live Python object only.
object.__setattr__(node, "target_machine", target_machine)
return node
remote_happiness = pin_remote_node(
NodeRemote(pipeline="demo_traktorist_pipeline", function="happiness"),
"machine-y",
)
pipeline = (
lift(format_feeling)
.bind(feeling=lift(remote_happiness).bind(a=300))
.alias("result")
.render("mixed_local_remote_pinned")
)
Use another user's Function and Pipeline on different machines
This pattern composes remote objects from two owner/library namespaces. The risk Function belongs to Alice's `risk` library and runs on that library's default machine. The report Pipeline belongs to Bob's `reports` library and runs on that library's default machine. The final summary function runs locally on the Pipeline owner machine.
from spl.core.common import Deployment, lift
from spl.core.entities.node_remote import NodeRemote
def library_url(owner: str, library: str) -> str:
return f"https://splime.io/api/owners/{owner}/libraries/{library}"
def normalize_customer(customer_id: int) -> dict:
return {"customer_id": customer_id}
def merge_outputs(score: dict, report: dict) -> dict:
return {
"score": score,
"report": report,
"summary_machine": "local pipeline machine",
}
# Requires execute/read access to alice/risk.
# alice/risk should define default_machine_id, for example alice-gpu-01.
risk_score = NodeRemote(
url=library_url("alice", "risk"),
name="score_customer",
)
# Requires execute/read access to bob/reports.
# bob/reports should define default_machine_id, for example bob-report-runner.
report_pipeline = NodeRemote(
url=library_url("bob", "reports"),
name="build_customer_report",
)
customer = lift(normalize_customer)
score = lift(risk_score).bind(customer=customer)
report = lift(report_pipeline).bind(score=score, customer=customer)
cross_library_pipeline = (
lift(merge_outputs)
.bind(score=score, report=report)
.alias("summary")
.render("cross_library_mixed_pipeline")
)
deployment = Deployment(client, cross_library_pipeline)
result_node = cross_library_pipeline.get_node_by_alias("summary")
run = deployment.run(customer_id=42)
print(run[result_node]["default"])
| Step | Object | Runs on |
|---|---|---|
| Normalize input | Local Python Function | Current Pipeline machine. |
| Risk score | Alice's remote Function in `risk` | `alice/risk.default_machine_id`. |
| Report | Bob's remote Pipeline in `reports` | `bob/reports.default_machine_id`. |
| Merge result | Local Python Function | Current Pipeline machine. |
See the server library instead of only local objects
server_objects = client.objects(scope="server", 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.queue(
"risk_report",
target_machine="alice-gpu-01",
owner="alice",
library="risk",
kwargs={"customer_id": 42},
output="report",
)
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)
Rebuild packages after code changes
cd spl-core
.\tools\build-package.ps1 -OutDir ..\dist-packages
cd ..\spl-daemon
.\tools\build-package.ps1 -OutDir ..\dist-packages
cd ..
.\.venv-spl\Scripts\python.exe -m pip install --find-links .\dist-packages --force-reinstall spl-framework spl-daemon
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` install cannot find `spl-framework`
Install with `--find-links` pointing to the directory that contains both locally built wheels: `python -m pip install --find-links .\dist-packages spl-daemon`.
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>
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