mainsequence 4.2.16__tar.gz → 4.2.25__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.
Potentially problematic release.
This version of mainsequence might be problematic. Click here for more details.
- {mainsequence-4.2.16/mainsequence.egg-info → mainsequence-4.2.25}/PKG-INFO +2 -2
- {mainsequence-4.2.16 → mainsequence-4.2.25}/agent_scaffold/skills/data_publishing/data_nodes/SKILL.md +7 -4
- {mainsequence-4.2.16 → mainsequence-4.2.25}/agent_scaffold/skills/data_publishing/meta_tables/SKILL.md +12 -5
- {mainsequence-4.2.16 → mainsequence-4.2.25}/mainsequence/cli/migrations.py +247 -22
- {mainsequence-4.2.16 → mainsequence-4.2.25}/mainsequence/client/metatables/core.py +28 -4
- {mainsequence-4.2.16 → mainsequence-4.2.25}/mainsequence/meta_tables/migrations.py +407 -216
- {mainsequence-4.2.16 → mainsequence-4.2.25}/mainsequence/meta_tables/sqlalchemy_contracts.py +27 -183
- {mainsequence-4.2.16 → mainsequence-4.2.25/mainsequence.egg-info}/PKG-INFO +2 -2
- {mainsequence-4.2.16 → mainsequence-4.2.25}/mainsequence.egg-info/requires.txt +1 -1
- {mainsequence-4.2.16 → mainsequence-4.2.25}/pyproject.toml +2 -2
- {mainsequence-4.2.16 → mainsequence-4.2.25}/tests/test_cli_migrations.py +93 -43
- {mainsequence-4.2.16 → mainsequence-4.2.25}/tests/test_meta_table_migrations.py +385 -59
- {mainsequence-4.2.16 → mainsequence-4.2.25}/tests/test_meta_tables_client_models.py +84 -0
- {mainsequence-4.2.16 → mainsequence-4.2.25}/tests/test_meta_tables_sqlalchemy_contracts.py +60 -49
- {mainsequence-4.2.16 → mainsequence-4.2.25}/LICENSE +0 -0
- {mainsequence-4.2.16 → mainsequence-4.2.25}/README.md +0 -0
- {mainsequence-4.2.16 → mainsequence-4.2.25}/agent_scaffold/AGENTS.md +0 -0
- {mainsequence-4.2.16 → mainsequence-4.2.25}/agent_scaffold/skills/a2a_communication/SKILL.md +0 -0
- {mainsequence-4.2.16 → mainsequence-4.2.25}/agent_scaffold/skills/application_surfaces/api_surfaces/SKILL.md +0 -0
- {mainsequence-4.2.16 → mainsequence-4.2.25}/agent_scaffold/skills/command_center/adapter_from_api/SKILL.md +0 -0
- {mainsequence-4.2.16 → mainsequence-4.2.25}/agent_scaffold/skills/command_center/api_mock_prototyping/SKILL.md +0 -0
- {mainsequence-4.2.16 → mainsequence-4.2.25}/agent_scaffold/skills/command_center/app_components/SKILL.md +0 -0
- {mainsequence-4.2.16 → mainsequence-4.2.25}/agent_scaffold/skills/command_center/connections/SKILL.md +0 -0
- {mainsequence-4.2.16 → mainsequence-4.2.25}/agent_scaffold/skills/command_center/workspace_analysis/SKILL.md +0 -0
- {mainsequence-4.2.16 → mainsequence-4.2.25}/agent_scaffold/skills/command_center/workspace_builder/SKILL.md +0 -0
- {mainsequence-4.2.16 → mainsequence-4.2.25}/agent_scaffold/skills/command_center/workspace_design/SKILL.md +0 -0
- {mainsequence-4.2.16 → mainsequence-4.2.25}/agent_scaffold/skills/dashboards/streamlit/SKILL.md +0 -0
- {mainsequence-4.2.16 → mainsequence-4.2.25}/agent_scaffold/skills/data_access/exploration/SKILL.md +0 -0
- {mainsequence-4.2.16 → mainsequence-4.2.25}/agent_scaffold/skills/maintenance/bug_auditor/SKILL.md +0 -0
- {mainsequence-4.2.16 → mainsequence-4.2.25}/agent_scaffold/skills/ms-markets/SKILL.md +0 -0
- {mainsequence-4.2.16 → mainsequence-4.2.25}/agent_scaffold/skills/platform_operations/access_control_and_sharing/SKILL.md +0 -0
- {mainsequence-4.2.16 → mainsequence-4.2.25}/agent_scaffold/skills/platform_operations/orchestration_and_releases/SKILL.md +0 -0
- {mainsequence-4.2.16 → mainsequence-4.2.25}/agent_scaffold/skills/project_builder/SKILL.md +0 -0
- {mainsequence-4.2.16 → mainsequence-4.2.25}/agent_scaffold/skills/project_to_agent/SKILL.md +0 -0
- {mainsequence-4.2.16 → mainsequence-4.2.25}/mainsequence/__init__.py +0 -0
- {mainsequence-4.2.16 → mainsequence-4.2.25}/mainsequence/__main__.py +0 -0
- {mainsequence-4.2.16 → mainsequence-4.2.25}/mainsequence/bootstrap.py +0 -0
- {mainsequence-4.2.16 → mainsequence-4.2.25}/mainsequence/cli/__init__.py +0 -0
- {mainsequence-4.2.16 → mainsequence-4.2.25}/mainsequence/cli/api.py +0 -0
- {mainsequence-4.2.16 → mainsequence-4.2.25}/mainsequence/cli/browser_auth.py +0 -0
- {mainsequence-4.2.16 → mainsequence-4.2.25}/mainsequence/cli/cli.py +0 -0
- {mainsequence-4.2.16 → mainsequence-4.2.25}/mainsequence/cli/config.py +0 -0
- {mainsequence-4.2.16 → mainsequence-4.2.25}/mainsequence/cli/docker_utils.py +0 -0
- {mainsequence-4.2.16 → mainsequence-4.2.25}/mainsequence/cli/doctor.py +0 -0
- {mainsequence-4.2.16 → mainsequence-4.2.25}/mainsequence/cli/local_ops.py +0 -0
- {mainsequence-4.2.16 → mainsequence-4.2.25}/mainsequence/cli/model_filters.py +0 -0
- {mainsequence-4.2.16 → mainsequence-4.2.25}/mainsequence/cli/project_status.py +0 -0
- {mainsequence-4.2.16 → mainsequence-4.2.25}/mainsequence/cli/pydantic_cli.py +0 -0
- {mainsequence-4.2.16 → mainsequence-4.2.25}/mainsequence/cli/sdk_utils.py +0 -0
- {mainsequence-4.2.16 → mainsequence-4.2.25}/mainsequence/cli/ssh_utils.py +0 -0
- {mainsequence-4.2.16 → mainsequence-4.2.25}/mainsequence/cli/ui.py +0 -0
- {mainsequence-4.2.16 → mainsequence-4.2.25}/mainsequence/client/__init__.py +0 -0
- {mainsequence-4.2.16 → mainsequence-4.2.25}/mainsequence/client/agent_runtime_models.py +0 -0
- {mainsequence-4.2.16 → mainsequence-4.2.25}/mainsequence/client/base.py +0 -0
- {mainsequence-4.2.16 → mainsequence-4.2.25}/mainsequence/client/client.py +0 -0
- {mainsequence-4.2.16 → mainsequence-4.2.25}/mainsequence/client/command_center/__init__.py +0 -0
- {mainsequence-4.2.16 → mainsequence-4.2.25}/mainsequence/client/command_center/app_component.py +0 -0
- {mainsequence-4.2.16 → mainsequence-4.2.25}/mainsequence/client/command_center/connections.py +0 -0
- {mainsequence-4.2.16 → mainsequence-4.2.25}/mainsequence/client/command_center/data_models.py +0 -0
- {mainsequence-4.2.16 → mainsequence-4.2.25}/mainsequence/client/command_center/workspace.py +0 -0
- {mainsequence-4.2.16 → mainsequence-4.2.25}/mainsequence/client/command_center/workspace_snapshot.py +0 -0
- {mainsequence-4.2.16 → mainsequence-4.2.25}/mainsequence/client/compute_validation.py +0 -0
- {mainsequence-4.2.16 → mainsequence-4.2.25}/mainsequence/client/data_sources_interfaces/__init__.py +0 -0
- {mainsequence-4.2.16 → mainsequence-4.2.25}/mainsequence/client/data_sources_interfaces/duckdb.py +0 -0
- {mainsequence-4.2.16 → mainsequence-4.2.25}/mainsequence/client/data_sources_interfaces/local_paths.py +0 -0
- {mainsequence-4.2.16 → mainsequence-4.2.25}/mainsequence/client/data_sources_interfaces/sqlite.py +0 -0
- {mainsequence-4.2.16 → mainsequence-4.2.25}/mainsequence/client/dtype_codec.py +0 -0
- {mainsequence-4.2.16 → mainsequence-4.2.25}/mainsequence/client/exceptions.py +0 -0
- {mainsequence-4.2.16 → mainsequence-4.2.25}/mainsequence/client/fastapi/__init__.py +0 -0
- {mainsequence-4.2.16 → mainsequence-4.2.25}/mainsequence/client/fastapi/auth.py +0 -0
- {mainsequence-4.2.16 → mainsequence-4.2.25}/mainsequence/client/metatables/__init__.py +0 -0
- {mainsequence-4.2.16 → mainsequence-4.2.25}/mainsequence/client/models_foundry.py +0 -0
- {mainsequence-4.2.16 → mainsequence-4.2.25}/mainsequence/client/models_helpers.py +0 -0
- {mainsequence-4.2.16 → mainsequence-4.2.25}/mainsequence/client/models_user.py +0 -0
- {mainsequence-4.2.16 → mainsequence-4.2.25}/mainsequence/client/utils.py +0 -0
- {mainsequence-4.2.16 → mainsequence-4.2.25}/mainsequence/defaults.py +0 -0
- {mainsequence-4.2.16 → mainsequence-4.2.25}/mainsequence/instrumentation/__init__.py +0 -0
- {mainsequence-4.2.16 → mainsequence-4.2.25}/mainsequence/instrumentation/utils.py +0 -0
- {mainsequence-4.2.16 → mainsequence-4.2.25}/mainsequence/logconf.py +0 -0
- {mainsequence-4.2.16 → mainsequence-4.2.25}/mainsequence/meta_tables/__init__.py +0 -0
- {mainsequence-4.2.16 → mainsequence-4.2.25}/mainsequence/meta_tables/__main__.py +0 -0
- {mainsequence-4.2.16 → mainsequence-4.2.25}/mainsequence/meta_tables/compiled_sql/__init__.py +0 -0
- {mainsequence-4.2.16 → mainsequence-4.2.25}/mainsequence/meta_tables/compiled_sql/v1.py +0 -0
- {mainsequence-4.2.16 → mainsequence-4.2.25}/mainsequence/meta_tables/data_nodes/__init__.py +0 -0
- {mainsequence-4.2.16 → mainsequence-4.2.25}/mainsequence/meta_tables/data_nodes/build_operations.py +0 -0
- {mainsequence-4.2.16 → mainsequence-4.2.25}/mainsequence/meta_tables/data_nodes/data_nodes.py +0 -0
- {mainsequence-4.2.16 → mainsequence-4.2.25}/mainsequence/meta_tables/data_nodes/models.py +0 -0
- {mainsequence-4.2.16 → mainsequence-4.2.25}/mainsequence/meta_tables/data_nodes/namespacing.py +0 -0
- {mainsequence-4.2.16 → mainsequence-4.2.25}/mainsequence/meta_tables/data_nodes/persist_managers.py +0 -0
- {mainsequence-4.2.16 → mainsequence-4.2.25}/mainsequence/meta_tables/data_nodes/run_operations.py +0 -0
- {mainsequence-4.2.16 → mainsequence-4.2.25}/mainsequence/meta_tables/data_nodes/utils.py +0 -0
- {mainsequence-4.2.16 → mainsequence-4.2.25}/mainsequence/meta_tables/future_registry.py +0 -0
- {mainsequence-4.2.16 → mainsequence-4.2.25}/mainsequence/meta_tables/hashing.py +0 -0
- {mainsequence-4.2.16 → mainsequence-4.2.25}/mainsequence/meta_tables/pydantic_metadata.py +0 -0
- {mainsequence-4.2.16 → mainsequence-4.2.25}/mainsequence/runtime_flags.py +0 -0
- {mainsequence-4.2.16 → mainsequence-4.2.25}/mainsequence.egg-info/SOURCES.txt +0 -0
- {mainsequence-4.2.16 → mainsequence-4.2.25}/mainsequence.egg-info/dependency_links.txt +0 -0
- {mainsequence-4.2.16 → mainsequence-4.2.25}/mainsequence.egg-info/entry_points.txt +0 -0
- {mainsequence-4.2.16 → mainsequence-4.2.25}/mainsequence.egg-info/top_level.txt +0 -0
- {mainsequence-4.2.16 → mainsequence-4.2.25}/setup.cfg +0 -0
- {mainsequence-4.2.16 → mainsequence-4.2.25}/tests/test_auth_precedence.py +0 -0
- {mainsequence-4.2.16 → mainsequence-4.2.25}/tests/test_build_operations_hashing.py +0 -0
- {mainsequence-4.2.16 → mainsequence-4.2.25}/tests/test_cli.py +0 -0
- {mainsequence-4.2.16 → mainsequence-4.2.25}/tests/test_cli_browser_auth.py +0 -0
- {mainsequence-4.2.16 → mainsequence-4.2.25}/tests/test_client.py +0 -0
- {mainsequence-4.2.16 → mainsequence-4.2.25}/tests/test_command_center_app_component_models.py +0 -0
- {mainsequence-4.2.16 → mainsequence-4.2.25}/tests/test_command_center_data_models.py +0 -0
- {mainsequence-4.2.16 → mainsequence-4.2.25}/tests/test_command_center_models.py +0 -0
- {mainsequence-4.2.16 → mainsequence-4.2.25}/tests/test_data_access_mixin_dimension_audit.py +0 -0
- {mainsequence-4.2.16 → mainsequence-4.2.25}/tests/test_data_node_storage_dimension_queries.py +0 -0
- {mainsequence-4.2.16 → mainsequence-4.2.25}/tests/test_data_node_update_flow.py +0 -0
- {mainsequence-4.2.16 → mainsequence-4.2.25}/tests/test_dependency_extras.py +0 -0
- {mainsequence-4.2.16 → mainsequence-4.2.25}/tests/test_duckdb_interface_dimensions.py +0 -0
- {mainsequence-4.2.16 → mainsequence-4.2.25}/tests/test_filter_normalization.py +0 -0
- {mainsequence-4.2.16 → mainsequence-4.2.25}/tests/test_logconf.py +0 -0
- {mainsequence-4.2.16 → mainsequence-4.2.25}/tests/test_models_user_request_bound_auth.py +0 -0
- {mainsequence-4.2.16 → mainsequence-4.2.25}/tests/test_pod_project_resolution.py +0 -0
- {mainsequence-4.2.16 → mainsequence-4.2.25}/tests/test_project_batch_jobs_from_file.py +0 -0
- {mainsequence-4.2.16 → mainsequence-4.2.25}/tests/test_run_configuration.py +0 -0
- {mainsequence-4.2.16 → mainsequence-4.2.25}/tests/test_secret_client_model.py +0 -0
- {mainsequence-4.2.16 → mainsequence-4.2.25}/tests/test_source_table_configuration.py +0 -0
- {mainsequence-4.2.16 → mainsequence-4.2.25}/tests/test_sqlite_interface_dimensions.py +0 -0
- {mainsequence-4.2.16 → mainsequence-4.2.25}/tests/test_update_runner_uid_runtime.py +0 -0
- {mainsequence-4.2.16 → mainsequence-4.2.25}/tests/test_update_statistics.py +0 -0
- {mainsequence-4.2.16 → mainsequence-4.2.25}/tests/test_update_uid_guards.py +0 -0
- {mainsequence-4.2.16 → mainsequence-4.2.25}/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.
|
|
3
|
+
Version: 4.2.25
|
|
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
|
|
@@ -68,7 +68,7 @@ Requires-Dist: pydantic
|
|
|
68
68
|
Requires-Dist: pytz
|
|
69
69
|
Requires-Dist: pyyaml
|
|
70
70
|
Requires-Dist: requests
|
|
71
|
-
Requires-Dist: sqlalchemy
|
|
71
|
+
Requires-Dist: sqlalchemy<3,>=2
|
|
72
72
|
Requires-Dist: structlog
|
|
73
73
|
Requires-Dist: tqdm
|
|
74
74
|
Requires-Dist: typer
|
|
@@ -148,7 +148,7 @@ class Base(DeclarativeBase):
|
|
|
148
148
|
|
|
149
149
|
class PricesTable(PlatformTimeIndexMetaData, Base):
|
|
150
150
|
__metatable_namespace__ = "<domain_namespace>"
|
|
151
|
-
__metatable_identifier__ = "<table_identifier>"
|
|
151
|
+
__metatable_identifier__ = "<project_name>.<table_identifier>"
|
|
152
152
|
__metatable_extra_hash_components__ = {"storage_name": "<stable_storage_name>"}
|
|
153
153
|
__metatable_description__ = (
|
|
154
154
|
"Daily close prices keyed by asset unique identifier for portfolio and "
|
|
@@ -387,9 +387,9 @@ DataNode storage table needs a platform-managed FK, use
|
|
|
387
387
|
`ForeignKey(Target.__table__.c.uid)`, table fullnames, or explicit target UID
|
|
388
388
|
maps in DataNode examples.
|
|
389
389
|
|
|
390
|
-
Do not ask users to name these foreign keys.
|
|
391
|
-
|
|
392
|
-
|
|
390
|
+
Do not ask users to name these foreign keys. Platform-managed
|
|
391
|
+
`MetaTableForeignKey(...)` contracts store logical relationships only. Alembic,
|
|
392
|
+
SQLAlchemy, and the database own physical FK constraint names.
|
|
393
393
|
|
|
394
394
|
Registration of the storage class follows the MetaTable migration lifecycle.
|
|
395
395
|
Migration tooling recursively resolves/registers unresolved FK target model
|
|
@@ -402,6 +402,9 @@ Do not add DataNode configuration fields just to mutate storage metadata.
|
|
|
402
402
|
|
|
403
403
|
Production-quality table identifiers, descriptions, labels, column docs, and
|
|
404
404
|
foreign-key metadata belong to the storage class/MetaTable registration path.
|
|
405
|
+
Prefix explicit table identifiers, explicit physical table names, and Alembic
|
|
406
|
+
version table names with the project or package name rather than using bare
|
|
407
|
+
names that can collide across projects.
|
|
405
408
|
|
|
406
409
|
Do not put schema or published table metadata on the DataNode configuration.
|
|
407
410
|
|
|
@@ -152,12 +152,17 @@ table. Do not use it for labels, descriptions, runtime options, test isolation,
|
|
|
152
152
|
backend UIDs, data-source UIDs, or updater scope. Use `hash_namespace` for test
|
|
153
153
|
or experiment isolation.
|
|
154
154
|
|
|
155
|
+
Prefix explicit table identifiers, explicit physical table names, and Alembic
|
|
156
|
+
version table names with the project or package name. Bare names such as
|
|
157
|
+
`Account`, `Asset`, or `alembic_version` can collide across projects sharing an
|
|
158
|
+
organization or database schema.
|
|
159
|
+
|
|
155
160
|
Register through the class API:
|
|
156
161
|
|
|
157
162
|
```python
|
|
158
163
|
class Account(PlatformManagedMetaTable, Base):
|
|
159
164
|
__metatable_namespace__ = "sdk-examples"
|
|
160
|
-
__metatable_identifier__ = "Account"
|
|
165
|
+
__metatable_identifier__ = "sdk_examples.Account"
|
|
161
166
|
__metatable_extra_hash_components__ = {"storage_name": "account"}
|
|
162
167
|
__metatable_description__ = (
|
|
163
168
|
"Customer account master records used to scope balances, holdings, and "
|
|
@@ -222,10 +227,9 @@ tooling resolves/registers unresolved target model classes, stores each
|
|
|
222
227
|
returned `MetaTable` in a local process registry keyed by `storage_hash`, and
|
|
223
228
|
uses the target `MetaTable.uid` in the child FK contract.
|
|
224
229
|
|
|
225
|
-
Do not require users to provide foreign-key names.
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
column is attached to the SQLAlchemy table.
|
|
230
|
+
Do not require users to provide foreign-key names. Platform-managed
|
|
231
|
+
`MetaTableForeignKey(...)` contracts store logical relationships only. Alembic,
|
|
232
|
+
SQLAlchemy, and the database own physical FK constraint names.
|
|
229
233
|
|
|
230
234
|
Use this pattern:
|
|
231
235
|
|
|
@@ -300,6 +304,9 @@ global identity. If it does not, the SDK derives the identifier from
|
|
|
300
304
|
`[project].name` in `pyproject.toml` plus
|
|
301
305
|
`<model.__module__>.<model.__qualname__>`. Pin an explicit identifier when a
|
|
302
306
|
class is renamed or moved but must keep the same platform identity.
|
|
307
|
+
When declaring an explicit identifier, explicit physical table name, or Alembic
|
|
308
|
+
version table name, prefix it with the project or package name rather than using
|
|
309
|
+
a bare table name.
|
|
303
310
|
|
|
304
311
|
Do not ask users to construct backend migration payloads, call low-level
|
|
305
312
|
migration request models, or use SDK helper functions directly. The backend
|
|
@@ -7,6 +7,7 @@ import re
|
|
|
7
7
|
import sys
|
|
8
8
|
from collections.abc import Mapping, Sequence
|
|
9
9
|
from contextlib import contextmanager
|
|
10
|
+
from types import SimpleNamespace
|
|
10
11
|
from typing import Any
|
|
11
12
|
|
|
12
13
|
import click
|
|
@@ -19,6 +20,7 @@ from mainsequence.client.metatables import (
|
|
|
19
20
|
from mainsequence.meta_tables.migrations import (
|
|
20
21
|
AlembicMetaTableMigration,
|
|
21
22
|
alembic_config_for_provider,
|
|
23
|
+
apply_mainsequence_migration_role,
|
|
22
24
|
load_alembic_metatable_migration_provider,
|
|
23
25
|
)
|
|
24
26
|
|
|
@@ -70,9 +72,7 @@ def _forward_alembic_logging():
|
|
|
70
72
|
previous_state = [(logger, logger.level, logger.propagate) for logger in loggers]
|
|
71
73
|
handler = _AlembicLogHandler()
|
|
72
74
|
handler.setLevel(logging.DEBUG)
|
|
73
|
-
handler.setFormatter(
|
|
74
|
-
logging.Formatter("[alembic] %(levelname)s %(name)s: %(message)s")
|
|
75
|
-
)
|
|
75
|
+
handler.setFormatter(logging.Formatter("[alembic] %(levelname)s %(name)s: %(message)s"))
|
|
76
76
|
root_logger = logging.getLogger("alembic")
|
|
77
77
|
root_logger.addHandler(handler)
|
|
78
78
|
for logger in loggers:
|
|
@@ -120,6 +120,72 @@ def _load_alembic_command(command_name: str) -> Any:
|
|
|
120
120
|
return command
|
|
121
121
|
|
|
122
122
|
|
|
123
|
+
def _emit_alembic_script_context(
|
|
124
|
+
config: Any,
|
|
125
|
+
*,
|
|
126
|
+
target_revision: str | None = None,
|
|
127
|
+
) -> None:
|
|
128
|
+
script_location = config.get_main_option("script_location")
|
|
129
|
+
version_table = config.get_main_option("version_table")
|
|
130
|
+
version_table_schema = config.get_main_option("version_table_schema")
|
|
131
|
+
version_table_label = (
|
|
132
|
+
f"{version_table_schema}.{version_table}"
|
|
133
|
+
if version_table_schema not in (None, "")
|
|
134
|
+
else version_table
|
|
135
|
+
)
|
|
136
|
+
_emit_status(
|
|
137
|
+
"Alembic script context "
|
|
138
|
+
f"script_location={script_location} version_table={version_table_label}"
|
|
139
|
+
)
|
|
140
|
+
try:
|
|
141
|
+
from alembic.script import ScriptDirectory
|
|
142
|
+
except ImportError:
|
|
143
|
+
_emit_status("Alembic ScriptDirectory is unavailable.")
|
|
144
|
+
return
|
|
145
|
+
|
|
146
|
+
try:
|
|
147
|
+
script = ScriptDirectory.from_config(config)
|
|
148
|
+
except Exception as exc:
|
|
149
|
+
_emit_status(f"Alembic script directory could not be resolved: {exc}")
|
|
150
|
+
return
|
|
151
|
+
|
|
152
|
+
_emit_status(
|
|
153
|
+
"Alembic script directory resolved "
|
|
154
|
+
f"path={getattr(script, 'dir', None)} versions={getattr(script, 'versions', None)}"
|
|
155
|
+
)
|
|
156
|
+
try:
|
|
157
|
+
heads = [str(head) for head in script.get_heads()]
|
|
158
|
+
except Exception as exc:
|
|
159
|
+
_emit_status(f"Alembic heads could not be resolved: {exc}")
|
|
160
|
+
heads = []
|
|
161
|
+
else:
|
|
162
|
+
_emit_status(f"Alembic heads={','.join(heads) if heads else '<none>'}")
|
|
163
|
+
|
|
164
|
+
if target_revision in (None, "", "base"):
|
|
165
|
+
return
|
|
166
|
+
if target_revision in ("head", "heads"):
|
|
167
|
+
for head in heads:
|
|
168
|
+
_emit_revision_path(script, head)
|
|
169
|
+
return
|
|
170
|
+
_emit_revision_path(script, target_revision)
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
def _emit_revision_path(script: Any, revision: str) -> None:
|
|
174
|
+
try:
|
|
175
|
+
resolved = script.get_revision(revision)
|
|
176
|
+
except Exception as exc:
|
|
177
|
+
_emit_status(f"Alembic revision {revision} could not be resolved: {exc}")
|
|
178
|
+
return
|
|
179
|
+
if resolved is None:
|
|
180
|
+
_emit_status(f"Alembic revision {revision} was not found.")
|
|
181
|
+
return
|
|
182
|
+
_emit_status(
|
|
183
|
+
"Alembic revision resolved "
|
|
184
|
+
f"revision={resolved.revision} path={getattr(resolved, 'path', None)} "
|
|
185
|
+
f"down_revision={resolved.down_revision}"
|
|
186
|
+
)
|
|
187
|
+
|
|
188
|
+
|
|
123
189
|
def _jsonable(value: Any) -> Any:
|
|
124
190
|
if hasattr(value, "model_dump"):
|
|
125
191
|
return value.model_dump(mode="json")
|
|
@@ -162,7 +228,11 @@ def _contract_physical_table_name(item: Any) -> Any:
|
|
|
162
228
|
contract = _item_value(item, "table_contract")
|
|
163
229
|
if contract is None:
|
|
164
230
|
return None
|
|
165
|
-
physical =
|
|
231
|
+
physical = (
|
|
232
|
+
contract.get("physical")
|
|
233
|
+
if isinstance(contract, Mapping)
|
|
234
|
+
else getattr(contract, "physical", None)
|
|
235
|
+
)
|
|
166
236
|
if physical is None:
|
|
167
237
|
return None
|
|
168
238
|
if isinstance(physical, Mapping):
|
|
@@ -170,6 +240,32 @@ def _contract_physical_table_name(item: Any) -> Any:
|
|
|
170
240
|
return getattr(physical, "table_name", None)
|
|
171
241
|
|
|
172
242
|
|
|
243
|
+
def _contract_physical_schema(item: Any) -> Any:
|
|
244
|
+
contract = _item_value(item, "table_contract")
|
|
245
|
+
if contract is None:
|
|
246
|
+
return None
|
|
247
|
+
physical = (
|
|
248
|
+
contract.get("physical")
|
|
249
|
+
if isinstance(contract, Mapping)
|
|
250
|
+
else getattr(contract, "physical", None)
|
|
251
|
+
)
|
|
252
|
+
if physical is None:
|
|
253
|
+
return None
|
|
254
|
+
if isinstance(physical, Mapping):
|
|
255
|
+
return (
|
|
256
|
+
physical.get("schema")
|
|
257
|
+
or physical.get("schema_")
|
|
258
|
+
or physical.get("table_schema")
|
|
259
|
+
or physical.get("schema_name")
|
|
260
|
+
)
|
|
261
|
+
return (
|
|
262
|
+
getattr(physical, "schema", None)
|
|
263
|
+
or getattr(physical, "schema_", None)
|
|
264
|
+
or getattr(physical, "table_schema", None)
|
|
265
|
+
or getattr(physical, "schema_name", None)
|
|
266
|
+
)
|
|
267
|
+
|
|
268
|
+
|
|
173
269
|
def _meta_table_uid(item: Any) -> str | None:
|
|
174
270
|
if item is None:
|
|
175
271
|
return None
|
|
@@ -191,9 +287,7 @@ def _include_alembic_registry_in_scope(
|
|
|
191
287
|
registry_uid = _meta_table_uid(registry_meta_table)
|
|
192
288
|
if registry_uid in (None, ""):
|
|
193
289
|
return
|
|
194
|
-
prepared.meta_table_uids = list(
|
|
195
|
-
dict.fromkeys([registry_uid, *list(prepared.meta_table_uids)])
|
|
196
|
-
)
|
|
290
|
+
prepared.meta_table_uids = list(dict.fromkeys([registry_uid, *list(prepared.meta_table_uids)]))
|
|
197
291
|
|
|
198
292
|
|
|
199
293
|
def _metatable_message(
|
|
@@ -210,13 +304,15 @@ def _metatable_message(
|
|
|
210
304
|
or model_name
|
|
211
305
|
)
|
|
212
306
|
uid = _item_value(item, "meta_table_uid") or _item_value(item, "uid")
|
|
213
|
-
physical_table_name = (
|
|
214
|
-
|
|
215
|
-
or _contract_physical_table_name(item)
|
|
307
|
+
physical_table_name = _item_value(item, "physical_table_name") or _contract_physical_table_name(
|
|
308
|
+
item
|
|
216
309
|
)
|
|
217
310
|
provisioning_status = _item_value(item, "provisioning_status")
|
|
218
311
|
created = _item_value(item, "created")
|
|
219
312
|
matched_by = _item_value(item, "matched_by")
|
|
313
|
+
finalized = _item_value(item, "finalized")
|
|
314
|
+
physical_table_exists = _item_value(item, "physical_table_exists")
|
|
315
|
+
error = _item_value(item, "error")
|
|
220
316
|
|
|
221
317
|
parts = [
|
|
222
318
|
f"POST {endpoint}",
|
|
@@ -234,6 +330,12 @@ def _metatable_message(
|
|
|
234
330
|
parts.append(f"created={created}")
|
|
235
331
|
if matched_by not in (None, ""):
|
|
236
332
|
parts.append(f"matched_by={matched_by}")
|
|
333
|
+
if finalized is not None:
|
|
334
|
+
parts.append(f"finalized={finalized}")
|
|
335
|
+
if physical_table_exists is not None:
|
|
336
|
+
parts.append(f"physical_table_exists={physical_table_exists}")
|
|
337
|
+
if error not in (None, "", {}):
|
|
338
|
+
parts.append(f"error={json.dumps(error, sort_keys=True, default=str)}")
|
|
237
339
|
return " ".join(parts)
|
|
238
340
|
|
|
239
341
|
|
|
@@ -252,18 +354,19 @@ def _emit_metatable_reservation_request(
|
|
|
252
354
|
models: Sequence[type[Any]],
|
|
253
355
|
tables: Sequence[Any],
|
|
254
356
|
) -> None:
|
|
255
|
-
|
|
357
|
+
table_names = []
|
|
256
358
|
for model, table in zip(models, tables, strict=True):
|
|
257
|
-
|
|
359
|
+
table_names.append(
|
|
258
360
|
str(
|
|
259
|
-
_item_value(table, "
|
|
361
|
+
_item_value(table, "physical_table_name")
|
|
362
|
+
or _item_value(table, "identifier")
|
|
260
363
|
or getattr(model, "__metatable_identifier__", None)
|
|
261
364
|
or getattr(model, "__name__", repr(model))
|
|
262
365
|
)
|
|
263
366
|
)
|
|
264
367
|
_emit_status(
|
|
265
368
|
f"Sending POST {RESERVE_MANAGED_ENDPOINT} request for {len(tables)} "
|
|
266
|
-
f"MetaTables
|
|
369
|
+
f"MetaTables table_names={','.join(table_names)}"
|
|
267
370
|
)
|
|
268
371
|
|
|
269
372
|
|
|
@@ -279,10 +382,12 @@ def _emit_metatable_reservation(model: type[Any], item: Any) -> None:
|
|
|
279
382
|
|
|
280
383
|
|
|
281
384
|
def _emit_metatable_finalization(model: type[Any], item: Any) -> None:
|
|
385
|
+
finalized = _item_value(item, "finalized")
|
|
386
|
+
action = "finalized" if finalized is not False else "finalize-failed"
|
|
282
387
|
_emit_progress(
|
|
283
388
|
_metatable_message(
|
|
284
389
|
endpoint=FINALIZE_MANAGED_ENDPOINT,
|
|
285
|
-
action=
|
|
390
|
+
action=action,
|
|
286
391
|
model=model,
|
|
287
392
|
item=item,
|
|
288
393
|
),
|
|
@@ -295,6 +400,9 @@ def _prepare_alembic_config(
|
|
|
295
400
|
timeout: float | None,
|
|
296
401
|
ttl_seconds: int,
|
|
297
402
|
alembic_output: _AlembicOutput,
|
|
403
|
+
stage_existing_schema_management: bool = True,
|
|
404
|
+
require_existing_contract_match: bool = True,
|
|
405
|
+
prepare_provider_metatables: bool = True,
|
|
298
406
|
) -> tuple[Any, Any]:
|
|
299
407
|
_emit_status("Ensuring Alembic registry MetaTable...")
|
|
300
408
|
registry_meta_table = migration.ensure_alembic_registry(
|
|
@@ -302,13 +410,23 @@ def _prepare_alembic_config(
|
|
|
302
410
|
on_metatable_registered=_emit_metatable_registration,
|
|
303
411
|
)
|
|
304
412
|
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
413
|
+
if prepare_provider_metatables:
|
|
414
|
+
_emit_status("Preparing platform-managed MetaTable reservations...")
|
|
415
|
+
prepared = migration.prepare_for_alembic(
|
|
416
|
+
timeout=timeout,
|
|
417
|
+
stage_existing_schema_management=stage_existing_schema_management,
|
|
418
|
+
require_existing_contract_match=require_existing_contract_match,
|
|
419
|
+
on_metatable_reservation_request=_emit_metatable_reservation_request,
|
|
420
|
+
on_metatable_reservation_status=_emit_status,
|
|
421
|
+
on_metatable_reserved=_emit_metatable_reservation,
|
|
422
|
+
)
|
|
423
|
+
else:
|
|
424
|
+
_emit_status("Skipping provider MetaTable reservations for read-only Alembic command.")
|
|
425
|
+
prepared = SimpleNamespace(
|
|
426
|
+
data_source_uid=migration._resolve_provider_data_source_uid(),
|
|
427
|
+
meta_table_uids=[],
|
|
428
|
+
owner_role_name=None,
|
|
429
|
+
)
|
|
312
430
|
_include_alembic_registry_in_scope(migration, prepared, registry_meta_table)
|
|
313
431
|
_emit_status(
|
|
314
432
|
"Prepared migration scope "
|
|
@@ -343,6 +461,103 @@ def _prepare_alembic_config(
|
|
|
343
461
|
return prepared, config
|
|
344
462
|
|
|
345
463
|
|
|
464
|
+
def _prepared_physical_table_refs(prepared: Any) -> list[tuple[str | None, str]]:
|
|
465
|
+
refs: list[tuple[str | None, str]] = []
|
|
466
|
+
for item in getattr(prepared, "reserved_tables", []) or []:
|
|
467
|
+
table_name = _item_value(item, "physical_table_name") or _contract_physical_table_name(item)
|
|
468
|
+
if table_name in (None, ""):
|
|
469
|
+
continue
|
|
470
|
+
schema = _contract_physical_schema(item)
|
|
471
|
+
refs.append((str(schema) if schema not in (None, "") else "public", str(table_name)))
|
|
472
|
+
return list(dict.fromkeys(refs))
|
|
473
|
+
|
|
474
|
+
|
|
475
|
+
def _alembic_script_heads(config: Any) -> list[str]:
|
|
476
|
+
try:
|
|
477
|
+
from alembic.script import ScriptDirectory
|
|
478
|
+
except ImportError as exc:
|
|
479
|
+
raise RuntimeError("Alembic is required for migration commands.") from exc
|
|
480
|
+
with _forward_alembic_logging():
|
|
481
|
+
return [str(head) for head in ScriptDirectory.from_config(config).get_heads()]
|
|
482
|
+
|
|
483
|
+
|
|
484
|
+
def _assert_autogenerate_baseline_visible(prepared: Any, config: Any) -> None:
|
|
485
|
+
script_heads = _alembic_script_heads(config)
|
|
486
|
+
if not script_heads:
|
|
487
|
+
_emit_status("No existing Alembic heads; allowing initial autogenerate baseline.")
|
|
488
|
+
return
|
|
489
|
+
|
|
490
|
+
table_refs = _prepared_physical_table_refs(prepared)
|
|
491
|
+
if not table_refs:
|
|
492
|
+
_emit_status(
|
|
493
|
+
"No provider physical table names were available for autogenerate "
|
|
494
|
+
"baseline visibility check."
|
|
495
|
+
)
|
|
496
|
+
return
|
|
497
|
+
|
|
498
|
+
try:
|
|
499
|
+
from sqlalchemy import create_engine, inspect
|
|
500
|
+
from sqlalchemy.pool import NullPool
|
|
501
|
+
except ImportError as exc:
|
|
502
|
+
raise RuntimeError("SQLAlchemy is required for Alembic autogenerate preflight.") from exc
|
|
503
|
+
|
|
504
|
+
sqlalchemy_url = getattr(config, "attributes", {}).get("mainsequence_migration_sqlalchemy_url")
|
|
505
|
+
if sqlalchemy_url in (None, ""):
|
|
506
|
+
raise RuntimeError(
|
|
507
|
+
"Alembic autogenerate preflight cannot inspect the scoped database because "
|
|
508
|
+
"the migration SQLAlchemy URL was not attached to the Alembic config."
|
|
509
|
+
)
|
|
510
|
+
|
|
511
|
+
_emit_status(
|
|
512
|
+
"Checking Alembic autogenerate baseline visibility "
|
|
513
|
+
f"script_heads={','.join(script_heads)} table_count={len(table_refs)}..."
|
|
514
|
+
)
|
|
515
|
+
visible: list[tuple[str | None, str]] = []
|
|
516
|
+
missing: list[tuple[str | None, str]] = []
|
|
517
|
+
engine = create_engine(str(sqlalchemy_url), poolclass=NullPool)
|
|
518
|
+
try:
|
|
519
|
+
with engine.connect() as connection:
|
|
520
|
+
apply_mainsequence_migration_role(connection, config)
|
|
521
|
+
inspector = inspect(connection)
|
|
522
|
+
for schema, table_name in table_refs:
|
|
523
|
+
if inspector.has_table(table_name, schema=schema):
|
|
524
|
+
visible.append((schema, table_name))
|
|
525
|
+
else:
|
|
526
|
+
missing.append((schema, table_name))
|
|
527
|
+
finally:
|
|
528
|
+
engine.dispose()
|
|
529
|
+
|
|
530
|
+
if visible:
|
|
531
|
+
_emit_status(
|
|
532
|
+
"Alembic autogenerate baseline is visible "
|
|
533
|
+
f"existing_tables={len(visible)} missing_tables={len(missing)}."
|
|
534
|
+
)
|
|
535
|
+
if missing:
|
|
536
|
+
sample = ",".join(_format_table_ref(ref) for ref in missing[:5])
|
|
537
|
+
_emit_status(
|
|
538
|
+
"Some provider physical tables were not visible during preflight; "
|
|
539
|
+
f"Alembic may treat them as new if they are not intentional additions. "
|
|
540
|
+
f"sample={sample}"
|
|
541
|
+
)
|
|
542
|
+
return
|
|
543
|
+
|
|
544
|
+
sample = ",".join(_format_table_ref(ref) for ref in table_refs[:5])
|
|
545
|
+
raise RuntimeError(
|
|
546
|
+
"Refusing to autogenerate a migration because this provider already has "
|
|
547
|
+
f"Alembic head(s) {','.join(script_heads)}, but the scoped migration "
|
|
548
|
+
"connection cannot see any provider physical tables. Alembic would emit a "
|
|
549
|
+
"duplicate initial create-all migration instead of a schema diff. Apply the "
|
|
550
|
+
"existing baseline with `mainsequence migrations upgrade ... head`, or fix "
|
|
551
|
+
"the scoped migration connection/table visibility before running revision "
|
|
552
|
+
f"again. checked_sample={sample}"
|
|
553
|
+
)
|
|
554
|
+
|
|
555
|
+
|
|
556
|
+
def _format_table_ref(ref: tuple[str | None, str]) -> str:
|
|
557
|
+
schema, table_name = ref
|
|
558
|
+
return f"{schema}.{table_name}" if schema not in (None, "") else table_name
|
|
559
|
+
|
|
560
|
+
|
|
346
561
|
def _next_sequential_revision_id(
|
|
347
562
|
migration: AlembicMetaTableMigration,
|
|
348
563
|
*,
|
|
@@ -411,7 +626,11 @@ def current(
|
|
|
411
626
|
timeout=timeout,
|
|
412
627
|
ttl_seconds=ttl_seconds,
|
|
413
628
|
alembic_output=alembic_output,
|
|
629
|
+
stage_existing_schema_management=False,
|
|
630
|
+
require_existing_contract_match=False,
|
|
631
|
+
prepare_provider_metatables=False,
|
|
414
632
|
)
|
|
633
|
+
_emit_alembic_script_context(config)
|
|
415
634
|
_emit_status("Starting Alembic current now...")
|
|
416
635
|
with _forward_alembic_logging():
|
|
417
636
|
command.current(config, verbose=verbose)
|
|
@@ -462,7 +681,11 @@ def revision(
|
|
|
462
681
|
timeout=timeout,
|
|
463
682
|
ttl_seconds=ttl_seconds,
|
|
464
683
|
alembic_output=alembic_output,
|
|
684
|
+
stage_existing_schema_management=False,
|
|
465
685
|
)
|
|
686
|
+
_emit_alembic_script_context(config, target_revision=head)
|
|
687
|
+
if autogenerate:
|
|
688
|
+
_assert_autogenerate_baseline_visible(prepared, config)
|
|
466
689
|
_emit_status(f"Starting Alembic revision now rev_id={resolved_rev_id}...")
|
|
467
690
|
with _forward_alembic_logging():
|
|
468
691
|
script = command.revision(
|
|
@@ -508,6 +731,7 @@ def upgrade(
|
|
|
508
731
|
ttl_seconds=ttl_seconds,
|
|
509
732
|
alembic_output=alembic_output,
|
|
510
733
|
)
|
|
734
|
+
_emit_alembic_script_context(config, target_revision=target_revision)
|
|
511
735
|
_emit_status(f"Starting Alembic upgrade now target={target_revision}...")
|
|
512
736
|
with _forward_alembic_logging():
|
|
513
737
|
command.upgrade(config, target_revision)
|
|
@@ -559,6 +783,7 @@ def downgrade(
|
|
|
559
783
|
ttl_seconds=ttl_seconds,
|
|
560
784
|
alembic_output=alembic_output,
|
|
561
785
|
)
|
|
786
|
+
_emit_alembic_script_context(config, target_revision=target_revision)
|
|
562
787
|
_emit_status(f"Starting Alembic downgrade now target={target_revision}...")
|
|
563
788
|
with _forward_alembic_logging():
|
|
564
789
|
command.downgrade(config, target_revision)
|
|
@@ -610,8 +610,9 @@ class ManagedMetaTableReservationTable(MetaTableRequestFields):
|
|
|
610
610
|
table_contract: MetaTableContract | dict[str, Any] = Field(
|
|
611
611
|
...,
|
|
612
612
|
description=(
|
|
613
|
-
"Relational table contract used to reserve physical
|
|
614
|
-
"
|
|
613
|
+
"Relational table contract used to reserve the physical table name "
|
|
614
|
+
"before Alembic renders SQL. Index and foreign-key names remain "
|
|
615
|
+
"client-authored or database-authored DDL metadata."
|
|
615
616
|
),
|
|
616
617
|
)
|
|
617
618
|
time_index_name: str | None = Field(
|
|
@@ -667,8 +668,9 @@ class ManagedMetaTableReservationItem(BasePydanticModel):
|
|
|
667
668
|
table_contract: dict[str, Any] = Field(
|
|
668
669
|
...,
|
|
669
670
|
description=(
|
|
670
|
-
"Backend-normalized contract containing resolved physical table
|
|
671
|
-
"
|
|
671
|
+
"Backend-normalized contract containing the resolved physical table "
|
|
672
|
+
"name. Index and foreign-key names are preserved only when authored "
|
|
673
|
+
"or observed; TS Manager does not generate them."
|
|
672
674
|
),
|
|
673
675
|
)
|
|
674
676
|
schema_management: dict[str, Any] | None = Field(
|
|
@@ -1415,6 +1417,28 @@ class MetaTable(BasePydanticModel, LabelableObjectMixin, ShareableObjectMixin, B
|
|
|
1415
1417
|
raise_for_response(response, payload=request_payload)
|
|
1416
1418
|
return response.json()
|
|
1417
1419
|
|
|
1420
|
+
@classmethod
|
|
1421
|
+
def filter_by_body(
|
|
1422
|
+
cls,
|
|
1423
|
+
*,
|
|
1424
|
+
timeout: int | float | tuple[float, float] | None = None,
|
|
1425
|
+
**filters: Any,
|
|
1426
|
+
) -> list[MetaTable]:
|
|
1427
|
+
response_json = cls._post_action(
|
|
1428
|
+
"filter",
|
|
1429
|
+
filters,
|
|
1430
|
+
timeout=timeout,
|
|
1431
|
+
expected_statuses=(200,),
|
|
1432
|
+
)
|
|
1433
|
+
if isinstance(response_json, dict) and isinstance(
|
|
1434
|
+
response_json.get("results"),
|
|
1435
|
+
list,
|
|
1436
|
+
):
|
|
1437
|
+
return [cls(**item) for item in response_json["results"]]
|
|
1438
|
+
if isinstance(response_json, list):
|
|
1439
|
+
return [cls(**item) for item in response_json]
|
|
1440
|
+
raise TypeError("MetaTable.filter_by_body expected a list or paginated response.")
|
|
1441
|
+
|
|
1418
1442
|
def _post_detail_action(
|
|
1419
1443
|
self,
|
|
1420
1444
|
action_name: str,
|