exobrain-database 0.1.0__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.
- exobrain/database/__init__.py +0 -0
- exobrain/database/migrations/README.md +1 -0
- exobrain/database/migrations/__init__.py +9 -0
- exobrain/database/migrations/alembic.ini +104 -0
- exobrain/database/migrations/env.py +198 -0
- exobrain/database/migrations/migration_manager.py +109 -0
- exobrain/database/migrations/script.py.mako +47 -0
- exobrain/database/migrations/versions/2023_08_17_0937-3ed3cf31c9d4_.py +161 -0
- exobrain/database/migrations/versions/2023_08_31_1322-aa6854636eac_.py +181 -0
- exobrain/database/migrations/versions/2023_09_01_0925-9667f0608380_.py +108 -0
- exobrain/database/migrations/versions/2023_09_13_1243-3cd1eb0d7b52_.py +238 -0
- exobrain/database/migrations/versions/2023_09_15_1447-52f33bc03521_.py +72 -0
- exobrain/database/migrations/versions/2023_09_19_0956-f8e096a41120_.py +70 -0
- exobrain/database/migrations/versions/2023_09_21_1642-74f18c0bab6e_.py +548 -0
- exobrain/database/migrations/versions/2023_09_28_1237-919ce06afab1_.py +71 -0
- exobrain/database/migrations/versions/2023_10_04_0718-03535f8dfe5f_.py +150 -0
- exobrain/database/migrations/versions/2023_10_05_1508-f64d4c00b610_.py +284 -0
- exobrain/database/migrations/versions/2023_10_06_0819-e544518a415c_.py +273 -0
- exobrain/database/migrations/versions/2023_10_06_1312-b9b232d63be3_.py +70 -0
- exobrain/database/migrations/versions/2023_10_25_0902-aab08fb68941_.py +70 -0
- exobrain/database/migrations/versions/2023_10_25_0937-d59ec80e6dd7_.py +68 -0
- exobrain/database/migrations/versions/2023_11_22_1628-84b116f3df26_.py +84 -0
- exobrain/database/migrations/versions/2023_11_22_1654-bcb01e365c42_.py +102 -0
- exobrain/database/migrations/versions/2023_11_27_1045-00ce519af235_.py +70 -0
- exobrain/database/migrations/versions/2023_11_27_1048-f779dc9fe429_.py +70 -0
- exobrain/database/migrations/versions/2023_11_29_1421-ad2b4c9c38fe_.py +68 -0
- exobrain/database/migrations/versions/2023_12_04_1029-f91d936478fb_.py +70 -0
- exobrain/database/migrations/versions/2023_12_11_1104-b33e0d6a5bdd_.py +71 -0
- exobrain/database/migrations/versions/2023_12_18_1406-73ac94585923_.py +70 -0
- exobrain/database/migrations/versions/2024_01_05_1022-f231770c573e_.py +83 -0
- exobrain/database/migrations/versions/2024_01_17_1606-b94c145b2310_.py +70 -0
- exobrain/database/migrations/versions/2024_01_18_1547-c450c961dfd0_.py +79 -0
- exobrain/database/migrations/versions/2024_01_18_1855-7101980bc238_.py +86 -0
- exobrain/database/migrations/versions/2024_01_22_1616-a35a2828dbcd_.py +75 -0
- exobrain/database/migrations/versions/2024_03_04_1549-4cc4ffdb6c02_.py +70 -0
- exobrain/database/migrations/versions/2024_03_13_1323-97a9ef3f38c3_.py +151 -0
- exobrain/database/migrations/versions/2024_03_20_1033-13bc4d2ab3ab_.py +96 -0
- exobrain/database/migrations/versions/2024_03_28_0905-ffd01ebb2c87_.py +71 -0
- exobrain/database/migrations/versions/2024_03_28_1332-33744d8620d3_.py +144 -0
- exobrain/database/migrations/versions/2024_04_05_0919-a14d98908583_.py +88 -0
- exobrain/database/migrations/versions/2024_04_29_1515-882409414d37_.py +84 -0
- exobrain/database/migrations/versions/2024_05_23_0959-24bb8fdb3ba7_.py +72 -0
- exobrain/database/migrations/versions/2024_05_24_1248-f2a78a625a74_.py +82 -0
- exobrain/database/migrations/versions/2024_06_07_1537-004b611f77cf_.py +297 -0
- exobrain/database/migrations/versions/2024_06_10_1306-f0fe99d57963_.py +76 -0
- exobrain/database/migrations/versions/2024_10_15_0646-ef0829fe3540_.py +70 -0
- exobrain/database/migrations/versions/2025_01_28_1020-ea2ebd0e2cdc_.py +126 -0
- exobrain/database/migrations/versions/2025_03_19_1341-adeeb7c46a3c_.py +112 -0
- exobrain/database/migrations/versions/2025_03_25_1449-8fb5b2059d5a_.py +78 -0
- exobrain/database/migrations/versions/2025_05_06_1503-88aa2ef323c5-org.json +338 -0
- exobrain/database/migrations/versions/2025_05_06_1503-88aa2ef323c5-public.json +464 -0
- exobrain/database/migrations/versions/2025_05_06_1503-88aa2ef323c5_add_constraints_to_fk.py +82 -0
- exobrain/database/migrations/versions/2025_05_20_1302-37f7dbf56615_add_is_learning_field_to_license.py +67 -0
- exobrain/database/migrations/versions/2025_05_21_0839-096c1ab6ea15_add_is_learning_field_to_predefined_.py +67 -0
- exobrain/database/migrations/versions/2025_06_03_0336-60d91382b626_add_grace_period_field_to_risk_conf.py +71 -0
- exobrain/database/migrations/versions/2025_06_17_1013-4894e248a31d_add_risk_conf_buffer_period_field_to_.py +76 -0
- exobrain/database/migrations/versions/2025_06_20_0845-c0e0c905cfb1_add_computing_strategy.py +89 -0
- exobrain/database/migrations/versions/2025_06_20_0919-360b6879ae25_add_fully_computed_field_to_running_.py +71 -0
- exobrain/database/migrations/versions/2025_06_21_1200-1a37475a962d_fix_feasible_field_in_running_risk_table_set_not_null.py +76 -0
- exobrain/database/migrations/versions/2025_06_21_1300-d456be82025a_add_computed_mapping_field_in_running_.py +85 -0
- exobrain/database/migrations/versions/2025_06_27_0154-317d388748a1_adding_auto_rejected_reason_code.py +112 -0
- exobrain/database/migrations/versions/2025_06_27_0227-0a9522eb04f0_update_predefined_actions_execution.py +183 -0
- exobrain/database/migrations/versions/2025_06_27_0610-9daee7d935b1_add_rejection_and_expiration_fields.py +153 -0
- exobrain/database/migrations/versions/2025_06_27_0617-ee6062e6dc4d_add_rejection_and_expiration_fields.py +205 -0
- exobrain/database/migrations/versions/2025_06_29_0828-90923eeab038_calculate_due_dates_and_due_indicators.py +133 -0
- exobrain/database/migrations/versions/2025_09_26_1624-2716d77c7904_make_context_fields_non_nullable.py +244 -0
- exobrain/database/migrations/versions/2025_09_26_1857-0ab6e7d1a127_add_note_column_to_running_action.py +34 -0
- exobrain/database/migrations/versions/2025_10_17_1620-6400ea4ce6f0_add_index_overdue_risks.py +55 -0
- exobrain/database/migrations/versions/2025_10_31_0222-67c46f78caca_convert_action_context_data_json_to_jsonb.py +71 -0
- exobrain/database/migrations/versions/2025_11_04_2117-a8a814d7f2ea_rename_execution_reason_code.py +55 -0
- exobrain/database/migrations/versions/2026_01_04_1647-87c0d5fd2688_apply_new_naming_rules_for_indexes.py +50 -0
- exobrain/database/migrations/versions/2026_01_04_1801-392b727bbaa1_remove_unused_exo_conf_table.py +96 -0
- exobrain/database/migrations/versions/2026_01_05_0840-4b12100a932c_add_constraints_to_organizations_settings.py +117 -0
- exobrain/database/migrations/versions/2026_01_05_0917-f67bd91a1913_change_copilot_uiConfiguration_json_to_jsonb.py +54 -0
- exobrain/database/migrations/versions/2026_01_05_0935-87de8335f045_constrain_organizations_preferences_fields.py +140 -0
- exobrain/database/migrations/versions/2026_01_05_1018-7271b6d49e2c_enforce_not_null_and_jsonb_for_predefined_tables.py +264 -0
- exobrain/database/migrations/versions/2026_01_05_1510-711567521aa6_convert_config_tables_json_to_jsonb.py +91 -0
- exobrain/database/migrations/versions/2026_01_05_1540-ce5a35090a82_convert_connection_json_to_jsonb.py +60 -0
- exobrain/database/migrations/versions/2026_01_06_0939-115812c0bcd6_make_scope_key_non_nullable.py +52 -0
- exobrain/database/migrations/versions/2026_01_06_1109-7dd6b5f271ed_convert_json_to_jsonb_for_dashboard_and_widgets.py +77 -0
- exobrain/database/migrations/versions/2026_01_06_1452-4e4ad858fdf9_convert_json_to_jsonb_for_running_entities.py +185 -0
- exobrain/database/migrations/versions/2026_01_09_1558-6954766729aa_simplify_one_to_many_relationships.py +72 -0
- exobrain/database/migrations/versions/2026_01_12_1639-e548f437e5de_simplify_one_to_many_relationships.py +266 -0
- exobrain/database/migrations/versions/2026_02_03_1456-abbb5a630567_simplify_one_to_many_relationships.py +285 -0
- exobrain/database/migrations/versions/2026_02_06_0827-95438a5ec210_simplify_one_to_many_relationships.py +85 -0
- exobrain/database/migrations/versions/2026_02_06_0835-881f104f8b2b_simplify_one_to_many_relationships.py +432 -0
- exobrain/database/migrations/versions/2026_04_20_1015-8f3c5e2d9a1b_add_running_action_summary_table_with_text_column.py +48 -0
- exobrain/database/migrations/versions/2026_04_30_1357-c3e8047f29f6_add_kpi_names_to_predefined_action.py +44 -0
- exobrain/database/migrations/versions/2026_05_07_0100-a1b2c3d4e5f6_create_scores_table.py +53 -0
- exobrain/database/model/__init__.py +4 -0
- exobrain/database/model/associations/__init__.py +41 -0
- exobrain/database/model/associations/action_conf_action_reason.py +20 -0
- exobrain/database/model/associations/copilot_license.py +20 -0
- exobrain/database/model/associations/exec_conf_execution_reason.py +19 -0
- exobrain/database/model/associations/predefined_action_action_reason.py +20 -0
- exobrain/database/model/associations/predefined_action_predefined_risk.py +20 -0
- exobrain/database/model/associations/predefined_execution_execution_reason.py +20 -0
- exobrain/database/model/associations/predefined_execution_predefined_action.py +20 -0
- exobrain/database/model/associations/predefined_risk_risk_reason.py +20 -0
- exobrain/database/model/associations/risk_conf_risk_reason.py +19 -0
- exobrain/database/model/associations/roles_action_conf.py +19 -0
- exobrain/database/model/associations/roles_exec_conf.py +19 -0
- exobrain/database/model/associations/roles_permissions.py +20 -0
- exobrain/database/model/associations/roles_predefined_action.py +20 -0
- exobrain/database/model/associations/roles_predefined_execution.py +20 -0
- exobrain/database/model/associations/roles_running_actions.py +19 -0
- exobrain/database/model/associations/roles_running_executions.py +19 -0
- exobrain/database/model/associations/roles_users.py +19 -0
- exobrain/database/model/associations/running_action_context_scopes.py +19 -0
- exobrain/database/model/associations/users_scopes.py +19 -0
- exobrain/database/model/base.py +46 -0
- exobrain/database/model/enums/__init__.py +13 -0
- exobrain/database/model/enums/config.py +30 -0
- exobrain/database/model/enums/currency.py +178 -0
- exobrain/database/model/enums/running.py +87 -0
- exobrain/database/model/field_types.py +6 -0
- exobrain/database/model/general/__init__.py +23 -0
- exobrain/database/model/general/copilot.py +50 -0
- exobrain/database/model/general/general_base.py +31 -0
- exobrain/database/model/general/organization_models.py +91 -0
- exobrain/database/model/general/permission.py +33 -0
- exobrain/database/model/general/predefined_models.py +225 -0
- exobrain/database/model/general/reason_models.py +147 -0
- exobrain/database/model/general/role.py +96 -0
- exobrain/database/model/org/__init__.py +43 -0
- exobrain/database/model/org/config_models.py +293 -0
- exobrain/database/model/org/connection.py +22 -0
- exobrain/database/model/org/dashboard_models.py +84 -0
- exobrain/database/model/org/org_base.py +31 -0
- exobrain/database/model/org/organization_settings.py +55 -0
- exobrain/database/model/org/running_models.py +778 -0
- exobrain/database/model/org/scope.py +51 -0
- exobrain/database/model/org/user.py +53 -0
- exobrain/database/py.typed +0 -0
- exobrain/database/tools/__init__.py +0 -0
- exobrain/database/tools/jsonb_filter.py +369 -0
- exobrain/database/tools/record_count.py +60 -0
- exobrain/database/tools/sql_query_logger.py +83 -0
- exobrain_database-0.1.0.dist-info/METADATA +166 -0
- exobrain_database-0.1.0.dist-info/RECORD +141 -0
- exobrain_database-0.1.0.dist-info/WHEEL +4 -0
|
File without changes
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
Generic single-database configuration.
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
# A generic, single database configuration.
|
|
2
|
+
|
|
3
|
+
[alembic]
|
|
4
|
+
script_location = %(here)s
|
|
5
|
+
version_locations = %(here)s/versions
|
|
6
|
+
file_template = %%(year)d_%%(month).2d_%%(day).2d_%%(hour).2d%%(minute).2d-%%(rev)s_%%(slug)s
|
|
7
|
+
|
|
8
|
+
timezone = UTC
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
# sys.path path, will be prepended to sys.path if present.
|
|
12
|
+
# defaults to the current working directory.
|
|
13
|
+
prepend_sys_path = .
|
|
14
|
+
|
|
15
|
+
# timezone to use when rendering the date within the migration file
|
|
16
|
+
# as well as the filename.
|
|
17
|
+
# If specified, requires the python-dateutil library that can be
|
|
18
|
+
# installed by adding `alembic[tz]` to the pip requirements
|
|
19
|
+
# string value is passed to dateutil.tz.gettz()
|
|
20
|
+
# leave blank for localtime
|
|
21
|
+
# timezone =
|
|
22
|
+
|
|
23
|
+
# max length of characters to apply to the
|
|
24
|
+
# "slug" field
|
|
25
|
+
# truncate_slug_length = 40
|
|
26
|
+
|
|
27
|
+
# set to 'true' to run the environment during
|
|
28
|
+
# the 'revision' command, regardless of autogenerate
|
|
29
|
+
# revision_environment = false
|
|
30
|
+
|
|
31
|
+
# set to 'true' to allow .pyc and .pyo files without
|
|
32
|
+
# a source .py file to be detected as revisions in the
|
|
33
|
+
# versions/ directory
|
|
34
|
+
# sourceless = false
|
|
35
|
+
|
|
36
|
+
# version location specification; This defaults
|
|
37
|
+
# to alembic/versions. When using multiple version
|
|
38
|
+
# directories, initial revisions must be specified with --version-path.
|
|
39
|
+
# The path separator used here should be the separator specified by "version_path_separator" below.
|
|
40
|
+
|
|
41
|
+
# version path separator; As mentioned above, this is the character used to split
|
|
42
|
+
# version_locations. The default within new alembic.ini files is "os", which uses os.pathsep.
|
|
43
|
+
# If this key is omitted entirely, it falls back to the legacy behavior of splitting on spaces and/or commas.
|
|
44
|
+
# Valid values for version_path_separator are:
|
|
45
|
+
#
|
|
46
|
+
# version_path_separator = :
|
|
47
|
+
# version_path_separator = ;
|
|
48
|
+
# version_path_separator = space
|
|
49
|
+
version_path_separator = :
|
|
50
|
+
|
|
51
|
+
# set to 'true' to search source files recursively
|
|
52
|
+
# in each "version_locations" directory
|
|
53
|
+
# new in Alembic version 1.10
|
|
54
|
+
# recursive_version_locations = false
|
|
55
|
+
|
|
56
|
+
# the output encoding used when revision files
|
|
57
|
+
# are written from script.py.mako
|
|
58
|
+
# output_encoding = utf-8
|
|
59
|
+
|
|
60
|
+
[post_write_hooks]
|
|
61
|
+
# post_write_hooks defines scripts or Python functions that are run
|
|
62
|
+
# on newly generated revision scripts. See the documentation for further
|
|
63
|
+
# detail and examples
|
|
64
|
+
|
|
65
|
+
# format using "black" - use the console_scripts runner, against the "black" entrypoint
|
|
66
|
+
# hooks = black
|
|
67
|
+
# black.type = console_scripts
|
|
68
|
+
# black.entrypoint = black
|
|
69
|
+
# black.options = -l 79 REVISION_SCRIPT_FILENAME
|
|
70
|
+
|
|
71
|
+
# Logging configuration
|
|
72
|
+
[loggers]
|
|
73
|
+
keys = root,sqlalchemy,alembic
|
|
74
|
+
|
|
75
|
+
[handlers]
|
|
76
|
+
keys = console
|
|
77
|
+
|
|
78
|
+
[formatters]
|
|
79
|
+
keys = generic
|
|
80
|
+
|
|
81
|
+
[logger_root]
|
|
82
|
+
level = WARN
|
|
83
|
+
handlers = console
|
|
84
|
+
qualname =
|
|
85
|
+
|
|
86
|
+
[logger_sqlalchemy]
|
|
87
|
+
level = WARN
|
|
88
|
+
handlers =
|
|
89
|
+
qualname = sqlalchemy.engine
|
|
90
|
+
|
|
91
|
+
[logger_alembic]
|
|
92
|
+
level = INFO
|
|
93
|
+
handlers =
|
|
94
|
+
qualname = alembic
|
|
95
|
+
|
|
96
|
+
[handler_console]
|
|
97
|
+
class = StreamHandler
|
|
98
|
+
args = (sys.stderr,)
|
|
99
|
+
level = NOTSET
|
|
100
|
+
formatter = generic
|
|
101
|
+
|
|
102
|
+
[formatter_generic]
|
|
103
|
+
format = %(levelname)-5.5s [%(name)s] %(message)s
|
|
104
|
+
datefmt = %H:%M:%S
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
import os
|
|
3
|
+
import typing as t
|
|
4
|
+
from logging.config import fileConfig
|
|
5
|
+
|
|
6
|
+
from alembic import context
|
|
7
|
+
from alembic.runtime.environment import NameFilterType
|
|
8
|
+
from sqlalchemy import URL, Connection, Engine, Result, Table, column, create_engine, select, table
|
|
9
|
+
from sqlalchemy.schema import CreateSchema
|
|
10
|
+
from sqlalchemy.sql import text
|
|
11
|
+
from sqlalchemy.sql.schema import SchemaItem
|
|
12
|
+
|
|
13
|
+
from exobrain.database.model.base import BASE
|
|
14
|
+
|
|
15
|
+
# Apply logging configuration from alembic.ini
|
|
16
|
+
if context.config.config_file_name:
|
|
17
|
+
fileConfig(context.config.config_file_name)
|
|
18
|
+
|
|
19
|
+
logger = logging.getLogger("alembic.runtime.migration")
|
|
20
|
+
|
|
21
|
+
base_metadata = [BASE.metadata]
|
|
22
|
+
"""Metadata for schema migrations which contains all declared models."""
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def list_schemas(connection: Connection) -> list[str]:
|
|
26
|
+
results: Result[t.Any] = connection.execute(
|
|
27
|
+
select(column("schema_name"))
|
|
28
|
+
.select_from(table(schema="information_schema", name="schemata"))
|
|
29
|
+
.where(column("schema_name").like("org_%"))
|
|
30
|
+
)
|
|
31
|
+
return [row.schema_name for row in results]
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class MigrationException(SystemExit):
|
|
35
|
+
"""Custom exception for migration errors."""
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def _get_alembic_commands() -> tuple[t.Any, ...]:
|
|
39
|
+
"""
|
|
40
|
+
Get the command-line options passed to the ``alembic`` script.
|
|
41
|
+
"""
|
|
42
|
+
cmd_opts = context.config.cmd_opts
|
|
43
|
+
if not cmd_opts or "cmd" not in cmd_opts or not cmd_opts.cmd:
|
|
44
|
+
logger.warning("No command were passed to the alembic script: %s", repr(cmd_opts))
|
|
45
|
+
return ()
|
|
46
|
+
return tuple(cmd_opts.cmd)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
# Use to generate the schema even if no org has been defined
|
|
50
|
+
def has_autogenerate() -> bool:
|
|
51
|
+
"""
|
|
52
|
+
Check if the current alembic command is an upgrade or autogenerate.
|
|
53
|
+
|
|
54
|
+
Possible commands include:
|
|
55
|
+
|
|
56
|
+
- `alembic upgrade`
|
|
57
|
+
- `alembic revision --autogenerate`
|
|
58
|
+
- `alembic revision --autogenerate -m "message"`
|
|
59
|
+
"""
|
|
60
|
+
commands = _get_alembic_commands()
|
|
61
|
+
is_upgrade = len(commands) > 0 and callable(commands[0]) and commands[0].__name__ == "upgrade"
|
|
62
|
+
is_autogenerate = len(commands) > 2 and "autogenerate" in commands[2] # noqa PLR2004
|
|
63
|
+
return is_upgrade or is_autogenerate
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def is_public(
|
|
67
|
+
schema_item: SchemaItem,
|
|
68
|
+
name: str | None, # noqa
|
|
69
|
+
type_: NameFilterType,
|
|
70
|
+
reflected: bool, # noqa
|
|
71
|
+
compare_to: SchemaItem | None, # noqa
|
|
72
|
+
) -> bool:
|
|
73
|
+
if type_ == "table" and schema_item.info.get("skip_autogenerate"):
|
|
74
|
+
return False
|
|
75
|
+
|
|
76
|
+
table: Table
|
|
77
|
+
if type_ == "table":
|
|
78
|
+
table = schema_item # type: ignore
|
|
79
|
+
elif type_ == "foreign_key_constraint":
|
|
80
|
+
table = schema_item.parent # type: ignore
|
|
81
|
+
else:
|
|
82
|
+
table = schema_item.table # type: ignore
|
|
83
|
+
|
|
84
|
+
return table.schema == "public" or table.info.get("schema_type") == "base"
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def is_org(
|
|
88
|
+
schema_item: SchemaItem,
|
|
89
|
+
name: str | None,
|
|
90
|
+
type_: NameFilterType,
|
|
91
|
+
reflected: bool, # noqa
|
|
92
|
+
compare_to: SchemaItem | None,
|
|
93
|
+
) -> bool:
|
|
94
|
+
if type_ == "table" and schema_item.info.get("skip_autogenerate"):
|
|
95
|
+
return False
|
|
96
|
+
|
|
97
|
+
return not is_public(schema_item, name, type_, reflected, compare_to)
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
# for public schemas
|
|
101
|
+
def run_base_migrations_online(connection: Connection) -> None:
|
|
102
|
+
logger.info("======= Processing public schema =======")
|
|
103
|
+
args = context.get_x_argument(as_dictionary=True)
|
|
104
|
+
schema = args.get("schema")
|
|
105
|
+
if schema and schema != "public":
|
|
106
|
+
logger.info("Skipping base migrations for schema: %s", schema)
|
|
107
|
+
return
|
|
108
|
+
|
|
109
|
+
context.configure(
|
|
110
|
+
connection=connection,
|
|
111
|
+
target_metadata=base_metadata,
|
|
112
|
+
include_object=is_public,
|
|
113
|
+
upgrade_token="base_up", # noqa: S106
|
|
114
|
+
downgrade_token="base_down", # noqa: S106
|
|
115
|
+
compare_type=True,
|
|
116
|
+
)
|
|
117
|
+
|
|
118
|
+
with context.begin_transaction():
|
|
119
|
+
connection.execute(text("SET search_path TO public"))
|
|
120
|
+
connection.dialect.default_schema_name = "public"
|
|
121
|
+
context.run_migrations(schema_type="base")
|
|
122
|
+
connection.commit()
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
# for organizations schemas
|
|
126
|
+
def run_orgs_migrations_online(connection: Connection) -> None:
|
|
127
|
+
args = context.get_x_argument(as_dictionary=True)
|
|
128
|
+
schema = args.get("schema")
|
|
129
|
+
if schema and schema == "public":
|
|
130
|
+
logger.info("Skipping organization migrations for schema: %s", schema)
|
|
131
|
+
return
|
|
132
|
+
schemas = [schema] if schema else list_schemas(connection)
|
|
133
|
+
|
|
134
|
+
# When the database is empty (without organizations),
|
|
135
|
+
# we create a default schema "org_1" for autogeneration.
|
|
136
|
+
if has_autogenerate() and not schemas:
|
|
137
|
+
schemas = ["org_1"]
|
|
138
|
+
|
|
139
|
+
for schema in filter(None, schemas):
|
|
140
|
+
logger.info("======= Processing organization schema: %s =======", schema)
|
|
141
|
+
|
|
142
|
+
context.configure(
|
|
143
|
+
connection=connection,
|
|
144
|
+
target_metadata=base_metadata,
|
|
145
|
+
include_object=is_org,
|
|
146
|
+
upgrade_token="org_up", # noqa: S106
|
|
147
|
+
downgrade_token="org_down", # noqa: S106
|
|
148
|
+
compare_type=True,
|
|
149
|
+
)
|
|
150
|
+
|
|
151
|
+
with context.begin_transaction():
|
|
152
|
+
if not connection.dialect.has_schema(connection, schema):
|
|
153
|
+
connection.execute(CreateSchema(schema, if_not_exists=True))
|
|
154
|
+
connection.commit()
|
|
155
|
+
connection.execute(text(f"SET search_path TO {schema}"))
|
|
156
|
+
connection.dialect.default_schema_name = schema
|
|
157
|
+
context.run_migrations(schema_type="org")
|
|
158
|
+
connection.commit()
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
def run_migrations_online() -> None:
|
|
162
|
+
"""
|
|
163
|
+
Execute database migrations for both base and organization schemas.
|
|
164
|
+
"""
|
|
165
|
+
sql_engine = os.getenv("SQL_ENGINE", "postgresql+psycopg")
|
|
166
|
+
sql_host = os.getenv("SQL_HOST", "localhost")
|
|
167
|
+
sql_database = os.getenv("SQL_DATABASE", "exobrain")
|
|
168
|
+
sql_user = os.getenv("SQL_USER", "postgres")
|
|
169
|
+
sql_port = int(os.getenv("SQL_PORT", "5432"))
|
|
170
|
+
sql_password = os.getenv("SQL_PASSWORD")
|
|
171
|
+
if not sql_password:
|
|
172
|
+
raise MigrationException("SQL_PASSWORD environment variable is required.")
|
|
173
|
+
|
|
174
|
+
db_url = URL.create(
|
|
175
|
+
drivername=sql_engine,
|
|
176
|
+
username=sql_user,
|
|
177
|
+
password=sql_password,
|
|
178
|
+
host=sql_host,
|
|
179
|
+
port=sql_port,
|
|
180
|
+
database=sql_database,
|
|
181
|
+
)
|
|
182
|
+
db_engine: Engine = create_engine(db_url)
|
|
183
|
+
|
|
184
|
+
with db_engine.connect() as connection:
|
|
185
|
+
# If the first argument is a function, and it is a downgrade, we reverse the order of migrations
|
|
186
|
+
# to ensure that base migrations are applied before organization migrations.
|
|
187
|
+
commands = _get_alembic_commands()
|
|
188
|
+
is_downgrade = len(commands) > 0 and callable(commands[0]) and commands[0].__name__ == "downgrade"
|
|
189
|
+
if is_downgrade:
|
|
190
|
+
run_orgs_migrations_online(connection)
|
|
191
|
+
run_base_migrations_online(connection)
|
|
192
|
+
else:
|
|
193
|
+
run_base_migrations_online(connection)
|
|
194
|
+
run_orgs_migrations_online(connection)
|
|
195
|
+
connection.commit()
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
run_migrations_online()
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Migration manager for Alembic database migrations.
|
|
3
|
+
|
|
4
|
+
This module provides utilities to run database schema migrations for multi-tenant
|
|
5
|
+
organization schemas using Alembic. Each organization has its own schema
|
|
6
|
+
and migrations can be applied independently to each organization's schema.
|
|
7
|
+
|
|
8
|
+
Functions:
|
|
9
|
+
- configure(org_id): Configure Alembic for a specific organization schema.
|
|
10
|
+
- upgrade(org_id): Upgrade the database schema to the latest version.
|
|
11
|
+
- downgrade(org_id): Downgrade the database schema to the base version.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
import argparse
|
|
15
|
+
import functools
|
|
16
|
+
from collections import abc
|
|
17
|
+
from pathlib import Path
|
|
18
|
+
from threading import Lock
|
|
19
|
+
from typing import ParamSpec, TypeVar
|
|
20
|
+
from uuid import UUID
|
|
21
|
+
|
|
22
|
+
from alembic import command
|
|
23
|
+
from alembic.config import Config
|
|
24
|
+
|
|
25
|
+
HERE = Path(__file__).parent
|
|
26
|
+
"""Path to the current directory of this module, which contains the Alembic configuration."""
|
|
27
|
+
|
|
28
|
+
_migration_lock = Lock()
|
|
29
|
+
"""Shared lock to ensure thread-safe migration operations."""
|
|
30
|
+
|
|
31
|
+
P = ParamSpec("P")
|
|
32
|
+
T = TypeVar("T")
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def _synchronized(func: abc.Callable[P, T]) -> abc.Callable[P, T]:
|
|
36
|
+
"""
|
|
37
|
+
Decorator to synchronize access to migration functions.
|
|
38
|
+
|
|
39
|
+
This ensures that only one migration operation can run at a time,
|
|
40
|
+
preventing concurrent migrations from interfering with each other.
|
|
41
|
+
"""
|
|
42
|
+
|
|
43
|
+
@functools.wraps(func)
|
|
44
|
+
def wrapper(*args: P.args, **kwargs: P.kwargs) -> T:
|
|
45
|
+
with _migration_lock:
|
|
46
|
+
return func(*args, **kwargs)
|
|
47
|
+
|
|
48
|
+
return wrapper
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def configure(org_id: UUID | int | str) -> Config:
|
|
52
|
+
"""
|
|
53
|
+
Create an Alembic configuration for a specific organization schema.
|
|
54
|
+
|
|
55
|
+
Args:
|
|
56
|
+
org_id: The organization identifier.
|
|
57
|
+
|
|
58
|
+
Returns:
|
|
59
|
+
An Alembic Config object configured for the organization's schema.
|
|
60
|
+
|
|
61
|
+
Raises:
|
|
62
|
+
TypeError: If org_id is not a valid type (UUID, int, or numeric string).
|
|
63
|
+
"""
|
|
64
|
+
if isinstance(org_id, UUID):
|
|
65
|
+
schema = f"org_{org_id.int}"
|
|
66
|
+
elif isinstance(org_id, int) or (isinstance(org_id, str) and org_id.isdigit()):
|
|
67
|
+
schema = f"org_{org_id}"
|
|
68
|
+
else:
|
|
69
|
+
error = f"Invalid org_id type: '{type(org_id).__name__}'. Must be UUID, int, or numeric str."
|
|
70
|
+
raise TypeError(error)
|
|
71
|
+
|
|
72
|
+
cmd_opts = argparse.Namespace(x=[f"schema={schema}"])
|
|
73
|
+
|
|
74
|
+
path = HERE.joinpath("alembic.ini")
|
|
75
|
+
if not path.is_file(): # pragma: no cover
|
|
76
|
+
error = f"Alembic configuration file not found at {path}"
|
|
77
|
+
raise NotImplementedError(error)
|
|
78
|
+
|
|
79
|
+
return Config(path, cmd_opts=cmd_opts)
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
@_synchronized
|
|
83
|
+
def upgrade(org_id: UUID | int | str) -> None:
|
|
84
|
+
"""
|
|
85
|
+
Upgrade the database schema to the latest version for a specific organization.
|
|
86
|
+
|
|
87
|
+
Args:
|
|
88
|
+
org_id: The organization identifier.
|
|
89
|
+
|
|
90
|
+
Raises:
|
|
91
|
+
TypeError: If org_id is not a valid type (UUID, int, or numeric string).
|
|
92
|
+
"""
|
|
93
|
+
config = configure(org_id)
|
|
94
|
+
command.upgrade(config, "head")
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
@_synchronized
|
|
98
|
+
def downgrade(org_id: UUID | int | str) -> None:
|
|
99
|
+
"""
|
|
100
|
+
Downgrade the database schema to the base version for a specific organization.
|
|
101
|
+
|
|
102
|
+
Args:
|
|
103
|
+
org_id: The organization identifier.
|
|
104
|
+
|
|
105
|
+
Raises:
|
|
106
|
+
TypeError: If org_id is not a valid type (UUID, int, or numeric string).
|
|
107
|
+
"""
|
|
108
|
+
config = configure(org_id)
|
|
109
|
+
command.downgrade(config, "head")
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
"""${message}
|
|
2
|
+
|
|
3
|
+
Revision ID: ${up_revision}
|
|
4
|
+
Revises: ${down_revision | comma,n}
|
|
5
|
+
Create Date: ${create_date}
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from collections.abc import Sequence
|
|
9
|
+
|
|
10
|
+
from alembic import op
|
|
11
|
+
${imports if imports else ""}
|
|
12
|
+
|
|
13
|
+
# revision identifiers, used by Alembic.
|
|
14
|
+
revision: str = ${repr(up_revision)}
|
|
15
|
+
down_revision: str | None = ${repr(down_revision)}
|
|
16
|
+
branch_labels: str | Sequence[str] | None = ${repr(branch_labels)}
|
|
17
|
+
depends_on: str | Sequence[str] | None = ${repr(depends_on)}
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def upgrade(schema_type: str) -> None:
|
|
21
|
+
if schema_type == "base":
|
|
22
|
+
upgrade_base()
|
|
23
|
+
else:
|
|
24
|
+
upgrade_organization()
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def downgrade(schema_type: str) -> None:
|
|
28
|
+
if schema_type == "base":
|
|
29
|
+
downgrade_base()
|
|
30
|
+
else:
|
|
31
|
+
downgrade_organization()
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def upgrade_base() -> None:
|
|
35
|
+
${context.get("base_up", "pass")}
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def downgrade_base() -> None:
|
|
39
|
+
${context.get("base_down", "pass")}
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def upgrade_organization() -> None:
|
|
43
|
+
${context.get("org_up", "pass")}
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def downgrade_organization() -> None:
|
|
47
|
+
${context.get("org_down", "pass")}
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
"""Add new tables for organizations and roles
|
|
2
|
+
|
|
3
|
+
Revision ID: 3ed3cf31c9d4
|
|
4
|
+
Revises:
|
|
5
|
+
Create Date: 2023-08-17 09:37:26.315461+00:00
|
|
6
|
+
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from collections.abc import Sequence
|
|
10
|
+
|
|
11
|
+
import sqlalchemy as sa
|
|
12
|
+
from alembic import op
|
|
13
|
+
|
|
14
|
+
# revision identifiers, used by Alembic.
|
|
15
|
+
revision: str = "3ed3cf31c9d4"
|
|
16
|
+
down_revision: str | None = None
|
|
17
|
+
branch_labels: str | Sequence[str] | None = None
|
|
18
|
+
depends_on: str | Sequence[str] | None = None
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def upgrade(schema_type):
|
|
22
|
+
if schema_type == "base":
|
|
23
|
+
return upgrade_base()
|
|
24
|
+
else:
|
|
25
|
+
return upgrade_organization()
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def downgrade(schema_type):
|
|
29
|
+
if schema_type == "base":
|
|
30
|
+
return downgrade_base()
|
|
31
|
+
else:
|
|
32
|
+
return downgrade_organization()
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
# BASE SCHEMA MIGRATIONS
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def upgrade_base():
|
|
39
|
+
# ### commands auto generated by Alembic - please adjust! ###
|
|
40
|
+
op.create_table(
|
|
41
|
+
"organizations",
|
|
42
|
+
sa.Column("name", sa.Text(), nullable=False),
|
|
43
|
+
sa.Column("description", sa.Text(), nullable=True),
|
|
44
|
+
sa.Column("emailAdmin", sa.Text(), nullable=False),
|
|
45
|
+
sa.Column("currency", sa.Enum("DOLLAR", "EURO", name="currency"), nullable=True),
|
|
46
|
+
sa.Column("id", sa.UUID(), nullable=False),
|
|
47
|
+
sa.Column("created_at", sa.DateTime(timezone=True), nullable=False),
|
|
48
|
+
sa.Column("updated_at", sa.DateTime(timezone=True), nullable=False),
|
|
49
|
+
sa.Column("created_by", sa.Text(), nullable=False),
|
|
50
|
+
sa.Column("updated_by", sa.Text(), nullable=False),
|
|
51
|
+
sa.PrimaryKeyConstraint("id"),
|
|
52
|
+
schema="public",
|
|
53
|
+
)
|
|
54
|
+
op.create_table(
|
|
55
|
+
"roles",
|
|
56
|
+
sa.Column("name", sa.Text(), nullable=False),
|
|
57
|
+
sa.Column("id", sa.UUID(), nullable=False),
|
|
58
|
+
sa.Column("created_at", sa.DateTime(timezone=True), nullable=False),
|
|
59
|
+
sa.Column("updated_at", sa.DateTime(timezone=True), nullable=False),
|
|
60
|
+
sa.Column("created_by", sa.Text(), nullable=False),
|
|
61
|
+
sa.Column("updated_by", sa.Text(), nullable=False),
|
|
62
|
+
sa.PrimaryKeyConstraint("id"),
|
|
63
|
+
schema="public",
|
|
64
|
+
)
|
|
65
|
+
# ### end Alembic commands ###
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def downgrade_base():
|
|
69
|
+
# First, drop the foreign key constraints in organization schemas
|
|
70
|
+
connection = op.get_bind()
|
|
71
|
+
result = connection.execute(
|
|
72
|
+
sa.text(
|
|
73
|
+
"SELECT schema_name FROM information_schema.schemata WHERE schema_name LIKE 'org_%'"
|
|
74
|
+
)
|
|
75
|
+
)
|
|
76
|
+
org_schemas = [row[0] for row in result]
|
|
77
|
+
|
|
78
|
+
# Drop constraints in each organization schema that reference the public.roles table
|
|
79
|
+
for schema in org_schemas:
|
|
80
|
+
# Constraints referencing roles
|
|
81
|
+
op.drop_constraint(
|
|
82
|
+
"roles_users_role_id_fkey",
|
|
83
|
+
"roles_users",
|
|
84
|
+
schema=schema,
|
|
85
|
+
type_="foreignkey",
|
|
86
|
+
)
|
|
87
|
+
op.drop_constraint(
|
|
88
|
+
"roles_action_conf_role_id_fkey",
|
|
89
|
+
"roles_action_conf",
|
|
90
|
+
schema=schema,
|
|
91
|
+
type_="foreignkey",
|
|
92
|
+
)
|
|
93
|
+
op.drop_constraint(
|
|
94
|
+
"roles_exec_conf_role_id_fkey",
|
|
95
|
+
"roles_exec_conf",
|
|
96
|
+
schema=schema,
|
|
97
|
+
type_="foreignkey",
|
|
98
|
+
)
|
|
99
|
+
op.drop_constraint(
|
|
100
|
+
"roles_running_executions_role_id_fkey",
|
|
101
|
+
"roles_running_executions",
|
|
102
|
+
schema=schema,
|
|
103
|
+
type_="foreignkey",
|
|
104
|
+
)
|
|
105
|
+
op.drop_constraint(
|
|
106
|
+
"roles_running_actions_role_id_fkey",
|
|
107
|
+
"roles_running_actions",
|
|
108
|
+
schema=schema,
|
|
109
|
+
type_="foreignkey",
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
# Now proceed with the original downgrade logic
|
|
113
|
+
op.drop_table("roles", schema="public")
|
|
114
|
+
op.drop_table("organizations", schema="public")
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
# END BASE SCHEMA MIGRATIONS
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
# ORGANIZATION SCHEMA MIGRATIONS
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def upgrade_organization():
|
|
124
|
+
# ### commands auto generated by Alembic - please adjust! ###
|
|
125
|
+
op.create_table(
|
|
126
|
+
"users",
|
|
127
|
+
sa.Column("email", sa.Text(), nullable=False),
|
|
128
|
+
sa.Column("firstname", sa.Text(), nullable=True),
|
|
129
|
+
sa.Column("lastname", sa.Text(), nullable=True),
|
|
130
|
+
sa.Column("displayname", sa.Text(), nullable=True),
|
|
131
|
+
sa.Column("preferredLanguage", sa.Text(), nullable=True),
|
|
132
|
+
sa.Column("officeLocation", sa.Text(), nullable=True),
|
|
133
|
+
sa.Column("jobTitle", sa.Text(), nullable=True),
|
|
134
|
+
sa.Column("id", sa.UUID(), nullable=False),
|
|
135
|
+
sa.Column("created_at", sa.DateTime(timezone=True), nullable=False),
|
|
136
|
+
sa.Column("updated_at", sa.DateTime(timezone=True), nullable=False),
|
|
137
|
+
sa.Column("created_by", sa.Text(), nullable=False),
|
|
138
|
+
sa.Column("updated_by", sa.Text(), nullable=False),
|
|
139
|
+
sa.PrimaryKeyConstraint("id"),
|
|
140
|
+
)
|
|
141
|
+
op.create_table(
|
|
142
|
+
"roles_users",
|
|
143
|
+
sa.Column("role_id", sa.UUID(), nullable=False),
|
|
144
|
+
sa.Column("users_id", sa.UUID(), nullable=False),
|
|
145
|
+
sa.ForeignKeyConstraint(
|
|
146
|
+
["role_id"], ["public.roles.id"], onupdate="CASCADE", ondelete="CASCADE"
|
|
147
|
+
),
|
|
148
|
+
sa.ForeignKeyConstraint(["users_id"], ["users.id"], onupdate="CASCADE", ondelete="CASCADE"),
|
|
149
|
+
sa.PrimaryKeyConstraint("role_id", "users_id"),
|
|
150
|
+
)
|
|
151
|
+
# ### end Alembic commands ###
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
def downgrade_organization():
|
|
155
|
+
# ### commands auto generated by Alembic - please adjust! ###
|
|
156
|
+
op.drop_table("roles_users")
|
|
157
|
+
op.drop_table("users")
|
|
158
|
+
# ### end Alembic commands ###
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
# END ORGANIZATION SCHEMA MIGRATIONS
|