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.
Files changed (50) hide show
  1. {flask_postgresql_audit-1.0.9 → flask_postgresql_audit-1.1.1}/.gitignore +4 -0
  2. {flask_postgresql_audit-1.0.9 → flask_postgresql_audit-1.1.1}/CHANGES.md +57 -0
  3. {flask_postgresql_audit-1.0.9 → flask_postgresql_audit-1.1.1}/PKG-INFO +130 -24
  4. flask_postgresql_audit-1.1.1/README.md +276 -0
  5. {flask_postgresql_audit-1.0.9 → flask_postgresql_audit-1.1.1}/flask_postgresql_audit/__init__.py +1 -1
  6. flask_postgresql_audit-1.1.1/flask_postgresql_audit/alembic/__init__.py +7 -0
  7. {flask_postgresql_audit-1.0.9 → flask_postgresql_audit-1.1.1}/flask_postgresql_audit/alembic/schema.py +5 -5
  8. flask_postgresql_audit-1.1.1/flask_postgresql_audit/alembic/setup.py +157 -0
  9. {flask_postgresql_audit-1.0.9 → flask_postgresql_audit-1.1.1}/flask_postgresql_audit/base.py +5 -6
  10. {flask_postgresql_audit-1.0.9 → flask_postgresql_audit-1.1.1}/flask_postgresql_audit/core.py +21 -12
  11. {flask_postgresql_audit-1.0.9 → flask_postgresql_audit-1.1.1}/flask_postgresql_audit/extensions/alembic_utils/pg_aggregate.py +1 -1
  12. {flask_postgresql_audit-1.0.9 → flask_postgresql_audit-1.1.1}/flask_postgresql_audit/extensions/document_staging/listener.py +10 -10
  13. {flask_postgresql_audit-1.0.9 → flask_postgresql_audit-1.1.1}/flask_postgresql_audit/extensions/document_staging/model.py +1 -2
  14. {flask_postgresql_audit-1.0.9 → flask_postgresql_audit-1.1.1}/flask_postgresql_audit/models.py +3 -3
  15. {flask_postgresql_audit-1.0.9 → flask_postgresql_audit-1.1.1}/flask_postgresql_audit/typing.py +1 -1
  16. {flask_postgresql_audit-1.0.9 → flask_postgresql_audit-1.1.1}/pyproject.toml +4 -4
  17. flask_postgresql_audit-1.1.1/tests/__init__.py +0 -0
  18. {flask_postgresql_audit-1.0.9 → flask_postgresql_audit-1.1.1}/tests/app.py +8 -5
  19. {flask_postgresql_audit-1.0.9 → flask_postgresql_audit-1.1.1}/tests/conftest.py +6 -8
  20. {flask_postgresql_audit-1.0.9 → flask_postgresql_audit-1.1.1}/tests/migrations/env.py +11 -5
  21. {flask_postgresql_audit-1.0.9 → flask_postgresql_audit-1.1.1}/tests/test_defaults.py +1 -1
  22. {flask_postgresql_audit-1.0.9 → flask_postgresql_audit-1.1.1}/tests/test_operators.py +5 -3
  23. {flask_postgresql_audit-1.0.9 → flask_postgresql_audit-1.1.1}/tests/utils.py +16 -9
  24. flask_postgresql_audit-1.0.9/README.md +0 -170
  25. flask_postgresql_audit-1.0.9/flask_postgresql_audit/alembic/__init__.py +0 -2
  26. flask_postgresql_audit-1.0.9/flask_postgresql_audit/alembic/setup.py +0 -95
  27. flask_postgresql_audit-1.0.9/flask_postgresql_audit/descriptors.py +0 -38
  28. flask_postgresql_audit-1.0.9/flask_postgresql_audit/extensions/__init__.py +0 -1
  29. flask_postgresql_audit-1.0.9/flask_postgresql_audit/templates/create_schema.sql +0 -2
  30. flask_postgresql_audit-1.0.9/flask_postgresql_audit/templates/drop_schema.sql +0 -1
  31. flask_postgresql_audit-1.0.9/flask_postgresql_audit/templates/row_audit_trigger.sql +0 -4
  32. flask_postgresql_audit-1.0.9/flask_postgresql_audit/templates/row_create_activity.sql +0 -48
  33. {flask_postgresql_audit-1.0.9 → flask_postgresql_audit-1.1.1}/LICENSE +0 -0
  34. {flask_postgresql_audit-1.0.9 → flask_postgresql_audit-1.1.1}/flask_postgresql_audit/alembic/entities.py +0 -0
  35. {flask_postgresql_audit-1.0.9/tests → flask_postgresql_audit-1.1.1/flask_postgresql_audit/extensions}/__init__.py +0 -0
  36. {flask_postgresql_audit-1.0.9 → flask_postgresql_audit-1.1.1}/flask_postgresql_audit/extensions/alembic_utils/__init__.py +0 -0
  37. {flask_postgresql_audit-1.0.9 → flask_postgresql_audit-1.1.1}/flask_postgresql_audit/extensions/document_staging/__init__.py +0 -0
  38. {flask_postgresql_audit-1.0.9 → flask_postgresql_audit-1.1.1}/flask_postgresql_audit/extensions/document_staging/enum.py +0 -0
  39. {flask_postgresql_audit-1.0.9 → flask_postgresql_audit-1.1.1}/flask_postgresql_audit/templates/audit_trigger_delete.sql +0 -0
  40. {flask_postgresql_audit-1.0.9 → flask_postgresql_audit-1.1.1}/flask_postgresql_audit/templates/audit_trigger_insert.sql +0 -0
  41. {flask_postgresql_audit-1.0.9 → flask_postgresql_audit-1.1.1}/flask_postgresql_audit/templates/audit_trigger_update.sql +0 -0
  42. {flask_postgresql_audit-1.0.9 → flask_postgresql_audit-1.1.1}/flask_postgresql_audit/templates/create_activity.sql +0 -0
  43. {flask_postgresql_audit-1.0.9 → flask_postgresql_audit-1.1.1}/flask_postgresql_audit/templates/get_pk_values.sql +0 -0
  44. {flask_postgresql_audit-1.0.9 → flask_postgresql_audit-1.1.1}/flask_postgresql_audit/templates/get_setting.sql +0 -0
  45. {flask_postgresql_audit-1.0.9 → flask_postgresql_audit-1.1.1}/flask_postgresql_audit/templates/get_table_relid.sql +0 -0
  46. {flask_postgresql_audit-1.0.9 → flask_postgresql_audit-1.1.1}/flask_postgresql_audit/templates/jsonb_subtract.sql +0 -0
  47. {flask_postgresql_audit-1.0.9 → flask_postgresql_audit-1.1.1}/flask_postgresql_audit/utils.py +0 -0
  48. {flask_postgresql_audit-1.0.9 → flask_postgresql_audit-1.1.1}/tests/migrations/README +0 -0
  49. {flask_postgresql_audit-1.0.9 → flask_postgresql_audit-1.1.1}/tests/migrations/alembic.ini +0 -0
  50. {flask_postgresql_audit-1.0.9 → flask_postgresql_audit-1.1.1}/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
@@ -63,3 +64,6 @@ uv.lock
63
64
 
64
65
  # VSCode
65
66
  .vscode
67
+
68
+ # Agents
69
+ .agents
@@ -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-postgresql_audit
3
- Version: 1.0.9
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.17.0
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.0.0
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__ = 'article'
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='Some article')
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 # 1
93
- activity.table_name # 'article'
94
- activity.verb # 'insert'
95
- activity.old_data # None
96
- activity.changed_data # {'id': '1', 'name': 'Some article'}
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 = 'Some other article'
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 # 2
105
- activity.table_name # 'article'
106
- activity.verb # 'update'
107
- activity.object_id # 1
108
- activity.old_data # {'id': '1', 'name': 'Some article'}
109
- activity.changed_data # {'name': 'Some other article'}
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 # 3
118
- activity.table_name # 'article'
119
- activity.verb # 'delete'
120
- activity.object_id # 1
121
- activity.old_data # {'id': '1', 'name': 'Some other article'}
122
- activity.changed_data # None
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
+ ![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
+ ## 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
@@ -1,4 +1,4 @@
1
1
  # ruff: noqa: F401
2
2
  from .core import Audit, PostgreSQLAudit
3
3
 
4
- __version__ = "1.0.9"
4
+ __version__ = "1.1.1"
@@ -0,0 +1,7 @@
1
+ # ruff: noqa: F401
2
+ from .setup import (
3
+ chain_revision_directives,
4
+ register_triggers,
5
+ reorder_migration_ops,
6
+ setup_db,
7
+ )
@@ -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 {name};
37
- REVOKE ALL ON SCHEMA {name} FROM public;
38
- """.format(name=operation.schema_name)
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 = "DROP SCHEMA {name} CASCADE;".format(name=operation.schema_name)
44
+ stmt = f'DROP SCHEMA "{operation.schema_name}" CASCADE;'
45
45
  operations.execute(text(stmt))
46
46
 
47
47