Skip to main content

Elastik V6 Engine: six verbs, one HTTP disk.

Project description

elastik Python SDK

Python client and launcher for the Elastik V6 Engine: six verbs, one HTTP disk.

The engine is small on purpose: PUT, GET, HEAD, POST, DELETE, and LISTEN. The SDK gives those verbs a Python shape without turning the core into a framework.

The beginner surface in one breath:

import secrets
import elastik

e = elastik.start(key=secrets.token_hex(32), write_token="write-token")
e.put("note", "hello")
print(e.get("note"))       # b"hello"
print(e.get_text("note"))  # hello
print(e.head("note"))      # lowercased HTTP headers
elastik.stop()

No hidden object model: put() replaces bytes, post() appends bytes, get() returns bytes, and head() returns headers.

API At A Glance

Want to... Use
Store/load bytes e.put(path, data), e.get(path)
Store/load text or JSON e.put_text / e.get_text, e.put_json / e.get_json
Use dict style e["note"] = b"hello", del e["note"], "note" in e
Stream a big read e.open(path) (read-only file-like, Range-backed)
Use pathlib style e / "home" / "note" (WorldRef)
Browse virtual directories e.ls("home"), e.tree("home"), ref.iterdir()
Move/delete prefixes e.mv(src, dst), e.rm(prefix, recursive=True)
Read several paths e.get_many([...]) (concurrent HTTP requests)
Watch changes @elastik.listen(pattern) + elastik.run(e)
Inspect metadata e.head, e.checksum, e.preview, e.diff
Use scratch space with e.tmp() as path:
Drop to raw HTTP e.request(method, path, headers=...)

Prefer e.put(...) instance methods in libraries, tests, and long-running tools where client lifecycle should be explicit. Use module-level elastik.put(...) in scripts and notebooks with exactly one core per process.

Path Contracts

Elastik apps can be coordinated by path names instead of API schemas.

// Frontend writes input and listens for output.
await fetch("/home/order/123", {
  method: "PUT",
  body: JSON.stringify({ sku: "tea", qty: 2 }),
  headers: { "Content-Type": "application/json" },
});

new EventSource("/listen/home/receipt/*");
# Business worker owns the workflow.
import elastik

@elastik.listen("/home/order/*")
def on_order(body, path, e):
    order_id = path.rsplit("/", 1)[-1]
    e.put_json(f"/home/receipt/{order_id}", {"status": "accepted"})

elastik.run()

The shared contract is only:

/home/order/{id}
/home/receipt/{id}

Use curl to inspect either side of the handoff:

curl.exe http://127.0.0.1:3105/home/order/123
curl.exe http://127.0.0.1:3105/home/receipt/123

In a source checkout, runnable examples live in sdk/examples/:

python sdk/examples/01_basic.py
python sdk/examples/02_listener.py
python sdk/examples/03_metadata_and_etag.py

Install

py -m pip install elastik

The package ships a platform-specific elastik-core binary in elastik/_bin/. No compile-on-install.

PyPI wheels are the normal install path. If your platform does not have a wheel yet, build the Rust core from source and use an editable checkout.

Starting A Core

You have two normal choices.

1. Start from Python

Use this in scripts, tests, notebooks, and local tools:

import secrets
import elastik

e = elastik.start(
    key=secrets.token_hex(32),   # required HMAC key for the audit chain
    read_token="read-token",     # optional: omit for public reads
    write_token="write-token",   # optional: ordinary PUT/POST
    approve_token="admin-token", # optional: DELETE and system namespaces
)

Python kwargs use underscores (read_token). CLI flags use hyphens (--read-token).

2. Start from a terminal

Use this when you want a long-running local service:

py -m elastik run --key dev-hmac-key --read-token read-token --write-token write-token --approve-token admin-token

Then connect from another process:

from elastik import Elastik

e = Elastik("http://127.0.0.1:3105", bearer_token="write-token")

Module-level elastik.put/get/... calls require either a prior elastik.start(...) or explicit environment like ELASTIK_URL and ELASTIK_WRITE_TOKEN. They do not silently assume that an unknown process on 127.0.0.1:3105 is yours.

Tokens

  • read_token: gates GET, HEAD, OPTIONS, /listen/*, and /proc/worlds.
  • write_token: ordinary write token for PUT and POST.
  • approve_token: admin token for DELETE and system namespaces.

If read_token is omitted, reads are public. If write_token is omitted, ordinary writes are disabled. If approve_token is omitted, destructive/admin operations are disabled.

Migration note: ELASTIK_TOKEN was the old write-token name. It still works as a temporary fallback when ELASTIK_WRITE_TOKEN is unset, but the SDK warns so you can rename it.

Paths

"foo" and "/foo" both mean /home/foo.

Explicit namespaces are allowed when you want their storage policy:

  • /home/*: durable SQLite storage.
  • /tmp/*, /dev/*, /sys/*: memory-backed storage.
  • /proc/version, /proc/worlds: core introspection endpoints.

Namespace roots like /home, /tmp, /lib, /etc, /var/log, and /proc/* internals are reserved. Store application data under a child path such as /home/myapp/data.

Concrete mapping:

Input path Stored/read path
"note" /home/note
"/note" /home/note
"tmp/scratch" /tmp/scratch
"/tmp/scratch" /tmp/scratch
"proc/worlds" /proc/worlds
"/proc/anything-else" rejected

list_paths() is the preferred name. list_keys() and the older list_worlds() name remain as aliases; all three read /proc/worlds.

print(e.get_text("/proc/version"))  # core version string
print(e.list_paths())               # /proc/worlds, one path per line

Metadata

Standard representation metadata has named arguments:

e.put(
    "report.pdf",
    pdf_bytes,
    content_type="application/pdf",
    content_disposition='attachment; filename="report.pdf"',
    cache_control="max-age=60",
)

Extra keyword arguments become plain X-Meta-* headers:

e.put("note", "hello", project="demo")
assert e.head("note")["x-meta-project"] == "demo"

Those X-Meta-* fields are just metadata. They do not affect auth, auditing, or routing unless your own SDK/userland code gives them meaning.

Any safe response header that does not have a named argument can be sent through headers=:

e.put(
    "logo.png",
    png_bytes,
    content_type="image/png",
    headers={"Access-Control-Allow-Origin": "*"},
)
assert e.head("logo.png")["access-control-allow-origin"] == "*"

The core blacklists credentials, hop-by-hop transport state, request controls, and core-generated headers such as ETag and Content-Length. Everything else is stored and replayed without the SDK needing to understand it.

The SDK also refuses wire-level headers that urllib must compute itself: Content-Length, Transfer-Encoding, Host, Connection, Keep-Alive, TE, Trailer, Upgrade, and HTTP2-Settings. Passing those through headers= raises ValueError instead of letting a bad length hang the request.

Authorization is allowed as an explicit escape hatch. If you pass it in headers=, it takes precedence over the client's bearer_token.

Bytes, Text, JSON

get() is byte-exact:

e.put("x", "hello")
assert e.get("x") == b"hello"

get() raises ElastikError(404, ...) when a path is missing. None only means 304 Not Modified from an if_none_match cache check; it never means "missing".

Use helpers when you want decoding:

e.put_text("note", "hello")
e.put_json("config", {"debug": True})
e.get_text("x")          # str
e.get_json("config")     # parsed JSON

head() returns a typed header dict (WorldMeta) for editor help:

meta = e.head("x")
print(meta["etag"])
print(meta["content-type"])

Common WorldMeta keys are optional HTTP headers: etag, content-type, content-length, content-encoding, content-language, content-disposition, cache-control, accept-ranges, and link.

Conditional And Partial Reads

The SDK exposes common HTTP controls directly:

etag = e.head("config")["etag"]
e.put("config", b"new", if_match=etag)     # optimistic update
e.put("lock", b"mine", create_only=True)   # If-None-Match: *
chunk = e.get("big.bin", range=(0, 1023))  # Range: bytes=0-1023

if_none_match is for ETag strings only. Use create_only=True for If-None-Match: *.

For anything not sugared, use the raw HTTP escape hatch:

r = e.request("OPTIONS", "note")
print(r.status, r.headers, r.body)

Python Ergonomics

The core is HTTP; the SDK adds small Python-shaped conveniences without hiding the wire.

e["note"] = "hello"             # PUT /home/note
assert e["note"] == b"hello"    # GET /home/note
assert "note" in e              # HEAD /home/note
assert e.exists("note")
assert e.sizeof("note") == 5
e.copy("note", "note-copy")      # GET + HEAD + PUT
del e["note"]                   # DELETE /home/note

Stored paths are canonicalized to <namespace>/<rest> with no leading slash: e["src"], e["/src"], and e["home/src"] all index the same path. Iteration returns that canonical core form from /proc/worlds, such as home/src or tmp/scratch.

The core store is flat: home/sensor/kitchen/temp is one key, not three real directories. The SDK gives Python users a virtual hierarchy by splitting paths on /, like pathlib, os, and shutil:

e.ls("home/sensor")              # immediate children; virtual dirs end in /
e.ls("home/sensor", depth=-1)    # all descendants
print(e.tree("home"))
e.du("home/sensor")              # {path: content_length}
e.mv("home/draft", "home/final") # copy+delete; refuses overwrite by default
e.rm("home/old", recursive=True) # refuses "" or namespace roots without force=True

mv() is copy+delete, not an atomic filesystem rename. Partial failures can leave source and destination paths side by side. rm("home", recursive=True) and rm("", recursive=True) are guarded footguns; pass force=True only when you really mean to delete a namespace or the whole store.

copy() buffers the source body in Python memory. That is fine for ordinary objects; for huge blobs, use a streaming tool or curl pipeline.

Elastik implements collections.abc.MutableMapping[str, bytes], so update(), pop(k, default), setdefault(), keys(), values(), and items() work too. Each mapping operation is still one HTTP request; there is no hidden transaction or batch endpoint.

More stdlib-shaped helpers are thin wrappers over HTTP:

e.get_cached("note")                  # GET + If-None-Match cache
e.checksum("note")                    # HEAD -> ETag
e.diff("note", "new text")            # local unified diff
e.preview("note", max_bytes=512)      # Range GET + text preview
e.put_gzip("log.gz", "hello")         # Content-Encoding: gzip
e.put_csv("table.csv", [["t", "v"]])  # text/csv
e.put_struct("dev/s0", ">ff", 1.0, 2.0)
e.get_many(["a", "b"])                # concurrent GETs, no batch endpoint

open(path, "rb") returns a read-only file-like object backed by Range GETs:

with e.open("report.pdf") as f:
    header = f.read(8)
    f.seek(1024)
    chunk = f.read(512)

Pathlib-shaped references are available when they make code clearer:

report = e / "home" / "reports" / "q1"
report.write("revenue up")
print(report.read_text())
print(report.stat()["etag"])

for child in (e / "home" / "reports").iterdir():
    print(child.name, child.suffix)

for pdf in (e / "home").rglob("*.pdf"):
    print(pdf.path)

Temporary paths are just /tmp/* paths with best-effort cleanup:

with e.tmp("scratch") as path:
    e.put(path, "working...")

Errors have subclasses when you want precise handling:

try:
    e.get("missing")
except elastik.NotFound:
    print("not there")
except elastik.PreconditionFailed:
    print("etag changed")

For bug reports and shell sanity checks:

import elastik
print(elastik.__version__)
elastik.show_config()

If you look directly inside the durable data/ directory, names are percent encoded because the core stores a flat keyspace safely on Windows and POSIX:

home/note.txt  -> data/home%2Fnote%2Etxt/universe.db

Use the ops helpers when you need to translate that layout:

python -m elastik decode-path "home%2Fnote%2Etxt"
python -m elastik ls-data .\data

Listening For Changes

@listen is optional. Do not call elastik.run() unless you registered at least one handler.

import elastik

e = elastik.start(key="dev-key", write_token="write-token")

@elastik.listen("/home/inbox/*")
def on_inbox(body, path, meta, e):
    if b"urgent" in body:
        e.put("/home/alerts/latest", body)

elastik.run(e)

Handler rules:

  • The first positional argument is always body.
  • Extra context is injected by name: path, etag, pattern, meta, e, method, and event.
  • world is still accepted as a compatibility alias for path.
  • You can do normal Python side effects inside the handler.
  • Advanced users may return Reply, Archive, MoveTo, or Drop action objects, but they are not required.

Use clear_routes() or unlisten(pattern) in tests/notebooks to reset handler state. Registering the same pattern twice raises unless you use listen(pattern, replace=True).

run() retries forever by default and logs failures to stderr. For supervised processes, prefer elastik.run(e, reconnect=False) and let your supervisor restart the process. For demos/tests, max_events=1 runs until one matching event is handled.

Environment Loading

import elastik loads a local .env once and fills only missing environment variables. Existing process env wins. Set ELASTIK_NO_DOTENV=1 before import to disable this, or call elastik.load_dotenv(path) explicitly when you want manual control.

Advanced Helpers

These are exported but not part of the beginner path:

  • request(): raw HTTP escape hatch.
  • binary_info(), is_running(), default_url(): launcher diagnostics.
  • TrustedShellPool: warm local shell process pool for trusted @listen handlers. It can execute arbitrary commands; do not feed it untrusted input.
  • MoveTo, Reply, Archive, Drop, Action, Ctx: optional reactor action vocabulary.

Stateless By Default

The SDK intentionally uses one-shot stdlib HTTP requests by default: no requests, no urllib3, no connection pool, no hidden keep-alive state.

That is slower than a tuned keep-alive client, but it is boring and hard to leak. High-frequency callers can use curl, ab -k, http.client, or a custom transport when they have measured a real bottleneck.

Testing Without A Core

For unit tests that only need SDK behavior, use the in-memory fake:

from elastik import FakeElastik

e = FakeElastik()
e.put("note", "hello")
assert e.get_text("note") == "hello"

FakeElastik is not a protocol test. Use the black-box tests or a real elastik.start(...) when you need wire-level HTTP behavior.

Source Checkout

git clone https://github.com/rangersui/Elastik
cd Elastik
python -m pip install -e .\sdk
python -m elastik run --key dev-hmac-key --read-token read-token --write-token write-token --approve-token admin-token

For the full project README, see:

https://github.com/rangersui/Elastik

Project details


Download files

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

Source Distributions

No source distribution files available for this release.See tutorial on generating distribution archives.

Built Distributions

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

elastik-6.2.1-py3-none-win_amd64.whl (1.4 MB view details)

Uploaded Python 3Windows x86-64

elastik-6.2.1-py3-none-manylinux_2_34_x86_64.whl (1.6 MB view details)

Uploaded Python 3manylinux: glibc 2.34+ x86-64

elastik-6.2.1-py3-none-macosx_11_0_arm64.whl (1.4 MB view details)

Uploaded Python 3macOS 11.0+ ARM64

File details

Details for the file elastik-6.2.1-py3-none-win_amd64.whl.

File metadata

  • Download URL: elastik-6.2.1-py3-none-win_amd64.whl
  • Upload date:
  • Size: 1.4 MB
  • Tags: Python 3, Windows x86-64
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.12

File hashes

Hashes for elastik-6.2.1-py3-none-win_amd64.whl
Algorithm Hash digest
SHA256 214947992e8f03a2e00d88753467ecf0f2b7306d2f8285650627e48f455f82a2
MD5 fdf30f7d38a6fda5d436dc7f06cd3e54
BLAKE2b-256 3eb734570df226107ae21f1903c6ef998bda4d730c4b07cd58d6aa8b223b5f19

See more details on using hashes here.

Provenance

The following attestation bundles were made for elastik-6.2.1-py3-none-win_amd64.whl:

Publisher: release.yml on rangersui/Elastik

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file elastik-6.2.1-py3-none-manylinux_2_34_x86_64.whl.

File metadata

File hashes

Hashes for elastik-6.2.1-py3-none-manylinux_2_34_x86_64.whl
Algorithm Hash digest
SHA256 62bb61c9e226b4af6e7c354bf19d74aa34c38b99d5b2c8d6c3cd804ebd76d53d
MD5 4b0cb86707a36d420b80cdc372e90eb4
BLAKE2b-256 42d8bd4019c793559b5bbeb45327f7ed5836935feec51662286eb42d59c60dc1

See more details on using hashes here.

Provenance

The following attestation bundles were made for elastik-6.2.1-py3-none-manylinux_2_34_x86_64.whl:

Publisher: release.yml on rangersui/Elastik

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file elastik-6.2.1-py3-none-macosx_11_0_arm64.whl.

File metadata

File hashes

Hashes for elastik-6.2.1-py3-none-macosx_11_0_arm64.whl
Algorithm Hash digest
SHA256 0b2d42746c32727dc66a4a3071758606420afbc339b515d7657ea1f437bc487e
MD5 d3819e329836e7f6ab4df3c72ad65451
BLAKE2b-256 e72e7931bb21d20627353eae338d0f499162ba071d2573bd137f87d33f6722b1

See more details on using hashes here.

Provenance

The following attestation bundles were made for elastik-6.2.1-py3-none-macosx_11_0_arm64.whl:

Publisher: release.yml on rangersui/Elastik

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

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