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.
- ul_db_utils/__init__.py +0 -0
- ul_db_utils/commands/__init__.py +0 -0
- ul_db_utils/commands/cmd_action.py +37 -0
- ul_db_utils/commands/cmd_docs.py +60 -0
- ul_db_utils/commands/cmd_dump.py +42 -0
- ul_db_utils/commands/cmd_restore.py +109 -0
- ul_db_utils/commands/cmd_waiting.py +26 -0
- ul_db_utils/commands/doc_request_sql.sql +16 -0
- ul_db_utils/conf.py +6 -0
- ul_db_utils/errors/__init__.py +0 -0
- ul_db_utils/errors/compare_null_error.py +9 -0
- ul_db_utils/errors/db_error.py +2 -0
- ul_db_utils/errors/db_filter_error.py +6 -0
- ul_db_utils/errors/db_sort_error.py +6 -0
- ul_db_utils/errors/deletion_not_allowed.py +7 -0
- ul_db_utils/errors/multiple_objects_returned.py +7 -0
- ul_db_utils/errors/unknow_field_error.py +13 -0
- ul_db_utils/errors/update_column_not_allowed_error.py +7 -0
- ul_db_utils/errors/update_not_allowed.py +7 -0
- ul_db_utils/main.py +33 -0
- ul_db_utils/model/__init__.py +0 -0
- ul_db_utils/model/api_user.py +14 -0
- ul_db_utils/model/base_api_user_log_model.py +124 -0
- ul_db_utils/model/base_immutable_model.py +67 -0
- ul_db_utils/model/base_mater_pg_view.py +137 -0
- ul_db_utils/model/base_model.py +92 -0
- ul_db_utils/model/base_undeletable_model.py +88 -0
- ul_db_utils/model/base_undeletable_user_log_model.py +103 -0
- ul_db_utils/model/base_user_log_model.py +117 -0
- ul_db_utils/model/media_storage/__init__.py +0 -0
- ul_db_utils/model/media_storage/media_file.py +36 -0
- ul_db_utils/model/media_storage/media_file_download_link.py +35 -0
- ul_db_utils/model/media_storage/media_file_type.py +23 -0
- ul_db_utils/model/methods/__init__.py +0 -0
- ul_db_utils/model/methods/make_immutable_column.py +14 -0
- ul_db_utils/model/referense_link.py +18 -0
- ul_db_utils/modules/__init__.py +0 -0
- ul_db_utils/modules/audit.sql +251 -0
- ul_db_utils/modules/audit_manager.py +59 -0
- ul_db_utils/modules/custom_query.py +57 -0
- ul_db_utils/modules/db.py +167 -0
- ul_db_utils/modules/db_context.py +18 -0
- ul_db_utils/modules/transaction_commit.py +20 -0
- ul_db_utils/py.typed +0 -0
- ul_db_utils/search/__init__.py +0 -0
- ul_db_utils/search/db_search.py +310 -0
- ul_db_utils/search/helpers.py +227 -0
- ul_db_utils/utils/__init__.py +0 -0
- ul_db_utils/utils/ensure/__init__.py +0 -0
- ul_db_utils/utils/ensure/ensure_bool.py +3 -0
- ul_db_utils/utils/ensure/ensure_choices.py +12 -0
- ul_db_utils/utils/ensure/ensure_dict_keys.py +11 -0
- ul_db_utils/utils/ensure/ensure_dict_keys_choice.py +12 -0
- ul_db_utils/utils/ensure/ensure_dict_keys_strict.py +14 -0
- ul_db_utils/utils/ensure/ensure_dict_str_keys.py +8 -0
- ul_db_utils/utils/ensure/ensure_dict_upper_keys.py +9 -0
- ul_db_utils/utils/ensure/ensure_float.py +3 -0
- ul_db_utils/utils/ensure/ensure_int.py +3 -0
- ul_db_utils/utils/ensure/ensure_int_positive.py +13 -0
- ul_db_utils/utils/ensure/ensure_len.py +6 -0
- ul_db_utils/utils/ensure/ensure_list.py +10 -0
- ul_db_utils/utils/ensure/ensure_list_of.py +11 -0
- ul_db_utils/utils/ensure/ensure_positive_int_non_zero.py +4 -0
- ul_db_utils/utils/ensure/ensure_set.py +8 -0
- ul_db_utils/utils/ensure/ensure_str.py +3 -0
- ul_db_utils/utils/ensure/ensure_type.py +8 -0
- ul_db_utils/utils/ensure/ensure_url_with_scheme_and_netloc.py +13 -0
- ul_db_utils/utils/ensure_db_object_exists.py +11 -0
- ul_db_utils/utils/get_model_template.py +24 -0
- ul_db_utils/utils/query_soft_delete.py +48 -0
- ul_db_utils/utils/remove_duplicated_spaces_of_string.py +3 -0
- ul_db_utils/utils/waiting_for_postgres.py +29 -0
- ul_db_utils-2.10.7.dist-info/LICENSE +0 -0
- ul_db_utils-2.10.7.dist-info/METADATA +167 -0
- ul_db_utils-2.10.7.dist-info/RECORD +78 -0
- ul_db_utils-2.10.7.dist-info/WHEEL +5 -0
- ul_db_utils-2.10.7.dist-info/entry_points.txt +2 -0
- ul_db_utils-2.10.7.dist-info/top_level.txt +1 -0
ul_db_utils/__init__.py
ADDED
|
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
|
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,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
|
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()
|