Skip to content

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) take scope_ids= to erase only one tenant's entries, failing closed like Visibility; the FastAPI set_actor() takes scope_id=; ActivityData and FieldChange are 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_closed raises AuditWriteError and on_error="log" logs, instead of a raw driver error escaping alog()

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 with DETACH ... CONCURRENTLY, finishing interrupted detaches; PartitionManager.health() reports partition coverage ahead, pending detaches and orphaned tables
  • ensure_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 relationships
  • AuditTrail.install(): the session listener writes one entity.created/updated/deleted row per flush and object, with a lazily created audit_transaction row per database transaction, inside the business transaction; on_error="log" isolates audit write failures in a savepoint
  • AuditTrail gains default_severity, system_severity, events and global_redact, and exposes context(), set_actor(), bind(), pseudonymize() and maintenance
  • Audit options read a column never set on a new instance as None instead of falling back as if it were not loaded
  • AuditTrail.log(): records an explicit event in the session's transaction, with payload validation, Pseudonymized payload fields, a per-entry actor= and a Target (also returned by the target option); warn_on_bulk warns about bulk UPDATE/DELETE statements on audited tables, which bypass the trail
  • audit.query.list_groups() / alist_groups(): lists audit entries grouped by transaction, newest first, with keyset pagination (Cursor), filters, Visibility restrictions and read-time compaction of per-flush rows; queries prune partitions and force custom plans
  • Async sessions: AuditTrail.install() accepts an async_sessionmaker or AsyncSession subclass with a sync_session_class, and AuditTrail.alog() records explicit events from async code
  • Durable and fail_closed events are written on a separate small pool and committed before log()/alog() returns, surviving the caller's rollback; a missing partition is created and the write retried once; fail_closed failures raise AuditWriteError
  • AuditTrail.dispose() / adispose() close the durable pool the library created
  • audit_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: AuditMiddleware fills 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, and set_actor() records the actor after authentication, also from sync dependencies
  • audit.query.get(), object_history(), related() and access_summary() (with async variants): one entry in detail, an object's history, entries of related objects and access counts; LabelResolver turns stored ids in changes into display labels
  • AuditTrail.scrub() / scrub_actor() (and async variants), enabled with allow_scrub=True: erase an object's stored values or an actor's personal context in one transaction, recorded as an audit.scrubbed entry; erased values are never merged away when entries are grouped
  • log() / alog() accept just the event and take the session from AuditTrail(session_provider=...); a missing session or one of the wrong kind raises a clear error
  • AuditTrail(default_query_window=...): list_groups() without since reads only that window; since=ALL_HISTORY reads the whole log, the cursor keeps the first page's bound, and Page.since tells where the window started
  • Under AsyncSession, loading an expired column of an Audited model (for example assigning to it after a commit) or an unloaded tracked collection raises AsyncLoadError, a MissingGreenlet subclass 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 overridden get_bind), one audit_transaction row per database; log() without obj and the query API use a bind of the audit tables, otherwise raise a clear UnboundExecutionError

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.

  • AuditContext and Actor: request context set with context(), updated in place with set_actor(), or attached to one session with bind()
  • Value serialization for audit entries and keyed HMAC pseudonyms with versioned keys; Pseudonymized[...] marks payload fields
  • AuditEvent with per-event severity, payload schema and durability, declared with event(); the default Severity levels; a registry that validates events against the configured severity enum
  • Relationship tracking: net added/removed changes 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/hash policies, and the Audited mixin with AuditOptions for labels, scope and target read from loaded attributes only