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.
Files changed (141) hide show
  1. exobrain/database/__init__.py +0 -0
  2. exobrain/database/migrations/README.md +1 -0
  3. exobrain/database/migrations/__init__.py +9 -0
  4. exobrain/database/migrations/alembic.ini +104 -0
  5. exobrain/database/migrations/env.py +198 -0
  6. exobrain/database/migrations/migration_manager.py +109 -0
  7. exobrain/database/migrations/script.py.mako +47 -0
  8. exobrain/database/migrations/versions/2023_08_17_0937-3ed3cf31c9d4_.py +161 -0
  9. exobrain/database/migrations/versions/2023_08_31_1322-aa6854636eac_.py +181 -0
  10. exobrain/database/migrations/versions/2023_09_01_0925-9667f0608380_.py +108 -0
  11. exobrain/database/migrations/versions/2023_09_13_1243-3cd1eb0d7b52_.py +238 -0
  12. exobrain/database/migrations/versions/2023_09_15_1447-52f33bc03521_.py +72 -0
  13. exobrain/database/migrations/versions/2023_09_19_0956-f8e096a41120_.py +70 -0
  14. exobrain/database/migrations/versions/2023_09_21_1642-74f18c0bab6e_.py +548 -0
  15. exobrain/database/migrations/versions/2023_09_28_1237-919ce06afab1_.py +71 -0
  16. exobrain/database/migrations/versions/2023_10_04_0718-03535f8dfe5f_.py +150 -0
  17. exobrain/database/migrations/versions/2023_10_05_1508-f64d4c00b610_.py +284 -0
  18. exobrain/database/migrations/versions/2023_10_06_0819-e544518a415c_.py +273 -0
  19. exobrain/database/migrations/versions/2023_10_06_1312-b9b232d63be3_.py +70 -0
  20. exobrain/database/migrations/versions/2023_10_25_0902-aab08fb68941_.py +70 -0
  21. exobrain/database/migrations/versions/2023_10_25_0937-d59ec80e6dd7_.py +68 -0
  22. exobrain/database/migrations/versions/2023_11_22_1628-84b116f3df26_.py +84 -0
  23. exobrain/database/migrations/versions/2023_11_22_1654-bcb01e365c42_.py +102 -0
  24. exobrain/database/migrations/versions/2023_11_27_1045-00ce519af235_.py +70 -0
  25. exobrain/database/migrations/versions/2023_11_27_1048-f779dc9fe429_.py +70 -0
  26. exobrain/database/migrations/versions/2023_11_29_1421-ad2b4c9c38fe_.py +68 -0
  27. exobrain/database/migrations/versions/2023_12_04_1029-f91d936478fb_.py +70 -0
  28. exobrain/database/migrations/versions/2023_12_11_1104-b33e0d6a5bdd_.py +71 -0
  29. exobrain/database/migrations/versions/2023_12_18_1406-73ac94585923_.py +70 -0
  30. exobrain/database/migrations/versions/2024_01_05_1022-f231770c573e_.py +83 -0
  31. exobrain/database/migrations/versions/2024_01_17_1606-b94c145b2310_.py +70 -0
  32. exobrain/database/migrations/versions/2024_01_18_1547-c450c961dfd0_.py +79 -0
  33. exobrain/database/migrations/versions/2024_01_18_1855-7101980bc238_.py +86 -0
  34. exobrain/database/migrations/versions/2024_01_22_1616-a35a2828dbcd_.py +75 -0
  35. exobrain/database/migrations/versions/2024_03_04_1549-4cc4ffdb6c02_.py +70 -0
  36. exobrain/database/migrations/versions/2024_03_13_1323-97a9ef3f38c3_.py +151 -0
  37. exobrain/database/migrations/versions/2024_03_20_1033-13bc4d2ab3ab_.py +96 -0
  38. exobrain/database/migrations/versions/2024_03_28_0905-ffd01ebb2c87_.py +71 -0
  39. exobrain/database/migrations/versions/2024_03_28_1332-33744d8620d3_.py +144 -0
  40. exobrain/database/migrations/versions/2024_04_05_0919-a14d98908583_.py +88 -0
  41. exobrain/database/migrations/versions/2024_04_29_1515-882409414d37_.py +84 -0
  42. exobrain/database/migrations/versions/2024_05_23_0959-24bb8fdb3ba7_.py +72 -0
  43. exobrain/database/migrations/versions/2024_05_24_1248-f2a78a625a74_.py +82 -0
  44. exobrain/database/migrations/versions/2024_06_07_1537-004b611f77cf_.py +297 -0
  45. exobrain/database/migrations/versions/2024_06_10_1306-f0fe99d57963_.py +76 -0
  46. exobrain/database/migrations/versions/2024_10_15_0646-ef0829fe3540_.py +70 -0
  47. exobrain/database/migrations/versions/2025_01_28_1020-ea2ebd0e2cdc_.py +126 -0
  48. exobrain/database/migrations/versions/2025_03_19_1341-adeeb7c46a3c_.py +112 -0
  49. exobrain/database/migrations/versions/2025_03_25_1449-8fb5b2059d5a_.py +78 -0
  50. exobrain/database/migrations/versions/2025_05_06_1503-88aa2ef323c5-org.json +338 -0
  51. exobrain/database/migrations/versions/2025_05_06_1503-88aa2ef323c5-public.json +464 -0
  52. exobrain/database/migrations/versions/2025_05_06_1503-88aa2ef323c5_add_constraints_to_fk.py +82 -0
  53. exobrain/database/migrations/versions/2025_05_20_1302-37f7dbf56615_add_is_learning_field_to_license.py +67 -0
  54. exobrain/database/migrations/versions/2025_05_21_0839-096c1ab6ea15_add_is_learning_field_to_predefined_.py +67 -0
  55. exobrain/database/migrations/versions/2025_06_03_0336-60d91382b626_add_grace_period_field_to_risk_conf.py +71 -0
  56. exobrain/database/migrations/versions/2025_06_17_1013-4894e248a31d_add_risk_conf_buffer_period_field_to_.py +76 -0
  57. exobrain/database/migrations/versions/2025_06_20_0845-c0e0c905cfb1_add_computing_strategy.py +89 -0
  58. exobrain/database/migrations/versions/2025_06_20_0919-360b6879ae25_add_fully_computed_field_to_running_.py +71 -0
  59. exobrain/database/migrations/versions/2025_06_21_1200-1a37475a962d_fix_feasible_field_in_running_risk_table_set_not_null.py +76 -0
  60. exobrain/database/migrations/versions/2025_06_21_1300-d456be82025a_add_computed_mapping_field_in_running_.py +85 -0
  61. exobrain/database/migrations/versions/2025_06_27_0154-317d388748a1_adding_auto_rejected_reason_code.py +112 -0
  62. exobrain/database/migrations/versions/2025_06_27_0227-0a9522eb04f0_update_predefined_actions_execution.py +183 -0
  63. exobrain/database/migrations/versions/2025_06_27_0610-9daee7d935b1_add_rejection_and_expiration_fields.py +153 -0
  64. exobrain/database/migrations/versions/2025_06_27_0617-ee6062e6dc4d_add_rejection_and_expiration_fields.py +205 -0
  65. exobrain/database/migrations/versions/2025_06_29_0828-90923eeab038_calculate_due_dates_and_due_indicators.py +133 -0
  66. exobrain/database/migrations/versions/2025_09_26_1624-2716d77c7904_make_context_fields_non_nullable.py +244 -0
  67. exobrain/database/migrations/versions/2025_09_26_1857-0ab6e7d1a127_add_note_column_to_running_action.py +34 -0
  68. exobrain/database/migrations/versions/2025_10_17_1620-6400ea4ce6f0_add_index_overdue_risks.py +55 -0
  69. exobrain/database/migrations/versions/2025_10_31_0222-67c46f78caca_convert_action_context_data_json_to_jsonb.py +71 -0
  70. exobrain/database/migrations/versions/2025_11_04_2117-a8a814d7f2ea_rename_execution_reason_code.py +55 -0
  71. exobrain/database/migrations/versions/2026_01_04_1647-87c0d5fd2688_apply_new_naming_rules_for_indexes.py +50 -0
  72. exobrain/database/migrations/versions/2026_01_04_1801-392b727bbaa1_remove_unused_exo_conf_table.py +96 -0
  73. exobrain/database/migrations/versions/2026_01_05_0840-4b12100a932c_add_constraints_to_organizations_settings.py +117 -0
  74. exobrain/database/migrations/versions/2026_01_05_0917-f67bd91a1913_change_copilot_uiConfiguration_json_to_jsonb.py +54 -0
  75. exobrain/database/migrations/versions/2026_01_05_0935-87de8335f045_constrain_organizations_preferences_fields.py +140 -0
  76. exobrain/database/migrations/versions/2026_01_05_1018-7271b6d49e2c_enforce_not_null_and_jsonb_for_predefined_tables.py +264 -0
  77. exobrain/database/migrations/versions/2026_01_05_1510-711567521aa6_convert_config_tables_json_to_jsonb.py +91 -0
  78. exobrain/database/migrations/versions/2026_01_05_1540-ce5a35090a82_convert_connection_json_to_jsonb.py +60 -0
  79. exobrain/database/migrations/versions/2026_01_06_0939-115812c0bcd6_make_scope_key_non_nullable.py +52 -0
  80. exobrain/database/migrations/versions/2026_01_06_1109-7dd6b5f271ed_convert_json_to_jsonb_for_dashboard_and_widgets.py +77 -0
  81. exobrain/database/migrations/versions/2026_01_06_1452-4e4ad858fdf9_convert_json_to_jsonb_for_running_entities.py +185 -0
  82. exobrain/database/migrations/versions/2026_01_09_1558-6954766729aa_simplify_one_to_many_relationships.py +72 -0
  83. exobrain/database/migrations/versions/2026_01_12_1639-e548f437e5de_simplify_one_to_many_relationships.py +266 -0
  84. exobrain/database/migrations/versions/2026_02_03_1456-abbb5a630567_simplify_one_to_many_relationships.py +285 -0
  85. exobrain/database/migrations/versions/2026_02_06_0827-95438a5ec210_simplify_one_to_many_relationships.py +85 -0
  86. exobrain/database/migrations/versions/2026_02_06_0835-881f104f8b2b_simplify_one_to_many_relationships.py +432 -0
  87. exobrain/database/migrations/versions/2026_04_20_1015-8f3c5e2d9a1b_add_running_action_summary_table_with_text_column.py +48 -0
  88. exobrain/database/migrations/versions/2026_04_30_1357-c3e8047f29f6_add_kpi_names_to_predefined_action.py +44 -0
  89. exobrain/database/migrations/versions/2026_05_07_0100-a1b2c3d4e5f6_create_scores_table.py +53 -0
  90. exobrain/database/model/__init__.py +4 -0
  91. exobrain/database/model/associations/__init__.py +41 -0
  92. exobrain/database/model/associations/action_conf_action_reason.py +20 -0
  93. exobrain/database/model/associations/copilot_license.py +20 -0
  94. exobrain/database/model/associations/exec_conf_execution_reason.py +19 -0
  95. exobrain/database/model/associations/predefined_action_action_reason.py +20 -0
  96. exobrain/database/model/associations/predefined_action_predefined_risk.py +20 -0
  97. exobrain/database/model/associations/predefined_execution_execution_reason.py +20 -0
  98. exobrain/database/model/associations/predefined_execution_predefined_action.py +20 -0
  99. exobrain/database/model/associations/predefined_risk_risk_reason.py +20 -0
  100. exobrain/database/model/associations/risk_conf_risk_reason.py +19 -0
  101. exobrain/database/model/associations/roles_action_conf.py +19 -0
  102. exobrain/database/model/associations/roles_exec_conf.py +19 -0
  103. exobrain/database/model/associations/roles_permissions.py +20 -0
  104. exobrain/database/model/associations/roles_predefined_action.py +20 -0
  105. exobrain/database/model/associations/roles_predefined_execution.py +20 -0
  106. exobrain/database/model/associations/roles_running_actions.py +19 -0
  107. exobrain/database/model/associations/roles_running_executions.py +19 -0
  108. exobrain/database/model/associations/roles_users.py +19 -0
  109. exobrain/database/model/associations/running_action_context_scopes.py +19 -0
  110. exobrain/database/model/associations/users_scopes.py +19 -0
  111. exobrain/database/model/base.py +46 -0
  112. exobrain/database/model/enums/__init__.py +13 -0
  113. exobrain/database/model/enums/config.py +30 -0
  114. exobrain/database/model/enums/currency.py +178 -0
  115. exobrain/database/model/enums/running.py +87 -0
  116. exobrain/database/model/field_types.py +6 -0
  117. exobrain/database/model/general/__init__.py +23 -0
  118. exobrain/database/model/general/copilot.py +50 -0
  119. exobrain/database/model/general/general_base.py +31 -0
  120. exobrain/database/model/general/organization_models.py +91 -0
  121. exobrain/database/model/general/permission.py +33 -0
  122. exobrain/database/model/general/predefined_models.py +225 -0
  123. exobrain/database/model/general/reason_models.py +147 -0
  124. exobrain/database/model/general/role.py +96 -0
  125. exobrain/database/model/org/__init__.py +43 -0
  126. exobrain/database/model/org/config_models.py +293 -0
  127. exobrain/database/model/org/connection.py +22 -0
  128. exobrain/database/model/org/dashboard_models.py +84 -0
  129. exobrain/database/model/org/org_base.py +31 -0
  130. exobrain/database/model/org/organization_settings.py +55 -0
  131. exobrain/database/model/org/running_models.py +778 -0
  132. exobrain/database/model/org/scope.py +51 -0
  133. exobrain/database/model/org/user.py +53 -0
  134. exobrain/database/py.typed +0 -0
  135. exobrain/database/tools/__init__.py +0 -0
  136. exobrain/database/tools/jsonb_filter.py +369 -0
  137. exobrain/database/tools/record_count.py +60 -0
  138. exobrain/database/tools/sql_query_logger.py +83 -0
  139. exobrain_database-0.1.0.dist-info/METADATA +166 -0
  140. exobrain_database-0.1.0.dist-info/RECORD +141 -0
  141. exobrain_database-0.1.0.dist-info/WHEEL +4 -0
File without changes
@@ -0,0 +1 @@
1
+ Generic single-database configuration.
@@ -0,0 +1,9 @@
1
+ """Database migration utilities for multi-tenant organization schemas."""
2
+
3
+ from exobrain.database.migrations.migration_manager import (
4
+ configure,
5
+ downgrade,
6
+ upgrade,
7
+ )
8
+
9
+ __all__ = ["configure", "upgrade", "downgrade"]
@@ -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