ul-db-utils 2.10.7__py3-none-any.whl

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 (78) hide show
  1. ul_db_utils/__init__.py +0 -0
  2. ul_db_utils/commands/__init__.py +0 -0
  3. ul_db_utils/commands/cmd_action.py +37 -0
  4. ul_db_utils/commands/cmd_docs.py +60 -0
  5. ul_db_utils/commands/cmd_dump.py +42 -0
  6. ul_db_utils/commands/cmd_restore.py +109 -0
  7. ul_db_utils/commands/cmd_waiting.py +26 -0
  8. ul_db_utils/commands/doc_request_sql.sql +16 -0
  9. ul_db_utils/conf.py +6 -0
  10. ul_db_utils/errors/__init__.py +0 -0
  11. ul_db_utils/errors/compare_null_error.py +9 -0
  12. ul_db_utils/errors/db_error.py +2 -0
  13. ul_db_utils/errors/db_filter_error.py +6 -0
  14. ul_db_utils/errors/db_sort_error.py +6 -0
  15. ul_db_utils/errors/deletion_not_allowed.py +7 -0
  16. ul_db_utils/errors/multiple_objects_returned.py +7 -0
  17. ul_db_utils/errors/unknow_field_error.py +13 -0
  18. ul_db_utils/errors/update_column_not_allowed_error.py +7 -0
  19. ul_db_utils/errors/update_not_allowed.py +7 -0
  20. ul_db_utils/main.py +33 -0
  21. ul_db_utils/model/__init__.py +0 -0
  22. ul_db_utils/model/api_user.py +14 -0
  23. ul_db_utils/model/base_api_user_log_model.py +124 -0
  24. ul_db_utils/model/base_immutable_model.py +67 -0
  25. ul_db_utils/model/base_mater_pg_view.py +137 -0
  26. ul_db_utils/model/base_model.py +92 -0
  27. ul_db_utils/model/base_undeletable_model.py +88 -0
  28. ul_db_utils/model/base_undeletable_user_log_model.py +103 -0
  29. ul_db_utils/model/base_user_log_model.py +117 -0
  30. ul_db_utils/model/media_storage/__init__.py +0 -0
  31. ul_db_utils/model/media_storage/media_file.py +36 -0
  32. ul_db_utils/model/media_storage/media_file_download_link.py +35 -0
  33. ul_db_utils/model/media_storage/media_file_type.py +23 -0
  34. ul_db_utils/model/methods/__init__.py +0 -0
  35. ul_db_utils/model/methods/make_immutable_column.py +14 -0
  36. ul_db_utils/model/referense_link.py +18 -0
  37. ul_db_utils/modules/__init__.py +0 -0
  38. ul_db_utils/modules/audit.sql +251 -0
  39. ul_db_utils/modules/audit_manager.py +59 -0
  40. ul_db_utils/modules/custom_query.py +57 -0
  41. ul_db_utils/modules/db.py +167 -0
  42. ul_db_utils/modules/db_context.py +18 -0
  43. ul_db_utils/modules/transaction_commit.py +20 -0
  44. ul_db_utils/py.typed +0 -0
  45. ul_db_utils/search/__init__.py +0 -0
  46. ul_db_utils/search/db_search.py +310 -0
  47. ul_db_utils/search/helpers.py +227 -0
  48. ul_db_utils/utils/__init__.py +0 -0
  49. ul_db_utils/utils/ensure/__init__.py +0 -0
  50. ul_db_utils/utils/ensure/ensure_bool.py +3 -0
  51. ul_db_utils/utils/ensure/ensure_choices.py +12 -0
  52. ul_db_utils/utils/ensure/ensure_dict_keys.py +11 -0
  53. ul_db_utils/utils/ensure/ensure_dict_keys_choice.py +12 -0
  54. ul_db_utils/utils/ensure/ensure_dict_keys_strict.py +14 -0
  55. ul_db_utils/utils/ensure/ensure_dict_str_keys.py +8 -0
  56. ul_db_utils/utils/ensure/ensure_dict_upper_keys.py +9 -0
  57. ul_db_utils/utils/ensure/ensure_float.py +3 -0
  58. ul_db_utils/utils/ensure/ensure_int.py +3 -0
  59. ul_db_utils/utils/ensure/ensure_int_positive.py +13 -0
  60. ul_db_utils/utils/ensure/ensure_len.py +6 -0
  61. ul_db_utils/utils/ensure/ensure_list.py +10 -0
  62. ul_db_utils/utils/ensure/ensure_list_of.py +11 -0
  63. ul_db_utils/utils/ensure/ensure_positive_int_non_zero.py +4 -0
  64. ul_db_utils/utils/ensure/ensure_set.py +8 -0
  65. ul_db_utils/utils/ensure/ensure_str.py +3 -0
  66. ul_db_utils/utils/ensure/ensure_type.py +8 -0
  67. ul_db_utils/utils/ensure/ensure_url_with_scheme_and_netloc.py +13 -0
  68. ul_db_utils/utils/ensure_db_object_exists.py +11 -0
  69. ul_db_utils/utils/get_model_template.py +24 -0
  70. ul_db_utils/utils/query_soft_delete.py +48 -0
  71. ul_db_utils/utils/remove_duplicated_spaces_of_string.py +3 -0
  72. ul_db_utils/utils/waiting_for_postgres.py +29 -0
  73. ul_db_utils-2.10.7.dist-info/LICENSE +0 -0
  74. ul_db_utils-2.10.7.dist-info/METADATA +167 -0
  75. ul_db_utils-2.10.7.dist-info/RECORD +78 -0
  76. ul_db_utils-2.10.7.dist-info/WHEEL +5 -0
  77. ul_db_utils-2.10.7.dist-info/entry_points.txt +2 -0
  78. ul_db_utils-2.10.7.dist-info/top_level.txt +1 -0
File without changes
File without changes
@@ -0,0 +1,37 @@
1
+ import os
2
+ import subprocess
3
+ import sys
4
+ from argparse import ArgumentParser
5
+ from typing import Any
6
+
7
+ from ul_py_tool.commands.cmd import Cmd
8
+
9
+
10
+ class CmdAction(Cmd):
11
+ app_dir: str
12
+ app_migration_dir_name: str
13
+ app_file_name: str
14
+
15
+ @property
16
+ def app_rel_dir(self) -> str:
17
+ return os.path.relpath(self.app_dir, os.getcwd())
18
+
19
+ @staticmethod
20
+ def add_parser_args(parser: ArgumentParser) -> None:
21
+ parser.add_argument('--app-dir', dest='app_dir', type=str, required=True)
22
+ parser.add_argument('--app-migration-dir-name', dest='app_migration_dir_name', type=str, default='migrations', required=False)
23
+ parser.add_argument('--app-flask-file-name', dest='app_file_name', type=str, default='main.py', required=False)
24
+
25
+ def run(self, *args: Any, **kwargs: Any) -> None:
26
+ migration_dir = os.path.join(self.app_dir, self.app_migration_dir_name)
27
+ flask_app_path = os.path.join(self.app_dir, self.app_file_name)
28
+ additional_args = ' '.join([f'{key}={value}' for key, value in kwargs.items()])
29
+
30
+ result = subprocess.run(
31
+ [f'FLASK_APP="{flask_app_path}" flask db {self.cmd} {additional_args} --directory "{migration_dir}"'],
32
+ shell=True,
33
+ stderr=sys.stderr,
34
+ stdout=sys.stdout,
35
+ )
36
+ if result.returncode == 1:
37
+ exit(1)
@@ -0,0 +1,60 @@
1
+ import argparse
2
+ import os
3
+ import re
4
+ from contextlib import closing
5
+ from typing import Any
6
+ from urllib.parse import urlparse
7
+
8
+ import psycopg2
9
+ from pydantic import PostgresDsn
10
+ from sqlalchemy import text
11
+ from ul_py_tool.commands.cmd import Cmd
12
+
13
+
14
+ class CmdDocs(Cmd):
15
+ uri: PostgresDsn
16
+ dest_path: str
17
+ schema_db: str
18
+
19
+ @staticmethod
20
+ def add_parser_args(parser: argparse.ArgumentParser) -> None:
21
+ def_dir = os.path.join(os.getcwd(), 'docs/db')
22
+ parser.add_argument('--db-uri', dest='uri', type=str, required=True)
23
+ parser.add_argument('--dest', dest='dest_path', type=str, default=def_dir, required=False)
24
+ parser.add_argument('--schema_db', dest='schema_db', type=str, default='public', required=False)
25
+
26
+ def run(self, *args: Any, **kwargs: Any) -> None:
27
+ parsed_db_uri = urlparse(self.uri)
28
+ db_name = parsed_db_uri.path.strip("/")
29
+ assert len(db_name) > 0
30
+
31
+ with open(os.path.join(os.path.dirname(__file__), 'doc_request_sql.sql'), 'rt') as sql_file:
32
+ doc_request_sql = sql_file.read()
33
+
34
+ doc_request_sql = text(doc_request_sql.format( # type: ignore
35
+ schema_db=self.schema_db,
36
+ ))
37
+
38
+ with closing(psycopg2.connect(self.uri)) as conn:
39
+ with conn.cursor() as cursor:
40
+ cursor.execute(str(doc_request_sql))
41
+ doc_data = cursor.fetchall()
42
+
43
+ for item in doc_data:
44
+ db_name = item[0]
45
+ schema_name = item[1]
46
+ table_name = item[2]
47
+ table_description = item[3]
48
+ file_path = os.path.join(self.dest_path, self.clean_slug(db_name), f"{self.clean_slug(schema_name)}__{self.clean_slug(table_name)}.md")
49
+ os.makedirs(os.path.dirname(file_path), exist_ok=True)
50
+ with open(file_path, "w+") as file:
51
+ file.write(f"# {db_name} \"{schema_name}.{table_name}\"\n\n"
52
+ f"{table_description}\n\n"
53
+ f"## Описание колонок\n\n"
54
+ f"| Название | Тип | Описание |\n"
55
+ f"| -------- | --- | -------- |\n")
56
+ for table_field_name, table_field_type, table_field_description in zip(item[4], item[5], item[6]):
57
+ file.write(f"| {table_field_name} | {table_field_type} | {table_field_description} |\n")
58
+
59
+ def clean_slug(self, text: str) -> str:
60
+ return re.sub(r"[^\w\d]+", "_", text)
@@ -0,0 +1,42 @@
1
+ import os
2
+ import subprocess
3
+ import sys
4
+ from argparse import ArgumentParser
5
+ from datetime import datetime
6
+ from typing import Any
7
+ from urllib.parse import urlparse
8
+
9
+ from ul_py_tool.commands.cmd import Cmd
10
+
11
+
12
+ class CmdDump(Cmd):
13
+ uri: str
14
+ dest_path: str
15
+
16
+ @staticmethod
17
+ def add_parser_args(parser: ArgumentParser) -> None:
18
+ def_file = os.path.join(os.getcwd(), '.tmp', f'dbdump_{datetime.now().isoformat()}.sql')
19
+ parser.add_argument('--db-uri', dest='uri', type=str, required=True)
20
+ parser.add_argument('--dest', dest='dest_path', type=str, default=def_file, required=False)
21
+
22
+ def run(self, *args: Any, **kwargs: Any) -> None:
23
+ parsed_db_uri = urlparse(self.uri)
24
+ db_name = parsed_db_uri.path.strip("/")
25
+ assert len(db_name) > 0
26
+ os.makedirs(os.path.dirname(self.dest_path), exist_ok=True)
27
+ result = subprocess.run(
28
+ [(
29
+ f'PGPASSWORD={parsed_db_uri.password} pg_dump --schema=public --data-only -Fc '
30
+ f'-h {parsed_db_uri.hostname} '
31
+ f'-p {parsed_db_uri.port} '
32
+ f'-U {parsed_db_uri.username} '
33
+ f'-d {db_name} '
34
+ f'-f {self.dest_path} '
35
+ )],
36
+ shell=True,
37
+ check=True,
38
+ stderr=sys.stderr,
39
+ stdout=sys.stdout,
40
+ )
41
+ if result.returncode == 1:
42
+ exit(1)
@@ -0,0 +1,109 @@
1
+ import argparse
2
+ import os
3
+ import subprocess
4
+ import sys
5
+ from typing import Any
6
+ from urllib.parse import urlparse
7
+
8
+ from ul_py_tool.commands.cmd import Cmd
9
+
10
+
11
+ class CmdRestore(Cmd):
12
+ uri: str
13
+ db_dump_file_path: str
14
+ flags: str
15
+ clean_data: int
16
+
17
+ @staticmethod
18
+ def add_parser_args(parser: argparse.ArgumentParser) -> None:
19
+ db_dump_path = os.path.join(os.getcwd(), '.tmp', 'dbdump.sql')
20
+ parser.add_argument('--db-uri', dest='uri', type=str, required=True)
21
+ parser.add_argument('--dump-file', dest='db_dump_file_path', type=str, default=db_dump_path, required=False)
22
+ parser.add_argument('--clean-data', dest='clean_data', type=int, default=1, required=False)
23
+ parser.add_argument('--flags', dest='flags', type=str, default='', required=False)
24
+
25
+ def run(self, *args: Any, **kwargs: Any) -> None:
26
+ parsed_db_uri = urlparse(self.uri)
27
+ db_name = parsed_db_uri.path.strip("/")
28
+ if not os.path.exists(self.db_dump_file_path):
29
+ raise ValueError(f'file {self.db_dump_file_path} was not found')
30
+
31
+ db_pref = f'-U {parsed_db_uri.username} -d {db_name} -h {parsed_db_uri.hostname} -p {parsed_db_uri.port}'
32
+ db_pwd = f'PGPASSWORD={parsed_db_uri.password}'
33
+
34
+ subprocess.run(
35
+ [f'{db_pwd} psql {db_pref} -c "create user replicator; "'],
36
+ shell=True,
37
+ check=False,
38
+ stderr=sys.stderr,
39
+ stdout=sys.stdout,
40
+ )
41
+ subprocess.run(
42
+ [f'{db_pwd} psql {db_pref} -c "create user postgres;"'],
43
+ shell=True,
44
+ check=False,
45
+ stderr=sys.stderr,
46
+ stdout=sys.stdout,
47
+ )
48
+ subprocess.run(
49
+ [f'{db_pwd} psql {db_pref} -c "create user viewer;"'],
50
+ shell=True,
51
+ check=False,
52
+ stderr=sys.stderr,
53
+ stdout=sys.stdout,
54
+ )
55
+ subprocess.run(
56
+ [f'{db_pwd} psql {db_pref} -c "create schema audit;"'],
57
+ shell=True,
58
+ check=False,
59
+ stderr=sys.stderr,
60
+ stdout=sys.stdout,
61
+ )
62
+ subprocess.run(
63
+ [f'{db_pwd} psql {db_pref} -c "create schema public;"'],
64
+ shell=True,
65
+ check=False,
66
+ stderr=sys.stderr,
67
+ stdout=sys.stdout,
68
+ )
69
+ subprocess.run(
70
+ [f'{db_pwd} psql {db_pref} -c "create schema cache;"'],
71
+ shell=True,
72
+ check=False,
73
+ stderr=sys.stderr,
74
+ stdout=sys.stdout,
75
+ )
76
+ subprocess.run(
77
+ [f'{db_pwd} psql {db_pref} -c "drop extension timescaledb;"'],
78
+ shell=True,
79
+ check=False,
80
+ stderr=sys.stderr,
81
+ stdout=sys.stdout,
82
+ )
83
+
84
+ if self.clean_data:
85
+ truncate_all_sql = (
86
+ f"CREATE OR REPLACE FUNCTION truncate_tables(username IN VARCHAR) RETURNS void AS \\$$ "
87
+ f"DECLARE statements CURSOR FOR SELECT tablename FROM pg_tables WHERE tableowner = username AND schemaname = 'public'; "
88
+ f"BEGIN FOR stmt IN statements LOOP EXECUTE 'TRUNCATE TABLE ' || quote_ident(stmt.tablename) || ' CASCADE;'; END LOOP; END; "
89
+ f"\\$$ LANGUAGE plpgsql; "
90
+ f"SELECT truncate_tables('{parsed_db_uri.username}');"
91
+ )
92
+
93
+ subprocess.run(
94
+ [f'{db_pwd} psql {db_pref} -c "{truncate_all_sql}"'],
95
+ shell=True,
96
+ check=True,
97
+ stderr=sys.stderr,
98
+ stdout=sys.stdout,
99
+ )
100
+
101
+ result = subprocess.run(
102
+ [f'{db_pwd} pg_restore {db_pref} --data-only --exclude-schema=audit {self.flags} < "{self.db_dump_file_path}"'],
103
+ shell=True,
104
+ check=True,
105
+ stderr=sys.stderr,
106
+ stdout=sys.stdout,
107
+ )
108
+ if result.returncode == 1:
109
+ exit(1)
@@ -0,0 +1,26 @@
1
+ import argparse
2
+ import logging
3
+ from typing import Any
4
+
5
+ from ul_py_tool.commands.cmd import Cmd
6
+
7
+ from ul_db_utils.utils.waiting_for_postgres import waiting_for_postgres
8
+
9
+ logger = logging.getLogger(__name__)
10
+
11
+
12
+ class CmdWaiting(Cmd):
13
+ uri: str
14
+ max_times: int
15
+ delay: int
16
+
17
+ @staticmethod
18
+ def add_parser_args(parser: argparse.ArgumentParser) -> None:
19
+ parser.add_argument('--db-uri', dest='uri', type=str, required=True)
20
+ parser.add_argument('--retry-times', dest='max_times', type=int, default=100, required=False)
21
+ parser.add_argument('--retry-delay-sec', dest='delay', type=int, default=1, required=False)
22
+
23
+ def run(self, *args: Any, **kwargs: Any) -> None:
24
+ if not waiting_for_postgres(self.uri, retry_max_count=self.max_times, retry_delay_s=float(self.delay)):
25
+ exit(2)
26
+ exit(0)
@@ -0,0 +1,16 @@
1
+ SELECT
2
+ current_database() as "db_name",
3
+ pgns.nspname as "schema_name",
4
+ tbl.relname as "table_name",
5
+ obj_description(tbl.oid, 'pg_class') as "table_description",
6
+ ARRAY_AGG(pgattr.attname) as "table_field_name",
7
+ ARRAY_AGG(pgtype.typname) as "table_field_type",
8
+ ARRAY_AGG(col_description(tbl.oid, pgattr.attnum)) as "table_field_description"
9
+ FROM pg_class tbl
10
+ LEFT OUTER JOIN pg_catalog.pg_namespace pgns ON pgns.nspname = '{schema_db}'
11
+ LEFT OUTER JOIN pg_attribute pgattr ON pgattr.attrelid = tbl.oid AND pgattr.attnum > 0 AND pgattr.atttypid != 0
12
+ LEFT OUTER JOIN pg_type pgtype ON pgtype.oid = pgattr.atttypid
13
+ WHERE tbl.relnamespace = pgns.oid AND tbl.reltype != 0
14
+ GROUP BY
15
+ db_name, schema_name, table_name, table_description
16
+ ORDER BY table_name;
ul_db_utils/conf.py ADDED
@@ -0,0 +1,6 @@
1
+ import os.path
2
+ from typing import Optional
3
+
4
+ THIS_LIBRARY_DIR = os.path.dirname(__file__)
5
+
6
+ APPLICATION__DB_URI: Optional[str] = os.environ.get('APPLICATION__DB_URI', None) # none only for backward compatibility
File without changes
@@ -0,0 +1,9 @@
1
+ from ul_db_utils.errors.db_error import DbError
2
+
3
+
4
+ class ComparisonToNullError(DbError):
5
+ """Raised when a client attempts to use a filter object that compares a
6
+ resource's attribute to ``NULL`` using the ``==`` operator instead of using
7
+ ``is_null``.
8
+ """
9
+ pass
@@ -0,0 +1,2 @@
1
+ class DbError(Exception):
2
+ pass
@@ -0,0 +1,6 @@
1
+ from ul_db_utils.errors.db_error import DbError
2
+
3
+
4
+ class DBFiltersError(DbError):
5
+ """Raised when a client attempts to filters with invalid query params"""
6
+ pass
@@ -0,0 +1,6 @@
1
+ from ul_db_utils.errors.db_error import DbError
2
+
3
+
4
+ class DBSortError(DbError):
5
+ """Raised when a client attempts to sort with invalid query params"""
6
+ pass
@@ -0,0 +1,7 @@
1
+ from ul_db_utils.errors.db_error import DbError
2
+
3
+
4
+ class DeletionNotAllowedError(DbError):
5
+ """Raised when db obj deletion not allowed.
6
+ """
7
+ pass
@@ -0,0 +1,7 @@
1
+ from ul_db_utils.errors.db_error import DbError
2
+
3
+
4
+ class MultipleObjectsReturnedError(DbError):
5
+ """Raised when db returned multiple objects.
6
+ """
7
+ pass
@@ -0,0 +1,13 @@
1
+ from ul_db_utils.errors.db_error import DbError
2
+
3
+
4
+ class UnknownFieldError(DbError):
5
+ """Raised when the user attempts to reference a field that does not
6
+ exist on a model in a search.
7
+
8
+ """
9
+
10
+ def __init__(self, field: str) -> None:
11
+
12
+ #: The name of the unknown attribute.
13
+ self.field = field
@@ -0,0 +1,7 @@
1
+ from ul_db_utils.errors.db_error import DbError
2
+
3
+
4
+ class UpdateColumnNotAllowedError(DbError):
5
+ """Raised when db table column update not allowed.
6
+ """
7
+ pass
@@ -0,0 +1,7 @@
1
+ from ul_db_utils.errors.db_error import DbError
2
+
3
+
4
+ class UpdateNotAllowedError(DbError):
5
+ """Raised when db table update not allowed.
6
+ """
7
+ pass
ul_db_utils/main.py ADDED
@@ -0,0 +1,33 @@
1
+ import logging
2
+
3
+ from ul_py_tool.commands.cmd import Cmd
4
+
5
+ from ul_db_utils.commands.cmd_action import CmdAction
6
+ from ul_db_utils.commands.cmd_docs import CmdDocs
7
+ from ul_db_utils.commands.cmd_dump import CmdDump
8
+ from ul_db_utils.commands.cmd_restore import CmdRestore
9
+ from ul_db_utils.commands.cmd_waiting import CmdWaiting
10
+
11
+ logger = logging.getLogger(__name__)
12
+
13
+
14
+ def main() -> None:
15
+ Cmd.main({
16
+ 'waiting': CmdWaiting,
17
+ 'dump': CmdDump,
18
+ 'restore': CmdRestore,
19
+ 'migrate': CmdAction,
20
+ 'upgrade': CmdAction,
21
+ 'downgrade': CmdAction,
22
+ 'init': CmdAction,
23
+ 'revision': CmdAction,
24
+ 'history': CmdAction,
25
+ 'branches': CmdAction,
26
+ 'current': CmdAction,
27
+ 'merge': CmdAction,
28
+ 'gen_docs': CmdDocs,
29
+ })
30
+
31
+
32
+ if __name__ == '__main__':
33
+ main()
File without changes
@@ -0,0 +1,14 @@
1
+ from sqlalchemy.dialects.postgresql import ARRAY
2
+
3
+ from ul_db_utils.model.base_model import BaseModel
4
+ from ul_db_utils.modules.db import db
5
+
6
+
7
+ class ApiUser(BaseModel):
8
+ __tablename__ = 'api_user'
9
+ __table_args__ = {"comment": "Пользователь API"}
10
+
11
+ date_expiration = db.Column(db.DateTime(), nullable=False, comment="Срок действия доступа")
12
+ name = db.Column(db.String(255), unique=True, nullable=False, comment="Имя пользователя")
13
+ note = db.Column(db.Text(), nullable=False, comment="Примечание")
14
+ permissions = db.Column(ARRAY(db.Integer()), nullable=False, comment="Список разрешений")
@@ -0,0 +1,124 @@
1
+ from datetime import datetime
2
+ from typing import Optional
3
+ from uuid import UUID
4
+
5
+ from sqlalchemy import inspect
6
+ from sqlalchemy.dialects.postgresql import UUID as PG_UUID
7
+ from sqlalchemy.engine.base import Connection
8
+ from sqlalchemy.ext.declarative import declared_attr
9
+
10
+ from ul_db_utils.model.base_model import BaseModel
11
+ from ul_db_utils.model.base_user_log_model import BaseUserLogModel
12
+ from ul_db_utils.modules import transaction_commit
13
+ from ul_db_utils.modules.db import db, DbMapper
14
+
15
+
16
+ class BaseApiUserLogModel(BaseModel):
17
+
18
+ __abstract__ = True
19
+
20
+ @declared_attr
21
+ def user_created_id(cls): # type: ignore
22
+ return db.Column(PG_UUID(as_uuid=True), db.ForeignKey('api_user.id'), nullable=False, comment="Идентификатор пользователя, создавшего запись")
23
+
24
+ @declared_attr
25
+ def user_modified_id(cls): # type: ignore
26
+ return db.Column(PG_UUID(as_uuid=True), db.ForeignKey('api_user.id'), nullable=False, comment="Идентификатор пользователя, изменившего запись")
27
+
28
+ def mark_as_created( # type: ignore
29
+ self,
30
+ user_created_id: UUID,
31
+ date_created: Optional[datetime] = None,
32
+ ) -> None:
33
+ """Util for set user_created_id and date_created"""
34
+ assert transaction_commit.transaction_context,\
35
+ f'database insert must be in {transaction_commit} context manager'
36
+ assert 'user_created_id' in [column.name for column in self.__table__.columns], \
37
+ f"model {self.__repr__()} must have user_created_id foreign key"
38
+ assert isinstance(user_created_id, UUID), f"user_created_id must be type of {UUID}"
39
+ self.user_created_id = user_created_id
40
+ self.user_modified_id = user_created_id
41
+ if date_created:
42
+ assert isinstance(date_created, datetime), f"date_created must be type of {datetime}"
43
+ self.date_created = date_created
44
+ self.date_modified = date_created
45
+ else:
46
+ self.date_created = datetime.utcnow()
47
+ self.date_modified = datetime.utcnow()
48
+
49
+ def mark_as_modified( # type: ignore
50
+ self,
51
+ user_modified_id: UUID,
52
+ date_modified: Optional[datetime] = None,
53
+ ) -> None:
54
+ """Util for set user_modified_id and date_modified"""
55
+ assert transaction_commit.transaction_context,\
56
+ f'database update must be in {transaction_commit} context manager'
57
+ assert isinstance(user_modified_id, UUID), f"user_modified_id must be type of {UUID}"
58
+ self.user_modified_id = user_modified_id
59
+ if date_modified is not None:
60
+ assert isinstance(date_modified, datetime), f"date_modified must be type of {datetime}"
61
+ self.date_modified = date_modified
62
+ else:
63
+ self.date_modified = datetime.utcnow()
64
+
65
+ def mark_as_deleted( # type: ignore
66
+ self,
67
+ user_modified_id: UUID,
68
+ date_modified: Optional[datetime] = None,
69
+ ) -> None:
70
+ """Util for set user_modified_id and date_modified"""
71
+ assert transaction_commit.transaction_context,\
72
+ f'database delete must be in {transaction_commit} context manager'
73
+
74
+ assert isinstance(user_modified_id, UUID), \
75
+ f"user_modified_id must be type of {UUID}, but {type(user_modified_id)} is given"
76
+
77
+ self.user_modified_id = user_modified_id
78
+ if date_modified is not None:
79
+ assert isinstance(date_modified, datetime), \
80
+ f"date_modified must be type of {datetime}, but {type(date_modified)} is given"
81
+
82
+ self.date_modified = date_modified
83
+ else:
84
+ self.date_modified = datetime.utcnow()
85
+ self.is_alive = False
86
+
87
+
88
+ @db.event.listens_for(db.Mapper, 'before_update')
89
+ def receive_before_update(mapper: db.Mapper, connection: Connection, target: BaseUserLogModel) -> None: # type: ignore
90
+ if issubclass(target.__class__, BaseUserLogModel):
91
+ assert transaction_commit.transaction_context,\
92
+ f'database updates must be in {transaction_commit} context manager'
93
+
94
+ unchanged_keys = inspect(target).attrs.get('id').state.unmodified # type: ignore
95
+
96
+ assert 'date_modified' not in unchanged_keys, \
97
+ "date_modified must be set on model update"
98
+
99
+ assert isinstance(target.date_modified, datetime), \
100
+ f"date_modified must be type of {datetime}, but {type(target.date_modified)} is given"
101
+
102
+ assert 'user_modified_id' not in unchanged_keys,\
103
+ "user_modified_id must be set on model update"
104
+
105
+ assert isinstance(target.user_modified_id, UUID), \
106
+ f"user_modified_id must be type of {UUID}, but {type(target.user_modified_id)} is given"
107
+
108
+
109
+ @db.event.listens_for(db.Mapper, 'before_insert')
110
+ def receive_before_create(mapper: DbMapper, connection: Connection, target: BaseUserLogModel) -> None:
111
+ if issubclass(target.__class__, BaseUserLogModel):
112
+ assert transaction_commit.transaction_context,\
113
+ f'database insert must be in {transaction_commit} context manager'
114
+ assert isinstance(target.date_created, datetime), \
115
+ f"date_created must be type of {datetime}, but {type(target.date_created)} is given"
116
+
117
+ assert isinstance(target.date_modified, datetime), \
118
+ f"date_modified must be type of {datetime}, but {type(target.date_modified)} is given"
119
+
120
+ assert isinstance(target.user_created_id, UUID), \
121
+ f"user_created_id must be type of {UUID}, but {type(target.user_created_id)} is given"
122
+
123
+ assert isinstance(target.user_modified_id, UUID), \
124
+ f"user_modified_id must be type of {UUID}, but {type(target.user_modified_id)} is given"
@@ -0,0 +1,67 @@
1
+ import uuid
2
+ from datetime import datetime
3
+ from typing import Optional
4
+
5
+ from flask_sqlalchemy import BaseQuery
6
+ from sqlalchemy.dialects.postgresql import UUID as PG_UUID
7
+ from sqlalchemy.engine.base import Connection
8
+ from sqlalchemy_serializer import SerializerMixin
9
+
10
+ from ul_db_utils.errors.deletion_not_allowed import DeletionNotAllowedError
11
+ from ul_db_utils.errors.update_not_allowed import UpdateNotAllowedError
12
+ from ul_db_utils.modules import transaction_commit
13
+ from ul_db_utils.modules.db import db, DbModel, DbMapper
14
+
15
+
16
+ class BaseImmutableModel(DbModel, SerializerMixin):
17
+ __abstract__ = True
18
+
19
+ query_class = BaseQuery
20
+
21
+ id = db.Column(PG_UUID(as_uuid=True), primary_key=True, default=uuid.uuid4, comment="Идентификатор записи")
22
+ date_created = db.Column(db.DateTime(), default=datetime.utcnow(), nullable=False, comment="Дата и время создания записи")
23
+ user_created_id = db.Column(PG_UUID(as_uuid=True), nullable=False, comment="Идентификатор пользователя, создавшего запись")
24
+
25
+ def mark_as_created(self, user_created_id: uuid.UUID, date_created: Optional[datetime] = None) -> None:
26
+ """Util for set user_created_id and date_created"""
27
+ assert transaction_commit.transaction_context, \
28
+ f'database insert must be in {transaction_commit} context manager'
29
+ assert 'user_created_id' in [column.name for column in self.__table__.columns], \
30
+ f"model {self.__repr__()} must have user_created_id foreign key"
31
+ assert isinstance(user_created_id, uuid.UUID), f"user_created_id must be type of {uuid.UUID}"
32
+ self.user_created_id = user_created_id
33
+ if date_created:
34
+ assert isinstance(date_created, datetime), f"date_created must be type of {datetime}"
35
+ self.date_created = date_created
36
+ else:
37
+ self.date_created = datetime.utcnow()
38
+
39
+ def mark_as_modified(self, user_modified_id: uuid.UUID, date_modified: Optional[datetime] = None) -> None:
40
+ """Util for set user_modified_id and date_modified"""
41
+ raise UpdateNotAllowedError()
42
+
43
+ def __repr__(self) -> str:
44
+ return self.__class__.__name__
45
+
46
+ def mark_as_deleted(
47
+ self,
48
+ user_modified_id: uuid.UUID,
49
+ date_modified: Optional[datetime] = None,
50
+ ) -> None:
51
+ raise DeletionNotAllowedError()
52
+
53
+
54
+ @db.event.listens_for(DbMapper, 'before_insert')
55
+ def receive_before_create(mapper: DbMapper, connection: Connection, target: BaseImmutableModel) -> None:
56
+ if issubclass(target.__class__, BaseImmutableModel):
57
+ assert transaction_commit.transaction_context, \
58
+ f'database updates must be in {transaction_commit} context manager'
59
+
60
+ assert isinstance(target.date_created, datetime), \
61
+ f"date_created must be type of {datetime}, but {type(target.date_created)} is given"
62
+
63
+
64
+ @db.event.listens_for(db.Mapper, 'before_update')
65
+ def receive_before_update(mapper: DbMapper, connection: Connection, target: BaseImmutableModel) -> None:
66
+ if issubclass(target.__class__, BaseImmutableModel):
67
+ raise UpdateNotAllowedError()