audit_trail.relations¶
Relationship deltas captured from attribute events.
Collection changes are recorded from the ORM's append / remove
attribute events, not from attr.history: history is reset by every flush,
so a removal written by an autoflush between two collection operations is no
longer visible when the unit of work ends. Events see every operation once,
and their cost is proportional to the delta, not to the collection size.
Loading a collection from the database (lazy, selectinload,
joinedload, refresh) populates it without firing these events, so a
load never looks like an edit.
Known limitations: changes that bypass the collection API (writing a foreign
key column directly, raw inserts into an association table) are invisible,
and a backref that moves an object away from a parent that is not loaded in
the session fires no remove on that parent.
RelationshipChange ¶
Bases: TypedDict
Net change of one tracked collection since the last flush.
Attributes:
| Name | Type | Description |
|---|---|---|
added |
list[str]
|
Object ids of members added to the collection. |
removed |
list[str]
|
Object ids of members removed from it. |
track_relationships ¶
Record net added / removed changes of collection relationships.
Registers append and remove listeners on each attribute (also
covering assignment, clear(), slicing and del, which emit the same
events) and an expire listener on its class. Listeners propagate to
subclasses, so registering an attribute already tracked on the same class
or a base class is a no-op.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
*attributes
|
InstrumentedAttribute[Any]
|
Class-bound collection relationships, e.g. |
()
|
Raises:
| Type | Description |
|---|---|
ValueError
|
If an attribute is not a collection |
Source code in audit_trail/relations.py
pop_relationship_changes ¶
Return and clear the net collection changes of obj.
Meant to be called from after_flush or later, when every added item
has a primary key. Items that still have none were not written by the flush
(SQLAlchemy skips objects not cascaded into the session), so they are
left out, matching the database.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
obj
|
object
|
A mapped instance whose relationships are tracked. |
required |
Returns:
| Type | Description |
|---|---|
dict[str, RelationshipChange]
|
|
dict[str, RelationshipChange]
|
each tracked attribute with a non-empty net change; empty when nothing |
dict[str, RelationshipChange]
|
changed. |
Source code in audit_trail/relations.py
discard_relationship_changes ¶
Drop the pending deltas of every object attached to session.
A belt-and-braces call for rollback listeners: the expire listener
already drops deltas whenever the ORM discards unflushed collection
state. Pending objects expunged by a rollback keep their deltas, just as
they keep their in-memory collections, so re-adding them to a session
reports what that session's flush writes.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
session
|
Session
|
The session being rolled back. |
required |