Skip to main content

Typed function-call recorder backed by SQLite

Project description

recorded

@recorder turns each function call into a queryable typed row. Four properties together — that's the library, and the reason it isn't a logging tool you already have:

  • Transparent at the call site. The wrapped function still returns its natural value; adding or removing @recorder is a safe edit at every call site, with no shape change for callers.
  • Idempotent by key. Pass key="..." and the second call returns the recorded response without re-running the function. Same key in another process joins the in-flight work.
  • Projected, not just logged. Declare data=Model and that slot becomes a queryable index over the full payload — the wide response stays on disk faithfully; data is the slice you'll filter on.
  • Read API shaped for LLMs. Pull rows back as typed objects, filter by data fields, or render any row as prompt-ready context with Job.to_prompt().
from recorded import recorder, last

@recorder(kind="orders.place")
def place_order(symbol: str, qty: int) -> dict:
    return broker.place_order(symbol=symbol, qty=qty)

place_order("AAPL", 100)            # runs as normal — but recorded
print(last(1)[0].response)          # {'order_id': 'ord-9281', ...}

The substrate is one SQLite file (jobs.db), four slots per row, a three-write lifecycle. The bare-call path above is what most apps run; durable submission via a leader process is opt-in and lives in its own Advanced section below.

Privacy: recorded writes function arguments, return values, and exceptions to SQLite verbatim. The DB at jobs.db becomes a durable record of everything wrapped — including any secrets the function handled. Use recorded.fastapi.capture_request(redact_headers=...) for HTTP request capture; for general functions, redact sensitive arguments yourself before they cross the decorator boundary. See configuration for redaction options.


What it isn't

  • A multi-process job queue. SQLite WAL handles concurrent writers fine, but cross-process JobHandle.wait() falls back to polling at 200ms cadence — don't expect Redis-grade fan-out latency.
  • A query DSL. where_data= is equality on top-level keys, by design. Anything richer goes through recorded.connection() and raw SQL — the library doesn't grow a query language.
  • A schema-versioning library. One table, one schema, frozen.
  • A distributed-tracing replacement. Use OpenTelemetry for spans across services. Use recorded when you want a row per call you can grep tomorrow.
  • A heavy dep. Stdlib only for the core. Pydantic / FastAPI / Starlette are auto-detected if installed but never imported.

Idempotency — the second call is free

@recorder(kind="orders.place")
def place_order(req): ...

place_order(req, key="order-42")    # runs the function, records the row
place_order(req, key="order-42")    # returns the cached response — no broker call

Works across processes via a partial unique index on (kind, key). Failed jobs retry by default; retry_failed=False raises JoinedSiblingFailedError carrying the prior error payload instead.

Deeper: usage/idempotency.md.

Queryable projections — data=Model + recorded.query(...)

class OrderView(BaseModel):
    order_id: str
    customer_id: int

@recorder(kind="orders.place", data=OrderView)
def place_order(req): ...

# Auto-projects from the response (or use attach() for fields not on the response).
recorded.query(kind="orders.*", where_data={"customer_id": 7})

The data slot is the queryable index — declare what you'll filter by; everything else stays in the wide response slot. The full response is recorded faithfully (the audit invariant); data is just the slice you'll grep on.

Deeper: usage/typed-slots.md.

Advanced: durable submission via leader process

Most users won't need this. The bare @recorder path above runs in the caller's process, records the row, and returns — no extra infrastructure. .submit() is the opt-in tier for "fire here, run there, wait or come back later for the result":

handle = build_report.submit(spec, key="daily-2026-04-27")

job = handle.wait_sync(timeout=60.0)        # from sync code
job = await handle.wait(timeout=60.0)       # from async code

.submit() enqueues a pending row and returns a handle; a separate leader process (python -m recorded run --import myapp.tasks) claims and executes the row. Without a leader running, .submit() raises ConfigurationError rather than silently degrading; misconfigured deployments fail loudly on the first submit. The reaper sweeps orphaned running rows on the next leader-process bootstrap, so a crashed leader doesn't leave idempotency keys held forever. recorder.is_leader_running() is the deployment health check.

Deeper: usage/workers.md.


Where to read next

The full feature tour, in roughly this order. Each guide is independent — skip to the one you need.

guide covers
usage/decorator.md the decorator, sync vs async, cross-mode shims
usage/configuration.md configure(), lifecycles, multi-recorder safety
usage/reading.md last, get, query, connection, the CLI
usage/idempotency.md key=, retry_failed, the partial-unique-index trick
usage/typed-slots.md the four slots, attach(), attach_error()
usage/queries.md recorded.query(...), raw SQL, Job.to_prompt()
usage/fastapi.md lifespan integration, capture_request, two-shape pattern
usage/workers.md .submit(), the leader process, the reaper
usage/errors.md exception hierarchy, what to catch where

Or jump in by need — see the usage guide index.

Architecture: docs/HOW.md is the contributor tour; docs/WHY.md is the authoritative spec.

Runnable examples: docs/examples/ — LLM-CLI replay, FastAPI service, batch HTTP consumer.


Install

pip install recorded
  • Python 3.10+
  • SQLite 3.35.0+ (for UPDATE ... RETURNING). The stdlib sqlite3 on most modern platforms qualifies; RHEL 8 / older Debian may need a SQLite update.
  • Pydantic is optional. Auto-detected at import time if installed.

Local development

uv venv
uv pip install -e ".[dev]"
uv run pytest

pip install -e ".[dev]" works equivalently if uv is unavailable. The test suite uses real SQLite (no stdlib mocks) and runs in ~5 seconds.

Project details


Download files

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

Source Distribution

recorded-0.1.0.tar.gz (149.4 kB view details)

Uploaded Source

Built Distribution

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

recorded-0.1.0-py3-none-any.whl (51.4 kB view details)

Uploaded Python 3

File details

Details for the file recorded-0.1.0.tar.gz.

File metadata

  • Download URL: recorded-0.1.0.tar.gz
  • Upload date:
  • Size: 149.4 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.12

File hashes

Hashes for recorded-0.1.0.tar.gz
Algorithm Hash digest
SHA256 6c03e51e048d20bf9245617892a46eb0c9cc2ccb763819474bc337d0bd3e256a
MD5 5621289e96c9053c9da932452ac01f3e
BLAKE2b-256 f928f3c0b592a3c2253367d4bda8d9db32f317e70adf035e82d609753144f32d

See more details on using hashes here.

Provenance

The following attestation bundles were made for recorded-0.1.0.tar.gz:

Publisher: publish.yml on kgruel/recorded

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

File details

Details for the file recorded-0.1.0-py3-none-any.whl.

File metadata

  • Download URL: recorded-0.1.0-py3-none-any.whl
  • Upload date:
  • Size: 51.4 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.12

File hashes

Hashes for recorded-0.1.0-py3-none-any.whl
Algorithm Hash digest
SHA256 dd7a199014d7215e9e1c203039ea1cf8c7b55e145f1827f3e49fdad43deb99dc
MD5 22426f51a50ef5b9b9233dcdf66bd18c
BLAKE2b-256 649fbb04baae5cf911632471e301a8766254510f8785373970860e8bc109dc38

See more details on using hashes here.

Provenance

The following attestation bundles were made for recorded-0.1.0-py3-none-any.whl:

Publisher: publish.yml on kgruel/recorded

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