flask-postgresql-audit 1.0.9__tar.gz → 1.1.1__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {flask_postgresql_audit-1.0.9 → flask_postgresql_audit-1.1.1}/.gitignore +4 -0
- {flask_postgresql_audit-1.0.9 → flask_postgresql_audit-1.1.1}/CHANGES.md +57 -0
- {flask_postgresql_audit-1.0.9 → flask_postgresql_audit-1.1.1}/PKG-INFO +130 -24
- flask_postgresql_audit-1.1.1/README.md +276 -0
- {flask_postgresql_audit-1.0.9 → flask_postgresql_audit-1.1.1}/flask_postgresql_audit/__init__.py +1 -1
- flask_postgresql_audit-1.1.1/flask_postgresql_audit/alembic/__init__.py +7 -0
- {flask_postgresql_audit-1.0.9 → flask_postgresql_audit-1.1.1}/flask_postgresql_audit/alembic/schema.py +5 -5
- flask_postgresql_audit-1.1.1/flask_postgresql_audit/alembic/setup.py +157 -0
- {flask_postgresql_audit-1.0.9 → flask_postgresql_audit-1.1.1}/flask_postgresql_audit/base.py +5 -6
- {flask_postgresql_audit-1.0.9 → flask_postgresql_audit-1.1.1}/flask_postgresql_audit/core.py +21 -12
- {flask_postgresql_audit-1.0.9 → flask_postgresql_audit-1.1.1}/flask_postgresql_audit/extensions/alembic_utils/pg_aggregate.py +1 -1
- {flask_postgresql_audit-1.0.9 → flask_postgresql_audit-1.1.1}/flask_postgresql_audit/extensions/document_staging/listener.py +10 -10
- {flask_postgresql_audit-1.0.9 → flask_postgresql_audit-1.1.1}/flask_postgresql_audit/extensions/document_staging/model.py +1 -2
- {flask_postgresql_audit-1.0.9 → flask_postgresql_audit-1.1.1}/flask_postgresql_audit/models.py +3 -3
- {flask_postgresql_audit-1.0.9 → flask_postgresql_audit-1.1.1}/flask_postgresql_audit/typing.py +1 -1
- {flask_postgresql_audit-1.0.9 → flask_postgresql_audit-1.1.1}/pyproject.toml +4 -4
- flask_postgresql_audit-1.1.1/tests/__init__.py +0 -0
- {flask_postgresql_audit-1.0.9 → flask_postgresql_audit-1.1.1}/tests/app.py +8 -5
- {flask_postgresql_audit-1.0.9 → flask_postgresql_audit-1.1.1}/tests/conftest.py +6 -8
- {flask_postgresql_audit-1.0.9 → flask_postgresql_audit-1.1.1}/tests/migrations/env.py +11 -5
- {flask_postgresql_audit-1.0.9 → flask_postgresql_audit-1.1.1}/tests/test_defaults.py +1 -1
- {flask_postgresql_audit-1.0.9 → flask_postgresql_audit-1.1.1}/tests/test_operators.py +5 -3
- {flask_postgresql_audit-1.0.9 → flask_postgresql_audit-1.1.1}/tests/utils.py +16 -9
- flask_postgresql_audit-1.0.9/README.md +0 -170
- flask_postgresql_audit-1.0.9/flask_postgresql_audit/alembic/__init__.py +0 -2
- flask_postgresql_audit-1.0.9/flask_postgresql_audit/alembic/setup.py +0 -95
- flask_postgresql_audit-1.0.9/flask_postgresql_audit/descriptors.py +0 -38
- flask_postgresql_audit-1.0.9/flask_postgresql_audit/extensions/__init__.py +0 -1
- flask_postgresql_audit-1.0.9/flask_postgresql_audit/templates/create_schema.sql +0 -2
- flask_postgresql_audit-1.0.9/flask_postgresql_audit/templates/drop_schema.sql +0 -1
- flask_postgresql_audit-1.0.9/flask_postgresql_audit/templates/row_audit_trigger.sql +0 -4
- flask_postgresql_audit-1.0.9/flask_postgresql_audit/templates/row_create_activity.sql +0 -48
- {flask_postgresql_audit-1.0.9 → flask_postgresql_audit-1.1.1}/LICENSE +0 -0
- {flask_postgresql_audit-1.0.9 → flask_postgresql_audit-1.1.1}/flask_postgresql_audit/alembic/entities.py +0 -0
- {flask_postgresql_audit-1.0.9/tests → flask_postgresql_audit-1.1.1/flask_postgresql_audit/extensions}/__init__.py +0 -0
- {flask_postgresql_audit-1.0.9 → flask_postgresql_audit-1.1.1}/flask_postgresql_audit/extensions/alembic_utils/__init__.py +0 -0
- {flask_postgresql_audit-1.0.9 → flask_postgresql_audit-1.1.1}/flask_postgresql_audit/extensions/document_staging/__init__.py +0 -0
- {flask_postgresql_audit-1.0.9 → flask_postgresql_audit-1.1.1}/flask_postgresql_audit/extensions/document_staging/enum.py +0 -0
- {flask_postgresql_audit-1.0.9 → flask_postgresql_audit-1.1.1}/flask_postgresql_audit/templates/audit_trigger_delete.sql +0 -0
- {flask_postgresql_audit-1.0.9 → flask_postgresql_audit-1.1.1}/flask_postgresql_audit/templates/audit_trigger_insert.sql +0 -0
- {flask_postgresql_audit-1.0.9 → flask_postgresql_audit-1.1.1}/flask_postgresql_audit/templates/audit_trigger_update.sql +0 -0
- {flask_postgresql_audit-1.0.9 → flask_postgresql_audit-1.1.1}/flask_postgresql_audit/templates/create_activity.sql +0 -0
- {flask_postgresql_audit-1.0.9 → flask_postgresql_audit-1.1.1}/flask_postgresql_audit/templates/get_pk_values.sql +0 -0
- {flask_postgresql_audit-1.0.9 → flask_postgresql_audit-1.1.1}/flask_postgresql_audit/templates/get_setting.sql +0 -0
- {flask_postgresql_audit-1.0.9 → flask_postgresql_audit-1.1.1}/flask_postgresql_audit/templates/get_table_relid.sql +0 -0
- {flask_postgresql_audit-1.0.9 → flask_postgresql_audit-1.1.1}/flask_postgresql_audit/templates/jsonb_subtract.sql +0 -0
- {flask_postgresql_audit-1.0.9 → flask_postgresql_audit-1.1.1}/flask_postgresql_audit/utils.py +0 -0
- {flask_postgresql_audit-1.0.9 → flask_postgresql_audit-1.1.1}/tests/migrations/README +0 -0
- {flask_postgresql_audit-1.0.9 → flask_postgresql_audit-1.1.1}/tests/migrations/alembic.ini +0 -0
- {flask_postgresql_audit-1.0.9 → flask_postgresql_audit-1.1.1}/tests/migrations/script.py.mako +0 -0
|
@@ -3,6 +3,63 @@
|
|
|
3
3
|
Here you can see the full list of changes between each Flask-PostgreSQL-Audit
|
|
4
4
|
release.
|
|
5
5
|
|
|
6
|
+
## 1.1.1 (2026-08-08)
|
|
7
|
+
|
|
8
|
+
- Fix test teardown downgrade error in migration listener.
|
|
9
|
+
- Fix `pg_audit_enabled` state thread-safety using `ContextVar`.
|
|
10
|
+
- Quote schema names and parameterize schema existence queries in Alembic operations.
|
|
11
|
+
- Remove unused templates and unused `hybridmethod` descriptor.
|
|
12
|
+
- Document `DocumentStaging` and `PGAggregate` extensions in `README.md`.
|
|
13
|
+
|
|
14
|
+
## 1.1.0 (2026-08-07)
|
|
15
|
+
|
|
16
|
+
- Bump `alembic` dependency to `>=1.18.0`.
|
|
17
|
+
- Refactor Alembic integration to use the official Plugin API and DispatchPriority system.
|
|
18
|
+
- Add multi-schema test matrix and schema-aware support to the test suite.
|
|
19
|
+
- Modernize Python type annotations, imports, string formatting, and clean up code using Ruff.
|
|
20
|
+
- Simplify context managers and type-hint fixtures in tests.
|
|
21
|
+
|
|
22
|
+
## 1.0.9 (2026-08-07)
|
|
23
|
+
|
|
24
|
+
- Change `fetch_activity` join clause to `outerjoin`.
|
|
25
|
+
- Resolve type checking errors.
|
|
26
|
+
|
|
27
|
+
## 1.0.8 (2026-03-30)
|
|
28
|
+
|
|
29
|
+
- Update `Audit` mixin class and refactor `fetch_activity` method, removing rowkey filter on class scope.
|
|
30
|
+
- Add PostgreSQL function `get_table_relid`.
|
|
31
|
+
- Fix `get_pk_values` returns wrong jsonb type if primary key columns are null.
|
|
32
|
+
|
|
33
|
+
## 1.0.7 (2025-12-15)
|
|
34
|
+
|
|
35
|
+
- Fix cartesian join when providing a table model to `fetch_activity()`.
|
|
36
|
+
|
|
37
|
+
## 1.0.6 (2025-12-15)
|
|
38
|
+
|
|
39
|
+
- Add `fetch_activity()` statement constructor with optional `verb` filtering, ordered by ID descending.
|
|
40
|
+
- Add `hybridmethod` descriptor.
|
|
41
|
+
- Add extension `document_staging`.
|
|
42
|
+
|
|
43
|
+
## 1.0.5 (2025-11-27)
|
|
44
|
+
|
|
45
|
+
- Add `alembic_utils` extension `pg_aggregate`.
|
|
46
|
+
|
|
47
|
+
## 1.0.4 (2025-11-17)
|
|
48
|
+
|
|
49
|
+
- Fix: use explicit schema prefix to call `get_pk_values()` in `create_activity`.
|
|
50
|
+
|
|
51
|
+
## 1.0.3 (2025-11-14)
|
|
52
|
+
|
|
53
|
+
- Fix: use schema prefix when calling `jsonb_subtract()` in audit trigger functions.
|
|
54
|
+
|
|
55
|
+
## 1.0.2 (2025-11-14)
|
|
56
|
+
|
|
57
|
+
- Support optional verbose value comparison in `jsonb_subtract()`.
|
|
58
|
+
|
|
59
|
+
## 1.0.1 (2025-10-30)
|
|
60
|
+
|
|
61
|
+
- Update authors in `pyproject.toml` and update `README.md`.
|
|
62
|
+
|
|
6
63
|
## 1.0.0 (2025-10-30)
|
|
7
64
|
|
|
8
65
|
- Forked from PostgreSQL-Audit.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
|
-
Name: flask-
|
|
3
|
-
Version: 1.
|
|
2
|
+
Name: flask-postgresql-audit
|
|
3
|
+
Version: 1.1.1
|
|
4
4
|
Summary: Versioning and auditing extension for PostgreSQL and SQLAlchemy.
|
|
5
5
|
Project-URL: Homepage, https://github.com/higamigu/flask-postgresql-audit
|
|
6
6
|
Author-email: Higamigu <higamigu@gmail.com>, Konsta Vesterinen <konsta@fastmonkeys.com>
|
|
@@ -19,7 +19,7 @@ Classifier: Topic :: Internet :: WWW/HTTP :: Dynamic Content
|
|
|
19
19
|
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
20
20
|
Requires-Python: >=3.11
|
|
21
21
|
Requires-Dist: alembic-utils>=0.8.8
|
|
22
|
-
Requires-Dist: alembic>=1.
|
|
22
|
+
Requires-Dist: alembic>=1.18.0
|
|
23
23
|
Requires-Dist: flask-sqlalchemy>=3.1.1
|
|
24
24
|
Requires-Dist: flask>=3.1.2
|
|
25
25
|
Requires-Dist: psycopg2>=2.9.11
|
|
@@ -52,7 +52,7 @@ uv add flask-postgresql-audit
|
|
|
52
52
|
```
|
|
53
53
|
or install directly from this repo
|
|
54
54
|
```
|
|
55
|
-
uv add git+https://github.com/higamigu/flask-postgresql-audit --tag v1.
|
|
55
|
+
uv add git+https://github.com/higamigu/flask-postgresql-audit --tag v1.1.0
|
|
56
56
|
```
|
|
57
57
|
|
|
58
58
|
## Usage
|
|
@@ -66,16 +66,18 @@ from my_app import app # your flask app
|
|
|
66
66
|
db = SQLAlchemy()
|
|
67
67
|
audit = PostgreSQLAudit()
|
|
68
68
|
|
|
69
|
+
|
|
69
70
|
class Article(db.Model, Audit):
|
|
70
|
-
__tablename__ =
|
|
71
|
+
__tablename__ = "article"
|
|
71
72
|
|
|
72
73
|
id = db.Column(db.Integer, primary_key=True)
|
|
73
74
|
name = db.Column(db.String)
|
|
74
75
|
|
|
76
|
+
|
|
75
77
|
db.init_app(app)
|
|
76
78
|
audit.init_app(app, db)
|
|
77
79
|
|
|
78
|
-
article = Article(name=
|
|
80
|
+
article = Article(name="Some article")
|
|
79
81
|
db.session.add(article)
|
|
80
82
|
db.session.commit()
|
|
81
83
|
```
|
|
@@ -89,24 +91,24 @@ Now we can check the newly created activity.
|
|
|
89
91
|
|
|
90
92
|
``` python
|
|
91
93
|
activity = db.session.scalar(select(audit.Activity))
|
|
92
|
-
activity.id
|
|
93
|
-
activity.table_name
|
|
94
|
-
activity.verb
|
|
95
|
-
activity.old_data
|
|
96
|
-
activity.changed_data
|
|
94
|
+
activity.id # 1
|
|
95
|
+
activity.table_name # 'article'
|
|
96
|
+
activity.verb # 'insert'
|
|
97
|
+
activity.old_data # None
|
|
98
|
+
activity.changed_data # {'id': '1', 'name': 'Some article'}
|
|
97
99
|
```
|
|
98
100
|
|
|
99
101
|
``` python
|
|
100
|
-
article.name =
|
|
102
|
+
article.name = "Some other article"
|
|
101
103
|
db.session.commit()
|
|
102
104
|
|
|
103
105
|
activity = db.session.scalar(select(audit.Activity).order_by(desc("id")))
|
|
104
|
-
activity.id
|
|
105
|
-
activity.table_name
|
|
106
|
-
activity.verb
|
|
107
|
-
activity.object_id
|
|
108
|
-
activity.old_data
|
|
109
|
-
activity.changed_data
|
|
106
|
+
activity.id # 2
|
|
107
|
+
activity.table_name # 'article'
|
|
108
|
+
activity.verb # 'update'
|
|
109
|
+
activity.object_id # 1
|
|
110
|
+
activity.old_data # {'id': '1', 'name': 'Some article'}
|
|
111
|
+
activity.changed_data # {'name': 'Some other article'}
|
|
110
112
|
```
|
|
111
113
|
|
|
112
114
|
``` python
|
|
@@ -114,14 +116,34 @@ db.session.delete(article)
|
|
|
114
116
|
db.session.commit()
|
|
115
117
|
|
|
116
118
|
activity = db.session.scalar(select(audit.Activity).order_by(desc("id")))
|
|
117
|
-
activity.id
|
|
118
|
-
activity.table_name
|
|
119
|
-
activity.verb
|
|
120
|
-
activity.object_id
|
|
121
|
-
activity.old_data
|
|
122
|
-
activity.changed_data
|
|
119
|
+
activity.id # 3
|
|
120
|
+
activity.table_name # 'article'
|
|
121
|
+
activity.verb # 'delete'
|
|
122
|
+
activity.object_id # 1
|
|
123
|
+
activity.old_data # {'id': '1', 'name': 'Some other article'}
|
|
124
|
+
activity.changed_data # None
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
## Querying Activity History
|
|
128
|
+
|
|
129
|
+
Instead of querying `audit.Activity` manually, you can use `fetch_activity()` to retrieve the audit trail for a model class, a single model instance, or a sequence/list of model instances:
|
|
130
|
+
|
|
131
|
+
``` python
|
|
132
|
+
# Query activity for a specific model class (e.g. Article)
|
|
133
|
+
stmt = audit.fetch_activity(Article)
|
|
134
|
+
activities = db.session.execute(stmt).all()
|
|
135
|
+
|
|
136
|
+
# Query activity for a specific article instance
|
|
137
|
+
stmt = audit.fetch_activity(article)
|
|
138
|
+
activities = db.session.execute(stmt).all()
|
|
139
|
+
|
|
140
|
+
# Query activity for multiple instances
|
|
141
|
+
stmt = audit.fetch_activity([article1, article2])
|
|
142
|
+
activities = db.session.execute(stmt).all()
|
|
123
143
|
```
|
|
124
144
|
|
|
145
|
+
The returned statement is a SQLAlchemy `select` object that joins `Activity` and `Transaction` tables, ordered by activity ID descending. You can further customize or execute this statement.
|
|
146
|
+
|
|
125
147
|
## Different Schema
|
|
126
148
|
You can isolate `pg_audit` objects entirely to a different schema by doing
|
|
127
149
|
|
|
@@ -159,6 +181,7 @@ You can customize actor id getter function by doing the following. Here is an ex
|
|
|
159
181
|
``` python
|
|
160
182
|
from flask_jwt_extended import jwt_required, current_user
|
|
161
183
|
|
|
184
|
+
|
|
162
185
|
@jwt_required(optional=True)
|
|
163
186
|
def actor_id_getter():
|
|
164
187
|
try:
|
|
@@ -166,6 +189,7 @@ def actor_id_getter():
|
|
|
166
189
|
except Exception:
|
|
167
190
|
return None
|
|
168
191
|
|
|
192
|
+
|
|
169
193
|
...
|
|
170
194
|
|
|
171
195
|
audit = PostgreSQLAudit(actor_id_getter=actor_id_getter)
|
|
@@ -188,6 +212,88 @@ handlers =
|
|
|
188
212
|
qualname = pg_audit
|
|
189
213
|
```
|
|
190
214
|
|
|
215
|
+
## Troubleshooting
|
|
216
|
+
|
|
217
|
+
### Activity not being recorded?
|
|
218
|
+
|
|
219
|
+
1. **Check triggers are installed**: Run `flask db upgrade` after installing the extension
|
|
220
|
+
2. **Verify audit is initialized**: Ensure you called `audit.init_app(app, db)` before using models
|
|
221
|
+
3. **Commit after changes**: Audit records are only created on `db.session.commit()`
|
|
222
|
+
|
|
223
|
+
### Actor tracking not working?
|
|
224
|
+
|
|
225
|
+
The default actor ID getter tries to import `flask_login.current_user`. If you're not using Flask-Login:
|
|
226
|
+
|
|
227
|
+
1. Provide your own `actor_id_getter` function:
|
|
228
|
+
```python
|
|
229
|
+
def my_actor_id():
|
|
230
|
+
return current_user.id if current_user.is_authenticated else None
|
|
231
|
+
```
|
|
232
|
+
|
|
233
|
+
2. Pass it to the extension: `audit = PostgreSQLAudit(actor_id_getter=my_actor_id)`
|
|
234
|
+
|
|
235
|
+
### Using a custom schema?
|
|
236
|
+
|
|
237
|
+
1. Set schema when creating audit: `audit = PostgreSQLAudit(schema_name="my_schema")`
|
|
238
|
+
2. Configure Alembic to track multiple schemas (see "Different Schema" section)
|
|
239
|
+
3. Add `"include_schemas=True"` to `alembic/env.py` in `run_migrations_online()`
|
|
240
|
+
|
|
241
|
+
### Fetching activity for custom models?
|
|
242
|
+
|
|
243
|
+
The `fetch_activity()` method works with:
|
|
244
|
+
- A model class: `audit.fetch_activity(MyModel)`
|
|
245
|
+
- A single instance: `audit.fetch_activity(my_instance)`
|
|
246
|
+
- Multiple instances: `audit.fetch_activity([instance1, instance2])`
|
|
247
|
+
|
|
248
|
+
## Extensions
|
|
249
|
+
|
|
250
|
+
### Document Staging
|
|
251
|
+
|
|
252
|
+
The `document_staging` extension provides document lifecycle management and version revision tracking for models.
|
|
253
|
+
|
|
254
|
+
```python
|
|
255
|
+
from flask_postgresql_audit.extensions.document_staging import (
|
|
256
|
+
DocumentStaging,
|
|
257
|
+
Docstatus,
|
|
258
|
+
attach_listener,
|
|
259
|
+
)
|
|
260
|
+
|
|
261
|
+
|
|
262
|
+
class Article(BaseModel, DocumentStaging):
|
|
263
|
+
__tablename__ = "article"
|
|
264
|
+
id: Mapped[int] = mapped_column(primary_key=True)
|
|
265
|
+
title: Mapped[str]
|
|
266
|
+
|
|
267
|
+
|
|
268
|
+
# Attach session listener for automatic actor and timestamp tracking
|
|
269
|
+
attach_listener()
|
|
270
|
+
```
|
|
271
|
+
|
|
272
|
+
#### Lifecycle & Transitions
|
|
273
|
+
|
|
274
|
+
- **`Docstatus` states**: `DRAFT` (0), `SUBMITTED` (1), `CANCELLED` (2), `REVISED` (3).
|
|
275
|
+
- **`doc.bump()`**: Advances document status (`DRAFT` -> `SUBMITTED` -> `CANCELLED`).
|
|
276
|
+
- **`doc.revise(new_doc)`**: Creates a new revision `new_doc` from a `CANCELLED` document and sets state to `REVISED`.
|
|
277
|
+
- **`doc.delete(session)`**: Deletes a `DRAFT` document. If it was a revision, reverts parent document status back to `CANCELLED`.
|
|
278
|
+
- **Automatic Fields**: `created_by`, `created_on`, `submitted_by`, `submitted_on`, `cancelled_by`, `cancelled_on` are automatically set during session flush events.
|
|
279
|
+
|
|
280
|
+
### Custom PostgreSQL Aggregates (`alembic_utils`)
|
|
281
|
+
|
|
282
|
+
`PGAggregate` extends `alembic_utils` to manage custom PostgreSQL aggregate functions in Alembic migrations.
|
|
283
|
+
|
|
284
|
+
```python
|
|
285
|
+
from flask_postgresql_audit.extensions.alembic_utils import PGAggregate
|
|
286
|
+
|
|
287
|
+
# Example: Define custom aggregate function entity
|
|
288
|
+
sum_agg = PGAggregate(
|
|
289
|
+
schema="public",
|
|
290
|
+
signature="custom_sum(numeric)",
|
|
291
|
+
definition="(SFUNC = numeric_add, STYPE = numeric, INITCOND = '0')",
|
|
292
|
+
)
|
|
293
|
+
```
|
|
294
|
+
|
|
295
|
+
Automatically detects and tracks custom `CREATE AGGREGATE` definitions during `alembic revision --autogenerate`.
|
|
296
|
+
|
|
191
297
|
## Running the tests
|
|
192
298
|
|
|
193
299
|
git clone https://github.com/higamigu/flask-postgresql-audit.git
|
|
@@ -0,0 +1,276 @@
|
|
|
1
|
+
# Flask-PostgreSQL-Audit
|
|
2
|
+
|
|
3
|
+

|
|
4
|
+
[](https://pypi.org/project/flask-postgresql-audit/)
|
|
5
|
+
|
|
6
|
+
Auditing extension for Flask-SQLAlchemy with PostgreSQL.
|
|
7
|
+
Forked from [PostgreSQL-Audit](https://github.com/kvesteri/postgresql-audit), tries to combine the best of breed from existing solutions such as
|
|
8
|
+
[SQLAlchemy-Continuum](https://github.com/kvesteri/SQLAlchemy-Continuum),
|
|
9
|
+
[Papertrail](https://github.com/airblade/paper_trail) and especially
|
|
10
|
+
[Audit Trigger by 2ndQuadrant](https://github.com/2ndQuadrant/audit-trigger).
|
|
11
|
+
|
|
12
|
+
- Stores audit recordss into single table called `pga_activity`
|
|
13
|
+
- Uses trigger based approach to keep INSERTs, UPDATEs
|
|
14
|
+
and DELETEs as fast as possible
|
|
15
|
+
- Tracks and stores actor identities into table called `pga_transaction`
|
|
16
|
+
- Uses Alembic and Alembic-Utils to generate necessary database objects for migration
|
|
17
|
+
|
|
18
|
+
## Installation
|
|
19
|
+
```
|
|
20
|
+
pip install flask-postgresql-audit
|
|
21
|
+
```
|
|
22
|
+
or using `uv`
|
|
23
|
+
```
|
|
24
|
+
uv add flask-postgresql-audit
|
|
25
|
+
```
|
|
26
|
+
or install directly from this repo
|
|
27
|
+
```
|
|
28
|
+
uv add git+https://github.com/higamigu/flask-postgresql-audit --tag v1.1.0
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
## Usage
|
|
32
|
+
|
|
33
|
+
``` python
|
|
34
|
+
from flask_sqlalchemy import SQLAlchemy
|
|
35
|
+
from flask_postgresql_audit import PostgreSQLAudit, Audit
|
|
36
|
+
|
|
37
|
+
from my_app import app # your flask app
|
|
38
|
+
|
|
39
|
+
db = SQLAlchemy()
|
|
40
|
+
audit = PostgreSQLAudit()
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class Article(db.Model, Audit):
|
|
44
|
+
__tablename__ = "article"
|
|
45
|
+
|
|
46
|
+
id = db.Column(db.Integer, primary_key=True)
|
|
47
|
+
name = db.Column(db.String)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
db.init_app(app)
|
|
51
|
+
audit.init_app(app, db)
|
|
52
|
+
|
|
53
|
+
article = Article(name="Some article")
|
|
54
|
+
db.session.add(article)
|
|
55
|
+
db.session.commit()
|
|
56
|
+
```
|
|
57
|
+
Then generate migration file
|
|
58
|
+
```
|
|
59
|
+
flask db migrate -m "pg audit initial migration"
|
|
60
|
+
flask db upgrade
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
Now we can check the newly created activity.
|
|
64
|
+
|
|
65
|
+
``` python
|
|
66
|
+
activity = db.session.scalar(select(audit.Activity))
|
|
67
|
+
activity.id # 1
|
|
68
|
+
activity.table_name # 'article'
|
|
69
|
+
activity.verb # 'insert'
|
|
70
|
+
activity.old_data # None
|
|
71
|
+
activity.changed_data # {'id': '1', 'name': 'Some article'}
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
``` python
|
|
75
|
+
article.name = "Some other article"
|
|
76
|
+
db.session.commit()
|
|
77
|
+
|
|
78
|
+
activity = db.session.scalar(select(audit.Activity).order_by(desc("id")))
|
|
79
|
+
activity.id # 2
|
|
80
|
+
activity.table_name # 'article'
|
|
81
|
+
activity.verb # 'update'
|
|
82
|
+
activity.object_id # 1
|
|
83
|
+
activity.old_data # {'id': '1', 'name': 'Some article'}
|
|
84
|
+
activity.changed_data # {'name': 'Some other article'}
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
``` python
|
|
88
|
+
db.session.delete(article)
|
|
89
|
+
db.session.commit()
|
|
90
|
+
|
|
91
|
+
activity = db.session.scalar(select(audit.Activity).order_by(desc("id")))
|
|
92
|
+
activity.id # 3
|
|
93
|
+
activity.table_name # 'article'
|
|
94
|
+
activity.verb # 'delete'
|
|
95
|
+
activity.object_id # 1
|
|
96
|
+
activity.old_data # {'id': '1', 'name': 'Some other article'}
|
|
97
|
+
activity.changed_data # None
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
## Querying Activity History
|
|
101
|
+
|
|
102
|
+
Instead of querying `audit.Activity` manually, you can use `fetch_activity()` to retrieve the audit trail for a model class, a single model instance, or a sequence/list of model instances:
|
|
103
|
+
|
|
104
|
+
``` python
|
|
105
|
+
# Query activity for a specific model class (e.g. Article)
|
|
106
|
+
stmt = audit.fetch_activity(Article)
|
|
107
|
+
activities = db.session.execute(stmt).all()
|
|
108
|
+
|
|
109
|
+
# Query activity for a specific article instance
|
|
110
|
+
stmt = audit.fetch_activity(article)
|
|
111
|
+
activities = db.session.execute(stmt).all()
|
|
112
|
+
|
|
113
|
+
# Query activity for multiple instances
|
|
114
|
+
stmt = audit.fetch_activity([article1, article2])
|
|
115
|
+
activities = db.session.execute(stmt).all()
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
The returned statement is a SQLAlchemy `select` object that joins `Activity` and `Transaction` tables, ordered by activity ID descending. You can further customize or execute this statement.
|
|
119
|
+
|
|
120
|
+
## Different Schema
|
|
121
|
+
You can isolate `pg_audit` objects entirely to a different schema by doing
|
|
122
|
+
|
|
123
|
+
``` python
|
|
124
|
+
from flask_postgresql_audit import PostgreSQLAudit
|
|
125
|
+
|
|
126
|
+
audit = PostgreSQLAudit(schema="audit")
|
|
127
|
+
|
|
128
|
+
...
|
|
129
|
+
```
|
|
130
|
+
And then you need to tell alembic to track other than `public` schema by adding following line in `alembic/env.py`
|
|
131
|
+
``` python
|
|
132
|
+
...
|
|
133
|
+
|
|
134
|
+
def run_migrations_online():
|
|
135
|
+
connectable = get_engine()
|
|
136
|
+
|
|
137
|
+
with connectable.connect() as connection:
|
|
138
|
+
context.configure(
|
|
139
|
+
connection=connection,
|
|
140
|
+
target_metadata=get_metadata(),
|
|
141
|
+
include_schemas=True, # add this arg
|
|
142
|
+
...
|
|
143
|
+
)
|
|
144
|
+
|
|
145
|
+
with context.begin_transaction():
|
|
146
|
+
context.run_migrations()
|
|
147
|
+
|
|
148
|
+
...
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
## Custom Actor ID getter
|
|
152
|
+
You can customize actor id getter function by doing the following. Here is an example using `current_user` from `flask_jwt_extended` library.
|
|
153
|
+
|
|
154
|
+
``` python
|
|
155
|
+
from flask_jwt_extended import jwt_required, current_user
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
@jwt_required(optional=True)
|
|
159
|
+
def actor_id_getter():
|
|
160
|
+
try:
|
|
161
|
+
return current_user.email or None
|
|
162
|
+
except Exception:
|
|
163
|
+
return None
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
...
|
|
167
|
+
|
|
168
|
+
audit = PostgreSQLAudit(actor_id_getter=actor_id_getter)
|
|
169
|
+
|
|
170
|
+
...
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
## Enable Alembic Logger
|
|
174
|
+
You can enable alembic logger for `pg_audit` by adding the following to your `alembic.ini`
|
|
175
|
+
``` ini
|
|
176
|
+
# Logging configuration
|
|
177
|
+
[loggers]
|
|
178
|
+
keys = root,sqlalchemy,alembic,alembic_utils,pg_audit # add 'pg_audit' here
|
|
179
|
+
|
|
180
|
+
...
|
|
181
|
+
|
|
182
|
+
[logger_pg_audit] # and add the logger for pg_audit here
|
|
183
|
+
level = INFO
|
|
184
|
+
handlers =
|
|
185
|
+
qualname = pg_audit
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
## Troubleshooting
|
|
189
|
+
|
|
190
|
+
### Activity not being recorded?
|
|
191
|
+
|
|
192
|
+
1. **Check triggers are installed**: Run `flask db upgrade` after installing the extension
|
|
193
|
+
2. **Verify audit is initialized**: Ensure you called `audit.init_app(app, db)` before using models
|
|
194
|
+
3. **Commit after changes**: Audit records are only created on `db.session.commit()`
|
|
195
|
+
|
|
196
|
+
### Actor tracking not working?
|
|
197
|
+
|
|
198
|
+
The default actor ID getter tries to import `flask_login.current_user`. If you're not using Flask-Login:
|
|
199
|
+
|
|
200
|
+
1. Provide your own `actor_id_getter` function:
|
|
201
|
+
```python
|
|
202
|
+
def my_actor_id():
|
|
203
|
+
return current_user.id if current_user.is_authenticated else None
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
2. Pass it to the extension: `audit = PostgreSQLAudit(actor_id_getter=my_actor_id)`
|
|
207
|
+
|
|
208
|
+
### Using a custom schema?
|
|
209
|
+
|
|
210
|
+
1. Set schema when creating audit: `audit = PostgreSQLAudit(schema_name="my_schema")`
|
|
211
|
+
2. Configure Alembic to track multiple schemas (see "Different Schema" section)
|
|
212
|
+
3. Add `"include_schemas=True"` to `alembic/env.py` in `run_migrations_online()`
|
|
213
|
+
|
|
214
|
+
### Fetching activity for custom models?
|
|
215
|
+
|
|
216
|
+
The `fetch_activity()` method works with:
|
|
217
|
+
- A model class: `audit.fetch_activity(MyModel)`
|
|
218
|
+
- A single instance: `audit.fetch_activity(my_instance)`
|
|
219
|
+
- Multiple instances: `audit.fetch_activity([instance1, instance2])`
|
|
220
|
+
|
|
221
|
+
## Extensions
|
|
222
|
+
|
|
223
|
+
### Document Staging
|
|
224
|
+
|
|
225
|
+
The `document_staging` extension provides document lifecycle management and version revision tracking for models.
|
|
226
|
+
|
|
227
|
+
```python
|
|
228
|
+
from flask_postgresql_audit.extensions.document_staging import (
|
|
229
|
+
DocumentStaging,
|
|
230
|
+
Docstatus,
|
|
231
|
+
attach_listener,
|
|
232
|
+
)
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
class Article(BaseModel, DocumentStaging):
|
|
236
|
+
__tablename__ = "article"
|
|
237
|
+
id: Mapped[int] = mapped_column(primary_key=True)
|
|
238
|
+
title: Mapped[str]
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
# Attach session listener for automatic actor and timestamp tracking
|
|
242
|
+
attach_listener()
|
|
243
|
+
```
|
|
244
|
+
|
|
245
|
+
#### Lifecycle & Transitions
|
|
246
|
+
|
|
247
|
+
- **`Docstatus` states**: `DRAFT` (0), `SUBMITTED` (1), `CANCELLED` (2), `REVISED` (3).
|
|
248
|
+
- **`doc.bump()`**: Advances document status (`DRAFT` -> `SUBMITTED` -> `CANCELLED`).
|
|
249
|
+
- **`doc.revise(new_doc)`**: Creates a new revision `new_doc` from a `CANCELLED` document and sets state to `REVISED`.
|
|
250
|
+
- **`doc.delete(session)`**: Deletes a `DRAFT` document. If it was a revision, reverts parent document status back to `CANCELLED`.
|
|
251
|
+
- **Automatic Fields**: `created_by`, `created_on`, `submitted_by`, `submitted_on`, `cancelled_by`, `cancelled_on` are automatically set during session flush events.
|
|
252
|
+
|
|
253
|
+
### Custom PostgreSQL Aggregates (`alembic_utils`)
|
|
254
|
+
|
|
255
|
+
`PGAggregate` extends `alembic_utils` to manage custom PostgreSQL aggregate functions in Alembic migrations.
|
|
256
|
+
|
|
257
|
+
```python
|
|
258
|
+
from flask_postgresql_audit.extensions.alembic_utils import PGAggregate
|
|
259
|
+
|
|
260
|
+
# Example: Define custom aggregate function entity
|
|
261
|
+
sum_agg = PGAggregate(
|
|
262
|
+
schema="public",
|
|
263
|
+
signature="custom_sum(numeric)",
|
|
264
|
+
definition="(SFUNC = numeric_add, STYPE = numeric, INITCOND = '0')",
|
|
265
|
+
)
|
|
266
|
+
```
|
|
267
|
+
|
|
268
|
+
Automatically detects and tracks custom `CREATE AGGREGATE` definitions during `alembic revision --autogenerate`.
|
|
269
|
+
|
|
270
|
+
## Running the tests
|
|
271
|
+
|
|
272
|
+
git clone https://github.com/higamigu/flask-postgresql-audit.git
|
|
273
|
+
cd flask-postgresql-audit
|
|
274
|
+
pip install tox
|
|
275
|
+
createdb postgresql_audit_test
|
|
276
|
+
tox
|
|
@@ -32,16 +32,16 @@ class SchemaDrop(SchemaOperation):
|
|
|
32
32
|
|
|
33
33
|
@Operations.implementation_for(SchemaCreate)
|
|
34
34
|
def create_schema(operations: Operations, operation: SchemaCreate):
|
|
35
|
-
stmt = """
|
|
36
|
-
CREATE SCHEMA {
|
|
37
|
-
REVOKE ALL ON SCHEMA {
|
|
38
|
-
"""
|
|
35
|
+
stmt = f"""
|
|
36
|
+
CREATE SCHEMA "{operation.schema_name}";
|
|
37
|
+
REVOKE ALL ON SCHEMA "{operation.schema_name}" FROM public;
|
|
38
|
+
"""
|
|
39
39
|
operations.execute(text(stmt))
|
|
40
40
|
|
|
41
41
|
|
|
42
42
|
@Operations.implementation_for(SchemaDrop)
|
|
43
43
|
def drop_schema(operations: Operations, operation: SchemaDrop):
|
|
44
|
-
stmt =
|
|
44
|
+
stmt = f'DROP SCHEMA "{operation.schema_name}" CASCADE;'
|
|
45
45
|
operations.execute(text(stmt))
|
|
46
46
|
|
|
47
47
|
|