flask-postgresql-audit 1.0.7__tar.gz → 1.0.9__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 (46) hide show
  1. {flask_postgresql_audit-1.0.7 → flask_postgresql_audit-1.0.9}/PKG-INFO +1 -1
  2. {flask_postgresql_audit-1.0.7 → flask_postgresql_audit-1.0.9}/flask_postgresql_audit/__init__.py +1 -1
  3. {flask_postgresql_audit-1.0.7 → flask_postgresql_audit-1.0.9}/flask_postgresql_audit/alembic/entities.py +9 -0
  4. {flask_postgresql_audit-1.0.7 → flask_postgresql_audit-1.0.9}/flask_postgresql_audit/alembic/setup.py +1 -0
  5. {flask_postgresql_audit-1.0.7 → flask_postgresql_audit-1.0.9}/flask_postgresql_audit/base.py +3 -2
  6. {flask_postgresql_audit-1.0.7 → flask_postgresql_audit-1.0.9}/flask_postgresql_audit/core.py +61 -38
  7. {flask_postgresql_audit-1.0.7 → flask_postgresql_audit-1.0.9}/flask_postgresql_audit/extensions/alembic_utils/pg_aggregate.py +2 -2
  8. {flask_postgresql_audit-1.0.7 → flask_postgresql_audit-1.0.9}/flask_postgresql_audit/models.py +1 -1
  9. {flask_postgresql_audit-1.0.7 → flask_postgresql_audit-1.0.9}/flask_postgresql_audit/templates/get_pk_values.sql +1 -1
  10. flask_postgresql_audit-1.0.9/flask_postgresql_audit/templates/get_table_relid.sql +4 -0
  11. {flask_postgresql_audit-1.0.7 → flask_postgresql_audit-1.0.9}/.gitignore +0 -0
  12. {flask_postgresql_audit-1.0.7 → flask_postgresql_audit-1.0.9}/CHANGES.md +0 -0
  13. {flask_postgresql_audit-1.0.7 → flask_postgresql_audit-1.0.9}/LICENSE +0 -0
  14. {flask_postgresql_audit-1.0.7 → flask_postgresql_audit-1.0.9}/README.md +0 -0
  15. {flask_postgresql_audit-1.0.7 → flask_postgresql_audit-1.0.9}/flask_postgresql_audit/alembic/__init__.py +0 -0
  16. {flask_postgresql_audit-1.0.7 → flask_postgresql_audit-1.0.9}/flask_postgresql_audit/alembic/schema.py +0 -0
  17. {flask_postgresql_audit-1.0.7 → flask_postgresql_audit-1.0.9}/flask_postgresql_audit/descriptors.py +0 -0
  18. {flask_postgresql_audit-1.0.7 → flask_postgresql_audit-1.0.9}/flask_postgresql_audit/extensions/__init__.py +0 -0
  19. {flask_postgresql_audit-1.0.7 → flask_postgresql_audit-1.0.9}/flask_postgresql_audit/extensions/alembic_utils/__init__.py +0 -0
  20. {flask_postgresql_audit-1.0.7 → flask_postgresql_audit-1.0.9}/flask_postgresql_audit/extensions/document_staging/__init__.py +0 -0
  21. {flask_postgresql_audit-1.0.7 → flask_postgresql_audit-1.0.9}/flask_postgresql_audit/extensions/document_staging/enum.py +0 -0
  22. {flask_postgresql_audit-1.0.7 → flask_postgresql_audit-1.0.9}/flask_postgresql_audit/extensions/document_staging/listener.py +0 -0
  23. {flask_postgresql_audit-1.0.7 → flask_postgresql_audit-1.0.9}/flask_postgresql_audit/extensions/document_staging/model.py +0 -0
  24. {flask_postgresql_audit-1.0.7 → flask_postgresql_audit-1.0.9}/flask_postgresql_audit/templates/audit_trigger_delete.sql +0 -0
  25. {flask_postgresql_audit-1.0.7 → flask_postgresql_audit-1.0.9}/flask_postgresql_audit/templates/audit_trigger_insert.sql +0 -0
  26. {flask_postgresql_audit-1.0.7 → flask_postgresql_audit-1.0.9}/flask_postgresql_audit/templates/audit_trigger_update.sql +0 -0
  27. {flask_postgresql_audit-1.0.7 → flask_postgresql_audit-1.0.9}/flask_postgresql_audit/templates/create_activity.sql +0 -0
  28. {flask_postgresql_audit-1.0.7 → flask_postgresql_audit-1.0.9}/flask_postgresql_audit/templates/create_schema.sql +0 -0
  29. {flask_postgresql_audit-1.0.7 → flask_postgresql_audit-1.0.9}/flask_postgresql_audit/templates/drop_schema.sql +0 -0
  30. {flask_postgresql_audit-1.0.7 → flask_postgresql_audit-1.0.9}/flask_postgresql_audit/templates/get_setting.sql +0 -0
  31. {flask_postgresql_audit-1.0.7 → flask_postgresql_audit-1.0.9}/flask_postgresql_audit/templates/jsonb_subtract.sql +0 -0
  32. {flask_postgresql_audit-1.0.7 → flask_postgresql_audit-1.0.9}/flask_postgresql_audit/templates/row_audit_trigger.sql +0 -0
  33. {flask_postgresql_audit-1.0.7 → flask_postgresql_audit-1.0.9}/flask_postgresql_audit/templates/row_create_activity.sql +0 -0
  34. {flask_postgresql_audit-1.0.7 → flask_postgresql_audit-1.0.9}/flask_postgresql_audit/typing.py +0 -0
  35. {flask_postgresql_audit-1.0.7 → flask_postgresql_audit-1.0.9}/flask_postgresql_audit/utils.py +0 -0
  36. {flask_postgresql_audit-1.0.7 → flask_postgresql_audit-1.0.9}/pyproject.toml +0 -0
  37. {flask_postgresql_audit-1.0.7 → flask_postgresql_audit-1.0.9}/tests/__init__.py +0 -0
  38. {flask_postgresql_audit-1.0.7 → flask_postgresql_audit-1.0.9}/tests/app.py +0 -0
  39. {flask_postgresql_audit-1.0.7 → flask_postgresql_audit-1.0.9}/tests/conftest.py +0 -0
  40. {flask_postgresql_audit-1.0.7 → flask_postgresql_audit-1.0.9}/tests/migrations/README +0 -0
  41. {flask_postgresql_audit-1.0.7 → flask_postgresql_audit-1.0.9}/tests/migrations/alembic.ini +0 -0
  42. {flask_postgresql_audit-1.0.7 → flask_postgresql_audit-1.0.9}/tests/migrations/env.py +0 -0
  43. {flask_postgresql_audit-1.0.7 → flask_postgresql_audit-1.0.9}/tests/migrations/script.py.mako +0 -0
  44. {flask_postgresql_audit-1.0.7 → flask_postgresql_audit-1.0.9}/tests/test_defaults.py +0 -0
  45. {flask_postgresql_audit-1.0.7 → flask_postgresql_audit-1.0.9}/tests/test_operators.py +0 -0
  46. {flask_postgresql_audit-1.0.7 → flask_postgresql_audit-1.0.9}/tests/utils.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: flask-postgresql_audit
3
- Version: 1.0.7
3
+ Version: 1.0.9
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>
@@ -1,4 +1,4 @@
1
1
  # ruff: noqa: F401
2
2
  from .core import Audit, PostgreSQLAudit
3
3
 
4
- __version__ = "1.0.7"
4
+ __version__ = "1.0.9"
@@ -55,6 +55,15 @@ def get_pk_values(**context):
55
55
  )
56
56
 
57
57
 
58
+ def get_table_relid(**context):
59
+ sql = parse_template("get_table_relid.sql", **context)
60
+ return PGFunction(
61
+ schema=context["schema_name"],
62
+ signature="get_table_relid(tablename text)",
63
+ definition=sql["definition"],
64
+ )
65
+
66
+
58
67
  def get_setting_factory(**context):
59
68
  sql = parse_template("get_setting.sql", **context)
60
69
  return PGFunction(
@@ -63,6 +63,7 @@ def setup_db(audit: "PostgreSQLAudit"):
63
63
  def register_core_entities(audit: "PostgreSQLAudit"):
64
64
  audit.pg_audit_entities.add(entities.btree_gist)
65
65
  audit.pg_audit_entities.add(entities.get_pk_values(**audit.context))
66
+ audit.pg_audit_entities.add(entities.get_table_relid(**audit.context))
66
67
  audit.pg_audit_entities.add(entities.get_setting_factory(**audit.context))
67
68
  audit.pg_audit_entities.add(entities.jsonb_subtract_factory(**audit.context))
68
69
  audit.pg_audit_entities.add(entities.create_activity_factory(**audit.context))
@@ -3,7 +3,8 @@ 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, mapped_column
6
+ from sqlalchemy.orm import Mapped, mapped_column, declared_attr
7
+
7
8
 
8
9
  from .typing import OMap
9
10
 
@@ -34,7 +35,7 @@ class ActivityBase:
34
35
 
35
36
  class TransactionBase:
36
37
  if TYPE_CHECKING:
37
- actor_id: OMap[Any]
38
+ actor_id: declared_attr[Any]
38
39
 
39
40
  id: Mapped[int] = mapped_column(BigInteger, primary_key=True)
40
41
  native_transaction_id: OMap[int] = mapped_column(BigInteger)
@@ -27,6 +27,12 @@ class ImproperlyConfigured(Exception):
27
27
 
28
28
  class Audit:
29
29
  __audit_args__: dict = {}
30
+ __audit_schema__: str = ""
31
+
32
+ __audit_relid__: orm.Mapped[int]
33
+ __audit_rowkey__: orm.Mapped[list[str]]
34
+
35
+ __audit_configured__: t.ClassVar[bool]
30
36
 
31
37
  if t.TYPE_CHECKING:
32
38
  __table__: sa.Table
@@ -34,10 +40,33 @@ class Audit:
34
40
 
35
41
  def __init__(self, **kw: t.Any): ...
36
42
 
37
- def __init_subclass__(cls) -> None:
43
+ def __init_subclass__(cls, *args, **kwargs) -> None:
38
44
  if not hasattr(cls, "__audit_args__"):
39
45
  cls.__audit_args__ = {}
40
46
 
47
+ event.listen(orm.Mapper, "mapper_configured", cls.__init_audit__)
48
+ super().__init_subclass__(*args, **kwargs)
49
+
50
+ @classmethod
51
+ def __init_audit__(cls, mapper: orm.Mapper[t.Self], class_: type[t.Self]):
52
+ is_configured = getattr(class_, "__audit_configured__", False)
53
+ has_schema = hasattr(class_, "__audit_schema__")
54
+ if not is_configured and has_schema:
55
+ fn_ = sa.func
56
+ if class_.__audit_schema__ and class_.__audit_schema__ != "public":
57
+ fn_ = getattr(sa.func, class_.__audit_schema__)
58
+
59
+ table = class_.__table__
60
+ jsonb = sa.func.to_jsonb(table.table_valued())
61
+
62
+ relid = orm.column_property(fn_.get_table_relid(class_.__tablename__))
63
+ rowkey = orm.column_property(fn_.get_pk_values(relid, jsonb))
64
+
65
+ mapper.add_property("__audit_relid__", relid)
66
+ mapper.add_property("__audit_rowkey__", rowkey)
67
+
68
+ class_.__audit_configured__ = True
69
+
41
70
 
42
71
  def get_modified_columns(obj: orm.DeclarativeBase):
43
72
  modified: set[sa.ColumnElement[t.Any]] = set()
@@ -65,6 +94,14 @@ def get_audit_models(registry: orm.registry):
65
94
  return models
66
95
 
67
96
 
97
+ def inject_model_metadata(models: t.Iterable[type[Audit]], **kwargs):
98
+ kwargs.setdefault("schema", "public")
99
+
100
+ for m in models:
101
+ for k, v in kwargs.items():
102
+ setattr(m, f"__audit_{k}__", v)
103
+
104
+
68
105
  def is_object_modified(obj: object):
69
106
  if isinstance(obj, Audit):
70
107
  excluded = set(obj.__class__.__audit_args__.get("exclude", []))
@@ -242,6 +279,7 @@ class PostgreSQLAudit:
242
279
  )
243
280
 
244
281
  self.pg_audit_classes = get_audit_models(self.Base.registry)
282
+ inject_model_metadata(self.pg_audit_classes, schema=self.schema_name)
245
283
 
246
284
  self.setup_db()
247
285
  self.attach_listeners()
@@ -249,40 +287,6 @@ class PostgreSQLAudit:
249
287
  app.extensions["postgresql-audit"] = self
250
288
 
251
289
  def fetch_activity(self, obj: type[Audit] | Audit | t.Sequence[Audit]):
252
- if inspect.isclass(obj):
253
- source = obj
254
- relid = sa.text(f"'{source.__tablename__}'::regclass::oid")
255
- jsonb = sa.func.to_jsonb(source.__table__.table_valued())
256
- activity_join_cond = sa.and_(
257
- self.Activity.table_name == source.__tablename__,
258
- self.Activity.row_key == self.func.get_pk_values(relid, jsonb),
259
- )
260
-
261
- else:
262
- objects = list(obj) if isinstance(obj, t.Sequence) else list([obj])
263
- unions = []
264
- for key, group in groupby(objects, key=lambda o: o.__class__):
265
- relid = sa.text(f"'{key.__tablename__}'::regclass::oid")
266
- jsonb = sa.func.to_jsonb(key.__table__.table_valued())
267
-
268
- union = sa.select(
269
- sa.literal(key.__tablename__).label("table_name"),
270
- self.func.get_pk_values(relid, jsonb).label("row_key"),
271
- )
272
-
273
- for pk in key.__table__.primary_key.columns:
274
- pk_attr: sa.ColumnElement[t.Any] = getattr(key, pk.name)
275
- pks = [getattr(o, pk.name) for o in group]
276
- union = union.filter(pk_attr.in_(pks))
277
-
278
- unions.append(union)
279
-
280
- source = sa.union(*unions).subquery("source")
281
- activity_join_cond = sa.and_(
282
- self.Activity.table_name == source.c.table_name,
283
- self.Activity.row_key == source.c.row_key,
284
- )
285
-
286
290
  stmt = (
287
291
  sa.select(
288
292
  self.Activity.id,
@@ -295,10 +299,29 @@ class PostgreSQLAudit:
295
299
  self.Activity.old_data,
296
300
  self.Activity.changed_data,
297
301
  )
298
- .select_from(source)
299
- .join(self.Activity, activity_join_cond)
300
- .join(self.Transaction)
302
+ .select_from(self.Activity)
303
+ .outerjoin(self.Transaction)
301
304
  .order_by(self.Activity.id.desc())
302
305
  )
303
306
 
307
+ if inspect.isclass(obj):
308
+ class_ = obj
309
+ stmt = stmt.filter(self.Activity.table_name == class_.__tablename__)
310
+
311
+ else:
312
+ objects = list(obj) if isinstance(obj, t.Sequence) else list([obj])
313
+ conditions = []
314
+ for class_, objs in groupby(objects, key=lambda o: o.__class__):
315
+ conditions.append(
316
+ sa.and_(
317
+ self.Activity.table_name == class_.__tablename__,
318
+ self.Activity.row_key.in_([o.__audit_rowkey__ for o in objs]),
319
+ )
320
+ )
321
+
322
+ if len(conditions) == 1:
323
+ stmt = stmt.filter(conditions[0])
324
+ else:
325
+ stmt = stmt.filter(sa.or_(*conditions))
326
+
304
327
  return stmt
@@ -49,7 +49,7 @@ class PGAggregate(ReplaceableEntity):
49
49
  )
50
50
 
51
51
  def to_sql_statement_create_or_replace(self):
52
- return self.to_sql_statement_create(replace=True)
52
+ yield self.to_sql_statement_create(replace=True)
53
53
 
54
54
  def to_sql_statement_drop(self, cascade=False):
55
55
  return sa.text(
@@ -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
  """
@@ -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:
@@ -11,7 +11,7 @@ CREATE OR REPLACE FUNCTION get_pk_values(relid oid, row_data jsonb) RETURNS json
11
11
  AND i.indisprimary;
12
12
 
13
13
  IF pk_columns IS NULL THEN
14
- RETURN '{}'::jsonb;
14
+ RETURN '[]'::jsonb;
15
15
  END IF;
16
16
 
17
17
  SELECT jsonb_agg(row_data->>col) INTO pk_values
@@ -0,0 +1,4 @@
1
+ CREATE OR REPLACE FUNCTION get_table_relid(tablename text) RETURNS oid AS $$
2
+ SELECT tablename::regclass::oid;
3
+ $$ LANGUAGE SQL;
4
+