flask-postgresql-audit 1.0.9__tar.gz → 1.1.0__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.0}/.gitignore +3 -0
- {flask_postgresql_audit-1.0.9 → flask_postgresql_audit-1.1.0}/PKG-INFO +2 -2
- {flask_postgresql_audit-1.0.9 → flask_postgresql_audit-1.1.0}/flask_postgresql_audit/__init__.py +1 -1
- flask_postgresql_audit-1.1.0/flask_postgresql_audit/alembic/__init__.py +7 -0
- {flask_postgresql_audit-1.0.9 → flask_postgresql_audit-1.1.0}/flask_postgresql_audit/alembic/schema.py +5 -5
- {flask_postgresql_audit-1.0.9 → flask_postgresql_audit-1.1.0}/flask_postgresql_audit/alembic/setup.py +58 -21
- {flask_postgresql_audit-1.0.9 → flask_postgresql_audit-1.1.0}/flask_postgresql_audit/base.py +5 -6
- {flask_postgresql_audit-1.0.9 → flask_postgresql_audit-1.1.0}/flask_postgresql_audit/core.py +8 -9
- {flask_postgresql_audit-1.0.9 → flask_postgresql_audit-1.1.0}/flask_postgresql_audit/descriptors.py +2 -2
- {flask_postgresql_audit-1.0.9 → flask_postgresql_audit-1.1.0}/flask_postgresql_audit/extensions/alembic_utils/pg_aggregate.py +1 -1
- {flask_postgresql_audit-1.0.9 → flask_postgresql_audit-1.1.0}/flask_postgresql_audit/extensions/document_staging/listener.py +10 -10
- {flask_postgresql_audit-1.0.9 → flask_postgresql_audit-1.1.0}/flask_postgresql_audit/extensions/document_staging/model.py +1 -2
- {flask_postgresql_audit-1.0.9 → flask_postgresql_audit-1.1.0}/flask_postgresql_audit/models.py +3 -3
- {flask_postgresql_audit-1.0.9 → flask_postgresql_audit-1.1.0}/pyproject.toml +3 -3
- flask_postgresql_audit-1.1.0/tests/__init__.py +0 -0
- {flask_postgresql_audit-1.0.9 → flask_postgresql_audit-1.1.0}/tests/app.py +8 -5
- {flask_postgresql_audit-1.0.9 → flask_postgresql_audit-1.1.0}/tests/conftest.py +6 -8
- {flask_postgresql_audit-1.0.9 → flask_postgresql_audit-1.1.0}/tests/migrations/env.py +11 -5
- {flask_postgresql_audit-1.0.9 → flask_postgresql_audit-1.1.0}/tests/test_operators.py +5 -3
- {flask_postgresql_audit-1.0.9 → flask_postgresql_audit-1.1.0}/tests/utils.py +16 -9
- flask_postgresql_audit-1.0.9/flask_postgresql_audit/alembic/__init__.py +0 -2
- flask_postgresql_audit-1.0.9/flask_postgresql_audit/extensions/__init__.py +0 -1
- {flask_postgresql_audit-1.0.9 → flask_postgresql_audit-1.1.0}/CHANGES.md +0 -0
- {flask_postgresql_audit-1.0.9 → flask_postgresql_audit-1.1.0}/LICENSE +0 -0
- {flask_postgresql_audit-1.0.9 → flask_postgresql_audit-1.1.0}/README.md +0 -0
- {flask_postgresql_audit-1.0.9 → flask_postgresql_audit-1.1.0}/flask_postgresql_audit/alembic/entities.py +0 -0
- {flask_postgresql_audit-1.0.9/tests → flask_postgresql_audit-1.1.0/flask_postgresql_audit/extensions}/__init__.py +0 -0
- {flask_postgresql_audit-1.0.9 → flask_postgresql_audit-1.1.0}/flask_postgresql_audit/extensions/alembic_utils/__init__.py +0 -0
- {flask_postgresql_audit-1.0.9 → flask_postgresql_audit-1.1.0}/flask_postgresql_audit/extensions/document_staging/__init__.py +0 -0
- {flask_postgresql_audit-1.0.9 → flask_postgresql_audit-1.1.0}/flask_postgresql_audit/extensions/document_staging/enum.py +0 -0
- {flask_postgresql_audit-1.0.9 → flask_postgresql_audit-1.1.0}/flask_postgresql_audit/templates/audit_trigger_delete.sql +0 -0
- {flask_postgresql_audit-1.0.9 → flask_postgresql_audit-1.1.0}/flask_postgresql_audit/templates/audit_trigger_insert.sql +0 -0
- {flask_postgresql_audit-1.0.9 → flask_postgresql_audit-1.1.0}/flask_postgresql_audit/templates/audit_trigger_update.sql +0 -0
- {flask_postgresql_audit-1.0.9 → flask_postgresql_audit-1.1.0}/flask_postgresql_audit/templates/create_activity.sql +0 -0
- {flask_postgresql_audit-1.0.9 → flask_postgresql_audit-1.1.0}/flask_postgresql_audit/templates/create_schema.sql +0 -0
- {flask_postgresql_audit-1.0.9 → flask_postgresql_audit-1.1.0}/flask_postgresql_audit/templates/drop_schema.sql +0 -0
- {flask_postgresql_audit-1.0.9 → flask_postgresql_audit-1.1.0}/flask_postgresql_audit/templates/get_pk_values.sql +0 -0
- {flask_postgresql_audit-1.0.9 → flask_postgresql_audit-1.1.0}/flask_postgresql_audit/templates/get_setting.sql +0 -0
- {flask_postgresql_audit-1.0.9 → flask_postgresql_audit-1.1.0}/flask_postgresql_audit/templates/get_table_relid.sql +0 -0
- {flask_postgresql_audit-1.0.9 → flask_postgresql_audit-1.1.0}/flask_postgresql_audit/templates/jsonb_subtract.sql +0 -0
- {flask_postgresql_audit-1.0.9 → flask_postgresql_audit-1.1.0}/flask_postgresql_audit/templates/row_audit_trigger.sql +0 -0
- {flask_postgresql_audit-1.0.9 → flask_postgresql_audit-1.1.0}/flask_postgresql_audit/templates/row_create_activity.sql +0 -0
- {flask_postgresql_audit-1.0.9 → flask_postgresql_audit-1.1.0}/flask_postgresql_audit/typing.py +0 -0
- {flask_postgresql_audit-1.0.9 → flask_postgresql_audit-1.1.0}/flask_postgresql_audit/utils.py +0 -0
- {flask_postgresql_audit-1.0.9 → flask_postgresql_audit-1.1.0}/tests/migrations/README +0 -0
- {flask_postgresql_audit-1.0.9 → flask_postgresql_audit-1.1.0}/tests/migrations/alembic.ini +0 -0
- {flask_postgresql_audit-1.0.9 → flask_postgresql_audit-1.1.0}/tests/migrations/script.py.mako +0 -0
- {flask_postgresql_audit-1.0.9 → flask_postgresql_audit-1.1.0}/tests/test_defaults.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: flask-postgresql_audit
|
|
3
|
-
Version: 1.0
|
|
3
|
+
Version: 1.1.0
|
|
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
|
|
@@ -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 = "DROP SCHEMA {
|
|
44
|
+
stmt = f"DROP SCHEMA {operation.schema_name} CASCADE;"
|
|
45
45
|
operations.execute(text(stmt))
|
|
46
46
|
|
|
47
47
|
|
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
import logging
|
|
2
|
-
from typing import TYPE_CHECKING
|
|
2
|
+
from typing import TYPE_CHECKING
|
|
3
3
|
|
|
4
4
|
from alembic.autogenerate import comparators
|
|
5
5
|
from alembic.autogenerate.api import AutogenContext
|
|
6
6
|
from alembic.operations import ops
|
|
7
|
+
from alembic.operations.ops import MigrationScript
|
|
8
|
+
from alembic.util import DispatchPriority
|
|
7
9
|
from alembic_utils.replaceable_entity import register_entities, registry
|
|
8
10
|
from alembic_utils.reversible_op import CreateOp
|
|
9
11
|
from sqlalchemy import Connection, text
|
|
@@ -26,38 +28,73 @@ def setup_db(audit: "PostgreSQLAudit"):
|
|
|
26
28
|
register_triggers(audit)
|
|
27
29
|
register_entities(audit.pg_audit_entities)
|
|
28
30
|
|
|
31
|
+
@comparators.dispatch_for("schema", priority=DispatchPriority.FIRST)
|
|
29
32
|
def compare_audit_schema(
|
|
30
33
|
autogen_context: AutogenContext,
|
|
31
34
|
upgrade_ops: ops.UpgradeOps,
|
|
32
|
-
schemas:
|
|
35
|
+
schemas: set[None] | set[str | None],
|
|
33
36
|
) -> None:
|
|
34
|
-
idx = 0
|
|
35
37
|
if connection := autogen_context.connection:
|
|
36
38
|
check_schema = """
|
|
37
39
|
SELECT TRUE FROM information_schema.schemata
|
|
38
40
|
WHERE schema_name = '{name}'
|
|
39
41
|
""".format(name=audit.context["schema_name"])
|
|
40
42
|
if not connection.scalar(text(check_schema)):
|
|
41
|
-
upgrade_ops.ops.
|
|
42
|
-
idx += 1
|
|
43
|
+
upgrade_ops.ops.append(SchemaCreate(audit.context["schema_name"]))
|
|
43
44
|
|
|
44
45
|
for ent in audit.pg_audit_entities:
|
|
45
46
|
if op := get_blind_migration_op(ent, connection):
|
|
46
|
-
registry._entities.pop(ent.identity)
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
47
|
+
registry._entities.pop(ent.identity, None)
|
|
48
|
+
upgrade_ops.ops.append(op)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def reorder_migration_ops(context, revision, directives: list[MigrationScript]):
|
|
52
|
+
"""
|
|
53
|
+
Alembic directive listener that reorders upgrade operations into correct
|
|
54
|
+
dependency order:
|
|
55
|
+
1. Schema creation & btree_gist extension
|
|
56
|
+
2. Tables, indexes, foreign keys, etc.
|
|
57
|
+
3. Triggers & Replaceable audit entities
|
|
58
|
+
"""
|
|
59
|
+
for directive in directives:
|
|
60
|
+
if not hasattr(directive, "upgrade_ops") or directive.upgrade_ops is None:
|
|
61
|
+
continue
|
|
62
|
+
|
|
63
|
+
upgrade_ops = directive.upgrade_ops.ops
|
|
64
|
+
|
|
65
|
+
schema_ops = []
|
|
66
|
+
extension_ops = []
|
|
67
|
+
trigger_ops = []
|
|
68
|
+
other_ops = []
|
|
69
|
+
|
|
70
|
+
for op in upgrade_ops:
|
|
71
|
+
if isinstance(op, SchemaCreate):
|
|
72
|
+
schema_ops.append(op)
|
|
73
|
+
elif (
|
|
74
|
+
isinstance(op, CreateOp)
|
|
75
|
+
and getattr(op.target, "signature", None) == "btree_gist"
|
|
76
|
+
):
|
|
77
|
+
extension_ops.append(op)
|
|
78
|
+
elif isinstance(op, CreateOp):
|
|
79
|
+
trigger_ops.append(op)
|
|
80
|
+
else:
|
|
81
|
+
other_ops.append(op)
|
|
82
|
+
|
|
83
|
+
# Reassemble the ops sequence in strict dependency order
|
|
84
|
+
directive.upgrade_ops.ops = schema_ops + extension_ops + other_ops + trigger_ops
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def chain_revision_directives(*callbacks):
|
|
88
|
+
"""
|
|
89
|
+
Chain multiple process_revision_directives callbacks together into a single
|
|
90
|
+
listener for Alembic's context.configure().
|
|
91
|
+
"""
|
|
92
|
+
|
|
93
|
+
def wrapper(context, revision, directives: list[MigrationScript]):
|
|
94
|
+
for cb in callbacks:
|
|
95
|
+
cb(context, revision, directives)
|
|
96
|
+
|
|
97
|
+
return wrapper
|
|
61
98
|
|
|
62
99
|
|
|
63
100
|
def register_core_entities(audit: "PostgreSQLAudit"):
|
|
@@ -86,7 +123,7 @@ def register_triggers(audit: "PostgreSQLAudit"):
|
|
|
86
123
|
|
|
87
124
|
def get_blind_migration_op(entity: "ReplaceableEntity", connection: Connection):
|
|
88
125
|
session = Session(bind=connection)
|
|
89
|
-
db_ents: list[
|
|
126
|
+
db_ents: list[ReplaceableEntity] = entity.from_database(session, entity.schema)
|
|
90
127
|
|
|
91
128
|
for db_ent in db_ents:
|
|
92
129
|
if entity.identity == db_ent.identity:
|
{flask_postgresql_audit-1.0.9 → flask_postgresql_audit-1.1.0}/flask_postgresql_audit/base.py
RENAMED
|
@@ -3,8 +3,7 @@ from typing import TYPE_CHECKING, Any
|
|
|
3
3
|
|
|
4
4
|
from sqlalchemy import BigInteger, Text, func, text
|
|
5
5
|
from sqlalchemy.dialects.postgresql import INET, JSONB
|
|
6
|
-
from sqlalchemy.orm import Mapped,
|
|
7
|
-
|
|
6
|
+
from sqlalchemy.orm import Mapped, declared_attr, mapped_column
|
|
8
7
|
|
|
9
8
|
from .typing import OMap
|
|
10
9
|
|
|
@@ -28,8 +27,8 @@ class ActivityBase:
|
|
|
28
27
|
changed_data: OMap[dict] = mapped_column(JSONB, default={}, server_default="{}")
|
|
29
28
|
|
|
30
29
|
def __repr__(self):
|
|
31
|
-
return
|
|
32
|
-
|
|
30
|
+
return (
|
|
31
|
+
f"<{self.__class__.__name__} table_name={self.table_name!r} id={self.id!r}>"
|
|
33
32
|
)
|
|
34
33
|
|
|
35
34
|
|
|
@@ -48,6 +47,6 @@ class TransactionBase:
|
|
|
48
47
|
return func.tsrange(cls.issued_at - interval, cls.issued_at)
|
|
49
48
|
|
|
50
49
|
def __repr__(self):
|
|
51
|
-
return
|
|
52
|
-
|
|
50
|
+
return (
|
|
51
|
+
f"<{self.__class__.__name__} id={self.id!r} issued_at={self.issued_at!r}>"
|
|
53
52
|
)
|
{flask_postgresql_audit-1.0.9 → flask_postgresql_audit-1.1.0}/flask_postgresql_audit/core.py
RENAMED
|
@@ -4,11 +4,10 @@ from contextlib import contextmanager
|
|
|
4
4
|
from itertools import groupby
|
|
5
5
|
|
|
6
6
|
import sqlalchemy as sa
|
|
7
|
-
import sqlalchemy.event as event
|
|
8
|
-
import sqlalchemy.orm as orm
|
|
9
7
|
from alembic_utils.replaceable_entity import ReplaceableEntity
|
|
10
8
|
from flask import Flask
|
|
11
9
|
from flask_sqlalchemy import SQLAlchemy
|
|
10
|
+
from sqlalchemy import event, orm
|
|
12
11
|
from sqlalchemy.dialects.postgresql import insert as pg_insert
|
|
13
12
|
from sqlalchemy.util import OrderedSet
|
|
14
13
|
|
|
@@ -26,7 +25,7 @@ class ImproperlyConfigured(Exception):
|
|
|
26
25
|
|
|
27
26
|
|
|
28
27
|
class Audit:
|
|
29
|
-
__audit_args__: dict = {}
|
|
28
|
+
__audit_args__: t.ClassVar[dict] = {}
|
|
30
29
|
__audit_schema__: str = ""
|
|
31
30
|
|
|
32
31
|
__audit_relid__: orm.Mapped[int]
|
|
@@ -72,7 +71,7 @@ def get_modified_columns(obj: orm.DeclarativeBase):
|
|
|
72
71
|
modified: set[sa.ColumnElement[t.Any]] = set()
|
|
73
72
|
mapper = sa.inspect(obj.__class__)
|
|
74
73
|
for key, attr in sa.inspect(obj).attrs.items():
|
|
75
|
-
if key in mapper.synonyms
|
|
74
|
+
if key in mapper.synonyms:
|
|
76
75
|
continue
|
|
77
76
|
if not attr.history.has_changes():
|
|
78
77
|
continue
|
|
@@ -148,10 +147,10 @@ class PostgreSQLAudit:
|
|
|
148
147
|
def __init__(
|
|
149
148
|
self,
|
|
150
149
|
*,
|
|
151
|
-
actor_cls:
|
|
150
|
+
actor_cls: str | None = None,
|
|
152
151
|
actor_id_getter: t.Callable[[], t.Any] = _default_actor_id,
|
|
153
152
|
client_address_getter: t.Callable[[], t.Any] = _default_client_addr,
|
|
154
|
-
schema_name:
|
|
153
|
+
schema_name: str | None = None,
|
|
155
154
|
**kw,
|
|
156
155
|
):
|
|
157
156
|
self._actor_cls = actor_cls
|
|
@@ -164,7 +163,7 @@ class PostgreSQLAudit:
|
|
|
164
163
|
|
|
165
164
|
@property
|
|
166
165
|
def context(self):
|
|
167
|
-
ctx =
|
|
166
|
+
ctx = {"schema_name": self.schema_name or "public"}
|
|
168
167
|
ctx["schema_prefix"] = f"{ctx['schema_name']}."
|
|
169
168
|
ctx["revoke_cmd"] = f"REVOKE ALL ON {ctx['schema_prefix']}activity FROM public;"
|
|
170
169
|
if "jsonb_subtract_verbose" in self.options:
|
|
@@ -262,7 +261,7 @@ class PostgreSQLAudit:
|
|
|
262
261
|
self.pg_audit_enabled = True
|
|
263
262
|
self.pg_audit_entities = OrderedSet()
|
|
264
263
|
|
|
265
|
-
self.Base: type[
|
|
264
|
+
self.Base: type[orm.DeclarativeBase] = db.Model # type: ignore
|
|
266
265
|
|
|
267
266
|
self.Transaction = transaction_model_factory(
|
|
268
267
|
self.Base,
|
|
@@ -309,7 +308,7 @@ class PostgreSQLAudit:
|
|
|
309
308
|
stmt = stmt.filter(self.Activity.table_name == class_.__tablename__)
|
|
310
309
|
|
|
311
310
|
else:
|
|
312
|
-
objects = list(obj) if isinstance(obj, t.Sequence) else
|
|
311
|
+
objects = list(obj) if isinstance(obj, t.Sequence) else [obj]
|
|
313
312
|
conditions = []
|
|
314
313
|
for class_, objs in groupby(objects, key=lambda o: o.__class__):
|
|
315
314
|
conditions.append(
|
{flask_postgresql_audit-1.0.9 → flask_postgresql_audit-1.1.0}/flask_postgresql_audit/descriptors.py
RENAMED
|
@@ -13,7 +13,7 @@ class hybridmethod(t.Generic[_P, _R]):
|
|
|
13
13
|
def __init__(
|
|
14
14
|
self,
|
|
15
15
|
inst_func: t.Callable[t.Concatenate[t.Any, _P], _R],
|
|
16
|
-
cls_func: t.
|
|
16
|
+
cls_func: t.Callable[t.Concatenate[t.Any, _P], _R] | None = None,
|
|
17
17
|
):
|
|
18
18
|
self.inst_func = inst_func
|
|
19
19
|
if cls_func is not None:
|
|
@@ -22,7 +22,7 @@ class hybridmethod(t.Generic[_P, _R]):
|
|
|
22
22
|
self.classmethod(inst_func)
|
|
23
23
|
|
|
24
24
|
def __get__(
|
|
25
|
-
self, instance:
|
|
25
|
+
self, instance: object | None, owner: type[object]
|
|
26
26
|
) -> t.Callable[t.Concatenate[t.Any, _P], _R]:
|
|
27
27
|
if instance is None:
|
|
28
28
|
return self.cls_func.__get__(owner, owner)
|
|
@@ -58,7 +58,7 @@ class PGAggregate(ReplaceableEntity):
|
|
|
58
58
|
)
|
|
59
59
|
|
|
60
60
|
@classmethod
|
|
61
|
-
def from_database(cls, sess: Session, schema: str="%") -> "list[PGAggregate]":
|
|
61
|
+
def from_database(cls, sess: Session, schema: str = "%") -> "list[PGAggregate]":
|
|
62
62
|
PG_GTE_11 = """
|
|
63
63
|
and p.prokind = 'a'
|
|
64
64
|
"""
|
|
@@ -1,9 +1,8 @@
|
|
|
1
1
|
import typing as t
|
|
2
|
-
from datetime import datetime
|
|
2
|
+
from datetime import UTC, datetime
|
|
3
3
|
|
|
4
|
-
import sqlalchemy.event as event
|
|
5
|
-
import sqlalchemy.orm as orm
|
|
6
4
|
from flask_sqlalchemy.session import Session
|
|
5
|
+
from sqlalchemy import event, orm
|
|
7
6
|
|
|
8
7
|
from flask_postgresql_audit.core import _default_actor_id
|
|
9
8
|
|
|
@@ -18,18 +17,19 @@ def attach_listener(actor_id_getter: t.Callable[[], t.Any] = _default_actor_id):
|
|
|
18
17
|
def __receive_before_flush__(
|
|
19
18
|
session: Session,
|
|
20
19
|
flush_context: orm.UOWTransaction,
|
|
21
|
-
instances: t.
|
|
20
|
+
instances: t.Sequence[_O] | None,
|
|
22
21
|
):
|
|
23
22
|
for obj in session.new:
|
|
24
|
-
if isinstance(obj, DocumentStaging)
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
23
|
+
if isinstance(obj, DocumentStaging) and (
|
|
24
|
+
obj.docstatus == Docstatus.DRAFT or obj.docstatus is None
|
|
25
|
+
):
|
|
26
|
+
obj.created_by = actor_id_getter()
|
|
27
|
+
obj.created_on = datetime.now(UTC)
|
|
28
28
|
for obj in session.dirty:
|
|
29
29
|
if isinstance(obj, DocumentStaging):
|
|
30
30
|
if obj.docstatus == Docstatus.SUBMITTED:
|
|
31
31
|
obj.submitted_by = actor_id_getter()
|
|
32
|
-
obj.submitted_on = datetime.now()
|
|
32
|
+
obj.submitted_on = datetime.now(UTC)
|
|
33
33
|
if obj.docstatus == Docstatus.CANCELLED:
|
|
34
34
|
obj.cancelled_by = actor_id_getter()
|
|
35
|
-
obj.cancelled_on = datetime.now()
|
|
35
|
+
obj.cancelled_on = datetime.now(UTC)
|
|
@@ -2,9 +2,8 @@ import typing as t
|
|
|
2
2
|
from datetime import datetime
|
|
3
3
|
|
|
4
4
|
import sqlalchemy as sa
|
|
5
|
-
import sqlalchemy.event as event
|
|
6
|
-
import sqlalchemy.orm as orm
|
|
7
5
|
from flask_sqlalchemy.session import Session
|
|
6
|
+
from sqlalchemy import event, orm
|
|
8
7
|
from sqlalchemy.dialects.postgresql import ENUM as PGEnum
|
|
9
8
|
|
|
10
9
|
from flask_postgresql_audit.typing import OMap
|
{flask_postgresql_audit-1.0.9 → flask_postgresql_audit-1.1.0}/flask_postgresql_audit/models.py
RENAMED
|
@@ -28,7 +28,7 @@ def activity_model_factory(
|
|
|
28
28
|
):
|
|
29
29
|
class PGAuditActivity(base_cls, activity_base):
|
|
30
30
|
__tablename__ = "pga_activity"
|
|
31
|
-
__table_args__ = {"schema": schema_name}
|
|
31
|
+
__table_args__: t.ClassVar[dict[str, t.Any]] = {"schema": schema_name}
|
|
32
32
|
|
|
33
33
|
transaction_id = mapped_column(sa.BigInteger, sa.ForeignKey(transaction_cls.id))
|
|
34
34
|
transaction = relationship(transaction_cls, backref="activities")
|
|
@@ -44,7 +44,7 @@ def transaction_model_factory(
|
|
|
44
44
|
**kwargs,
|
|
45
45
|
):
|
|
46
46
|
if actor_cls:
|
|
47
|
-
actor_mapper:
|
|
47
|
+
actor_mapper: Mapper[DeclarativeBase] = sa.inspect(actor_cls)
|
|
48
48
|
if len(actor_mapper.primary_key) != 1:
|
|
49
49
|
raise PGAuditFactoryError(
|
|
50
50
|
"PGAudit does not support actor class"
|
|
@@ -59,7 +59,7 @@ def transaction_model_factory(
|
|
|
59
59
|
@declared_attr
|
|
60
60
|
def actor_id(cls) -> OMap[t.Any]:
|
|
61
61
|
if actor_cls:
|
|
62
|
-
return mapped_column(actor_pk.type, ForeignKey(actor_pk))
|
|
62
|
+
return mapped_column(actor_pk.type, ForeignKey(actor_pk)) # type: ignore
|
|
63
63
|
return mapped_column(sa.Text)
|
|
64
64
|
|
|
65
65
|
if actor_cls:
|
|
@@ -25,7 +25,7 @@ classifiers = [
|
|
|
25
25
|
"Topic :: Software Development :: Libraries :: Python Modules",
|
|
26
26
|
]
|
|
27
27
|
dependencies = [
|
|
28
|
-
"alembic>=1.
|
|
28
|
+
"alembic>=1.18.0",
|
|
29
29
|
"alembic-utils>=0.8.8",
|
|
30
30
|
"flask>=3.1.2",
|
|
31
31
|
"flask-sqlalchemy>=3.1.1",
|
|
@@ -38,9 +38,9 @@ Homepage = "https://github.com/higamigu/flask-postgresql-audit"
|
|
|
38
38
|
[dependency-groups]
|
|
39
39
|
dev = [
|
|
40
40
|
"pytest>=8.4.2",
|
|
41
|
-
"ruff>=0.
|
|
41
|
+
"ruff>=0.16.1",
|
|
42
42
|
"setuptools>=80.9.0",
|
|
43
|
-
"tox>=4.31.0"
|
|
43
|
+
"tox>=4.31.0",
|
|
44
44
|
]
|
|
45
45
|
|
|
46
46
|
[tool.hatch.version]
|
|
File without changes
|
|
@@ -10,18 +10,21 @@ from flask_postgresql_audit import Audit, PostgreSQLAudit
|
|
|
10
10
|
|
|
11
11
|
|
|
12
12
|
class DefaultConfig:
|
|
13
|
-
DB_USER = os.environ.get("
|
|
14
|
-
DB_PASSWORD = os.environ.get("
|
|
15
|
-
DB_NAME = os.environ.get("
|
|
13
|
+
DB_USER = os.environ.get("POSTGRES_USER", "postgres")
|
|
14
|
+
DB_PASSWORD = os.environ.get("POSTGRES_PASSWORD", "postgres")
|
|
15
|
+
DB_NAME = os.environ.get("POSTGRES_DB", "pg_audit_test")
|
|
16
|
+
DB_PORT = os.environ.get("POSTGRES_PORT", "5432")
|
|
16
17
|
|
|
17
18
|
SQLALCHEMY_DATABASE_URI = (
|
|
18
|
-
f"postgresql://{DB_USER}:{DB_PASSWORD}@localhost/{DB_NAME}"
|
|
19
|
+
f"postgresql://{DB_USER}:{DB_PASSWORD}@localhost:{DB_PORT}/{DB_NAME}"
|
|
19
20
|
)
|
|
20
21
|
SQLALCHEMY_ECHO = False
|
|
21
22
|
|
|
23
|
+
PGA_SCHEMA_NAME = os.environ.get("SCHEMA_NAME", None)
|
|
24
|
+
|
|
22
25
|
|
|
23
26
|
db = SQLAlchemy()
|
|
24
|
-
audit = PostgreSQLAudit()
|
|
27
|
+
audit = PostgreSQLAudit(schema_name=DefaultConfig.PGA_SCHEMA_NAME)
|
|
25
28
|
|
|
26
29
|
|
|
27
30
|
class BaseModel(db.Model):
|
|
@@ -10,18 +10,16 @@ ALEMBIC_CONFIG = REPO_ROOT / "tests/migrations"
|
|
|
10
10
|
@pytest.fixture(scope="session")
|
|
11
11
|
def test_client():
|
|
12
12
|
test_client = app.test_client()
|
|
13
|
-
with app.app_context():
|
|
14
|
-
|
|
15
|
-
yield test_client
|
|
13
|
+
with app.app_context(), alembic_migration(db, ALEMBIC_CONFIG):
|
|
14
|
+
yield test_client
|
|
16
15
|
|
|
17
16
|
|
|
18
17
|
@pytest.fixture(autouse=True)
|
|
19
18
|
def join_transaction_mode(test_client):
|
|
20
|
-
with db.engine.connect() as connection:
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
db.session.close()
|
|
19
|
+
with db.engine.connect() as connection, test_session(connection) as session:
|
|
20
|
+
db.session = session
|
|
21
|
+
yield
|
|
22
|
+
db.session.close()
|
|
25
23
|
|
|
26
24
|
|
|
27
25
|
@pytest.fixture
|
|
@@ -4,6 +4,10 @@ from alembic import context
|
|
|
4
4
|
from alembic.operations.ops import MigrationScript
|
|
5
5
|
from sqlalchemy import engine_from_config, pool
|
|
6
6
|
|
|
7
|
+
from flask_postgresql_audit.alembic import (
|
|
8
|
+
chain_revision_directives,
|
|
9
|
+
reorder_migration_ops,
|
|
10
|
+
)
|
|
7
11
|
from tests.app import db
|
|
8
12
|
|
|
9
13
|
# this is the Alembic Config object, which provides
|
|
@@ -30,10 +34,9 @@ target_metadata = db.metadata
|
|
|
30
34
|
def process_revision_directives(context, revision, directives: list[MigrationScript]):
|
|
31
35
|
if getattr(config.cmd_opts, "autogenerate", False):
|
|
32
36
|
script = directives[0]
|
|
33
|
-
if upgrade_ops := script.upgrade_ops:
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
print("No changes in schema detected.")
|
|
37
|
+
if (upgrade_ops := script.upgrade_ops) and upgrade_ops.is_empty():
|
|
38
|
+
directives[:] = []
|
|
39
|
+
print("No changes in schema detected.")
|
|
37
40
|
|
|
38
41
|
|
|
39
42
|
def run_migrations_online() -> None:
|
|
@@ -54,7 +57,10 @@ def run_migrations_online() -> None:
|
|
|
54
57
|
connection=connection,
|
|
55
58
|
target_metadata=target_metadata,
|
|
56
59
|
include_schemas=True,
|
|
57
|
-
process_revision_directives=
|
|
60
|
+
process_revision_directives=chain_revision_directives(
|
|
61
|
+
reorder_migration_ops,
|
|
62
|
+
process_revision_directives,
|
|
63
|
+
),
|
|
58
64
|
)
|
|
59
65
|
|
|
60
66
|
with context.begin_transaction():
|
|
@@ -3,7 +3,9 @@ import json
|
|
|
3
3
|
import pytest
|
|
4
4
|
from sqlalchemy import text
|
|
5
5
|
|
|
6
|
-
from .app import db
|
|
6
|
+
from .app import audit, db
|
|
7
|
+
|
|
8
|
+
JSONB_SUBTRACT = "SELECT {schema}jsonb_subtract((:new)::jsonb, (:old)::jsonb)"
|
|
7
9
|
|
|
8
10
|
|
|
9
11
|
@pytest.mark.parametrize(
|
|
@@ -24,8 +26,8 @@ from .app import db
|
|
|
24
26
|
def test_jsonb_subtract(test_client, old, new, result):
|
|
25
27
|
assert (
|
|
26
28
|
db.session.scalar(
|
|
27
|
-
text("
|
|
28
|
-
|
|
29
|
+
text(JSONB_SUBTRACT.format(schema=audit.context["schema_prefix"])),
|
|
30
|
+
{"old": json.dumps(old), "new": json.dumps(new)},
|
|
29
31
|
)
|
|
30
32
|
== result
|
|
31
33
|
)
|
|
@@ -52,30 +52,37 @@ def alembic_config(cmd: str, engine: Engine, alembic_cfg_path: Path, **kw):
|
|
|
52
52
|
|
|
53
53
|
def revision(engine: Engine, alembic_cfg_path: Path, **kw):
|
|
54
54
|
stdout = StringIO()
|
|
55
|
-
with
|
|
56
|
-
|
|
57
|
-
|
|
55
|
+
with (
|
|
56
|
+
alembic_config("revision", engine, alembic_cfg_path, **kw) as cfg,
|
|
57
|
+
contextlib.redirect_stdout(stdout),
|
|
58
|
+
):
|
|
59
|
+
command.revision(cfg, **kw)
|
|
58
60
|
return stdout.getvalue()
|
|
59
61
|
|
|
60
62
|
|
|
61
63
|
def upgrade(engine: Engine, alembic_cfg_path: Path, **kw):
|
|
62
64
|
stdout = StringIO()
|
|
63
|
-
with
|
|
64
|
-
|
|
65
|
-
|
|
65
|
+
with (
|
|
66
|
+
alembic_config("upgrade", engine, alembic_cfg_path, **kw) as cfg,
|
|
67
|
+
contextlib.redirect_stdout(stdout),
|
|
68
|
+
):
|
|
69
|
+
command.upgrade(cfg, **kw)
|
|
66
70
|
return stdout.getvalue()
|
|
67
71
|
|
|
68
72
|
|
|
69
73
|
def downgrade(engine: Engine, alembic_cfg_path: Path, **kw):
|
|
70
74
|
stdout = StringIO()
|
|
71
|
-
with
|
|
72
|
-
|
|
73
|
-
|
|
75
|
+
with (
|
|
76
|
+
alembic_config("downgrade", engine, alembic_cfg_path, **kw) as cfg,
|
|
77
|
+
contextlib.redirect_stdout(stdout),
|
|
78
|
+
):
|
|
79
|
+
command.upgrade(cfg, **kw)
|
|
74
80
|
return stdout.getvalue()
|
|
75
81
|
|
|
76
82
|
|
|
77
83
|
def clear_migrations(engine: Engine, alembic_cfg_path: Path, **kw):
|
|
78
84
|
with engine.begin() as connection:
|
|
85
|
+
connection.execute(text("DROP EXTENSION IF EXISTS btree_gist CASCADE;"))
|
|
79
86
|
connection.execute(text("DROP SCHEMA IF EXISTS public CASCADE;"))
|
|
80
87
|
connection.execute(text("DROP SCHEMA IF EXISTS audit CASCADE;"))
|
|
81
88
|
connection.execute(text("CREATE SCHEMA public;"))
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
# ruff: noqa: F401
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{flask_postgresql_audit-1.0.9 → flask_postgresql_audit-1.1.0}/flask_postgresql_audit/typing.py
RENAMED
|
File without changes
|
{flask_postgresql_audit-1.0.9 → flask_postgresql_audit-1.1.0}/flask_postgresql_audit/utils.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{flask_postgresql_audit-1.0.9 → flask_postgresql_audit-1.1.0}/tests/migrations/script.py.mako
RENAMED
|
File without changes
|
|
File without changes
|