mainsequence 4.2.4__tar.gz → 4.2.16__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (128) hide show
  1. {mainsequence-4.2.4/mainsequence.egg-info → mainsequence-4.2.16}/PKG-INFO +1 -1
  2. mainsequence-4.2.16/mainsequence/cli/migrations.py +641 -0
  3. {mainsequence-4.2.4 → mainsequence-4.2.16}/mainsequence/client/metatables/core.py +473 -41
  4. {mainsequence-4.2.4 → mainsequence-4.2.16}/mainsequence/meta_tables/migrations.py +292 -45
  5. {mainsequence-4.2.4 → mainsequence-4.2.16}/mainsequence/meta_tables/sqlalchemy_contracts.py +40 -28
  6. {mainsequence-4.2.4 → mainsequence-4.2.16/mainsequence.egg-info}/PKG-INFO +1 -1
  7. {mainsequence-4.2.4 → mainsequence-4.2.16}/pyproject.toml +1 -1
  8. mainsequence-4.2.16/tests/test_cli_migrations.py +432 -0
  9. {mainsequence-4.2.4 → mainsequence-4.2.16}/tests/test_meta_table_migrations.py +392 -44
  10. {mainsequence-4.2.4 → mainsequence-4.2.16}/tests/test_meta_tables_client_models.py +255 -6
  11. mainsequence-4.2.4/mainsequence/cli/migrations.py +0 -282
  12. mainsequence-4.2.4/tests/test_cli_migrations.py +0 -162
  13. {mainsequence-4.2.4 → mainsequence-4.2.16}/LICENSE +0 -0
  14. {mainsequence-4.2.4 → mainsequence-4.2.16}/README.md +0 -0
  15. {mainsequence-4.2.4 → mainsequence-4.2.16}/agent_scaffold/AGENTS.md +0 -0
  16. {mainsequence-4.2.4 → mainsequence-4.2.16}/agent_scaffold/skills/a2a_communication/SKILL.md +0 -0
  17. {mainsequence-4.2.4 → mainsequence-4.2.16}/agent_scaffold/skills/application_surfaces/api_surfaces/SKILL.md +0 -0
  18. {mainsequence-4.2.4 → mainsequence-4.2.16}/agent_scaffold/skills/command_center/adapter_from_api/SKILL.md +0 -0
  19. {mainsequence-4.2.4 → mainsequence-4.2.16}/agent_scaffold/skills/command_center/api_mock_prototyping/SKILL.md +0 -0
  20. {mainsequence-4.2.4 → mainsequence-4.2.16}/agent_scaffold/skills/command_center/app_components/SKILL.md +0 -0
  21. {mainsequence-4.2.4 → mainsequence-4.2.16}/agent_scaffold/skills/command_center/connections/SKILL.md +0 -0
  22. {mainsequence-4.2.4 → mainsequence-4.2.16}/agent_scaffold/skills/command_center/workspace_analysis/SKILL.md +0 -0
  23. {mainsequence-4.2.4 → mainsequence-4.2.16}/agent_scaffold/skills/command_center/workspace_builder/SKILL.md +0 -0
  24. {mainsequence-4.2.4 → mainsequence-4.2.16}/agent_scaffold/skills/command_center/workspace_design/SKILL.md +0 -0
  25. {mainsequence-4.2.4 → mainsequence-4.2.16}/agent_scaffold/skills/dashboards/streamlit/SKILL.md +0 -0
  26. {mainsequence-4.2.4 → mainsequence-4.2.16}/agent_scaffold/skills/data_access/exploration/SKILL.md +0 -0
  27. {mainsequence-4.2.4 → mainsequence-4.2.16}/agent_scaffold/skills/data_publishing/data_nodes/SKILL.md +0 -0
  28. {mainsequence-4.2.4 → mainsequence-4.2.16}/agent_scaffold/skills/data_publishing/meta_tables/SKILL.md +0 -0
  29. {mainsequence-4.2.4 → mainsequence-4.2.16}/agent_scaffold/skills/maintenance/bug_auditor/SKILL.md +0 -0
  30. {mainsequence-4.2.4 → mainsequence-4.2.16}/agent_scaffold/skills/ms-markets/SKILL.md +0 -0
  31. {mainsequence-4.2.4 → mainsequence-4.2.16}/agent_scaffold/skills/platform_operations/access_control_and_sharing/SKILL.md +0 -0
  32. {mainsequence-4.2.4 → mainsequence-4.2.16}/agent_scaffold/skills/platform_operations/orchestration_and_releases/SKILL.md +0 -0
  33. {mainsequence-4.2.4 → mainsequence-4.2.16}/agent_scaffold/skills/project_builder/SKILL.md +0 -0
  34. {mainsequence-4.2.4 → mainsequence-4.2.16}/agent_scaffold/skills/project_to_agent/SKILL.md +0 -0
  35. {mainsequence-4.2.4 → mainsequence-4.2.16}/mainsequence/__init__.py +0 -0
  36. {mainsequence-4.2.4 → mainsequence-4.2.16}/mainsequence/__main__.py +0 -0
  37. {mainsequence-4.2.4 → mainsequence-4.2.16}/mainsequence/bootstrap.py +0 -0
  38. {mainsequence-4.2.4 → mainsequence-4.2.16}/mainsequence/cli/__init__.py +0 -0
  39. {mainsequence-4.2.4 → mainsequence-4.2.16}/mainsequence/cli/api.py +0 -0
  40. {mainsequence-4.2.4 → mainsequence-4.2.16}/mainsequence/cli/browser_auth.py +0 -0
  41. {mainsequence-4.2.4 → mainsequence-4.2.16}/mainsequence/cli/cli.py +0 -0
  42. {mainsequence-4.2.4 → mainsequence-4.2.16}/mainsequence/cli/config.py +0 -0
  43. {mainsequence-4.2.4 → mainsequence-4.2.16}/mainsequence/cli/docker_utils.py +0 -0
  44. {mainsequence-4.2.4 → mainsequence-4.2.16}/mainsequence/cli/doctor.py +0 -0
  45. {mainsequence-4.2.4 → mainsequence-4.2.16}/mainsequence/cli/local_ops.py +0 -0
  46. {mainsequence-4.2.4 → mainsequence-4.2.16}/mainsequence/cli/model_filters.py +0 -0
  47. {mainsequence-4.2.4 → mainsequence-4.2.16}/mainsequence/cli/project_status.py +0 -0
  48. {mainsequence-4.2.4 → mainsequence-4.2.16}/mainsequence/cli/pydantic_cli.py +0 -0
  49. {mainsequence-4.2.4 → mainsequence-4.2.16}/mainsequence/cli/sdk_utils.py +0 -0
  50. {mainsequence-4.2.4 → mainsequence-4.2.16}/mainsequence/cli/ssh_utils.py +0 -0
  51. {mainsequence-4.2.4 → mainsequence-4.2.16}/mainsequence/cli/ui.py +0 -0
  52. {mainsequence-4.2.4 → mainsequence-4.2.16}/mainsequence/client/__init__.py +0 -0
  53. {mainsequence-4.2.4 → mainsequence-4.2.16}/mainsequence/client/agent_runtime_models.py +0 -0
  54. {mainsequence-4.2.4 → mainsequence-4.2.16}/mainsequence/client/base.py +0 -0
  55. {mainsequence-4.2.4 → mainsequence-4.2.16}/mainsequence/client/client.py +0 -0
  56. {mainsequence-4.2.4 → mainsequence-4.2.16}/mainsequence/client/command_center/__init__.py +0 -0
  57. {mainsequence-4.2.4 → mainsequence-4.2.16}/mainsequence/client/command_center/app_component.py +0 -0
  58. {mainsequence-4.2.4 → mainsequence-4.2.16}/mainsequence/client/command_center/connections.py +0 -0
  59. {mainsequence-4.2.4 → mainsequence-4.2.16}/mainsequence/client/command_center/data_models.py +0 -0
  60. {mainsequence-4.2.4 → mainsequence-4.2.16}/mainsequence/client/command_center/workspace.py +0 -0
  61. {mainsequence-4.2.4 → mainsequence-4.2.16}/mainsequence/client/command_center/workspace_snapshot.py +0 -0
  62. {mainsequence-4.2.4 → mainsequence-4.2.16}/mainsequence/client/compute_validation.py +0 -0
  63. {mainsequence-4.2.4 → mainsequence-4.2.16}/mainsequence/client/data_sources_interfaces/__init__.py +0 -0
  64. {mainsequence-4.2.4 → mainsequence-4.2.16}/mainsequence/client/data_sources_interfaces/duckdb.py +0 -0
  65. {mainsequence-4.2.4 → mainsequence-4.2.16}/mainsequence/client/data_sources_interfaces/local_paths.py +0 -0
  66. {mainsequence-4.2.4 → mainsequence-4.2.16}/mainsequence/client/data_sources_interfaces/sqlite.py +0 -0
  67. {mainsequence-4.2.4 → mainsequence-4.2.16}/mainsequence/client/dtype_codec.py +0 -0
  68. {mainsequence-4.2.4 → mainsequence-4.2.16}/mainsequence/client/exceptions.py +0 -0
  69. {mainsequence-4.2.4 → mainsequence-4.2.16}/mainsequence/client/fastapi/__init__.py +0 -0
  70. {mainsequence-4.2.4 → mainsequence-4.2.16}/mainsequence/client/fastapi/auth.py +0 -0
  71. {mainsequence-4.2.4 → mainsequence-4.2.16}/mainsequence/client/metatables/__init__.py +0 -0
  72. {mainsequence-4.2.4 → mainsequence-4.2.16}/mainsequence/client/models_foundry.py +0 -0
  73. {mainsequence-4.2.4 → mainsequence-4.2.16}/mainsequence/client/models_helpers.py +0 -0
  74. {mainsequence-4.2.4 → mainsequence-4.2.16}/mainsequence/client/models_user.py +0 -0
  75. {mainsequence-4.2.4 → mainsequence-4.2.16}/mainsequence/client/utils.py +0 -0
  76. {mainsequence-4.2.4 → mainsequence-4.2.16}/mainsequence/defaults.py +0 -0
  77. {mainsequence-4.2.4 → mainsequence-4.2.16}/mainsequence/instrumentation/__init__.py +0 -0
  78. {mainsequence-4.2.4 → mainsequence-4.2.16}/mainsequence/instrumentation/utils.py +0 -0
  79. {mainsequence-4.2.4 → mainsequence-4.2.16}/mainsequence/logconf.py +0 -0
  80. {mainsequence-4.2.4 → mainsequence-4.2.16}/mainsequence/meta_tables/__init__.py +0 -0
  81. {mainsequence-4.2.4 → mainsequence-4.2.16}/mainsequence/meta_tables/__main__.py +0 -0
  82. {mainsequence-4.2.4 → mainsequence-4.2.16}/mainsequence/meta_tables/compiled_sql/__init__.py +0 -0
  83. {mainsequence-4.2.4 → mainsequence-4.2.16}/mainsequence/meta_tables/compiled_sql/v1.py +0 -0
  84. {mainsequence-4.2.4 → mainsequence-4.2.16}/mainsequence/meta_tables/data_nodes/__init__.py +0 -0
  85. {mainsequence-4.2.4 → mainsequence-4.2.16}/mainsequence/meta_tables/data_nodes/build_operations.py +0 -0
  86. {mainsequence-4.2.4 → mainsequence-4.2.16}/mainsequence/meta_tables/data_nodes/data_nodes.py +0 -0
  87. {mainsequence-4.2.4 → mainsequence-4.2.16}/mainsequence/meta_tables/data_nodes/models.py +0 -0
  88. {mainsequence-4.2.4 → mainsequence-4.2.16}/mainsequence/meta_tables/data_nodes/namespacing.py +0 -0
  89. {mainsequence-4.2.4 → mainsequence-4.2.16}/mainsequence/meta_tables/data_nodes/persist_managers.py +0 -0
  90. {mainsequence-4.2.4 → mainsequence-4.2.16}/mainsequence/meta_tables/data_nodes/run_operations.py +0 -0
  91. {mainsequence-4.2.4 → mainsequence-4.2.16}/mainsequence/meta_tables/data_nodes/utils.py +0 -0
  92. {mainsequence-4.2.4 → mainsequence-4.2.16}/mainsequence/meta_tables/future_registry.py +0 -0
  93. {mainsequence-4.2.4 → mainsequence-4.2.16}/mainsequence/meta_tables/hashing.py +0 -0
  94. {mainsequence-4.2.4 → mainsequence-4.2.16}/mainsequence/meta_tables/pydantic_metadata.py +0 -0
  95. {mainsequence-4.2.4 → mainsequence-4.2.16}/mainsequence/runtime_flags.py +0 -0
  96. {mainsequence-4.2.4 → mainsequence-4.2.16}/mainsequence.egg-info/SOURCES.txt +0 -0
  97. {mainsequence-4.2.4 → mainsequence-4.2.16}/mainsequence.egg-info/dependency_links.txt +0 -0
  98. {mainsequence-4.2.4 → mainsequence-4.2.16}/mainsequence.egg-info/entry_points.txt +0 -0
  99. {mainsequence-4.2.4 → mainsequence-4.2.16}/mainsequence.egg-info/requires.txt +0 -0
  100. {mainsequence-4.2.4 → mainsequence-4.2.16}/mainsequence.egg-info/top_level.txt +0 -0
  101. {mainsequence-4.2.4 → mainsequence-4.2.16}/setup.cfg +0 -0
  102. {mainsequence-4.2.4 → mainsequence-4.2.16}/tests/test_auth_precedence.py +0 -0
  103. {mainsequence-4.2.4 → mainsequence-4.2.16}/tests/test_build_operations_hashing.py +0 -0
  104. {mainsequence-4.2.4 → mainsequence-4.2.16}/tests/test_cli.py +0 -0
  105. {mainsequence-4.2.4 → mainsequence-4.2.16}/tests/test_cli_browser_auth.py +0 -0
  106. {mainsequence-4.2.4 → mainsequence-4.2.16}/tests/test_client.py +0 -0
  107. {mainsequence-4.2.4 → mainsequence-4.2.16}/tests/test_command_center_app_component_models.py +0 -0
  108. {mainsequence-4.2.4 → mainsequence-4.2.16}/tests/test_command_center_data_models.py +0 -0
  109. {mainsequence-4.2.4 → mainsequence-4.2.16}/tests/test_command_center_models.py +0 -0
  110. {mainsequence-4.2.4 → mainsequence-4.2.16}/tests/test_data_access_mixin_dimension_audit.py +0 -0
  111. {mainsequence-4.2.4 → mainsequence-4.2.16}/tests/test_data_node_storage_dimension_queries.py +0 -0
  112. {mainsequence-4.2.4 → mainsequence-4.2.16}/tests/test_data_node_update_flow.py +0 -0
  113. {mainsequence-4.2.4 → mainsequence-4.2.16}/tests/test_dependency_extras.py +0 -0
  114. {mainsequence-4.2.4 → mainsequence-4.2.16}/tests/test_duckdb_interface_dimensions.py +0 -0
  115. {mainsequence-4.2.4 → mainsequence-4.2.16}/tests/test_filter_normalization.py +0 -0
  116. {mainsequence-4.2.4 → mainsequence-4.2.16}/tests/test_logconf.py +0 -0
  117. {mainsequence-4.2.4 → mainsequence-4.2.16}/tests/test_meta_tables_sqlalchemy_contracts.py +0 -0
  118. {mainsequence-4.2.4 → mainsequence-4.2.16}/tests/test_models_user_request_bound_auth.py +0 -0
  119. {mainsequence-4.2.4 → mainsequence-4.2.16}/tests/test_pod_project_resolution.py +0 -0
  120. {mainsequence-4.2.4 → mainsequence-4.2.16}/tests/test_project_batch_jobs_from_file.py +0 -0
  121. {mainsequence-4.2.4 → mainsequence-4.2.16}/tests/test_run_configuration.py +0 -0
  122. {mainsequence-4.2.4 → mainsequence-4.2.16}/tests/test_secret_client_model.py +0 -0
  123. {mainsequence-4.2.4 → mainsequence-4.2.16}/tests/test_source_table_configuration.py +0 -0
  124. {mainsequence-4.2.4 → mainsequence-4.2.16}/tests/test_sqlite_interface_dimensions.py +0 -0
  125. {mainsequence-4.2.4 → mainsequence-4.2.16}/tests/test_update_runner_uid_runtime.py +0 -0
  126. {mainsequence-4.2.4 → mainsequence-4.2.16}/tests/test_update_statistics.py +0 -0
  127. {mainsequence-4.2.4 → mainsequence-4.2.16}/tests/test_update_uid_guards.py +0 -0
  128. {mainsequence-4.2.4 → mainsequence-4.2.16}/tests/test_workspace_snapshot.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: mainsequence
3
- Version: 4.2.4
3
+ Version: 4.2.16
4
4
  Summary: Main Sequence SDK
5
5
  Author-email: Main Sequence GmbH <dev@main-sequence.io>
6
6
  License: MainSequence GmbH SDK License Agreement
@@ -0,0 +1,641 @@
1
+ from __future__ import annotations
2
+
3
+ import dataclasses
4
+ import json
5
+ import logging
6
+ import re
7
+ import sys
8
+ from collections.abc import Mapping, Sequence
9
+ from contextlib import contextmanager
10
+ from typing import Any
11
+
12
+ import click
13
+ import typer
14
+
15
+ from mainsequence.client.metatables import (
16
+ DynamicTableDataSource,
17
+ DynamicTableDataSourceMigrationConnectionRequest,
18
+ )
19
+ from mainsequence.meta_tables.migrations import (
20
+ AlembicMetaTableMigration,
21
+ alembic_config_for_provider,
22
+ load_alembic_metatable_migration_provider,
23
+ )
24
+
25
+ migrations = typer.Typer(help="Alembic-owned MetaTable migration commands")
26
+ REGISTER_ENDPOINT = "/orm/api/ts_manager/meta_table/register/"
27
+ RESERVE_MANAGED_ENDPOINT = "/orm/api/ts_manager/meta_table/reserve-managed/"
28
+ FINALIZE_MANAGED_ENDPOINT = "/orm/api/ts_manager/meta_table/finalize-managed/"
29
+ ALEMBIC_PROVIDER_RESET_ENDPOINT = "/orm/api/ts_manager/meta_table/alembic-provider-reset/"
30
+
31
+
32
+ class _AlembicOutput:
33
+ def __init__(self) -> None:
34
+ self._chunks: list[str] = []
35
+
36
+ def write(self, data: str) -> int:
37
+ text = str(data)
38
+ self._chunks.append(text)
39
+ sys.stderr.write(text)
40
+ sys.stderr.flush()
41
+ return len(text)
42
+
43
+ def flush(self) -> None:
44
+ sys.stderr.flush()
45
+
46
+ @property
47
+ def has_visible_output(self) -> bool:
48
+ return any(chunk.strip() for chunk in self._chunks)
49
+
50
+
51
+ class _AlembicLogHandler(logging.Handler):
52
+ def emit(self, record: logging.LogRecord) -> None:
53
+ try:
54
+ message = self.format(record)
55
+ except Exception:
56
+ self.handleError(record)
57
+ return
58
+ print(message, file=sys.stderr, flush=True)
59
+
60
+
61
+ @contextmanager
62
+ def _forward_alembic_logging():
63
+ logger_names = {"alembic"}
64
+ logger_names.update(
65
+ name
66
+ for name in logging.Logger.manager.loggerDict
67
+ if name == "alembic" or name.startswith("alembic.")
68
+ )
69
+ loggers = [logging.getLogger(name) for name in logger_names]
70
+ previous_state = [(logger, logger.level, logger.propagate) for logger in loggers]
71
+ handler = _AlembicLogHandler()
72
+ handler.setLevel(logging.DEBUG)
73
+ handler.setFormatter(
74
+ logging.Formatter("[alembic] %(levelname)s %(name)s: %(message)s")
75
+ )
76
+ root_logger = logging.getLogger("alembic")
77
+ root_logger.addHandler(handler)
78
+ for logger in loggers:
79
+ logger.setLevel(logging.DEBUG)
80
+ logger.propagate = True
81
+ root_logger.propagate = False
82
+ try:
83
+ yield
84
+ finally:
85
+ root_logger.removeHandler(handler)
86
+ for logger, level, propagate in previous_state:
87
+ logger.setLevel(level)
88
+ logger.propagate = propagate
89
+
90
+
91
+ def _emit_status(message: str) -> None:
92
+ print(f"[mainsequence migrations] {message}", file=sys.stderr, flush=True)
93
+
94
+
95
+ def _emit_progress(message: str) -> None:
96
+ print(message, file=sys.stderr, flush=True)
97
+
98
+
99
+ def _load_migration(provider: str | None) -> AlembicMetaTableMigration:
100
+ provider_label = provider or "<default>"
101
+ _emit_status(f"Loading migration provider {provider_label}...")
102
+ try:
103
+ migration = load_alembic_metatable_migration_provider(provider)
104
+ except Exception as exc:
105
+ raise typer.BadParameter(str(exc), param_hint="--provider") from exc
106
+ _emit_status(
107
+ "Loaded migration provider "
108
+ f"package={migration.package} migration_namespace={migration.migration_namespace}"
109
+ )
110
+ return migration
111
+
112
+
113
+ def _load_alembic_command(command_name: str) -> Any:
114
+ _emit_status(f"Importing Alembic command module for {command_name}...")
115
+ try:
116
+ from alembic import command
117
+ except ImportError as exc:
118
+ raise typer.BadParameter("Alembic is required for migration commands.") from exc
119
+ _emit_status(f"Imported Alembic command module for {command_name}.")
120
+ return command
121
+
122
+
123
+ def _jsonable(value: Any) -> Any:
124
+ if hasattr(value, "model_dump"):
125
+ return value.model_dump(mode="json")
126
+ if dataclasses.is_dataclass(value):
127
+ return dataclasses.asdict(value)
128
+ if isinstance(value, dict):
129
+ return {str(key): _jsonable(item) for key, item in value.items()}
130
+ if isinstance(value, (list, tuple, set)):
131
+ return [_jsonable(item) for item in value]
132
+ return value
133
+
134
+
135
+ def _emit(payload: Any, *, json_output: bool = False) -> None:
136
+ if json_output or _json_output_enabled():
137
+ typer.echo(json.dumps(_jsonable(payload), indent=2, ensure_ascii=False))
138
+ return
139
+ if isinstance(payload, str):
140
+ typer.echo(payload)
141
+ return
142
+ for key, value in _jsonable(payload).items():
143
+ typer.echo(f"{key}: {value}")
144
+
145
+
146
+ def _json_output_enabled() -> bool:
147
+ ctx = click.get_current_context(silent=True)
148
+ if ctx is None:
149
+ return False
150
+ root = ctx.find_root()
151
+ obj = getattr(root, "obj", None) or {}
152
+ return bool(obj.get("json_output"))
153
+
154
+
155
+ def _item_value(item: Any, key: str) -> Any:
156
+ if isinstance(item, Mapping):
157
+ return item.get(key)
158
+ return getattr(item, key, None)
159
+
160
+
161
+ def _contract_physical_table_name(item: Any) -> Any:
162
+ contract = _item_value(item, "table_contract")
163
+ if contract is None:
164
+ return None
165
+ physical = contract.get("physical") if isinstance(contract, Mapping) else getattr(contract, "physical", None)
166
+ if physical is None:
167
+ return None
168
+ if isinstance(physical, Mapping):
169
+ return physical.get("table_name")
170
+ return getattr(physical, "table_name", None)
171
+
172
+
173
+ def _meta_table_uid(item: Any) -> str | None:
174
+ if item is None:
175
+ return None
176
+ if isinstance(item, Mapping):
177
+ uid = item.get("meta_table_uid") or item.get("uid")
178
+ else:
179
+ uid = getattr(item, "meta_table_uid", None) or getattr(item, "uid", None)
180
+ if uid in (None, ""):
181
+ return None
182
+ return str(uid)
183
+
184
+
185
+ def _include_alembic_registry_in_scope(
186
+ migration: AlembicMetaTableMigration,
187
+ prepared: Any,
188
+ registry_meta_table: Any,
189
+ ) -> None:
190
+ registry_meta_table = registry_meta_table or migration.alembic_registry.get_meta_table()
191
+ registry_uid = _meta_table_uid(registry_meta_table)
192
+ if registry_uid in (None, ""):
193
+ return
194
+ prepared.meta_table_uids = list(
195
+ dict.fromkeys([registry_uid, *list(prepared.meta_table_uids)])
196
+ )
197
+
198
+
199
+ def _metatable_message(
200
+ *,
201
+ endpoint: str,
202
+ action: str,
203
+ model: type[Any],
204
+ item: Any,
205
+ ) -> str:
206
+ model_name = getattr(model, "__name__", repr(model))
207
+ identifier = (
208
+ _item_value(item, "identifier")
209
+ or getattr(model, "__metatable_identifier__", None)
210
+ or model_name
211
+ )
212
+ uid = _item_value(item, "meta_table_uid") or _item_value(item, "uid")
213
+ physical_table_name = (
214
+ _item_value(item, "physical_table_name")
215
+ or _contract_physical_table_name(item)
216
+ )
217
+ provisioning_status = _item_value(item, "provisioning_status")
218
+ created = _item_value(item, "created")
219
+ matched_by = _item_value(item, "matched_by")
220
+
221
+ parts = [
222
+ f"POST {endpoint}",
223
+ f"{action} MetaTable identifier={identifier}",
224
+ ]
225
+ if model_name != identifier:
226
+ parts.append(f"model={model_name}")
227
+ if uid not in (None, ""):
228
+ parts.append(f"uid={uid}")
229
+ if physical_table_name not in (None, ""):
230
+ parts.append(f"physical_table={physical_table_name}")
231
+ if provisioning_status not in (None, ""):
232
+ parts.append(f"provisioning_status={provisioning_status}")
233
+ if created is not None:
234
+ parts.append(f"created={created}")
235
+ if matched_by not in (None, ""):
236
+ parts.append(f"matched_by={matched_by}")
237
+ return " ".join(parts)
238
+
239
+
240
+ def _emit_metatable_registration(model: type[Any], item: Any) -> None:
241
+ _emit_progress(
242
+ _metatable_message(
243
+ endpoint=REGISTER_ENDPOINT,
244
+ action="registered",
245
+ model=model,
246
+ item=item,
247
+ ),
248
+ )
249
+
250
+
251
+ def _emit_metatable_reservation_request(
252
+ models: Sequence[type[Any]],
253
+ tables: Sequence[Any],
254
+ ) -> None:
255
+ identifiers = []
256
+ for model, table in zip(models, tables, strict=True):
257
+ identifiers.append(
258
+ str(
259
+ _item_value(table, "identifier")
260
+ or getattr(model, "__metatable_identifier__", None)
261
+ or getattr(model, "__name__", repr(model))
262
+ )
263
+ )
264
+ _emit_status(
265
+ f"Sending POST {RESERVE_MANAGED_ENDPOINT} request for {len(tables)} "
266
+ f"MetaTables identifiers={','.join(identifiers)}"
267
+ )
268
+
269
+
270
+ def _emit_metatable_reservation(model: type[Any], item: Any) -> None:
271
+ _emit_progress(
272
+ _metatable_message(
273
+ endpoint=RESERVE_MANAGED_ENDPOINT,
274
+ action="reserved",
275
+ model=model,
276
+ item=item,
277
+ ),
278
+ )
279
+
280
+
281
+ def _emit_metatable_finalization(model: type[Any], item: Any) -> None:
282
+ _emit_progress(
283
+ _metatable_message(
284
+ endpoint=FINALIZE_MANAGED_ENDPOINT,
285
+ action="finalized",
286
+ model=model,
287
+ item=item,
288
+ ),
289
+ )
290
+
291
+
292
+ def _prepare_alembic_config(
293
+ migration: AlembicMetaTableMigration,
294
+ *,
295
+ timeout: float | None,
296
+ ttl_seconds: int,
297
+ alembic_output: _AlembicOutput,
298
+ ) -> tuple[Any, Any]:
299
+ _emit_status("Ensuring Alembic registry MetaTable...")
300
+ registry_meta_table = migration.ensure_alembic_registry(
301
+ timeout=timeout,
302
+ on_metatable_registered=_emit_metatable_registration,
303
+ )
304
+
305
+ _emit_status("Preparing platform-managed MetaTable reservations...")
306
+ prepared = migration.prepare_for_alembic(
307
+ timeout=timeout,
308
+ on_metatable_reservation_request=_emit_metatable_reservation_request,
309
+ on_metatable_reservation_status=_emit_status,
310
+ on_metatable_reserved=_emit_metatable_reservation,
311
+ )
312
+ _include_alembic_registry_in_scope(migration, prepared, registry_meta_table)
313
+ _emit_status(
314
+ "Prepared migration scope "
315
+ f"data_source_uid={prepared.data_source_uid} "
316
+ f"meta_table_count={len(prepared.meta_table_uids)}"
317
+ )
318
+ _emit_status(f"Loading DynamicTableDataSource uid={prepared.data_source_uid}...")
319
+ data_source = DynamicTableDataSource.get_by_uid(prepared.data_source_uid)
320
+ _emit_status(
321
+ "Requesting scoped migration connection "
322
+ f"meta_table_count={len(prepared.meta_table_uids)} ttl_seconds={ttl_seconds}..."
323
+ )
324
+ connection = data_source.issue_migration_connection(
325
+ DynamicTableDataSourceMigrationConnectionRequest(
326
+ package=migration.package,
327
+ migration_namespace=migration.migration_namespace,
328
+ meta_table_uids=prepared.meta_table_uids,
329
+ ttl_seconds=ttl_seconds,
330
+ ),
331
+ timeout=timeout,
332
+ )
333
+ _emit_status("Scoped migration connection acquired.")
334
+ _emit_status("Building Alembic config...")
335
+ config = alembic_config_for_provider(
336
+ migration,
337
+ sqlalchemy_url=connection.uri,
338
+ owner_role_name=connection.owner_role_name or prepared.owner_role_name,
339
+ stdout=alembic_output,
340
+ output_buffer=alembic_output,
341
+ )
342
+ _emit_status("Alembic config built.")
343
+ return prepared, config
344
+
345
+
346
+ def _next_sequential_revision_id(
347
+ migration: AlembicMetaTableMigration,
348
+ *,
349
+ alembic_output: _AlembicOutput,
350
+ ) -> str:
351
+ _emit_status("Importing Alembic ScriptDirectory for revision id scan...")
352
+ try:
353
+ from alembic.script import ScriptDirectory
354
+ except ImportError as exc:
355
+ raise RuntimeError("Alembic is required for revision generation.") from exc
356
+ _emit_status("Imported Alembic ScriptDirectory.")
357
+
358
+ _emit_status("Scanning Alembic revision directory for next sequential id...")
359
+ with _forward_alembic_logging():
360
+ script = ScriptDirectory.from_config(
361
+ alembic_config_for_provider(
362
+ migration,
363
+ sqlalchemy_url="postgresql://",
364
+ stdout=alembic_output,
365
+ output_buffer=alembic_output,
366
+ )
367
+ )
368
+ heads = list(script.get_heads())
369
+ if len(heads) > 1:
370
+ raise typer.BadParameter(
371
+ "Sequential revision IDs require a single Alembic head. Pass --rev-id "
372
+ "explicitly for branched histories.",
373
+ param_hint="--rev-id",
374
+ )
375
+ if heads and not re.fullmatch(r"\d{4,}", str(heads[0])):
376
+ raise typer.BadParameter(
377
+ "Sequential revision IDs require the current Alembic head to be numeric. "
378
+ "Pass --rev-id explicitly for non-numeric histories.",
379
+ param_hint="--rev-id",
380
+ )
381
+
382
+ numeric_revisions: list[int] = []
383
+ with _forward_alembic_logging():
384
+ for revision in script.walk_revisions():
385
+ revision_id = str(revision.revision)
386
+ if re.fullmatch(r"\d{4,}", revision_id):
387
+ numeric_revisions.append(int(revision_id))
388
+ next_revision_id = f"{max(numeric_revisions, default=0) + 1:04d}"
389
+ _emit_status(f"Next Alembic revision id is {next_revision_id}.")
390
+ return next_revision_id
391
+
392
+
393
+ @migrations.command("current")
394
+ def current(
395
+ provider: str | None = typer.Option(
396
+ None,
397
+ "--provider",
398
+ help="Migration provider reference, for example msm.migrations:migration.",
399
+ ),
400
+ verbose: bool = typer.Option(False, "--verbose", "-v"),
401
+ timeout: float | None = typer.Option(None, "--timeout"),
402
+ ttl_seconds: int = typer.Option(900, "--ttl-seconds", min=1),
403
+ ) -> None:
404
+ """Read current Alembic revision through a scoped migration credential."""
405
+
406
+ command = _load_alembic_command("current")
407
+ migration = _load_migration(provider)
408
+ alembic_output = _AlembicOutput()
409
+ _, config = _prepare_alembic_config(
410
+ migration,
411
+ timeout=timeout,
412
+ ttl_seconds=ttl_seconds,
413
+ alembic_output=alembic_output,
414
+ )
415
+ _emit_status("Starting Alembic current now...")
416
+ with _forward_alembic_logging():
417
+ command.current(config, verbose=verbose)
418
+ if not alembic_output.has_visible_output:
419
+ _emit_status(
420
+ "Alembic current produced no revision output. The version table is "
421
+ "empty or Alembic found no current revision for this migration scope."
422
+ )
423
+ _emit_status("Alembic current finished.")
424
+
425
+
426
+ @migrations.command("revision")
427
+ def revision(
428
+ message: str | None = typer.Option(
429
+ None,
430
+ "--message",
431
+ "-m",
432
+ help="Alembic revision message. Defaults to 'migration'.",
433
+ ),
434
+ autogenerate: bool = typer.Option(
435
+ True,
436
+ "--autogenerate/--no-autogenerate",
437
+ help="Use Alembic autogenerate against the reserved MetaTable metadata.",
438
+ ),
439
+ provider: str | None = typer.Option(
440
+ None,
441
+ "--provider",
442
+ help="Migration provider reference, for example msm.migrations:migration.",
443
+ ),
444
+ rev_id: str | None = typer.Option(None, "--rev-id", help="Explicit Alembic revision id."),
445
+ head: str = typer.Option("head", "--head", help="Alembic head to base the revision on."),
446
+ timeout: float | None = typer.Option(None, "--timeout"),
447
+ ttl_seconds: int = typer.Option(900, "--ttl-seconds", min=1),
448
+ json_output: bool = typer.Option(False, "--json", help="Emit JSON."),
449
+ ) -> None:
450
+ """Create a normal Alembic revision for the selected provider."""
451
+
452
+ command = _load_alembic_command("revision")
453
+ migration = _load_migration(provider)
454
+ resolved_message = (message or "").strip() or "migration"
455
+ alembic_output = _AlembicOutput()
456
+ resolved_rev_id = rev_id or _next_sequential_revision_id(
457
+ migration,
458
+ alembic_output=alembic_output,
459
+ )
460
+ prepared, config = _prepare_alembic_config(
461
+ migration,
462
+ timeout=timeout,
463
+ ttl_seconds=ttl_seconds,
464
+ alembic_output=alembic_output,
465
+ )
466
+ _emit_status(f"Starting Alembic revision now rev_id={resolved_rev_id}...")
467
+ with _forward_alembic_logging():
468
+ script = command.revision(
469
+ config,
470
+ message=resolved_message,
471
+ autogenerate=autogenerate,
472
+ rev_id=resolved_rev_id,
473
+ head=head,
474
+ )
475
+ _emit_status("Alembic revision finished.")
476
+ _emit(
477
+ {
478
+ "revision": getattr(script, "revision", None),
479
+ "path": getattr(script, "path", None),
480
+ "package": migration.package,
481
+ "migration_namespace": migration.migration_namespace,
482
+ "meta_table_uids": prepared.meta_table_uids,
483
+ },
484
+ json_output=json_output,
485
+ )
486
+
487
+
488
+ @migrations.command("upgrade")
489
+ def upgrade(
490
+ target_revision: str = typer.Argument("head", help="Target Alembic revision."),
491
+ provider: str | None = typer.Option(
492
+ None,
493
+ "--provider",
494
+ help="Migration provider reference, for example msm.migrations:migration.",
495
+ ),
496
+ timeout: float | None = typer.Option(None, "--timeout"),
497
+ ttl_seconds: int = typer.Option(900, "--ttl-seconds", min=1),
498
+ json_output: bool = typer.Option(False, "--json", help="Emit JSON."),
499
+ ) -> None:
500
+ """Run Alembic upgrade directly and finalize reserved MetaTables."""
501
+
502
+ command = _load_alembic_command("upgrade")
503
+ migration = _load_migration(provider)
504
+ alembic_output = _AlembicOutput()
505
+ prepared, config = _prepare_alembic_config(
506
+ migration,
507
+ timeout=timeout,
508
+ ttl_seconds=ttl_seconds,
509
+ alembic_output=alembic_output,
510
+ )
511
+ _emit_status(f"Starting Alembic upgrade now target={target_revision}...")
512
+ with _forward_alembic_logging():
513
+ command.upgrade(config, target_revision)
514
+ _emit_status("Finalizing MetaTable catalog after upgrade...")
515
+ finalize_response = migration.finalize_metatable_catalog(
516
+ prepared=prepared,
517
+ alembic_revision=target_revision,
518
+ timeout=timeout,
519
+ on_metatable_finalized=_emit_metatable_finalization,
520
+ on_metatable_finalize_status=_emit_status,
521
+ )
522
+ _emit_status("MetaTable catalog finalization finished.")
523
+ _emit(
524
+ {
525
+ "ok": True,
526
+ "revision": target_revision,
527
+ "package": migration.package,
528
+ "migration_namespace": migration.migration_namespace,
529
+ "meta_table_uids": prepared.meta_table_uids,
530
+ "finalized_count": finalize_response.finalized_count,
531
+ "active_count": finalize_response.active_count,
532
+ "reserved_count": finalize_response.reserved_count,
533
+ "failed_count": finalize_response.failed_count,
534
+ },
535
+ json_output=json_output,
536
+ )
537
+
538
+
539
+ @migrations.command("downgrade")
540
+ def downgrade(
541
+ target_revision: str = typer.Argument(..., help="Target Alembic downgrade revision."),
542
+ provider: str | None = typer.Option(
543
+ None,
544
+ "--provider",
545
+ help="Migration provider reference, for example msm.migrations:migration.",
546
+ ),
547
+ timeout: float | None = typer.Option(None, "--timeout"),
548
+ ttl_seconds: int = typer.Option(900, "--ttl-seconds", min=1),
549
+ json_output: bool = typer.Option(False, "--json", help="Emit JSON."),
550
+ ) -> None:
551
+ """Run Alembic downgrade directly and finalize reserved MetaTables."""
552
+
553
+ command = _load_alembic_command("downgrade")
554
+ migration = _load_migration(provider)
555
+ alembic_output = _AlembicOutput()
556
+ prepared, config = _prepare_alembic_config(
557
+ migration,
558
+ timeout=timeout,
559
+ ttl_seconds=ttl_seconds,
560
+ alembic_output=alembic_output,
561
+ )
562
+ _emit_status(f"Starting Alembic downgrade now target={target_revision}...")
563
+ with _forward_alembic_logging():
564
+ command.downgrade(config, target_revision)
565
+ _emit_status("Finalizing MetaTable catalog after downgrade...")
566
+ finalize_response = migration.finalize_metatable_catalog(
567
+ prepared=prepared,
568
+ alembic_revision=target_revision,
569
+ timeout=timeout,
570
+ on_metatable_finalized=_emit_metatable_finalization,
571
+ on_metatable_finalize_status=_emit_status,
572
+ )
573
+ _emit_status("MetaTable catalog finalization finished.")
574
+ _emit(
575
+ {
576
+ "ok": True,
577
+ "revision": target_revision,
578
+ "package": migration.package,
579
+ "migration_namespace": migration.migration_namespace,
580
+ "meta_table_uids": prepared.meta_table_uids,
581
+ "finalized_count": finalize_response.finalized_count,
582
+ "active_count": finalize_response.active_count,
583
+ "reserved_count": finalize_response.reserved_count,
584
+ "failed_count": finalize_response.failed_count,
585
+ },
586
+ json_output=json_output,
587
+ )
588
+
589
+
590
+ @migrations.command("reset")
591
+ def reset(
592
+ provider: str | None = typer.Option(
593
+ None,
594
+ "--provider",
595
+ help="Migration provider reference, for example msm.migrations:migration.",
596
+ ),
597
+ confirm_reset: bool = typer.Option(
598
+ False,
599
+ "--confirm-reset",
600
+ help="Required confirmation for destructive provider-scoped reset.",
601
+ ),
602
+ drop_physical_tables: bool = typer.Option(
603
+ True,
604
+ "--drop-physical-tables/--keep-physical-tables",
605
+ help="Drop provider physical tables during reset.",
606
+ ),
607
+ clear_alembic_version_table: bool = typer.Option(
608
+ True,
609
+ "--clear-alembic-version-table/--keep-alembic-version-table",
610
+ help="Clear the provider Alembic version table during reset.",
611
+ ),
612
+ include_reserved: bool = typer.Option(
613
+ True,
614
+ "--include-reserved/--active-only",
615
+ help="Include already-reserved provider MetaTables in reset results.",
616
+ ),
617
+ timeout: float | None = typer.Option(None, "--timeout"),
618
+ json_output: bool = typer.Option(False, "--json", help="Emit JSON."),
619
+ ) -> None:
620
+ """Reset an Alembic-managed provider catalog/physical state."""
621
+
622
+ if not confirm_reset:
623
+ raise typer.BadParameter(
624
+ "Pass --confirm-reset to call the destructive provider reset endpoint.",
625
+ param_hint="--confirm-reset",
626
+ )
627
+ migration = _load_migration(provider)
628
+ _emit_status(
629
+ "Calling provider reset endpoint "
630
+ f"{ALEMBIC_PROVIDER_RESET_ENDPOINT} provider={migration.migration_provider_key}..."
631
+ )
632
+ response = migration.reset_alembic_provider(
633
+ confirm_reset=True,
634
+ drop_physical_tables=drop_physical_tables,
635
+ clear_alembic_version_table=clear_alembic_version_table,
636
+ include_reserved=include_reserved,
637
+ timeout=timeout,
638
+ on_reset_status=_emit_status,
639
+ )
640
+ _emit_status("Alembic provider reset finished.")
641
+ _emit(response, json_output=json_output)