origofs-py — Python bindings for origofs
Async-native bindings so you can drive an origofs workspace from Python — write your own FastAPI endpoints, inject the authenticated user/agent behind each write, and orchestrate FUSE/NFS mounts.
Every I/O method returns an awaitable, so it drops straight into async def
handlers. Structured results come back as plain dict/list (JSON-serializable).
Install
pip install origofs
abi3 wheels, so one per platform covers CPython ≥ 3.9 and no Rust toolchain is needed at install time.
Build from source
For a platform without a wheel, or to work on the bindings:
cd crates/origofs-py
python -m venv .venv && . .venv/bin/activate
pip install maturin
maturin develop # builds the extension module + installs it
pytest tests/ # end-to-end test
Wheels: maturin build --release (abi3, one wheel works on CPython ≥ 3.9).
Use
import origofs
ws = await origofs.Workspace.open_local("meta.db", "cas")
# ...or multi-writer on Postgres with S3-shared content (the production combo):
# cfg = origofs.S3Config(bucket="my-bucket", region="us-east-1") # + endpoint/keys
# ws = await origofs.Workspace.open_pg_s3(dsn, cfg) # or open_pg_s3_packed
# object-store constructors verify content integrity on read (a bit-rotted object
# errors instead of being served). open_object_memory(db) runs the same adapter
# with no network, for local dev/tests.
# Map your app's user id to an origofs actor (idempotent; no side table needed):
actor_id = await ws.find_or_create_human("user_42", "Dan") # your id -> origofs actor
# then inject the identity you resolved in your endpoint:
ctx = origofs.WriteCtx.session(actor_id, session_id) # or origofs.WriteCtx.actor(actor_id)
await ws.write_as(ctx, "/notes.txt", b"hello") # attributed -> blame + audit
diff = await ws.diff("main", "feature") # [{"path","status"}, ...]
sid = await ws.suggest(ctx, "/x", b"proposed") # agent proposes; not applied
await ws.accept_suggestion(sid, origofs.WriteCtx.actor(reviewer)) # applied, credited
Errors map to Python exceptions: missing path → FileNotFoundError, bad arg →
ValueError, a stale suggestion base → origofs.ConflictError.
FastAPI router (bring your own auth)
origofs has no built-in authentication — a blame trail is only trustworthy if the
identity behind each write is, and that's yours to own. origofs.fastapi.build_router
gives you every workspace endpoint wired up, with attribution driven by an auth
dependency you provide:
from fastapi import FastAPI, Header, HTTPException
import origofs
from origofs.fastapi import build_router
async def authn(x_actor_id: int = Header(...)) -> origofs.WriteCtx:
# decode your JWT / session / agent token -> the origofs actor to attribute to
if x_actor_id is None:
raise HTTPException(401)
return origofs.WriteCtx.actor(x_actor_id)
app = FastAPI()
app.include_router(build_router(ws, authn=authn), prefix="/fs")
Every mutating route depends on authn and passes its WriteCtx straight to the
workspace — the request body never names an actor, so a client can't forge
attribution. Reads are open by default; pass reader=<dependency> to gate them,
or dependencies=[...] (forwarded to APIRouter) to gate everything. POST /actors/POST /sessions mint new actor/session ids the same way — an
already-authenticated caller (e.g. a trusted backend onboarding a new user)
provisions them, not anonymous self-registration. GET /files/{path} streams
(never buffers a whole file in memory) and honors a single-range Range header
(206/416), so large files and partial fetches behave like a static file
server. Needs the fastapi extra (pip install "origofs[fastapi]"). See
examples/fastapi_router.py.
fsspec filesystem (pandas / Dask / PyArrow / Zarr)
origofs.fsspec.OrigoFileSystem exposes a workspace as an
fsspec filesystem, so the PyData stack
can read and write origofs paths directly — and because every origofs I/O method is
already a coroutine, it's a genuine fsspec.asyn.AsyncFileSystem: the same
filesystem is usable synchronously (fs.ls, fs.cat_file, …) and by awaiting
the _-prefixed coroutines on your own loop.
import origofs.fsspec # registers the "origofs://" protocol
import pandas as pd
# read/write straight from your usual tools:
df = pd.read_parquet("origofs:///data/events.parquet",
storage_options={"db_path": "meta.db", "cas_dir": "cas"})
from origofs.fsspec import OrigoFileSystem
fs = OrigoFileSystem(db_path="meta.db", cas_dir="cas") # sync (fsspec loop)
fs.pipe_file("/notes.txt", b"hello")
fs.cat_file("/notes.txt", start=0, end=5) # ranged read (only the covering chunks)
afs = OrigoFileSystem(db_path="meta.db", cas_dir="cas", asynchronous=True)
await afs._pipe_file("/notes.txt", b"hello") # await on your loop
Point it at any backend with connection kwargs (backend="pg_s3", dsn=…, s3={…})
or hand it an already-open workspace (OrigoFileSystem(ws=ws)) to share one store
with the rest of your app. Attribution rides along: pass actor=/session=
(or a ctx=origofs.WriteCtx), and every write lands attributed — fs.blame(path)
credits it — with the same per-call override (fs.pipe_file(p, data, actor=42))
and server-owns-identity discipline as the rest of origofs. Ranged reads go through
read_range, so a large file isn't slurped whole. Listing caching is off by
default (an origofs working tree is live and multi-writer). Needs the fsspec extra
(pip install "origofs[fsspec]").
It passes fsspec's own conformance suite (fsspec.tests.abstract — copy/get/put/
pipe/open, including the recursive, trailing-slash, and glob edge cases); see
tests/test_fsspec_compliance.py.
Pathlib API — because it's a well-behaved filesystem, it also works with
universal-pathlib as a first-class,
explicitly-registered protocol (pip install "origofs[upath]"):
from upath import UPath
root = UPath("origofs:///", db_path="meta.db", cas_dir="cas") # or storage_options=…
(root / "notes.txt").write_text("hello")
for child in root.iterdir():
print(child, child.stat().st_size)
RAG with provenance (passages that know who wrote them)
Retrieval over origofs isn't "S3 + embeddings": every passage carries who wrote
it (blame) and where it came from, and is keyed by a content hash so only
genuinely-changed passages need re-embedding. The technology-agnostic half lives in
the Rust core (Workspace.passages) — no embeddings, no vector store, no framework
types — so any stack consumes the same records.
# framework-neutral: provenance-carrying passage records
from origofs.rag import read_passages
passages = await read_passages(ws, root="/docs", segmentation="content_defined")
for p in passages:
p.text, p.hash, p.authors # -> who wrote this passage (precise, per-byte)
# or drop straight into LlamaIndex — one Document per passage, provenance in metadata
from origofs.llamaindex import SimpleWorkspaceReader
from llama_index.core import VectorStoreIndex
docs = SimpleWorkspaceReader(ws, root="/docs", convert="markitdown").load_data()
index = VectorStoreIndex.from_documents(docs) # node.metadata = {path, authors, passage_hash, …}
Segmentation is a real choice: content_defined (the default) puts boundaries
where the bytes decide, so an edit near the top of a file doesn't reshuffle every
later passage's hash — that edit-stability is what makes incremental re-embedding
cheap. Also fixed, lines, whole_file, or a custom splitter.
Non-text documents (PDF, DOCX, images, …) go through a pluggable Converter
that projects them to Markdown first; a converted passage's provenance is
document-level (source path + who added it + which converter), since the
extracted text no longer maps to the original bytes. MarkItDownConverter ships as
the batteries-included one (pip install "origofs[markitdown]"), but you can pass
unstructured, pandoc, an LLM, or a plain callable(path, data, mime) -> str.
Needs the relevant extras: pip install "origofs[llamaindex,markitdown]" (the
origofs.rag core needs none). See examples/rag_provenance.py — it plays the
whole story (a human + an agent co-author docs, a PDF is converted, and each
retrieved answer names its author and source) with no API keys.
Live change feed (push)
On Postgres, subscribe gives a real push feed (LISTEN/NOTIFY) — await recv()
blocks until the next batch instead of polling watch. Ideal behind a FastAPI
SSE/WebSocket endpoint:
sub = await ws.subscribe(after_seq=0, branch="main") # PG only; raises on SQLite
while True:
events = await sub.recv() # woken by NOTIFY; [] once the connection closes
if not events:
break
for e in events:
... # push to the client
Run an agent in a live overlay
origofs.overlay.run launches an agent in a fast native kernel overlay while its
edits stream into origofs, attributed to an actor — the way agents are meant to work
day to day. It shells out to the origofs CLI (the overlay is host orchestration,
not embedded in the extension), operating on a workspace directory the API
also opens:
import origofs
from origofs.overlay import run
ws_dir = "./ws"
api = await origofs.Workspace.open_local(f"{ws_dir}/meta.db", f"{ws_dir}/cas")
actor = await api.find_or_create_agent("agent-token", "claude", "opus")
code = await run(ws_dir, actor, ["claude", "-p", "refactor the parser"])
Requires the origofs binary on PATH and a Linux host with unprivileged
user-namespace overlays.
Mount orchestration
mount = ws.mount("/mnt/origofs") # FUSE, in the background; returns a handle
mount.unmount() # or use `with ws.mount(...) as m:`
import asyncio
nfs = asyncio.create_task(ws.serve_nfs("127.0.0.1:11111")) # runs until cancelled
nfs.cancel()
Recover from the content store (if the DB is lost)
Your files live in the content store as a self-describing graph; the metadata DB holds refs + attribution. If the DB is lost, point a fresh one at the same content store and rebuild — committed files, directories, and branch names come back (blame/attribution and uncommitted edits don't; they're DB-only):
# same S3/dir as before, brand-new metadata DB:
ws = await origofs.Workspace.open_pg_s3(new_dsn, cfg) # or open_local(new_db, cas)
info = await ws.scan() # read-only: what's recoverable
# {"commits_found": 12, "used_mirror": True, "branches": [("main", "…"), …], …}
report = await ws.rebuild() # restores refs + working tree
# {"files": 340, "dirs": 27, "checked_out": "main", "used_mirror": True, …}
Reading every object also integrity-checks it (report["corrupt"] counts any that
failed). The DB stays the thing to back up — so also run Postgres PITR / a replica.
Alembic migrations for the metadata schema
A workspace already migrates its own metadata schema forward automatically on
open (ws.schema_version() / ws.migrate()) — origofs.db is for the times
you want that schema managed by Alembic instead: a CI-driven migration
step, a schema-diffing tool, or provisioning a fresh database before the
engine ever touches it. origofs.db.models declares every table from
crates/origofs-core/src/migrations.rs as SQLAlchemy models (the single
source of truth origofs.db's packaged migrations autogenerate against), so
a database Alembic creates is fully interoperable with one the engine creates.
Needs the db extra (pip install "origofs[db]") plus a driver for your
backend (Postgres: psycopg[binary]; SQLite's sqlite3 is stdlib).
Provision or upgrade a database — run this once (a deploy step, an init
container, or before the first origofs.Workspace.open_*) and the workspace
API opens straight into an already-migrated store:
import origofs.db
origofs.db.upgrade("sqlite:///meta.db") # dev/solo
origofs.db.upgrade("postgresql+psycopg://user:pass@host/dbname") # multi-writer/production
# ...or skip passing a URL at all and set it once in the environment:
# os.environ["ORIGOFS_DATABASE_URL"] = "postgresql+psycopg://…"
# origofs.db.upgrade()
Roll back or inspect history — get_alembic_config hands you a real
alembic.config.Config for anything beyond upgrade/downgrade:
origofs.db.downgrade("sqlite:///meta.db") # one revision back
origofs.db.downgrade("sqlite:///meta.db", "base") # drop every origofs table
from alembic import command
cfg = origofs.db.get_alembic_config("sqlite:///meta.db")
command.current(cfg) # the revision(s) currently applied
command.history(cfg) # the full revision list
Two migration ledgers, one schema. Alembic tracks its own progress in
alembic_version; the engine tracks its own in schema_meta
(crates/origofs-core/src/migrations.rs). The packaged initial revision
creates every table and stamps schema_meta through the latest version the
engine knows about, so both ledgers agree from the moment Alembic creates the
database — the engine never re-runs (or, for the destructive V11/V13 table
rebuilds, re-applies) a migration Alembic already handled. Whichever tool
creates the database, ws.schema_version() reports up_to_date: True.
Query the schema directly — every table is a plain SQLAlchemy model
(origofs.db.Actor, .EditOp, .BlobBlame, .Suggestion, .FsEvent, …),
handy for read-only reporting/analytics queries that sit alongside the async
workspace API:
from sqlalchemy import create_engine, select
from sqlalchemy.orm import Session as DbSession # origofs.db.Session is the *table* model
from origofs.db import Actor, EditOp
engine = create_engine("sqlite:///meta.db")
with DbSession(engine) as db:
edits_by_actor = db.execute(
select(Actor.display_name, EditOp.path)
.join(EditOp, EditOp.actor_id == Actor.id)
).all()
Developing origofs itself — after editing python/origofs/db/models.py,
draft the next migration from the origofs-py crate root (uses the
alembic.ini there, not origofs.db's programmatic config):
cd crates/origofs-py
alembic -x db_url=sqlite:///./dev.db revision --autogenerate -m "…"
alembic -x db_url=sqlite:///./dev.db upgrade head
Examples
examples/collab_app.py— the one to start from. A complete little service (bearer-token auth mapped to actors, the full workspace API, a live SSE feed) that also runs itself:python examples/collab_app.pyplays the whole story end to end — a human writes, an agent suggests, a reviewer accepts, and blame ends up crediting both — with no server or curl needed.examples/fastapi_router.py— the minimalbuild_routerone-liner with your own header auth.examples/fastapi_app.py— the same surface written out as hand-rolled endpoints, if you'd rather own each route.examples/web/(repo root) — a full-stack React + PlateJS editor over this router, showing per-block and per-line attribution (blame), version lineage (commit history + diffs), and the agent-suggestion review queue, with a live SSE feed. The visual companion tocollab_app.py.
API surface
python/origofs/__init__.pyi is the complete, authoritative list — every
method with its signature, its return shape as a TypedDict, and a note on what
it is for. Your editor already reads it. This section is a map, not an index:
enumerating the methods here means maintaining a second copy that silently falls
behind, which is how the bindings drifted from the engine in the first place.
Three tests keep the stub honest rather than aspirational.
test_parity.py::test_every_sdk_method_is_bound_or_has_a_reason diffs the Rust
Workspace against the pyo3 one, so an engine method with no binding fails
rather than going unnoticed; test_the_type_stub_declares_every_binding diffs
the stub against the bindings; and test_stub_records.py builds one live
instance of every declared record and compares its keys.
Workspace covers, roughly in the order you meet them:
- opening —
open_localandopen_pgfor a local store;open_s3/open_gcsand theiropen_pg_*forms for object storage, each with_packed(few big PUTs),_encrypted(at rest), and_cached(a bounded local read tier) variants;open_object_memoryfor tests. - files —
read·read_range·read_to_path·write·write_path·ls·stat·mkdir_p·remove·rename·symlink·link·chmod·chown· the*xattrfamily ·statfs. - attribution — the
_asform of every mutation (write_as,mkdir_as,remove_or_propose, …),write_as_blamedfor explicit byte-range authorship,blame,edit_ops, andrevert_session_as. - identity and permission —
create_human/create_agent/find_or_create_*·create_session·set_write_policy·grant/revoke/list_grants/effective_perms·ensure_may_write_at/ensure_may_write_workspace·require_attributionandensure_attributed. - versioning —
commit_as·log·status·diff· branches ·checkout_as·merge/merge_branch·conflicts· locks. - review —
suggest·list_suggestions·suggestion_diff·accept_suggestion/reject_suggestion. - collaboration —
watch·subscribe·record_event·presence· thecoeditfamily (CRDT documents, tree documents, the cross-worker relay). - operations —
gc·flush·repack·trash·usage/quota·backup_metadata·dump_as/load·resync/push_objects/fetch_objects·rebuild/scan·migrate·ready·bench·file_layout. - mounting —
mount(FUSE, Linux) ·serve_nfs(Unix).
Plus WriteCtx, Scope, S3Config, GcsConfig, CacheConfig, Mount,
content_hash() and fuse_mountable().
One asymmetry worth knowing: dump_as takes a WriteCtx where the Rust
dump does not, and the unauthorized form is not bound. A dump is
whole-store — every workspace, every actor including its auth_subject, every
ACL grant, all blame — and none of it is path-scoped, so no Scope narrows it
and no subtree grant bounds it. It is checked as write at /. load has no
_as counterpart for the opposite reason: it cannot be ACL-gated, because the
identities a check would consult are the ones it installs, so it refuses any
store that already has actors or grants.
Integrations (own extras): origofs.fastapi (HTTP router) · origofs.fsspec
(OrigoFileSystem, the fsspec filesystem — also a UPath("origofs://…") via
universal-pathlib) · origofs.rag (provenance-carrying passages) +
origofs.llamaindex (SimpleWorkspaceReader) + origofs.converters
(MarkItDownConverter) · origofs.overlay (agent overlay) · origofs.db
(SQLAlchemy models + Alembic migrations for the metadata schema).
Metadata
Release files for origofs 0.0.7
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| origofs-0.0.7.tar.gz | 1.3 MB | Details |
Built distributions (wheels)
| File | Reset | |||
|---|---|---|---|---|
| origofs-0.0.7-cp39-abi3-win_amd64.whl | CPython 3.9 | abi3 | Windows x86-64 | Details |
| origofs-0.0.7-cp39-abi3-manylinux_2_28_x86_64.whl | CPython 3.9 | abi3 | Linux glibc 2.28+ x86-64 | Details |
| origofs-0.0.7-cp39-abi3-manylinux_2_28_aarch64.whl | CPython 3.9 | abi3 | Linux glibc 2.28+ ARM64 | Details |
| origofs-0.0.7-cp39-abi3-macosx_11_0_arm64.whl | CPython 3.9 | abi3 | macOS 11.0+ ARM64 | Details |
| origofs-0.0.7-cp39-abi3-macosx_10_12_x86_64.whl | CPython 3.9 | abi3 | macOS 10.12+ x86-64 | Details |
Total release size: 57.9 MB
Release files / origofs-0.0.7.tar.gz
| Download URL | origofs-0.0.7.tar.gz |
|---|---|
| Size | 1.3 MB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
9860787da5605f725d7f114820c25a5d903738b9565ee7b2d460c1fb2e9b31d8
|
|
BLAKE2b-256 checksum How to use checksums |
2f73f55836c7e0dd9d7c23a50bec4703e35e247c2651d5bdaec7a6cf42c103ec
|
| 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 7, 2026.
Transparency logRelease files / origofs-0.0.7-cp39-abi3-win_amd64.whl
| Download URL | origofs-0.0.7-cp39-abi3-win_amd64.whl |
|---|---|
| Size | 10.5 MB |
| Tags | CPython 3.9 Windows x86-64 abi3 |
|
SHA-256 checksum How to use checksums |
aa5720be28c8a7492efd5963b5d00a78a87e43b5724b1854b8caab06a3b8adfa
|
|
BLAKE2b-256 checksum How to use checksums |
ef453e6c2c1e039bed89363093ea33055841d343730d482787eaae906e75e57f
|
| 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 7, 2026.
Transparency logRelease files / origofs-0.0.7-cp39-abi3-manylinux_2_28_x86_64.whl
| Download URL | origofs-0.0.7-cp39-abi3-manylinux_2_28_x86_64.whl |
|---|---|
| Size | 12.0 MB |
| Tags | CPython 3.9 Linux glibc 2.28+ x86-64 abi3 |
|
SHA-256 checksum How to use checksums |
cd20d57d3b539f318529f5085d33a5197629248071ae90ee9a02da721d2f794c
|
|
BLAKE2b-256 checksum How to use checksums |
e9a0a9d69b451e31e3c59829dfc0dfcd630f1f1eaea10d2dcfeb1821b4a31f6a
|
| 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 7, 2026.
Transparency logRelease files / origofs-0.0.7-cp39-abi3-manylinux_2_28_aarch64.whl
| Download URL | origofs-0.0.7-cp39-abi3-manylinux_2_28_aarch64.whl |
|---|---|
| Size | 13.1 MB |
| Tags | CPython 3.9 Linux glibc 2.28+ ARM64 abi3 |
|
SHA-256 checksum How to use checksums |
0eeab0e1963ee748318833c99dcb42b600d7fe14413085d099c48a485ea5360a
|
|
BLAKE2b-256 checksum How to use checksums |
03ba315d3f102a6873af44033d4acff3d2b4ebbc609479a99d09d1247e589722
|
| 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 7, 2026.
Transparency logRelease files / origofs-0.0.7-cp39-abi3-macosx_11_0_arm64.whl
| Download URL | origofs-0.0.7-cp39-abi3-macosx_11_0_arm64.whl |
|---|---|
| Size | 10.5 MB |
| Tags | CPython 3.9 abi3 macOS 11.0+ ARM64 |
|
SHA-256 checksum How to use checksums |
4adcc0f6a15a4bed617a9300f5240f0a7daf23f8c9fb5ae5a9860d47f7289f64
|
|
BLAKE2b-256 checksum How to use checksums |
912be73e4c4a45dedcd41dfc7a74f3d60ba890b3e9d2482aaa8b6186229ca09d
|
| 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 7, 2026.
Transparency logRelease files / origofs-0.0.7-cp39-abi3-macosx_10_12_x86_64.whl
| Download URL | origofs-0.0.7-cp39-abi3-macosx_10_12_x86_64.whl |
|---|---|
| Size | 10.6 MB |
| Tags | CPython 3.9 abi3 macOS 10.12+ x86-64 |
|
SHA-256 checksum How to use checksums |
e68058acef01713e09ed5c584c061c02a2238844ed41b76cb065522ccef2dfb0
|
|
BLAKE2b-256 checksum How to use checksums |
7f781a66ea28d7edfd5e060722dd355c9412b9ccf5b4838d9f51f3bd3aea096b
|
| 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 7, 2026.
Transparency log