Audit Trail
AuditMixin adds columns to track who created and updated each record and when. The columns are always present on models that use the mixin; the behavior (auto-populating fields from the request) is activated separately via a Pyramid directive.
Columns
| Column | Type | Set on |
|---|---|---|
created_at |
DateTime |
Insert (default: datetime.now(UTC)) |
updated_at |
DateTime |
Update (via onupdate) |
created_by |
String(40) |
Insert (from request.authenticated_userid) |
updated_by |
String(40) |
Update (from request.authenticated_userid) |
created_ip |
String(40) |
Insert (from request.client_addr) |
updated_ip |
String(40) |
Update (from request.client_addr) |
Usage
Adding audit columns to a model
Use AuditMixin directly or inherit from Model (which includes it):
from pyramid_sa import Base
from pyramid_sa.models.audit import AuditMixin
class Item(AuditMixin, Base):
__tablename__ = "items"
id: Mapped[int] = mapped_column(primary_key=True)
name: Mapped[str] = mapped_column(String(255))
Or use Model which bundles AuditMixin + SoftDeleteMixin + Base:
from pyramid_sa import Model
class Item(Model):
__tablename__ = "items"
id: Mapped[int] = mapped_column(primary_key=True)
name: Mapped[str] = mapped_column(String(255))
Activating automatic field population
The columns exist on the model regardless, but to have created_by, updated_by, created_ip, and updated_ip populated automatically from the request, call the directive in your app factory:
config.include("pyramid_sa")
config.sa_enable_audit()
This registers before_insert and before_update event listeners on the SQLAlchemy mapper. The listeners:
- Check if the target model is an
AuditMixininstance - Read
request.authenticated_useridfrom the session'sinfo["request"] - Set
created_by/updated_by(truncated to 40 chars) andcreated_ip/updated_ip
Columns vs behavior
Mixins provide columns at import time. Directives activate behavior at app startup.
A model can have AuditMixin columns without calling sa_enable_audit(). In that case, created_at and updated_at still work (they use SQLAlchemy's default and onupdate), but created_by, updated_by, created_ip, and updated_ip will remain NULL unless you set them manually.
This separation is intentional — it allows shared schemas where some apps populate audit fields and others don't.