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.
Files changed (51) hide show
  1. {flask_postgresql_audit-1.1.0 → flask_postgresql_audit-1.1.2}/.gitignore +2 -0
  2. {flask_postgresql_audit-1.1.0 → flask_postgresql_audit-1.1.2}/CHANGES.md +63 -0
  3. flask_postgresql_audit-1.1.2/PKG-INFO +326 -0
  4. flask_postgresql_audit-1.1.2/README.md +299 -0
  5. {flask_postgresql_audit-1.1.0 → flask_postgresql_audit-1.1.2}/flask_postgresql_audit/__init__.py +1 -1
  6. {flask_postgresql_audit-1.1.0 → flask_postgresql_audit-1.1.2}/flask_postgresql_audit/alembic/entities.py +9 -0
  7. {flask_postgresql_audit-1.1.0 → flask_postgresql_audit-1.1.2}/flask_postgresql_audit/alembic/schema.py +3 -3
  8. {flask_postgresql_audit-1.1.0 → flask_postgresql_audit-1.1.2}/flask_postgresql_audit/alembic/setup.py +30 -4
  9. {flask_postgresql_audit-1.1.0 → flask_postgresql_audit-1.1.2}/flask_postgresql_audit/core.py +13 -3
  10. flask_postgresql_audit-1.1.2/flask_postgresql_audit/templates/jsonb_array_subtract.sql +29 -0
  11. {flask_postgresql_audit-1.1.0 → flask_postgresql_audit-1.1.2}/flask_postgresql_audit/typing.py +1 -1
  12. {flask_postgresql_audit-1.1.0 → flask_postgresql_audit-1.1.2}/pyproject.toml +1 -1
  13. {flask_postgresql_audit-1.1.0 → flask_postgresql_audit-1.1.2}/tests/app.py +12 -0
  14. {flask_postgresql_audit-1.1.0 → flask_postgresql_audit-1.1.2}/tests/test_defaults.py +1 -1
  15. flask_postgresql_audit-1.1.2/tests/test_document_staging.py +106 -0
  16. {flask_postgresql_audit-1.1.0 → flask_postgresql_audit-1.1.2}/tests/test_operators.py +32 -0
  17. flask_postgresql_audit-1.1.2/tests/test_pg_aggregate.py +96 -0
  18. {flask_postgresql_audit-1.1.0 → flask_postgresql_audit-1.1.2}/tests/utils.py +1 -1
  19. flask_postgresql_audit-1.1.0/PKG-INFO +0 -197
  20. flask_postgresql_audit-1.1.0/README.md +0 -170
  21. flask_postgresql_audit-1.1.0/flask_postgresql_audit/descriptors.py +0 -38
  22. flask_postgresql_audit-1.1.0/flask_postgresql_audit/templates/create_schema.sql +0 -2
  23. flask_postgresql_audit-1.1.0/flask_postgresql_audit/templates/drop_schema.sql +0 -1
  24. flask_postgresql_audit-1.1.0/flask_postgresql_audit/templates/row_audit_trigger.sql +0 -4
  25. flask_postgresql_audit-1.1.0/flask_postgresql_audit/templates/row_create_activity.sql +0 -48
  26. {flask_postgresql_audit-1.1.0 → flask_postgresql_audit-1.1.2}/LICENSE +0 -0
  27. {flask_postgresql_audit-1.1.0 → flask_postgresql_audit-1.1.2}/flask_postgresql_audit/alembic/__init__.py +0 -0
  28. {flask_postgresql_audit-1.1.0 → flask_postgresql_audit-1.1.2}/flask_postgresql_audit/base.py +0 -0
  29. {flask_postgresql_audit-1.1.0 → flask_postgresql_audit-1.1.2}/flask_postgresql_audit/extensions/__init__.py +0 -0
  30. {flask_postgresql_audit-1.1.0 → flask_postgresql_audit-1.1.2}/flask_postgresql_audit/extensions/alembic_utils/__init__.py +0 -0
  31. {flask_postgresql_audit-1.1.0 → flask_postgresql_audit-1.1.2}/flask_postgresql_audit/extensions/alembic_utils/pg_aggregate.py +0 -0
  32. {flask_postgresql_audit-1.1.0 → flask_postgresql_audit-1.1.2}/flask_postgresql_audit/extensions/document_staging/__init__.py +0 -0
  33. {flask_postgresql_audit-1.1.0 → flask_postgresql_audit-1.1.2}/flask_postgresql_audit/extensions/document_staging/enum.py +0 -0
  34. {flask_postgresql_audit-1.1.0 → flask_postgresql_audit-1.1.2}/flask_postgresql_audit/extensions/document_staging/listener.py +0 -0
  35. {flask_postgresql_audit-1.1.0 → flask_postgresql_audit-1.1.2}/flask_postgresql_audit/extensions/document_staging/model.py +0 -0
  36. {flask_postgresql_audit-1.1.0 → flask_postgresql_audit-1.1.2}/flask_postgresql_audit/models.py +0 -0
  37. {flask_postgresql_audit-1.1.0 → flask_postgresql_audit-1.1.2}/flask_postgresql_audit/templates/audit_trigger_delete.sql +0 -0
  38. {flask_postgresql_audit-1.1.0 → flask_postgresql_audit-1.1.2}/flask_postgresql_audit/templates/audit_trigger_insert.sql +0 -0
  39. {flask_postgresql_audit-1.1.0 → flask_postgresql_audit-1.1.2}/flask_postgresql_audit/templates/audit_trigger_update.sql +0 -0
  40. {flask_postgresql_audit-1.1.0 → flask_postgresql_audit-1.1.2}/flask_postgresql_audit/templates/create_activity.sql +0 -0
  41. {flask_postgresql_audit-1.1.0 → flask_postgresql_audit-1.1.2}/flask_postgresql_audit/templates/get_pk_values.sql +0 -0
  42. {flask_postgresql_audit-1.1.0 → flask_postgresql_audit-1.1.2}/flask_postgresql_audit/templates/get_setting.sql +0 -0
  43. {flask_postgresql_audit-1.1.0 → flask_postgresql_audit-1.1.2}/flask_postgresql_audit/templates/get_table_relid.sql +0 -0
  44. {flask_postgresql_audit-1.1.0 → flask_postgresql_audit-1.1.2}/flask_postgresql_audit/templates/jsonb_subtract.sql +0 -0
  45. {flask_postgresql_audit-1.1.0 → flask_postgresql_audit-1.1.2}/flask_postgresql_audit/utils.py +0 -0
  46. {flask_postgresql_audit-1.1.0 → flask_postgresql_audit-1.1.2}/tests/__init__.py +0 -0
  47. {flask_postgresql_audit-1.1.0 → flask_postgresql_audit-1.1.2}/tests/conftest.py +0 -0
  48. {flask_postgresql_audit-1.1.0 → flask_postgresql_audit-1.1.2}/tests/migrations/README +0 -0
  49. {flask_postgresql_audit-1.1.0 → flask_postgresql_audit-1.1.2}/tests/migrations/alembic.ini +0 -0
  50. {flask_postgresql_audit-1.1.0 → flask_postgresql_audit-1.1.2}/tests/migrations/env.py +0 -0
  51. {flask_postgresql_audit-1.1.0 → flask_postgresql_audit-1.1.2}/tests/migrations/script.py.mako +0 -0
@@ -22,6 +22,7 @@ var/
22
22
  *.egg-info/
23
23
  .installed.cfg
24
24
  *.egg
25
+ .env*
25
26
 
26
27
  # PyInstaller
27
28
  # Usually these files are written by a python script from a template
@@ -66,3 +67,4 @@ uv.lock
66
67
 
67
68
  # Agents
68
69
  .agents
70
+ AGENTS.md
@@ -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
+ ![BuildStatus](https://github.com/higamigu/flask-postgresql-audit/actions/workflows/test.yml/badge.svg?branch=main)
31
+ [![VersionStatus](https://img.shields.io/pypi/v/flask-postgresql-audit.svg)](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
+ ![BuildStatus](https://github.com/higamigu/flask-postgresql-audit/actions/workflows/test.yml/badge.svg?branch=main)
4
+ [![VersionStatus](https://img.shields.io/pypi/v/flask-postgresql-audit.svg)](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
@@ -1,4 +1,4 @@
1
1
  # ruff: noqa: F401
2
2
  from .core import Audit, PostgreSQLAudit
3
3
 
4
- __version__ = "1.1.0"
4
+ __version__ = "1.1.2"