flask-postgresql-audit 1.1.0__tar.gz → 1.1.2__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.1.0 → flask_postgresql_audit-1.1.2}/.gitignore +2 -0
- {flask_postgresql_audit-1.1.0 → flask_postgresql_audit-1.1.2}/CHANGES.md +63 -0
- flask_postgresql_audit-1.1.2/PKG-INFO +326 -0
- flask_postgresql_audit-1.1.2/README.md +299 -0
- {flask_postgresql_audit-1.1.0 → flask_postgresql_audit-1.1.2}/flask_postgresql_audit/__init__.py +1 -1
- {flask_postgresql_audit-1.1.0 → flask_postgresql_audit-1.1.2}/flask_postgresql_audit/alembic/entities.py +9 -0
- {flask_postgresql_audit-1.1.0 → flask_postgresql_audit-1.1.2}/flask_postgresql_audit/alembic/schema.py +3 -3
- {flask_postgresql_audit-1.1.0 → flask_postgresql_audit-1.1.2}/flask_postgresql_audit/alembic/setup.py +30 -4
- {flask_postgresql_audit-1.1.0 → flask_postgresql_audit-1.1.2}/flask_postgresql_audit/core.py +13 -3
- flask_postgresql_audit-1.1.2/flask_postgresql_audit/templates/jsonb_array_subtract.sql +29 -0
- {flask_postgresql_audit-1.1.0 → flask_postgresql_audit-1.1.2}/flask_postgresql_audit/typing.py +1 -1
- {flask_postgresql_audit-1.1.0 → flask_postgresql_audit-1.1.2}/pyproject.toml +1 -1
- {flask_postgresql_audit-1.1.0 → flask_postgresql_audit-1.1.2}/tests/app.py +12 -0
- {flask_postgresql_audit-1.1.0 → flask_postgresql_audit-1.1.2}/tests/test_defaults.py +1 -1
- flask_postgresql_audit-1.1.2/tests/test_document_staging.py +106 -0
- {flask_postgresql_audit-1.1.0 → flask_postgresql_audit-1.1.2}/tests/test_operators.py +32 -0
- flask_postgresql_audit-1.1.2/tests/test_pg_aggregate.py +96 -0
- {flask_postgresql_audit-1.1.0 → flask_postgresql_audit-1.1.2}/tests/utils.py +1 -1
- flask_postgresql_audit-1.1.0/PKG-INFO +0 -197
- flask_postgresql_audit-1.1.0/README.md +0 -170
- flask_postgresql_audit-1.1.0/flask_postgresql_audit/descriptors.py +0 -38
- flask_postgresql_audit-1.1.0/flask_postgresql_audit/templates/create_schema.sql +0 -2
- flask_postgresql_audit-1.1.0/flask_postgresql_audit/templates/drop_schema.sql +0 -1
- flask_postgresql_audit-1.1.0/flask_postgresql_audit/templates/row_audit_trigger.sql +0 -4
- flask_postgresql_audit-1.1.0/flask_postgresql_audit/templates/row_create_activity.sql +0 -48
- {flask_postgresql_audit-1.1.0 → flask_postgresql_audit-1.1.2}/LICENSE +0 -0
- {flask_postgresql_audit-1.1.0 → flask_postgresql_audit-1.1.2}/flask_postgresql_audit/alembic/__init__.py +0 -0
- {flask_postgresql_audit-1.1.0 → flask_postgresql_audit-1.1.2}/flask_postgresql_audit/base.py +0 -0
- {flask_postgresql_audit-1.1.0 → flask_postgresql_audit-1.1.2}/flask_postgresql_audit/extensions/__init__.py +0 -0
- {flask_postgresql_audit-1.1.0 → flask_postgresql_audit-1.1.2}/flask_postgresql_audit/extensions/alembic_utils/__init__.py +0 -0
- {flask_postgresql_audit-1.1.0 → flask_postgresql_audit-1.1.2}/flask_postgresql_audit/extensions/alembic_utils/pg_aggregate.py +0 -0
- {flask_postgresql_audit-1.1.0 → flask_postgresql_audit-1.1.2}/flask_postgresql_audit/extensions/document_staging/__init__.py +0 -0
- {flask_postgresql_audit-1.1.0 → flask_postgresql_audit-1.1.2}/flask_postgresql_audit/extensions/document_staging/enum.py +0 -0
- {flask_postgresql_audit-1.1.0 → flask_postgresql_audit-1.1.2}/flask_postgresql_audit/extensions/document_staging/listener.py +0 -0
- {flask_postgresql_audit-1.1.0 → flask_postgresql_audit-1.1.2}/flask_postgresql_audit/extensions/document_staging/model.py +0 -0
- {flask_postgresql_audit-1.1.0 → flask_postgresql_audit-1.1.2}/flask_postgresql_audit/models.py +0 -0
- {flask_postgresql_audit-1.1.0 → flask_postgresql_audit-1.1.2}/flask_postgresql_audit/templates/audit_trigger_delete.sql +0 -0
- {flask_postgresql_audit-1.1.0 → flask_postgresql_audit-1.1.2}/flask_postgresql_audit/templates/audit_trigger_insert.sql +0 -0
- {flask_postgresql_audit-1.1.0 → flask_postgresql_audit-1.1.2}/flask_postgresql_audit/templates/audit_trigger_update.sql +0 -0
- {flask_postgresql_audit-1.1.0 → flask_postgresql_audit-1.1.2}/flask_postgresql_audit/templates/create_activity.sql +0 -0
- {flask_postgresql_audit-1.1.0 → flask_postgresql_audit-1.1.2}/flask_postgresql_audit/templates/get_pk_values.sql +0 -0
- {flask_postgresql_audit-1.1.0 → flask_postgresql_audit-1.1.2}/flask_postgresql_audit/templates/get_setting.sql +0 -0
- {flask_postgresql_audit-1.1.0 → flask_postgresql_audit-1.1.2}/flask_postgresql_audit/templates/get_table_relid.sql +0 -0
- {flask_postgresql_audit-1.1.0 → flask_postgresql_audit-1.1.2}/flask_postgresql_audit/templates/jsonb_subtract.sql +0 -0
- {flask_postgresql_audit-1.1.0 → flask_postgresql_audit-1.1.2}/flask_postgresql_audit/utils.py +0 -0
- {flask_postgresql_audit-1.1.0 → flask_postgresql_audit-1.1.2}/tests/__init__.py +0 -0
- {flask_postgresql_audit-1.1.0 → flask_postgresql_audit-1.1.2}/tests/conftest.py +0 -0
- {flask_postgresql_audit-1.1.0 → flask_postgresql_audit-1.1.2}/tests/migrations/README +0 -0
- {flask_postgresql_audit-1.1.0 → flask_postgresql_audit-1.1.2}/tests/migrations/alembic.ini +0 -0
- {flask_postgresql_audit-1.1.0 → flask_postgresql_audit-1.1.2}/tests/migrations/env.py +0 -0
- {flask_postgresql_audit-1.1.0 → flask_postgresql_audit-1.1.2}/tests/migrations/script.py.mako +0 -0
|
@@ -3,6 +3,69 @@
|
|
|
3
3
|
Here you can see the full list of changes between each Flask-PostgreSQL-Audit
|
|
4
4
|
release.
|
|
5
5
|
|
|
6
|
+
## 1.1.2 (2026-08-21)
|
|
7
|
+
|
|
8
|
+
- Add dedicated `jsonb_array_subtract()` function to support set difference operations on JSONB arrays.
|
|
9
|
+
- Add unit tests for `DocumentStaging` and `PGAggregate` extensions.
|
|
10
|
+
- Add `reorder_migration_ops` troubleshooting guide to `README.md`.
|
|
11
|
+
|
|
12
|
+
## 1.1.1 (2026-08-08)
|
|
13
|
+
|
|
14
|
+
- Fix test teardown downgrade error in migration listener.
|
|
15
|
+
- Fix `pg_audit_enabled` state thread-safety using `ContextVar`.
|
|
16
|
+
- Quote schema names and parameterize schema existence queries in Alembic operations.
|
|
17
|
+
- Remove unused templates and unused `hybridmethod` descriptor.
|
|
18
|
+
- Document `DocumentStaging` and `PGAggregate` extensions in `README.md`.
|
|
19
|
+
|
|
20
|
+
## 1.1.0 (2026-08-07)
|
|
21
|
+
|
|
22
|
+
- Bump `alembic` dependency to `>=1.18.0`.
|
|
23
|
+
- Refactor Alembic integration to use the official Plugin API and DispatchPriority system.
|
|
24
|
+
- Add multi-schema test matrix and schema-aware support to the test suite.
|
|
25
|
+
- Modernize Python type annotations, imports, string formatting, and clean up code using Ruff.
|
|
26
|
+
- Simplify context managers and type-hint fixtures in tests.
|
|
27
|
+
|
|
28
|
+
## 1.0.9 (2026-08-07)
|
|
29
|
+
|
|
30
|
+
- Change `fetch_activity` join clause to `outerjoin`.
|
|
31
|
+
- Resolve type checking errors.
|
|
32
|
+
|
|
33
|
+
## 1.0.8 (2026-03-30)
|
|
34
|
+
|
|
35
|
+
- Update `Audit` mixin class and refactor `fetch_activity` method, removing rowkey filter on class scope.
|
|
36
|
+
- Add PostgreSQL function `get_table_relid`.
|
|
37
|
+
- Fix `get_pk_values` returns wrong jsonb type if primary key columns are null.
|
|
38
|
+
|
|
39
|
+
## 1.0.7 (2025-12-15)
|
|
40
|
+
|
|
41
|
+
- Fix cartesian join when providing a table model to `fetch_activity()`.
|
|
42
|
+
|
|
43
|
+
## 1.0.6 (2025-12-15)
|
|
44
|
+
|
|
45
|
+
- Add `fetch_activity()` statement constructor with optional `verb` filtering, ordered by ID descending.
|
|
46
|
+
- Add `hybridmethod` descriptor.
|
|
47
|
+
- Add extension `document_staging`.
|
|
48
|
+
|
|
49
|
+
## 1.0.5 (2025-11-27)
|
|
50
|
+
|
|
51
|
+
- Add `alembic_utils` extension `pg_aggregate`.
|
|
52
|
+
|
|
53
|
+
## 1.0.4 (2025-11-17)
|
|
54
|
+
|
|
55
|
+
- Fix: use explicit schema prefix to call `get_pk_values()` in `create_activity`.
|
|
56
|
+
|
|
57
|
+
## 1.0.3 (2025-11-14)
|
|
58
|
+
|
|
59
|
+
- Fix: use schema prefix when calling `jsonb_subtract()` in audit trigger functions.
|
|
60
|
+
|
|
61
|
+
## 1.0.2 (2025-11-14)
|
|
62
|
+
|
|
63
|
+
- Support optional verbose value comparison in `jsonb_subtract()`.
|
|
64
|
+
|
|
65
|
+
## 1.0.1 (2025-10-30)
|
|
66
|
+
|
|
67
|
+
- Update authors in `pyproject.toml` and update `README.md`.
|
|
68
|
+
|
|
6
69
|
## 1.0.0 (2025-10-30)
|
|
7
70
|
|
|
8
71
|
- Forked from PostgreSQL-Audit.
|
|
@@ -0,0 +1,326 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: flask-postgresql-audit
|
|
3
|
+
Version: 1.1.2
|
|
4
|
+
Summary: Versioning and auditing extension for PostgreSQL and SQLAlchemy.
|
|
5
|
+
Project-URL: Homepage, https://github.com/higamigu/flask-postgresql-audit
|
|
6
|
+
Author-email: Higamigu <higamigu@gmail.com>, Konsta Vesterinen <konsta@fastmonkeys.com>
|
|
7
|
+
License-Expression: BSD-2-Clause
|
|
8
|
+
License-File: LICENSE
|
|
9
|
+
Classifier: Environment :: Web Environment
|
|
10
|
+
Classifier: Intended Audience :: Developers
|
|
11
|
+
Classifier: License :: OSI Approved :: BSD License
|
|
12
|
+
Classifier: Operating System :: OS Independent
|
|
13
|
+
Classifier: Programming Language :: Python
|
|
14
|
+
Classifier: Programming Language :: Python :: 3
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
18
|
+
Classifier: Topic :: Internet :: WWW/HTTP :: Dynamic Content
|
|
19
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
20
|
+
Requires-Python: >=3.11
|
|
21
|
+
Requires-Dist: alembic-utils>=0.8.8
|
|
22
|
+
Requires-Dist: alembic>=1.18.0
|
|
23
|
+
Requires-Dist: flask-sqlalchemy>=3.1.1
|
|
24
|
+
Requires-Dist: flask>=3.1.2
|
|
25
|
+
Requires-Dist: psycopg2>=2.9.11
|
|
26
|
+
Description-Content-Type: text/markdown
|
|
27
|
+
|
|
28
|
+
# Flask-PostgreSQL-Audit
|
|
29
|
+
|
|
30
|
+

|
|
31
|
+
[](https://pypi.org/project/flask-postgresql-audit/)
|
|
32
|
+
|
|
33
|
+
Auditing extension for Flask-SQLAlchemy with PostgreSQL.
|
|
34
|
+
Forked from [PostgreSQL-Audit](https://github.com/kvesteri/postgresql-audit), tries to combine the best of breed from existing solutions such as
|
|
35
|
+
[SQLAlchemy-Continuum](https://github.com/kvesteri/SQLAlchemy-Continuum),
|
|
36
|
+
[Papertrail](https://github.com/airblade/paper_trail) and especially
|
|
37
|
+
[Audit Trigger by 2ndQuadrant](https://github.com/2ndQuadrant/audit-trigger).
|
|
38
|
+
|
|
39
|
+
- Stores audit recordss into single table called `pga_activity`
|
|
40
|
+
- Uses trigger based approach to keep INSERTs, UPDATEs
|
|
41
|
+
and DELETEs as fast as possible
|
|
42
|
+
- Tracks and stores actor identities into table called `pga_transaction`
|
|
43
|
+
- Uses Alembic and Alembic-Utils to generate necessary database objects for migration
|
|
44
|
+
|
|
45
|
+
## Installation
|
|
46
|
+
```
|
|
47
|
+
pip install flask-postgresql-audit
|
|
48
|
+
```
|
|
49
|
+
or using `uv`
|
|
50
|
+
```
|
|
51
|
+
uv add flask-postgresql-audit
|
|
52
|
+
```
|
|
53
|
+
or install directly from this repo
|
|
54
|
+
```
|
|
55
|
+
uv add git+https://github.com/higamigu/flask-postgresql-audit --tag v1.1.0
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
## Usage
|
|
59
|
+
|
|
60
|
+
``` python
|
|
61
|
+
from flask_sqlalchemy import SQLAlchemy
|
|
62
|
+
from flask_postgresql_audit import PostgreSQLAudit, Audit
|
|
63
|
+
|
|
64
|
+
from my_app import app # your flask app
|
|
65
|
+
|
|
66
|
+
db = SQLAlchemy()
|
|
67
|
+
audit = PostgreSQLAudit()
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
class Article(db.Model, Audit):
|
|
71
|
+
__tablename__ = "article"
|
|
72
|
+
|
|
73
|
+
id = db.Column(db.Integer, primary_key=True)
|
|
74
|
+
name = db.Column(db.String)
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
db.init_app(app)
|
|
78
|
+
audit.init_app(app, db)
|
|
79
|
+
|
|
80
|
+
article = Article(name="Some article")
|
|
81
|
+
db.session.add(article)
|
|
82
|
+
db.session.commit()
|
|
83
|
+
```
|
|
84
|
+
Then generate migration file
|
|
85
|
+
```
|
|
86
|
+
flask db migrate -m "pg audit initial migration"
|
|
87
|
+
flask db upgrade
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
Now we can check the newly created activity.
|
|
91
|
+
|
|
92
|
+
``` python
|
|
93
|
+
activity = db.session.scalar(select(audit.Activity))
|
|
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'}
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
``` python
|
|
102
|
+
article.name = "Some other article"
|
|
103
|
+
db.session.commit()
|
|
104
|
+
|
|
105
|
+
activity = db.session.scalar(select(audit.Activity).order_by(desc("id")))
|
|
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'}
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
``` python
|
|
115
|
+
db.session.delete(article)
|
|
116
|
+
db.session.commit()
|
|
117
|
+
|
|
118
|
+
activity = db.session.scalar(select(audit.Activity).order_by(desc("id")))
|
|
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()
|
|
143
|
+
```
|
|
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
|
+
|
|
147
|
+
## Different Schema
|
|
148
|
+
You can isolate `pg_audit` objects entirely to a different schema by doing
|
|
149
|
+
|
|
150
|
+
``` python
|
|
151
|
+
from flask_postgresql_audit import PostgreSQLAudit
|
|
152
|
+
|
|
153
|
+
audit = PostgreSQLAudit(schema="audit")
|
|
154
|
+
|
|
155
|
+
...
|
|
156
|
+
```
|
|
157
|
+
And then you need to tell alembic to track other than `public` schema by adding following line in `alembic/env.py`
|
|
158
|
+
``` python
|
|
159
|
+
...
|
|
160
|
+
|
|
161
|
+
def run_migrations_online():
|
|
162
|
+
connectable = get_engine()
|
|
163
|
+
|
|
164
|
+
with connectable.connect() as connection:
|
|
165
|
+
context.configure(
|
|
166
|
+
connection=connection,
|
|
167
|
+
target_metadata=get_metadata(),
|
|
168
|
+
include_schemas=True, # add this arg
|
|
169
|
+
...
|
|
170
|
+
)
|
|
171
|
+
|
|
172
|
+
with context.begin_transaction():
|
|
173
|
+
context.run_migrations()
|
|
174
|
+
|
|
175
|
+
...
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
## Custom Actor ID getter
|
|
179
|
+
You can customize actor id getter function by doing the following. Here is an example using `current_user` from `flask_jwt_extended` library.
|
|
180
|
+
|
|
181
|
+
``` python
|
|
182
|
+
from flask_jwt_extended import jwt_required, current_user
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
@jwt_required(optional=True)
|
|
186
|
+
def actor_id_getter():
|
|
187
|
+
try:
|
|
188
|
+
return current_user.email or None
|
|
189
|
+
except Exception:
|
|
190
|
+
return None
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
...
|
|
194
|
+
|
|
195
|
+
audit = PostgreSQLAudit(actor_id_getter=actor_id_getter)
|
|
196
|
+
|
|
197
|
+
...
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
## Enable Alembic Logger
|
|
201
|
+
You can enable alembic logger for `pg_audit` by adding the following to your `alembic.ini`
|
|
202
|
+
``` ini
|
|
203
|
+
# Logging configuration
|
|
204
|
+
[loggers]
|
|
205
|
+
keys = root,sqlalchemy,alembic,alembic_utils,pg_audit # add 'pg_audit' here
|
|
206
|
+
|
|
207
|
+
...
|
|
208
|
+
|
|
209
|
+
[logger_pg_audit] # and add the logger for pg_audit here
|
|
210
|
+
level = INFO
|
|
211
|
+
handlers =
|
|
212
|
+
qualname = pg_audit
|
|
213
|
+
```
|
|
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
|
+
### Migration dependency or trigger ordering issues?
|
|
249
|
+
|
|
250
|
+
If Alembic migration generation creates triggers before tables or schema dependencies:
|
|
251
|
+
|
|
252
|
+
Use `reorder_migration_ops` and `chain_revision_directives` in `alembic/env.py`:
|
|
253
|
+
|
|
254
|
+
```python
|
|
255
|
+
from flask_postgresql_audit.alembic import (
|
|
256
|
+
chain_revision_directives,
|
|
257
|
+
reorder_migration_ops,
|
|
258
|
+
)
|
|
259
|
+
|
|
260
|
+
context.configure(
|
|
261
|
+
connection=connection,
|
|
262
|
+
target_metadata=target_metadata,
|
|
263
|
+
include_schemas=True,
|
|
264
|
+
process_revision_directives=chain_revision_directives(
|
|
265
|
+
reorder_migration_ops,
|
|
266
|
+
process_revision_directives,
|
|
267
|
+
),
|
|
268
|
+
)
|
|
269
|
+
```
|
|
270
|
+
|
|
271
|
+
## Extensions
|
|
272
|
+
|
|
273
|
+
### Document Staging
|
|
274
|
+
|
|
275
|
+
The `document_staging` extension provides document lifecycle management and version revision tracking for models.
|
|
276
|
+
|
|
277
|
+
```python
|
|
278
|
+
from flask_postgresql_audit.extensions.document_staging import (
|
|
279
|
+
DocumentStaging,
|
|
280
|
+
Docstatus,
|
|
281
|
+
attach_listener,
|
|
282
|
+
)
|
|
283
|
+
|
|
284
|
+
|
|
285
|
+
class Article(BaseModel, DocumentStaging):
|
|
286
|
+
__tablename__ = "article"
|
|
287
|
+
id: Mapped[int] = mapped_column(primary_key=True)
|
|
288
|
+
title: Mapped[str]
|
|
289
|
+
|
|
290
|
+
|
|
291
|
+
# Attach session listener for automatic actor and timestamp tracking
|
|
292
|
+
attach_listener()
|
|
293
|
+
```
|
|
294
|
+
|
|
295
|
+
#### Lifecycle & Transitions
|
|
296
|
+
|
|
297
|
+
- **`Docstatus` states**: `DRAFT` (0), `SUBMITTED` (1), `CANCELLED` (2), `REVISED` (3).
|
|
298
|
+
- **`doc.bump()`**: Advances document status (`DRAFT` -> `SUBMITTED` -> `CANCELLED`).
|
|
299
|
+
- **`doc.revise(new_doc)`**: Creates a new revision `new_doc` from a `CANCELLED` document and sets state to `REVISED`.
|
|
300
|
+
- **`doc.delete(session)`**: Deletes a `DRAFT` document. If it was a revision, reverts parent document status back to `CANCELLED`.
|
|
301
|
+
- **Automatic Fields**: `created_by`, `created_on`, `submitted_by`, `submitted_on`, `cancelled_by`, `cancelled_on` are automatically set during session flush events.
|
|
302
|
+
|
|
303
|
+
### Custom PostgreSQL Aggregates (`alembic_utils`)
|
|
304
|
+
|
|
305
|
+
`PGAggregate` extends `alembic_utils` to manage custom PostgreSQL aggregate functions in Alembic migrations.
|
|
306
|
+
|
|
307
|
+
```python
|
|
308
|
+
from flask_postgresql_audit.extensions.alembic_utils import PGAggregate
|
|
309
|
+
|
|
310
|
+
# Example: Define custom aggregate function entity
|
|
311
|
+
sum_agg = PGAggregate(
|
|
312
|
+
schema="public",
|
|
313
|
+
signature="custom_sum(numeric)",
|
|
314
|
+
definition="(SFUNC = numeric_add, STYPE = numeric, INITCOND = '0')",
|
|
315
|
+
)
|
|
316
|
+
```
|
|
317
|
+
|
|
318
|
+
Automatically detects and tracks custom `CREATE AGGREGATE` definitions during `alembic revision --autogenerate`.
|
|
319
|
+
|
|
320
|
+
## Running the tests
|
|
321
|
+
|
|
322
|
+
git clone https://github.com/higamigu/flask-postgresql-audit.git
|
|
323
|
+
cd flask-postgresql-audit
|
|
324
|
+
pip install tox
|
|
325
|
+
createdb postgresql_audit_test
|
|
326
|
+
tox
|
|
@@ -0,0 +1,299 @@
|
|
|
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
|
+
### Migration dependency or trigger ordering issues?
|
|
222
|
+
|
|
223
|
+
If Alembic migration generation creates triggers before tables or schema dependencies:
|
|
224
|
+
|
|
225
|
+
Use `reorder_migration_ops` and `chain_revision_directives` in `alembic/env.py`:
|
|
226
|
+
|
|
227
|
+
```python
|
|
228
|
+
from flask_postgresql_audit.alembic import (
|
|
229
|
+
chain_revision_directives,
|
|
230
|
+
reorder_migration_ops,
|
|
231
|
+
)
|
|
232
|
+
|
|
233
|
+
context.configure(
|
|
234
|
+
connection=connection,
|
|
235
|
+
target_metadata=target_metadata,
|
|
236
|
+
include_schemas=True,
|
|
237
|
+
process_revision_directives=chain_revision_directives(
|
|
238
|
+
reorder_migration_ops,
|
|
239
|
+
process_revision_directives,
|
|
240
|
+
),
|
|
241
|
+
)
|
|
242
|
+
```
|
|
243
|
+
|
|
244
|
+
## Extensions
|
|
245
|
+
|
|
246
|
+
### Document Staging
|
|
247
|
+
|
|
248
|
+
The `document_staging` extension provides document lifecycle management and version revision tracking for models.
|
|
249
|
+
|
|
250
|
+
```python
|
|
251
|
+
from flask_postgresql_audit.extensions.document_staging import (
|
|
252
|
+
DocumentStaging,
|
|
253
|
+
Docstatus,
|
|
254
|
+
attach_listener,
|
|
255
|
+
)
|
|
256
|
+
|
|
257
|
+
|
|
258
|
+
class Article(BaseModel, DocumentStaging):
|
|
259
|
+
__tablename__ = "article"
|
|
260
|
+
id: Mapped[int] = mapped_column(primary_key=True)
|
|
261
|
+
title: Mapped[str]
|
|
262
|
+
|
|
263
|
+
|
|
264
|
+
# Attach session listener for automatic actor and timestamp tracking
|
|
265
|
+
attach_listener()
|
|
266
|
+
```
|
|
267
|
+
|
|
268
|
+
#### Lifecycle & Transitions
|
|
269
|
+
|
|
270
|
+
- **`Docstatus` states**: `DRAFT` (0), `SUBMITTED` (1), `CANCELLED` (2), `REVISED` (3).
|
|
271
|
+
- **`doc.bump()`**: Advances document status (`DRAFT` -> `SUBMITTED` -> `CANCELLED`).
|
|
272
|
+
- **`doc.revise(new_doc)`**: Creates a new revision `new_doc` from a `CANCELLED` document and sets state to `REVISED`.
|
|
273
|
+
- **`doc.delete(session)`**: Deletes a `DRAFT` document. If it was a revision, reverts parent document status back to `CANCELLED`.
|
|
274
|
+
- **Automatic Fields**: `created_by`, `created_on`, `submitted_by`, `submitted_on`, `cancelled_by`, `cancelled_on` are automatically set during session flush events.
|
|
275
|
+
|
|
276
|
+
### Custom PostgreSQL Aggregates (`alembic_utils`)
|
|
277
|
+
|
|
278
|
+
`PGAggregate` extends `alembic_utils` to manage custom PostgreSQL aggregate functions in Alembic migrations.
|
|
279
|
+
|
|
280
|
+
```python
|
|
281
|
+
from flask_postgresql_audit.extensions.alembic_utils import PGAggregate
|
|
282
|
+
|
|
283
|
+
# Example: Define custom aggregate function entity
|
|
284
|
+
sum_agg = PGAggregate(
|
|
285
|
+
schema="public",
|
|
286
|
+
signature="custom_sum(numeric)",
|
|
287
|
+
definition="(SFUNC = numeric_add, STYPE = numeric, INITCOND = '0')",
|
|
288
|
+
)
|
|
289
|
+
```
|
|
290
|
+
|
|
291
|
+
Automatically detects and tracks custom `CREATE AGGREGATE` definitions during `alembic revision --autogenerate`.
|
|
292
|
+
|
|
293
|
+
## Running the tests
|
|
294
|
+
|
|
295
|
+
git clone https://github.com/higamigu/flask-postgresql-audit.git
|
|
296
|
+
cd flask-postgresql-audit
|
|
297
|
+
pip install tox
|
|
298
|
+
createdb postgresql_audit_test
|
|
299
|
+
tox
|