Tiny HTTP byte store and Python SDK.
Project description
elastik Python SDK
Tiny Python client and launcher for elastik-core: an HTTP byte store with
metadata and change events.
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: gatesGET,HEAD,OPTIONS,/listen/*, and/proc/worlds.write_token: ordinary write token forPUTandPOST.approve_token: admin token forDELETEand 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, andevent. worldis still accepted as a compatibility alias forpath.- You can do normal Python side effects inside the handler.
- Advanced users may return
Reply,Archive,MoveTo, orDropaction 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@listenhandlers. 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:
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distributions
Built Distributions
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file elastik-6.2.0-py3-none-win_amd64.whl.
File metadata
- Download URL: elastik-6.2.0-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
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
bd62bc356bd1d57072cad44e39cebab3a486181daa2bfcc52372700353db11ba
|
|
| MD5 |
44a17159829dbeb1f4da3b6dff988374
|
|
| BLAKE2b-256 |
3a7fb63b2698187c3ba6de17f49650e1b2b0de7a43541fdd6437c8a7dd63dd0d
|
Provenance
The following attestation bundles were made for elastik-6.2.0-py3-none-win_amd64.whl:
Publisher:
release.yml on rangersui/Elastik
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
elastik-6.2.0-py3-none-win_amd64.whl -
Subject digest:
bd62bc356bd1d57072cad44e39cebab3a486181daa2bfcc52372700353db11ba - Sigstore transparency entry: 1418536860
- Sigstore integration time:
-
Permalink:
rangersui/Elastik@ebe916319e58e53b1f6f6ed39873aba7b5b71c47 -
Branch / Tag:
refs/tags/v6.2.0 - Owner: https://github.com/rangersui
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@ebe916319e58e53b1f6f6ed39873aba7b5b71c47 -
Trigger Event:
push
-
Statement type:
File details
Details for the file elastik-6.2.0-py3-none-manylinux_2_34_x86_64.whl.
File metadata
- Download URL: elastik-6.2.0-py3-none-manylinux_2_34_x86_64.whl
- Upload date:
- Size: 1.6 MB
- Tags: Python 3, manylinux: glibc 2.34+ x86-64
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
acafb23493dce71b8cfa9e40a41529d3ec39f3afbefad1389aaf37b136fa2d2c
|
|
| MD5 |
4d2e222df5decbef13b13e2370b05832
|
|
| BLAKE2b-256 |
89a4aef14e010c7d12cb0c52579ab619fe33529981bcb5ada0a1aa46f8c3bd0f
|
Provenance
The following attestation bundles were made for elastik-6.2.0-py3-none-manylinux_2_34_x86_64.whl:
Publisher:
release.yml on rangersui/Elastik
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
elastik-6.2.0-py3-none-manylinux_2_34_x86_64.whl -
Subject digest:
acafb23493dce71b8cfa9e40a41529d3ec39f3afbefad1389aaf37b136fa2d2c - Sigstore transparency entry: 1418536892
- Sigstore integration time:
-
Permalink:
rangersui/Elastik@ebe916319e58e53b1f6f6ed39873aba7b5b71c47 -
Branch / Tag:
refs/tags/v6.2.0 - Owner: https://github.com/rangersui
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@ebe916319e58e53b1f6f6ed39873aba7b5b71c47 -
Trigger Event:
push
-
Statement type:
File details
Details for the file elastik-6.2.0-py3-none-macosx_11_0_arm64.whl.
File metadata
- Download URL: elastik-6.2.0-py3-none-macosx_11_0_arm64.whl
- Upload date:
- Size: 1.4 MB
- Tags: Python 3, macOS 11.0+ ARM64
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
555d86daaeb7aa36d877cf919949c6b55b5d1245b2c5b639f81e4048b4015093
|
|
| MD5 |
d47ca02e5cd0068b86b0aa6210fa9e32
|
|
| BLAKE2b-256 |
ca2764abb0c7a0ea8b560fb3366b0465e91b65eeb1a159445a1043059511d9c2
|
Provenance
The following attestation bundles were made for elastik-6.2.0-py3-none-macosx_11_0_arm64.whl:
Publisher:
release.yml on rangersui/Elastik
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
elastik-6.2.0-py3-none-macosx_11_0_arm64.whl -
Subject digest:
555d86daaeb7aa36d877cf919949c6b55b5d1245b2c5b639f81e4048b4015093 - Sigstore transparency entry: 1418536819
- Sigstore integration time:
-
Permalink:
rangersui/Elastik@ebe916319e58e53b1f6f6ed39873aba7b5b71c47 -
Branch / Tag:
refs/tags/v6.2.0 - Owner: https://github.com/rangersui
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@ebe916319e58e53b1f6f6ed39873aba7b5b71c47 -
Trigger Event:
push
-
Statement type: