Cursus — object-backed experiment
An asset-first Python engine and browser console whose only required durable storage is an object store. Experimental alpha, not production-ready. This branch uses SlateDB 0.16 for transactional metadata and obstore 0.11 for immutable JSON outputs. No PostgreSQL, Redis, or local metadata database is required.
This is a standalone implementation on feat/s3-state-backend. The earlier feat/asset-engine-ui branch contained only CI scaffolding when this experiment resumed; there is no PostgreSQL migration or drop-in backend switch in this branch. main is not changed.
Local development
Requires Python 3.12+ and uv. On supported platforms, SlateDB's Python wheel includes the native engine.
The repo is a monorepo of source trees that ship as one PyPI distribution, cursus: packages/sdk holds the cursus asset SDK that project files import; apps/server holds the cursus_server control plane (API, engine, storage, cursus CLI); apps/worker holds cursus_worker, the task-execution package the server spawns locally and the base for remote workers; apps/console is the pnpm/Vite web app whose committed bundle is embedded in the wheel. The root pyproject.toml bundles all three trees, so uv sync installs a single editable cursus package and uv build produces one wheel.
uv sync --locked
uv run cursus serve --insecure
# http://127.0.0.1:8000
The default file:///.../.cursus store uses the same object-store interfaces as S3, backed by ordinary files. No emulator is needed for day-to-day development. This is persistent local object storage, not a remote backup of a local database. Do not delete it expecting recovery from elsewhere.
Select sample_quality in the Materialize dialog. This executes:
source_files -> samples + measurements -> sample_summary -> sample_quality
daily_observations -> daily_report (daily partitions)
The console provides an asset catalog, dependency overview, data previews, committed output references, checkpoints, runs, attempts/logs, backfill requests, pause/resume/cancel/repair controls, interval/cron/changed-output automations, and backend diagnostics. It is a Vite/TanStack single-page app served from the Python package; the built bundle is committed, so no Node runtime is needed to run it — only to rebuild (pnpm -C apps/console build) or test.
uv run cursus run sample_quality
uv run cursus run daily_report --partition 2026-01-01 --partition 2026-01-02
uv run cursus manifest --project cursus_server.demo:project
uv run cursus selftest --state-url file:///tmp/cursus-test-objects
--project accepts module:attribute or a file path, Dagster-style — example/lab.py is a self-contained weather-station pipeline you can run directly:
uv run cursus serve --insecure --project example/lab.py
uv run cursus run climate_report --project example/lab.py
One coordinator per namespace. Running cursus run while cursus serve uses the same namespace replaces/fences that server's writer. Use the UI/API to submit work to a running server, or give the CLI a different namespace.
S3 and compatible services
Provision a private bucket, then configure both native clients using environment variables:
export CURSUS_STATE_URL=s3://your-private-bucket/orchestrator
export CURSUS_NAMESPACE=development
export AWS_ACCESS_KEY_ID=...
export AWS_SECRET_ACCESS_KEY=...
export AWS_REGION=us-east-1
export AWS_CONDITIONAL_PUT=etag
export CURSUS_API_TOKEN="$(python -c 'import secrets; print(secrets.token_urlsafe(32))')"
uv run cursus serve
For a custom S3 endpoint:
export AWS_ENDPOINT=https://your-s3-endpoint
export AWS_VIRTUAL_HOSTED_STYLE_REQUEST=false
# Only for an HTTP emulator, never an unencrypted production connection:
# export AWS_ALLOW_HTTP=true
AWS_ENDPOINT is the Rust object-store client's setting, not just boto3's AWS_ENDPOINT_URL. The server never accepts credentials in a storage URL. Workers have these environment variables stripped, but they are trusted code with the same host identity: subprocesses are not security sandboxes.
Use HTTPS in front of any remote listener and set a strong CURSUS_API_TOKEN. --insecure is restricted to loopback in the CLI. The UI keeps the token in tab-scoped session storage. This is shared trusted-team authentication, not RBAC or tenancy isolation.
Backend conformance
Before using a new S3-compatible service:
uv run cursus selftest --state-url "$CURSUS_STATE_URL"
This explicitly writes synthetic data into a fresh probe-UUID namespace. It checks create-if-absent and racing conditional updates on S3, a real subprocess pipeline, multi-output commits, unchanged-input skips, deletions and empty replacements, a backfill, reopening without coordinator state, durable command receipts, and native writer takeover. Test objects remain in that isolated namespace for inspection; remove only that prefix after all test processes are stopped. No existing workspace is deleted.
An emulator passing these checks is not proof that a different provider has equivalent semantics. See Railway deployment.
Authoring
from cursus import (
AssetContext, Batch, ByKey, Inventory, Project, ReplaceKeys, asset,
)
@asset
def source_files():
return Inventory([
{"id": "a.csv", "revision": "2", "records": [{"value": 42}]},
], complete=True)
@asset(inputs={"files": "source_files"}, incremental=ByKey("files"))
def measurements(ctx: AssetContext, files):
changed = ctx.changes["upserted_keys"]
affected = changed + ctx.changes["deleted_keys"]
rows = [
{"source_file": f["id"], **record}
for f in files if str(f["id"]) in changed
for record in f["records"]
]
return ReplaceKeys("source_file", affected, rows)
project = Project([source_files, measurements])
Save as my_project.py and run uv run cursus serve --insecure --project my_project.py from that directory — the attribute defaults to project (or a single Project instance is auto-detected); file.py:attribute selects another name, and a bare module name also defaults to project.
An ordinary return value replaces a snapshot. Explicit operations are Replace, Inventory, ReplaceKeys, Upsert, and AppendBatch. Batch(outputs={...}, cursor=...) returns multiple output mutations and the next user-managed cursor together. Checkpoints are scoped by producer and partition.
For ByKey, successful batches acknowledge only their selected source revisions. Incomplete inventories never imply deletions. Changing request configuration, producer code, or non-keyed dependencies conservatively reprocesses keyed items. A changed explicit version requires mode="recompute"; use it for incompatible state/schema changes. Resource and helper-module changes are not automatically fingerprinted: bump the asset version when they change semantics. An incomplete source inventory cannot be used for recompute. Recompute processes the full bounded scope at once rather than incrementally replacing an incomplete scope.
A multi-output producer executes as a unit. fill_missing reuses only complete published scopes; it is not a freshness check. A paused run starts no further attempts, but already running attempts may finish. Cancellation fences publication, not arbitrary external side effects. Repair retries failed work and retains already committed batches.
Automations take one of three triggers: Every(seconds), Cron(expression, timezone), or OnCommit(assets). Commit triggers mark the automation pending inside the publication transaction; the coordinator then submits the run once every pinned target input resolves, so mid-batch commits coalesce and triggers never fire against still-running upstream work. Triggered runs never re-run upstream producers; a commit that changes nothing fires nothing. Asset functions can emit structured entries with ctx.log("message", **fields), shown alongside captured stdout on each attempt.
Tests
uv run pytest -q -m 'not live'
pnpm install --frozen-lockfile && pnpm -C apps/console exec playwright install chromium && pnpm -C apps/console test
The Python suite uses real SlateDB on filesystem storage, including abrupt process death and writer takeover. It also starts Moto as an HTTP S3 emulator and runs the remote conformance test. Synthetic fault injection separately verifies acknowledgement timing and ambiguous-outcome behavior. The live-provider test is opt-in:
CURSUS_TEST_S3_URL=s3://isolated-test-bucket/prefix uv run pytest -q -m live
GitHub Actions verifies Python contracts, browser behavior on desktop/mobile, wheel contents, and a native end-to-end probe (cursus selftest). uv.lock and pnpm-lock.yaml are committed, as is the built console under apps/server/src/cursus_server/web so deployments need no Node runtime. CI uses locked installs; the storage engines are explicitly version-pinned.
Boundaries
This is a feasibility implementation, not a claim of production readiness or benchmarked scalability. It deliberately serializes metadata transitions through remote acknowledgement. Inventory diffs and JSON snapshot rewrites are in-memory; queue/catalog/history scans need pagination and better indexing at larger scale. Limits are 64 MiB per JSON output, 1,000 partitions and 5,000 planned tasks per request. There is no Parquet/S3 table adapter, external-destination transaction recovery, distributed execution backend, arbitrary partition mapping, retained historical code image, artifact garbage collector, or zero-downtime multi-replica writer election. The durable outbox exists; an external event consumer is not implemented.
Read the design and safety invariants before extending the backend. OpenAPI/UI endpoints are trusted-team surfaces, not a hardened multi-tenant service.
Releasing
The single cursus distribution is published to PyPI by .github/workflows/publish.yml using Trusted Publishing — no long-lived token is stored. One pending publisher is registered against this repository, the publish.yml workflow, and the pypi environment.
To cut a release, bump the version in the root pyproject.toml, commit, and push a tag; the workflow builds the distribution in one job and publishes it from a second, protected job:
uv version 0.0.2
git commit -am 'release: 0.0.2'
git tag v0.0.2 && git push origin main v0.0.2
It can also be run on demand from the Actions tab (workflow_dispatch).
License
Apache-2.0. The repository's visibility is unchanged.
Metadata
Release files for cursus 0.0.1
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| cursus-0.0.1.tar.gz | 310.0 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| cursus-0.0.1-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 627.6 kB
Release files / cursus-0.0.1.tar.gz
| Download URL | cursus-0.0.1.tar.gz |
|---|---|
| Size | 310.0 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
1cb33e9b1d8b84fd29a05a3b3ff2bb16cd9e90cc145df6d59a4f410972fdb24a
|
|
BLAKE2b-256 checksum How to use checksums |
787db7d1d56be04365fe1cd2f6d687b383e46b74b1b087d1b72bdd03d5369051
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Sep 20, 2026.
Transparency logRelease files / cursus-0.0.1-py3-none-any.whl
| Download URL | cursus-0.0.1-py3-none-any.whl |
|---|---|
| Size | 317.5 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
3d437a278c47889ed69ef6ea07d5d989f6341eb9a8e7f9d167778101180e7ca1
|
|
BLAKE2b-256 checksum How to use checksums |
fb2db68df3d482753f3ca9e17e26f91a1318a7f8ea3db1f0213e286f16b451ea
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Sep 20, 2026.
Transparency log