{"id":51544980,"url":"https://github.com/robertolima-dev/rust-py-audit","last_synced_at":"2026-07-09T17:01:53.193Z","repository":{"id":365553656,"uuid":"1272578864","full_name":"robertolima-dev/rust-py-audit","owner":"robertolima-dev","description":null,"archived":false,"fork":false,"pushed_at":"2026-06-17T22:14:59.000Z","size":42,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-06-17T22:15:44.775Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":null,"language":"Rust","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/robertolima-dev.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-06-17T18:42:30.000Z","updated_at":"2026-06-17T22:10:41.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/robertolima-dev/rust-py-audit","commit_stats":null,"previous_names":["robertolima-dev/rust-py-audit"],"tags_count":5,"template":false,"template_full_name":null,"purl":"pkg:github/robertolima-dev/rust-py-audit","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/robertolima-dev%2Frust-py-audit","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/robertolima-dev%2Frust-py-audit/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/robertolima-dev%2Frust-py-audit/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/robertolima-dev%2Frust-py-audit/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/robertolima-dev","download_url":"https://codeload.github.com/robertolima-dev/rust-py-audit/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/robertolima-dev%2Frust-py-audit/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":35306717,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-26T15:22:16.424Z","status":"online","status_checked_at":"2026-07-09T02:00:07.329Z","response_time":57,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2026-07-09T17:01:52.454Z","updated_at":"2026-07-09T17:01:53.180Z","avatar_url":"https://github.com/robertolima-dev.png","language":"Rust","funding_links":[],"categories":[],"sub_categories":[],"readme":"# rust-py-audit\n\n[![PyPI](https://img.shields.io/pypi/v/rust-py-audit?color=e8673a\u0026label=PyPI)](https://pypi.org/project/rust-py-audit/)\n[![Python](https://img.shields.io/pypi/pyversions/rust-py-audit?color=4b8bbe)](https://pypi.org/project/rust-py-audit/)\n[![License](https://img.shields.io/pypi/l/rust-py-audit?color=3fb950)](https://github.com/robertolima-dev/rust-py-audit/blob/main/LICENSE)\n[![GitHub](https://img.shields.io/github/stars/robertolima-dev/rust-py-audit?style=flat\u0026color=e8673a)](https://github.com/robertolima-dev/rust-py-audit)\n\n🌐 **[rust-py-audit.vercel.app](https://rust-py-audit.vercel.app/)**\n\nEvent audit logging library for Python applications, with a Rust core.\n\nRecords audit events (`who did what, when, on which resource`) in a fast, structured way, and chains each event to the previous one with SHA-256 — any later edit, deletion, or reordering of the log file is detectable with `verify()`.\n\n---\n\n## Features\n\n- **`AuditLogger`** — simple API: `log(...)`, `verify()`, `last_hash()`\n- **Hash chain (SHA-256)** — each event embeds the hash of the previous event; altering any recorded event breaks the chain in a detectable way\n- **JSONL storage** — one event per line, append-only, no database required\n- **Thread-safe** — a single `AuditLogger` can be shared across threads (e.g. a multi-threaded WSGI server, or one middleware instance serving concurrent requests); the hash chain stays linear under concurrency\n- **Free-form `metadata`** — any JSON-serializable `dict` (IP, reason, request_id, etc.)\n- **FastAPI middleware** — automatically logs state-changing requests (POST/PUT/PATCH/DELETE)\n- **Django middleware** — same idea, supports WSGI and ASGI\n- **[ImmutableLog](https://immutablelog.com/en/) integration** — `local`/`remote`/`hybrid` modes, automatic retry, pending queue, and `flush_pending()` (see [dedicated section](#immutablelog-integration))\n- **Rust core** — hash generation, serialization, I/O, and the ImmutableLog HTTP client all run in Rust via PyO3; the Python API stays simple\n\n---\n\n## Requirements\n\n- Python 3.10+\n- No required runtime dependencies\n\nOptional, installed separately:\n- `fastapi` + `starlette` — for `rust_py_audit.fastapi.AuditMiddleware`\n- `django` — for `rust_py_audit.django.AuditMiddleware`\n\n---\n\n## Installation\n\n```bash\npip install rust-py-audit\n```\n\nWith optional extras:\n\n```bash\npip install \"rust-py-audit[fastapi]\"\npip install \"rust-py-audit[django]\"\n```\n\n---\n\n## Quick Start\n\n```python\nfrom rust_py_audit import AuditLogger\n\naudit = AuditLogger(app_name=\"billing-api\", file_path=\"./audit.jsonl\")\n\nevent = audit.log(\n    actor_id=\"user_123\",\n    action=\"DELETE_INVOICE\",\n    resource=\"invoice\",\n    resource_id=\"inv_987\",\n    metadata={\"ip\": \"192.168.0.10\", \"reason\": \"duplicate invoice\"},\n)\n\nprint(event[\"id\"])     # uuid v4\nprint(event[\"hash\"])   # sha256, 64 hex characters\n\nprint(audit.last_hash())  # hash of the last recorded event\n\nresult = audit.verify()\nprint(result)\n# {\"valid\": True, \"total_events\": 1, \"last_hash\": \"...\"}\n```\n\n---\n\n## Chain integrity\n\nEach event records the hash of the previous event (`previous_hash`) and its own hash (`hash`), computed from the event's content + `previous_hash`. The first event in the chain has `previous_hash = null`.\n\n```json\n{\"id\":\"evt_123\",\"timestamp\":\"2026-06-17T10:00:00Z\",\"app_name\":\"billing-api\",\"actor_id\":\"user_123\",\"action\":\"DELETE_INVOICE\",\"resource\":\"invoice\",\"resource_id\":\"inv_987\",\"metadata\":{\"ip\":\"192.168.0.10\"},\"previous_hash\":null,\"hash\":\"abc123...\"}\n```\n\n`verify()` re-reads the file from scratch and recomputes everything — it never trusts any in-memory cache:\n\n```python\nresult = audit.verify()\n```\n\nIf the chain is intact:\n\n```python\n{\"valid\": True, \"total_events\": 10, \"last_hash\": \"...\"}\n```\n\nIf any event was edited, removed, or reordered:\n\n```python\n{\"valid\": False, \"total_events\": 10, \"error_index\": 4, \"reason\": \"hash_mismatch\"}\n# or \"reason\": \"broken_chain\" (removed/reordered/forged event)\n```\n\n---\n\n## FastAPI\n\n```python\nfrom fastapi import FastAPI\nfrom rust_py_audit.fastapi import AuditMiddleware\n\napp = FastAPI()\napp.add_middleware(AuditMiddleware, app_name=\"billing-api\", file_path=\"./audit.jsonl\")\n\n\n@app.delete(\"/invoices/{invoice_id}\")\nasync def delete_invoice(invoice_id: str):\n    return {\"deleted\": invoice_id}\n```\n\nBy default, only `POST`/`PUT`/`PATCH`/`DELETE` requests are logged. `actor_id` comes from the `X-User-Id` header (adjustable via `actor_header=`); falls back to `\"anonymous\"` if absent.\n\nSee the full example in [`examples/fastapi_app.py`](examples/fastapi_app.py).\n\n---\n\n## Django\n\n```python\n# settings.py\nMIDDLEWARE = [\n    \"rust_py_audit.django.AuditMiddleware\",\n    # ... other middlewares ...\n]\n\n# Optional:\nRUST_PY_AUDIT_APP_NAME = \"my-django-app\"\nRUST_PY_AUDIT_FILE_PATH = \"./audit.jsonl\"\nRUST_PY_AUDIT_METHODS = {\"POST\", \"PUT\", \"PATCH\", \"DELETE\"}\n```\n\n`actor_id` comes from `request.user.pk` when there's an authenticated user (via `django.contrib.auth`); falls back to `\"anonymous\"` otherwise. The middleware supports both WSGI and ASGI applications automatically.\n\nSee the full example in [`examples/django_example/`](examples/django_example/).\n\n---\n\n## ImmutableLog Integration\n\n`rust_py_audit` can send each audit event to [ImmutableLog](https://immutablelog.com/en/) ([documentation](https://immutablelog.com/en/documentation/)), in addition to — or instead of — writing locally.\n\n### Operating modes\n\n| `mode` | Writes local JSONL | Sends to ImmutableLog | Typical use |\n|---|---|---|---|\n| `\"local\"` (default) | ✅ | ❌ | The library's original behavior, no external dependency |\n| `\"remote\"` | ❌ (except for pending entries, see below) | ✅ | ImmutableLog is the single source of truth; a delivery failure raises an exception |\n| `\"hybrid\"` | ✅ | ✅ | Local chain + remote receipt; a delivery failure NEVER raises — a transient (retryable) failure becomes `status=\"pending\"` (queued for `flush_pending()`), a permanent one becomes `status=\"failed\"` (not queued) |\n\n`mode=\"local\"` is the default — existing code calling `AuditLogger(app_name, file_path)` keeps working unchanged.\n\n### Basic example\n\n```python\nfrom rust_py_audit import AuditLogger\n\naudit = AuditLogger(\n    app_name=\"billing-api\",\n    file_path=\"./audit.jsonl\",\n    mode=\"hybrid\",\n    immutablelog_url=\"https://api.immutablelog.com\",\n    immutablelog_api_key=\"iml_live_xxxxx\",\n    timeout_ms=500,\n    retry_enabled=True,\n    max_retries=3,\n)\n\nevent = audit.log(\n    actor_id=\"user_123\",\n    action=\"DELETE_INVOICE\",\n    resource=\"invoice\",\n    resource_id=\"inv_987\",\n    metadata={\"ip\": \"192.168.0.10\", \"reason\": \"duplicate invoice\"},\n)\n\nprint(event[\"immutablelog\"])\n# {\"status\": \"delivered\", \"tx_id\": \"tx_...\", \"payload_hash\": \"...\", ...}\n# or {\"status\": \"pending\", \"tx_id\": None, ...} on a transient failure (hybrid mode)\n# or {\"status\": \"failed\", \"tx_id\": None, ...} on a permanent failure (hybrid mode)\n\n# Retries delivery of every event still marked \"pending\":\nprint(audit.flush_pending())\n# {\"flushed\": 1, \"failed\": 0, \"still_pending\": 0, \"total\": 1}\n```\n\n### Environment variables\n\n`mode`, `immutablelog_url`, and `immutablelog_api_key` accept `None` (the default) to fall back to an environment variable — handy for not hardcoding credentials:\n\n```bash\nexport RUST_PY_AUDIT_MODE=hybrid\nexport IMMUTABLELOG_URL=https://api.immutablelog.com\nexport IMMUTABLELOG_API_KEY=iml_live_xxxxx\n```\n\n```python\n# Without passing mode/immutablelog_url/immutablelog_api_key explicitly,\n# they come from the environment variables above:\naudit = AuditLogger(app_name=\"billing-api\", file_path=\"./audit.jsonl\")\n```\n\nAn explicit parameter always takes priority over the environment variable. `mode=\"remote\"`/`\"hybrid\"` without `immutablelog_url`/`immutablelog_api_key` (neither as a parameter nor as an env var) raises `ValueError` when the `AuditLogger` is created — failing fast instead of only on the first `log()` call.\n\n### Severity, `immutable_trail`, and `env`\n\n`audit.log(...)` accepts two optional parameters that only affect what gets sent to ImmutableLog (they never enter the hash):\n\n```python\nevent = audit.log(\n    actor_id=\"user_123\",\n    action=\"DELETE_INVOICE\",\n    resource=\"invoice\",\n    resource_id=\"inv_987\",\n    severity=\"error\",                    # meta.type — defaults to \"info\" if omitted\n    immutable_trail=\"order-2026-00441\",  # meta.immutable_trail — groups related events\n)\n```\n\n- `severity` must be one of `\"error\"`, `\"warning\"`, `\"info\"`, `\"success\"` — any other value raises `ValueError`, in any `mode` (even `\"local\"`, where `severity` is just stored without being used).\n- `immutable_trail` is sanitized automatically (trimmed, `:` replaced with `-`, truncated at 256 chars); if it ends up empty after that, the field is omitted instead of being sent broken.\n- Both are preserved locally (without affecting `hash`) precisely so that `flush_pending()` can resend later with the same original classification.\n- `immutablelog_env` (on the `AuditLogger` constructor, falling back to the `IMMUTABLELOG_ENV` env var) sets `meta.env` — useful for telling `staging`/`production` apart in ImmutableLog.\n\n### FastAPI\n\n```python\nfrom fastapi import FastAPI\nfrom rust_py_audit.fastapi import AuditMiddleware\n\napp = FastAPI()\napp.add_middleware(\n    AuditMiddleware,\n    app_name=\"billing-api\",\n    file_path=\"./audit.jsonl\",\n    mode=\"hybrid\",\n    immutablelog_url=\"https://api.immutablelog.com\",\n    immutablelog_api_key=\"iml_live_xxxxx\",\n    immutablelog_env=\"production\",\n    trail_header=\"X-Audit-Trail\",  # default — read from the request, becomes meta.immutable_trail\n)\n```\n\nIn `mode=\"remote\"`/`\"hybrid\"`, the middleware computes `severity` automatically from the response's `status_code` (`\u003e=400` → `\"error\"`, `300-399` → `\"info\"`, `200-299` → `\"success\"`). In `mode=\"remote\"`, if delivery fails the middleware logs a `logging.warning(...)` and moves on — an audit failure never takes down the actual response already computed by the application.\n\n### Django\n\n```python\n# settings.py\nMIDDLEWARE = [\n    \"rust_py_audit.django.AuditMiddleware\",\n    # ... other middlewares ...\n]\n\nRUST_PY_AUDIT_MODE = \"hybrid\"\nRUST_PY_AUDIT_FILE_PATH = \"./audit.jsonl\"\nRUST_PY_AUDIT_IMMUTABLELOG_URL = \"https://api.immutablelog.com\"\nRUST_PY_AUDIT_IMMUTABLELOG_API_KEY = \"iml_live_xxxxx\"\nRUST_PY_AUDIT_IMMUTABLELOG_ENV = \"production\"\nRUST_PY_AUDIT_TRAIL_HEADER = \"X-Audit-Trail\"  # default — read from the request, becomes meta.immutable_trail\n```\n\nSame behavior as FastAPI: `severity` computed from `status_code`, and delivery failures logged via `logging.warning(...)` without affecting the response.\n\n### Retry and idempotency\n\n- `retry_enabled`/`max_retries` control how many times a **retryable** failure is retried (the same `Idempotency-Key` is used on every attempt — never creates duplicate events on ImmutableLog).\n- **Retryable**: `5xx` and timeouts.\n- **Permanent** (never retried): `400`, `401`, `403`, `429`, and any other client error.\n- In `mode=\"remote\"`, exhausting retries (or a permanent error) raises `RuntimeError`.\n- In `mode=\"hybrid\"`, delivery never raises:\n  - a **retryable** failure (5xx/timeout) marks the event `status=\"pending\"` and queues it in `audit.pending.jsonl` — call `audit.flush_pending()` (manually, or from a cron/worker) to retry later;\n  - a **permanent** failure (4xx) marks the event `status=\"failed\"` and does **not** queue it (retrying would never succeed, so it stays out of the queue instead of getting stuck there forever). The event is still recorded locally and the chain stays valid — `\"failed\"` is just operational metadata.\n\nIn `mode=\"hybrid\"`, the event is appended to the local JSONL **once**, already carrying its final receipt (an O(1) append per event, not a full-file rewrite). A network failure still records the event locally (as `pending`/`failed`), so it is never lost to a failed delivery; the only loss window is the process being killed mid-request, and even then a delivery that did reach ImmutableLog is preserved in the remote store.\n\n### Integrity guarantee\n\nThe local hash (`event[\"hash\"]`) is computed **before** any delivery attempt and never includes the `immutablelog` field — the remote receipt is operational metadata, attached afterward, and never invalidates `verify()`:\n\n```python\naudit.log(...)        # hash computed, event already recorded/chained\naudit.flush_pending()  # only updates event[\"immutablelog\"]; event[\"hash\"] doesn't change\naudit.verify()         # still valid, even after flush_pending()\n```\n\n---\n\n## API Reference\n\n### `AuditLogger(app_name, file_path=\"./audit.jsonl\", mode=None, immutablelog_url=None, immutablelog_api_key=None, timeout_ms=500, retry_enabled=True, max_retries=3, immutablelog_env=None)`\n\n| Parameter | Type | Description |\n|---|---|---|\n| `app_name` | `str` | Application name, recorded on every event |\n| `file_path` | `str` | Path to the JSONL file. If it already exists, the chain resumes from the last recorded hash |\n| `mode` | `str \\| None` | `\"local\"` (default) / `\"remote\"` / `\"hybrid\"`. `None` falls back to `RUST_PY_AUDIT_MODE`, and finally to `\"local\"` |\n| `immutablelog_url` | `str \\| None` | ImmutableLog base URL. `None` falls back to `IMMUTABLELOG_URL`. Required (one way or another) in `mode=\"remote\"`/`\"hybrid\"` |\n| `immutablelog_api_key` | `str \\| None` | API key (`Bearer`). `None` falls back to `IMMUTABLELOG_API_KEY`. Same requirement as `immutablelog_url` |\n| `timeout_ms` | `int` | HTTP request timeout to ImmutableLog, in milliseconds |\n| `retry_enabled` | `bool` | If `True`, retries retryable errors (5xx, timeout) up to `max_retries` times |\n| `max_retries` | `int` | Maximum number of retries (in addition to the initial attempt) |\n| `immutablelog_env` | `str \\| None` | Logical environment (`meta.env`, e.g. `\"production\"`). `None` falls back to `IMMUTABLELOG_ENV`; if neither is set, the field is omitted |\n\nSee [ImmutableLog Integration](#immutablelog-integration) for details on each mode.\n\n---\n\n### `audit.log(actor_id, action, resource, resource_id, metadata=None, severity=None, immutable_trail=None) → dict`\n\nRecords an event and returns the full event (already with `id`, `timestamp`, `hash`, etc.) as a `dict`. `severity`/`immutable_trail` are optional and only affect delivery to ImmutableLog — see [Severity, immutable_trail, and env](#severity-immutable_trail-and-env).\n\n| Event field | Type | Description |\n|---|---|---|\n| `id` | `str` | UUID v4 |\n| `timestamp` | `str` | RFC3339 / UTC, e.g.: `2026-06-17T10:00:00Z` |\n| `app_name` | `str` | Comes from the `AuditLogger` |\n| `actor_id` | `str` | Who performed the action |\n| `action` | `str` | E.g.: `DELETE_INVOICE` |\n| `resource` | `str` | E.g.: `invoice` |\n| `resource_id` | `str` | E.g.: `inv_987` |\n| `metadata` | `dict` | Free-form — any JSON-serializable value |\n| `previous_hash` | `str \\| None` | Hash of the previous event in the chain |\n| `hash` | `str` | SHA-256 (64 hex chars) of the event + `previous_hash` |\n| `severity` | `str \\| absent` | Only present if passed to `log()`. Becomes `meta.type` on ImmutableLog |\n| `immutable_trail` | `str \\| absent` | Only present if passed to `log()` (and not empty after sanitization). Becomes `meta.immutable_trail` |\n| `immutablelog` | `dict \\| absent` | Only present in `mode=\"remote\"`/`\"hybrid\"`. `status` is `\"delivered\"`, `\"pending\"` (transient failure, queued), or `\"failed\"` (permanent failure, not queued); other fields (`tx_id`, `payload_hash`, `duplicate`, `request_id`, ...) come from the ImmutableLog response |\n\nIn `mode=\"remote\"`, a permanent failure or exhausted retries raise `RuntimeError` instead of returning the dict.\n\n---\n\n### `audit.verify() → dict`\n\nRe-reads the file and revalidates the entire chain from scratch. See [Chain integrity](#chain-integrity). Unaffected by the `immutablelog` field — only the hashed fields matter (see [Integrity guarantee](#integrity-guarantee)).\n\n---\n\n### `audit.last_hash() → str | None`\n\nHash of the last recorded event (in-memory cache, O(1)) — `None` if no event has been recorded yet.\n\n---\n\n### `audit.flush_pending() → dict`\n\nAttempts to redeliver to ImmutableLog every event marked as `pending` (recorded in `audit.pending.jsonl`, derived from `file_path`). Only relevant in `mode=\"hybrid\"` — other modes never populate this queue.\n\n```python\n{\"flushed\": 1, \"failed\": 0, \"still_pending\": 0, \"total\": 1}\n```\n\nFor each queued event:\n- **delivered** → updates `event[\"immutablelog\"]` in `audit.jsonl` to `\"delivered\"` (without changing `hash`) and removes it from the queue (counts toward `flushed`);\n- **permanent failure** → marks it `\"failed\"` in `audit.jsonl` and removes it from the queue, so it doesn't stay stuck forever (counts toward `failed`);\n- **retryable failure** → left in the queue for the next call (counts toward `still_pending`).\n\n---\n\n## Roadmap\n\n`rust-py-audit` is mature (v0.3.0): the SHA-256 hash chain, JSONL storage,\nFastAPI/Django middlewares, and the ImmutableLog `local`/`remote`/`hybrid`\nintegration are shipped and stable. Directional ideas under consideration (log\nrotation, incremental verification, more delivery backends, an async client, a\nFlask middleware) are tracked in [ROADMAP.md](./ROADMAP.md).\n\n---\n\n## Building from Source\n\nRequires Rust and [maturin](https://github.com/PyO3/maturin).\n\n```bash\ngit clone https://github.com/robertolima-dev/rust-py-audit\ncd rust-py-audit\n\npython3 -m venv .venv\nsource .venv/bin/activate\npip install maturin\n\n# Development build (installs into the current Python environment)\nmaturin develop\n\n# Release wheel\nmaturin build --release\n```\n\n### Running tests\n\n```bash\n# Rust unit tests\ncargo test --no-default-features\n\n# Python integration tests\npip install -e \".[dev]\"\npytest tests/\n```\n\n---\n\n## Architecture\n\n```\nPython API (rust_py_audit)\n    ├── AuditLogger(...)         ──► src/audit_logger.rs (PyO3 #[pyclass])\n    │       ├── log()            ──► src/event.rs               (AuditEvent)\n    │       │                    ──► src/hash.rs                (deterministic SHA-256)\n    │       │                    ──► src/storage.rs             (append/update in JSONL)\n    │       │                    ──► src/immutablelog_client.rs (POST /v1/events, via reqwest)\n    │       │                    ──► src/retry.rs               (retry with Idempotency-Key)\n    │       ├── verify()         ──► src/verifier.rs (revalidates the local chain)\n    │       ├── flush_pending()  ──► redelivers audit.pending.jsonl\n    │       └── last_hash()      ──► in-memory cache\n    │\n    ├── fastapi.AuditMiddleware ──► audit.log() on every mutating request\n    └── django.AuditMiddleware  ──► same idea, WSGI/ASGI\n```\n\n`src/immutablelog_config.rs` holds `AuditMode`/`ImmutableLogConfig`; `src/immutablelog_receipt.rs` defines the `ImmutableLogReceipt` attached to each event.\n\nThe core is compiled into a native extension (`.so`/`.pyd`) by [maturin](https://github.com/PyO3/maturin) and [PyO3](https://pyo3.rs). The Python layer is thin — it just routes calls and provides the framework adapters.\n\n---\n\n## License\n\nMIT — see [LICENSE](LICENSE).\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Frobertolima-dev%2Frust-py-audit","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Frobertolima-dev%2Frust-py-audit","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Frobertolima-dev%2Frust-py-audit/lists"}