Official Python client for the CairnMark file service
Project description
cairnmark-python
The official Python client for CairnMark — a self-hostable file service over S3-compatible storage with a queryable Postgres metadata layer.
Sync and async clients, streaming uploads/downloads, client-side checksum
verification, typed errors, safe retries, lazy pagination. One dependency
(httpx). Python ≥ 3.10, fully typed (py.typed). Requires a CairnMark
server ≥ v1.1.0 (cursor pagination and 409 Retry-After).
pip install cairnmark # not yet on PyPI — until then: pip install <path to this repo>
Quickstart
from cairnmark import CairnMark
with CairnMark("http://localhost:8080") as cm:
# Upload with tags; idempotency_key="auto" makes retries duplicate-safe.
f = cm.upload(
b"hello from Python",
filename="hello.txt",
content_type="text/plain",
metadata={"env": "demo"},
idempotency_key="auto",
)
print("uploaded:", f.id)
# Download — follows the presign redirect and verifies the SHA-256.
cm.download_to_file(f.id, "hello-copy.txt")
# Search by tag, lazily across pages.
for file in cm.iter_files(tags={"env": "demo"}):
print("found:", file.id, file.filename)
Async is the same surface with await (and aiter_bytes/aread/aclose on
downloads):
from cairnmark import AsyncCairnMark
async with AsyncCairnMark("http://localhost:8080") as cm:
f = await cm.upload(b"hello", filename="hello.txt")
async with await cm.download(f.id, verify=True) as dl:
data = await dl.aread()
async for file in cm.iter_files(tags={"env": "demo"}):
...
Methods
| Method | Does |
|---|---|
upload(content, *, filename, content_type, size, metadata, idempotency_key) |
Stream bytes / a file-like / an iterator up. idempotency_key="auto" generates a key. |
upload_file(path, ...) |
Upload from disk; name and size inferred. |
download(id, *, verify, offset, length) |
Open a content stream (context manager). Default follows the presign redirect; offset/length for a 206 range; verify for checksum checking. |
download_to_file(id, path) |
Download to disk, checksum-verified; removes the file on failure. |
presign_url(id) |
Mint the presigned object-store URL without following it. |
get_metadata(id) |
Fetch the file record (a frozen File dataclass). |
update_metadata(id, tags, mode="merge"|"replace") |
Patch tags. |
delete(id) |
Soft-delete. |
list(...) / iter_files(...) |
One page / lazy iteration over every match. |
health() / ready() |
Liveness / readiness probes (raise on failure). |
Configuration
CairnMark(base_url, ...) / AsyncCairnMark(base_url, ...) keyword options:
| Option | Default | Does |
|---|---|---|
headers |
None |
Default headers on every request; the hook for gateway credentials. |
timeout |
30.0 |
httpx per-operation timeout (connect / single socket read) — a float or an httpx.Timeout for fine-grained control. Doesn't cut off large streams. |
retries |
2 |
Retries after a network error or 5xx (so up to retries + 1 attempts). 0 disables. |
user_agent |
cairnmark-python/<version> |
Override the User-Agent. |
transport |
None |
Custom httpx.BaseTransport (AsyncBaseTransport for async) — proxies, mocking, UDS. |
Errors
Every non-2xx response raises a subclass of APIError (which carries
.status, .message, and .retry_after on 409), itself a CairnMarkError:
InvalidRequestError (400) · NotFoundError (404) ·
IdempotencyConflictError (409) · IdempotencyGoneError (410) ·
TooLargeError (413) · RangeNotSatisfiableError (416) · ServerError (5xx)
— plus ChecksumMismatchError from verified download streams.
Semantics worth knowing
- Retries. Network errors and 5xx are retried with jittered backoff
(default 2 retries;
retries=to change). Uploads retry only when they carry an idempotency key and the body can be rewound (bytes or a seekable file) — otherwise a retry could duplicate the file. A 409 (same key still in flight) waits out the server'sRetry-After. - Checksum verification. The presigned object-store response carries no
checksum header, so
verify=Truehashes the stream client-side against the stored SHA-256 (fetched from metadata) and raisesChecksumMismatchErroras the last chunk is consumed. Range downloads can't be verified. - Timeouts.
timeout=is httpx's per-operation timeout (connect, single socket read), so large streams aren't cut off mid-transfer. - Auth. The server has none; put it behind your gateway and inject
credentials with
headers=(or a customtransport=).
Development
uv sync
uv run pytest # unit tests (respx-mocked, no server needed)
uv run ruff check src tests && uv run pyright src
# integration: full round-trip against a live server
(cd ../CairnMark && docker compose up -d --build)
uv run pytest -m integration # honors CAIRNMARK_BASE_URL, default localhost:8080
License
MIT © 2026 Michael Ramirez
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 Distribution
Built Distribution
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 cairnmark-0.1.0.tar.gz.
File metadata
- Download URL: cairnmark-0.1.0.tar.gz
- Upload date:
- Size: 29.0 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: uv/0.11.23 {"installer":{"name":"uv","version":"0.11.23","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"macOS","version":null,"id":null,"libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
683a2fc45e84da6030cf5f24b4b996215332481846cf27cac1d9d4741b8f0880
|
|
| MD5 |
c645d7ea175c58c5c9eddc1e3b3b2120
|
|
| BLAKE2b-256 |
4d16b5874394a0eaf7941be7a6751ce49960dc5e3f893a9d64da61341ac88f39
|
File details
Details for the file cairnmark-0.1.0-py3-none-any.whl.
File metadata
- Download URL: cairnmark-0.1.0-py3-none-any.whl
- Upload date:
- Size: 17.5 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: uv/0.11.23 {"installer":{"name":"uv","version":"0.11.23","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"macOS","version":null,"id":null,"libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
d3337ef6436c096859e72f09cacc7dd9f104a2701f77a61022ff387def17829d
|
|
| MD5 |
e821688d19ea2fa6af2445ae572109d1
|
|
| BLAKE2b-256 |
bdea2e68e49e1b95d182c54c12d00d18378236329f96d882092c888817de555f
|