flask-postgresql-audit 1.0.3__tar.gz → 1.0.5__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.3 → flask_postgresql_audit-1.0.5}/PKG-INFO +1 -1
- {flask_postgresql_audit-1.0.3 → flask_postgresql_audit-1.0.5}/flask_postgresql_audit/__init__.py +1 -1
- {flask_postgresql_audit-1.0.3 → flask_postgresql_audit-1.0.5}/flask_postgresql_audit/alembic/entities.py +9 -0
- {flask_postgresql_audit-1.0.3 → flask_postgresql_audit-1.0.5}/flask_postgresql_audit/alembic/setup.py +1 -0
- {flask_postgresql_audit-1.0.3 → flask_postgresql_audit-1.0.5}/flask_postgresql_audit/base.py +1 -0
- flask_postgresql_audit-1.0.5/flask_postgresql_audit/extensions/alembic_utils/__init__.py +9 -0
- flask_postgresql_audit-1.0.5/flask_postgresql_audit/extensions/alembic_utils/pg_aggregate.py +119 -0
- {flask_postgresql_audit-1.0.3 → flask_postgresql_audit-1.0.5}/flask_postgresql_audit/templates/create_activity.sql +6 -3
- flask_postgresql_audit-1.0.5/flask_postgresql_audit/templates/get_pk_values.sql +22 -0
- flask_postgresql_audit-1.0.5/tests/__init__.py +0 -0
- {flask_postgresql_audit-1.0.3 → flask_postgresql_audit-1.0.5}/tests/test_defaults.py +1 -0
- {flask_postgresql_audit-1.0.3 → flask_postgresql_audit-1.0.5}/.gitignore +0 -0
- {flask_postgresql_audit-1.0.3 → flask_postgresql_audit-1.0.5}/CHANGES.md +0 -0
- {flask_postgresql_audit-1.0.3 → flask_postgresql_audit-1.0.5}/LICENSE +0 -0
- {flask_postgresql_audit-1.0.3 → flask_postgresql_audit-1.0.5}/README.md +0 -0
- {flask_postgresql_audit-1.0.3 → flask_postgresql_audit-1.0.5}/flask_postgresql_audit/alembic/__init__.py +0 -0
- {flask_postgresql_audit-1.0.3 → flask_postgresql_audit-1.0.5}/flask_postgresql_audit/alembic/schema.py +0 -0
- {flask_postgresql_audit-1.0.3 → flask_postgresql_audit-1.0.5}/flask_postgresql_audit/core.py +0 -0
- {flask_postgresql_audit-1.0.3/tests → flask_postgresql_audit-1.0.5/flask_postgresql_audit/extensions}/__init__.py +0 -0
- {flask_postgresql_audit-1.0.3 → flask_postgresql_audit-1.0.5}/flask_postgresql_audit/models.py +0 -0
- {flask_postgresql_audit-1.0.3 → flask_postgresql_audit-1.0.5}/flask_postgresql_audit/templates/audit_trigger_delete.sql +0 -0
- {flask_postgresql_audit-1.0.3 → flask_postgresql_audit-1.0.5}/flask_postgresql_audit/templates/audit_trigger_insert.sql +0 -0
- {flask_postgresql_audit-1.0.3 → flask_postgresql_audit-1.0.5}/flask_postgresql_audit/templates/audit_trigger_update.sql +0 -0
- {flask_postgresql_audit-1.0.3 → flask_postgresql_audit-1.0.5}/flask_postgresql_audit/templates/create_schema.sql +0 -0
- {flask_postgresql_audit-1.0.3 → flask_postgresql_audit-1.0.5}/flask_postgresql_audit/templates/drop_schema.sql +0 -0
- {flask_postgresql_audit-1.0.3 → flask_postgresql_audit-1.0.5}/flask_postgresql_audit/templates/get_setting.sql +0 -0
- {flask_postgresql_audit-1.0.3 → flask_postgresql_audit-1.0.5}/flask_postgresql_audit/templates/jsonb_subtract.sql +0 -0
- {flask_postgresql_audit-1.0.3 → flask_postgresql_audit-1.0.5}/flask_postgresql_audit/templates/row_audit_trigger.sql +0 -0
- {flask_postgresql_audit-1.0.3 → flask_postgresql_audit-1.0.5}/flask_postgresql_audit/templates/row_create_activity.sql +0 -0
- {flask_postgresql_audit-1.0.3 → flask_postgresql_audit-1.0.5}/flask_postgresql_audit/typing.py +0 -0
- {flask_postgresql_audit-1.0.3 → flask_postgresql_audit-1.0.5}/flask_postgresql_audit/utils.py +0 -0
- {flask_postgresql_audit-1.0.3 → flask_postgresql_audit-1.0.5}/pyproject.toml +0 -0
- {flask_postgresql_audit-1.0.3 → flask_postgresql_audit-1.0.5}/tests/app.py +0 -0
- {flask_postgresql_audit-1.0.3 → flask_postgresql_audit-1.0.5}/tests/conftest.py +0 -0
- {flask_postgresql_audit-1.0.3 → flask_postgresql_audit-1.0.5}/tests/migrations/README +0 -0
- {flask_postgresql_audit-1.0.3 → flask_postgresql_audit-1.0.5}/tests/migrations/alembic.ini +0 -0
- {flask_postgresql_audit-1.0.3 → flask_postgresql_audit-1.0.5}/tests/migrations/env.py +0 -0
- {flask_postgresql_audit-1.0.3 → flask_postgresql_audit-1.0.5}/tests/migrations/script.py.mako +0 -0
- {flask_postgresql_audit-1.0.3 → flask_postgresql_audit-1.0.5}/tests/test_operators.py +0 -0
- {flask_postgresql_audit-1.0.3 → flask_postgresql_audit-1.0.5}/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.
|
|
3
|
+
Version: 1.0.5
|
|
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>
|
|
@@ -46,6 +46,15 @@ def trigger_update_factory(**context):
|
|
|
46
46
|
)
|
|
47
47
|
|
|
48
48
|
|
|
49
|
+
def get_pk_values(**context):
|
|
50
|
+
sql = parse_template("get_pk_values.sql", **context)
|
|
51
|
+
return PGFunction(
|
|
52
|
+
schema=context["schema_name"],
|
|
53
|
+
signature="get_pk_values(relid oid, row_data jsonb)",
|
|
54
|
+
definition=sql["definition"],
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
|
|
49
58
|
def get_setting_factory(**context):
|
|
50
59
|
sql = parse_template("get_setting.sql", **context)
|
|
51
60
|
return PGFunction(
|
|
@@ -62,6 +62,7 @@ def setup_db(audit: "PostgreSQLAudit"):
|
|
|
62
62
|
|
|
63
63
|
def register_core_entities(audit: "PostgreSQLAudit"):
|
|
64
64
|
audit.pg_audit_entities.add(entities.btree_gist)
|
|
65
|
+
audit.pg_audit_entities.add(entities.get_pk_values(**audit.context))
|
|
65
66
|
audit.pg_audit_entities.add(entities.get_setting_factory(**audit.context))
|
|
66
67
|
audit.pg_audit_entities.add(entities.jsonb_subtract_factory(**audit.context))
|
|
67
68
|
audit.pg_audit_entities.add(entities.create_activity_factory(**audit.context))
|
{flask_postgresql_audit-1.0.3 → flask_postgresql_audit-1.0.5}/flask_postgresql_audit/base.py
RENAMED
|
@@ -22,6 +22,7 @@ class ActivityBase:
|
|
|
22
22
|
native_transaction_id: OMap[int] = mapped_column(BigInteger, index=True)
|
|
23
23
|
verb: OMap[str] = mapped_column(Text)
|
|
24
24
|
|
|
25
|
+
row_key: OMap[list[str]] = mapped_column(JSONB, default=[], server_default="[]")
|
|
25
26
|
old_data: OMap[dict] = mapped_column(JSONB, default={}, server_default="{}")
|
|
26
27
|
changed_data: OMap[dict] = mapped_column(JSONB, default={}, server_default="{}")
|
|
27
28
|
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import alembic_utils
|
|
2
|
+
from alembic_utils.experimental import collect_subclasses
|
|
3
|
+
from alembic_utils.replaceable_entity import ReplaceableEntity
|
|
4
|
+
|
|
5
|
+
from .pg_aggregate import PGAggregate
|
|
6
|
+
|
|
7
|
+
original_types = set(collect_subclasses(alembic_utils, ReplaceableEntity))
|
|
8
|
+
extension_types = {PGAggregate}
|
|
9
|
+
allowed_types = original_types | extension_types
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import re
|
|
2
|
+
import typing as t
|
|
3
|
+
|
|
4
|
+
import sqlalchemy as sa
|
|
5
|
+
from alembic_utils.exceptions import SQLParseFailure
|
|
6
|
+
from alembic_utils.replaceable_entity import ReplaceableEntity
|
|
7
|
+
from alembic_utils.statement import strip_terminating_semicolon
|
|
8
|
+
from sqlalchemy.orm import Session
|
|
9
|
+
|
|
10
|
+
T = t.TypeVar("T", bound="ReplaceableEntity")
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class PGAggregate(ReplaceableEntity):
|
|
14
|
+
def __init__(self, schema: str, signature: str, definition: str):
|
|
15
|
+
super().__init__(schema, signature, definition)
|
|
16
|
+
self.definition: str = strip_terminating_semicolon(definition)
|
|
17
|
+
|
|
18
|
+
@property
|
|
19
|
+
def type_(self):
|
|
20
|
+
return "aggregate"
|
|
21
|
+
|
|
22
|
+
@property
|
|
23
|
+
def literal_signature(self):
|
|
24
|
+
name, remainder = self.signature.split("(", 1)
|
|
25
|
+
return '"' + name.strip() + '"(' + remainder
|
|
26
|
+
|
|
27
|
+
@classmethod
|
|
28
|
+
def from_sql(cls, sql: str) -> "PGAggregate":
|
|
29
|
+
pattern = r"^CREATE\s+AGGREGATE\s+(?:(?P<schema>\w+)\.)?(?P<signature>.+)\s+(?P<definition>\(.*\))$"
|
|
30
|
+
result = re.search(pattern, sql, re.IGNORECASE)
|
|
31
|
+
if result is not None and isinstance(result, re.Match):
|
|
32
|
+
raw_signature: str = result["signature"]
|
|
33
|
+
signature = (
|
|
34
|
+
"".join(raw_signature.split('"', 2))
|
|
35
|
+
if raw_signature.startswith('"')
|
|
36
|
+
else raw_signature
|
|
37
|
+
)
|
|
38
|
+
return cls(
|
|
39
|
+
schema=result["schema"],
|
|
40
|
+
signature=signature,
|
|
41
|
+
definition=result["definition"],
|
|
42
|
+
)
|
|
43
|
+
raise SQLParseFailure(f'Failed to parse SQL into PGAggregate """{sql}"""')
|
|
44
|
+
|
|
45
|
+
def to_sql_statement_create(self, *, replace: bool = False):
|
|
46
|
+
return sa.text(
|
|
47
|
+
f"{'CREATE OR REPLACE' if replace else 'CREATE'} AGGREGATE "
|
|
48
|
+
f"{self.literal_schema}.{self.literal_signature} {self.definition}"
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
def to_sql_statement_create_or_replace(self):
|
|
52
|
+
return self.to_sql_statement_create(replace=True)
|
|
53
|
+
|
|
54
|
+
def to_sql_statement_drop(self, cascade=False):
|
|
55
|
+
return sa.text(
|
|
56
|
+
f"DROP AGGREGATE {self.literal_schema}.{self.literal_signature}"
|
|
57
|
+
+ (" CASCADE" if cascade else "")
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
@classmethod
|
|
61
|
+
def from_database(cls, sess: Session, schema: str) -> "list[PGAggregate]":
|
|
62
|
+
PG_GTE_11 = """
|
|
63
|
+
and p.prokind = 'a'
|
|
64
|
+
"""
|
|
65
|
+
|
|
66
|
+
# NOTE: not tested
|
|
67
|
+
# PG_LT_11 = """
|
|
68
|
+
# and p.proisagg
|
|
69
|
+
# and not p.proiswindow
|
|
70
|
+
# """
|
|
71
|
+
|
|
72
|
+
# Retrieve the postgres server version e.g. 90603 for 9.6.3 or 120003 for 12.3
|
|
73
|
+
pg_version_str = sess.execute(sa.text("show server_version_num")).scalar_one()
|
|
74
|
+
pg_version = int(pg_version_str)
|
|
75
|
+
|
|
76
|
+
sql = sa.text(
|
|
77
|
+
"""
|
|
78
|
+
WITH extension_functions AS (
|
|
79
|
+
SELECT objid AS extension_function_oid, classid
|
|
80
|
+
FROM pg_depend
|
|
81
|
+
WHERE deptype='e' -- depends on an extension
|
|
82
|
+
AND classid = 'pg_proc'::regclass -- is a proc/function
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
SELECT n.nspname AS function_schema
|
|
86
|
+
, p.proname AS function_name
|
|
87
|
+
, pg_get_function_arguments(p.oid) AS function_arguments
|
|
88
|
+
, format('CREATE AGGREGATE %s (SFUNC = %s, STYPE = %s%s%s%s%s)'
|
|
89
|
+
, format('%s.%s(%s)', n.nspname, p.proname, pg_get_function_arguments(p.oid))
|
|
90
|
+
, r.aggtransfn
|
|
91
|
+
, r.aggtranstype::regtype
|
|
92
|
+
, ', SORTOP = ' || NULLIF(r.aggsortop, 0)::regoper
|
|
93
|
+
, ', INITCOND = ' || quote_nullable(r.agginitval)
|
|
94
|
+
, ', FINALFUNC = ' || NULLIF(r.aggfinalfn, 0)
|
|
95
|
+
, CASE WHEN r.aggfinalextra THEN ', FINALFUNC_EXTRA' END
|
|
96
|
+
-- add more to cover special cases like moving-aggregate etc.
|
|
97
|
+
) AS create_statement
|
|
98
|
+
, t.typname AS return_type
|
|
99
|
+
, l.lanname AS function_language
|
|
100
|
+
FROM pg_aggregate r
|
|
101
|
+
LEFT JOIN pg_proc p ON p.oid = aggfnoid
|
|
102
|
+
LEFT JOIN pg_namespace n ON p.pronamespace = n.oid
|
|
103
|
+
LEFT JOIN pg_language l ON p.prolang = l.oid
|
|
104
|
+
LEFT JOIN pg_type t ON t.oid = p.prorettype
|
|
105
|
+
LEFT JOIN extension_functions ef ON p.oid = ef.extension_function_oid
|
|
106
|
+
WHERE n.nspname NOT IN ('pg_catalog', 'information_schema')
|
|
107
|
+
AND ef.extension_function_oid IS NULL -- Filter out functions from extensions
|
|
108
|
+
AND n.nspname = :schema
|
|
109
|
+
"""
|
|
110
|
+
+ (PG_GTE_11 if pg_version >= 110000 else "")
|
|
111
|
+
).bindparams(schema=schema)
|
|
112
|
+
|
|
113
|
+
rows = sess.execute(sql)
|
|
114
|
+
db_aggregates = [cls.from_sql(x[3]) for x in rows]
|
|
115
|
+
|
|
116
|
+
for agg in db_aggregates:
|
|
117
|
+
assert agg is not None
|
|
118
|
+
|
|
119
|
+
return db_aggregates
|
|
@@ -20,7 +20,7 @@ BEGIN
|
|
|
20
20
|
IF (TG_OP = 'UPDATE') THEN
|
|
21
21
|
INSERT INTO ${schema_prefix}pga_activity(
|
|
22
22
|
id, schema_name, table_name, relid, issued_at, native_transaction_id,
|
|
23
|
-
verb, old_data, changed_data, transaction_id)
|
|
23
|
+
verb, row_key, old_data, changed_data, transaction_id)
|
|
24
24
|
SELECT
|
|
25
25
|
nextval('${schema_prefix}pga_activity_id_seq') as id,
|
|
26
26
|
TG_TABLE_SCHEMA::text AS schema_name,
|
|
@@ -29,6 +29,7 @@ BEGIN
|
|
|
29
29
|
statement_timestamp() AT TIME ZONE 'UTC' AS issued_at,
|
|
30
30
|
txid_current() AS native_transaction_id,
|
|
31
31
|
LOWER(TG_OP) AS verb,
|
|
32
|
+
${schema_prefix}get_pk_values(TG_RELID, new_data) AS row_key,
|
|
32
33
|
old_data - excluded_cols AS old_data,
|
|
33
34
|
${schema_prefix}jsonb_subtract(new_data, old_data) - excluded_cols AS changed_data,
|
|
34
35
|
_transaction_id AS transaction_id
|
|
@@ -48,7 +49,7 @@ BEGIN
|
|
|
48
49
|
ELSIF (TG_OP = 'INSERT') THEN
|
|
49
50
|
INSERT INTO ${schema_prefix}pga_activity(
|
|
50
51
|
id, schema_name, table_name, relid, issued_at, native_transaction_id,
|
|
51
|
-
verb, old_data, changed_data, transaction_id)
|
|
52
|
+
verb, row_key, old_data, changed_data, transaction_id)
|
|
52
53
|
SELECT
|
|
53
54
|
nextval('${schema_prefix}pga_activity_id_seq') as id,
|
|
54
55
|
TG_TABLE_SCHEMA::text AS schema_name,
|
|
@@ -57,6 +58,7 @@ BEGIN
|
|
|
57
58
|
statement_timestamp() AT TIME ZONE 'UTC' AS issued_at,
|
|
58
59
|
txid_current() AS native_transaction_id,
|
|
59
60
|
LOWER(TG_OP) AS verb,
|
|
61
|
+
${schema_prefix}get_pk_values(TG_RELID, to_jsonb(new_table)) AS row_key,
|
|
60
62
|
'{}'::jsonb AS old_data,
|
|
61
63
|
row_to_json(new_table.*)::jsonb - excluded_cols AS changed_data,
|
|
62
64
|
_transaction_id AS transaction_id
|
|
@@ -64,7 +66,7 @@ BEGIN
|
|
|
64
66
|
ELSEIF TG_OP = 'DELETE' THEN
|
|
65
67
|
INSERT INTO ${schema_prefix}pga_activity(
|
|
66
68
|
id, schema_name, table_name, relid, issued_at, native_transaction_id,
|
|
67
|
-
verb, old_data, changed_data, transaction_id)
|
|
69
|
+
verb, row_key, old_data, changed_data, transaction_id)
|
|
68
70
|
SELECT
|
|
69
71
|
nextval('${schema_prefix}pga_activity_id_seq') as id,
|
|
70
72
|
TG_TABLE_SCHEMA::text AS schema_name,
|
|
@@ -73,6 +75,7 @@ BEGIN
|
|
|
73
75
|
statement_timestamp() AT TIME ZONE 'UTC' AS issued_at,
|
|
74
76
|
txid_current() AS native_transaction_id,
|
|
75
77
|
LOWER(TG_OP) AS verb,
|
|
78
|
+
${schema_prefix}get_pk_values(TG_RELID, to_jsonb(old_table)) AS row_key,
|
|
76
79
|
row_to_json(old_table.*)::jsonb - excluded_cols AS old_data,
|
|
77
80
|
'{}'::jsonb AS changed_data,
|
|
78
81
|
_transaction_id AS transaction_id
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
CREATE OR REPLACE FUNCTION get_pk_values(relid oid, row_data jsonb) RETURNS jsonb AS $$
|
|
2
|
+
DECLARE
|
|
3
|
+
pk_columns text [];
|
|
4
|
+
pk_values jsonb;
|
|
5
|
+
BEGIN
|
|
6
|
+
SELECT array_agg(a.attname ORDER BY a.attnum) INTO pk_columns
|
|
7
|
+
FROM pg_index i
|
|
8
|
+
JOIN pg_attribute a ON a.attrelid = i.indrelid
|
|
9
|
+
AND a.attnum = ANY(i.indkey)
|
|
10
|
+
WHERE i.indrelid = relid
|
|
11
|
+
AND i.indisprimary;
|
|
12
|
+
|
|
13
|
+
IF pk_columns IS NULL THEN
|
|
14
|
+
RETURN '{}'::jsonb;
|
|
15
|
+
END IF;
|
|
16
|
+
|
|
17
|
+
SELECT jsonb_agg(row_data->>col) INTO pk_values
|
|
18
|
+
FROM unnest(pk_columns) AS col;
|
|
19
|
+
|
|
20
|
+
RETURN pk_values;
|
|
21
|
+
END;
|
|
22
|
+
$$ LANGUAGE plpgsql;
|
|
File without changes
|
|
@@ -9,6 +9,7 @@ class TestCustomSchemaactivityCreation:
|
|
|
9
9
|
def test_insert(self, user):
|
|
10
10
|
stmt = sa.select(audit.Activity).limit(1)
|
|
11
11
|
if activity := db.session.scalar(stmt):
|
|
12
|
+
assert activity.row_key == ["1"]
|
|
12
13
|
assert activity.old_data == {}
|
|
13
14
|
assert activity.changed_data == {"id": user.id, "name": "John", "age": 15}
|
|
14
15
|
assert activity.table_name == "user"
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{flask_postgresql_audit-1.0.3 → flask_postgresql_audit-1.0.5}/flask_postgresql_audit/core.py
RENAMED
|
File without changes
|
|
File without changes
|
{flask_postgresql_audit-1.0.3 → flask_postgresql_audit-1.0.5}/flask_postgresql_audit/models.py
RENAMED
|
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.3 → flask_postgresql_audit-1.0.5}/flask_postgresql_audit/typing.py
RENAMED
|
File without changes
|
{flask_postgresql_audit-1.0.3 → flask_postgresql_audit-1.0.5}/flask_postgresql_audit/utils.py
RENAMED
|
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.3 → flask_postgresql_audit-1.0.5}/tests/migrations/script.py.mako
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|