Changelog¶
Unreleased¶
0.2.0 - 2026-09-27¶
- Repository: a reproducible benchmark (
benchmark/) and an example multi-tenant FastAPI application (examples/fastapi_app/), neither part of the package scrub()/scrub_actor()(and async variants) takescope_ids=to erase only one tenant's entries, failing closed likeVisibility; the FastAPIset_actor()takesscope_id=;ActivityDataandFieldChangeare exported for typing API responses- With asyncpg, a durable engine that cannot connect (refused, unresolvable host, timeout, or on SQLAlchemy 2.0 a missing database or bad password) now follows the durable write policy:
fail_closedraisesAuditWriteErrorandon_error="log"logs, instead of a raw driver error escapingalog()
0.1.0 - 2026-09-26¶
First release with the full audit trail: the flush listener, domain events, durable writes, async sessions, the read API, erasure, retention, the FastAPI integration and testing helpers. The API may still change before 1.0.
PartitionManager.drop_expired(): drops monthly partitions past their per-severity retention withDETACH ... CONCURRENTLY, finishing interrupted detaches;PartitionManager.health()reports partition coverage ahead, pending detaches and orphaned tablesensure_partitions()works with tables declared without a schema and returns schema-qualified names- Renovate opens pull requests for dependency and GitHub Actions updates
check_models(): static checks for CI that report sensitive-looking columns without an audit policy, JSON columns whose in-place changes go undetected, relationships tracked on both sides, and tracked names that are not collection relationshipsAuditTrail.install(): the session listener writes oneentity.created/updated/deletedrow per flush and object, with a lazily createdaudit_transactionrow per database transaction, inside the business transaction;on_error="log"isolates audit write failures in a savepointAuditTrailgainsdefault_severity,system_severity,eventsandglobal_redact, and exposescontext(),set_actor(),bind(),pseudonymize()andmaintenance- Audit options read a column never set on a new instance as
Noneinstead of falling back as if it were not loaded AuditTrail.log(): records an explicit event in the session's transaction, with payload validation,Pseudonymizedpayload fields, a per-entryactor=and aTarget(also returned by thetargetoption);warn_on_bulkwarns about bulkUPDATE/DELETEstatements on audited tables, which bypass the trailaudit.query.list_groups()/alist_groups(): lists audit entries grouped by transaction, newest first, with keyset pagination (Cursor), filters,Visibilityrestrictions and read-time compaction of per-flush rows; queries prune partitions and force custom plans- Async sessions:
AuditTrail.install()accepts anasync_sessionmakerorAsyncSessionsubclass with async_session_class, andAuditTrail.alog()records explicit events from async code - Durable and
fail_closedevents are written on a separate small pool and committed beforelog()/alog()returns, surviving the caller's rollback; a missing partition is created and the write retried once;fail_closedfailures raiseAuditWriteError AuditTrail.dispose()/adispose()close the durable pool the library createdaudit_trail.testing:assert_audited()/assert_not_audited()(and async variants) that show the closest entries and their differences on failure, table setup helpers, and an opt-in pytest plugin (pytest_plugins = ["audit_trail.pytest_plugin"])audit_trail.integrations.fastapi:AuditMiddlewarefills the request part of the audit context (client IP behind trusted proxies, user agent, method, path, request id),session_dependency()yields sessions for sync and async factories, andset_actor()records the actor after authentication, also from sync dependenciesaudit.query.get(),object_history(),related()andaccess_summary()(with async variants): one entry in detail, an object's history, entries of related objects and access counts;LabelResolverturns stored ids in changes into display labelsAuditTrail.scrub()/scrub_actor()(and async variants), enabled withallow_scrub=True: erase an object's stored values or an actor's personal context in one transaction, recorded as anaudit.scrubbedentry; erased values are never merged away when entries are groupedlog()/alog()accept just the event and take the session fromAuditTrail(session_provider=...); a missing session or one of the wrong kind raises a clear errorAuditTrail(default_query_window=...):list_groups()withoutsincereads only that window;since=ALL_HISTORYreads the whole log, the cursor keeps the first page's bound, andPage.sincetells where the window started- Under
AsyncSession, loading an expired column of anAuditedmodel (for example assigning to it after a commit) or an unloaded tracked collection raisesAsyncLoadError, aMissingGreenletsubclass that names the attribute and the fix - Sessions without a default bind work: audit rows go on the connection the flush used for the object (per mapper or table
binds, or an overriddenget_bind), oneaudit_transactionrow per database;log()withoutobjand the query API use a bind of the audit tables, otherwise raise a clearUnboundExecutionError
0.1.0a1 - 2026-09-25¶
Pre-release to verify the publishing pipeline. The flush listener and the read API are not included yet, so no audit entries are written.
AuditContextandActor: request context set withcontext(), updated in place withset_actor(), or attached to one session withbind()- Value serialization for audit entries and keyed HMAC pseudonyms with versioned keys;
Pseudonymized[...]marks payload fields AuditEventwith per-event severity, payload schema and durability, declared withevent(); the defaultSeveritylevels; a registry that validates events against the configured severity enum- Relationship tracking: net
added/removedchanges of chosen collection relationships, captured from ORM events - Audit tables partitioned by severity and month, DDL helpers for migrations, and
PartitionManager.ensure_partitions(), which creates missing partitions under a lock timeout - Entity change sets with per-column
exclude/redact/hashpolicies, and theAuditedmixin withAuditOptionsfor labels, scope and target read from loaded attributes only