mainsequence 4.2.1__tar.gz → 4.2.2__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 (127) hide show
  1. {mainsequence-4.2.1/mainsequence.egg-info → mainsequence-4.2.2}/PKG-INFO +1 -1
  2. {mainsequence-4.2.1 → mainsequence-4.2.2}/agent_scaffold/skills/data_publishing/meta_tables/SKILL.md +23 -0
  3. {mainsequence-4.2.1 → mainsequence-4.2.2}/mainsequence/client/metatables/core.py +19 -0
  4. {mainsequence-4.2.1 → mainsequence-4.2.2}/mainsequence/meta_tables/data_nodes/persist_managers.py +7 -0
  5. {mainsequence-4.2.1 → mainsequence-4.2.2}/mainsequence/meta_tables/migrations.py +75 -11
  6. {mainsequence-4.2.1 → mainsequence-4.2.2}/mainsequence/meta_tables/sqlalchemy_contracts.py +40 -0
  7. {mainsequence-4.2.1 → mainsequence-4.2.2/mainsequence.egg-info}/PKG-INFO +1 -1
  8. {mainsequence-4.2.1 → mainsequence-4.2.2}/pyproject.toml +1 -1
  9. {mainsequence-4.2.1 → mainsequence-4.2.2}/tests/test_meta_table_migrations.py +74 -3
  10. {mainsequence-4.2.1 → mainsequence-4.2.2}/tests/test_meta_tables_sqlalchemy_contracts.py +52 -0
  11. {mainsequence-4.2.1 → mainsequence-4.2.2}/tests/test_run_configuration.py +6 -2
  12. {mainsequence-4.2.1 → mainsequence-4.2.2}/LICENSE +0 -0
  13. {mainsequence-4.2.1 → mainsequence-4.2.2}/README.md +0 -0
  14. {mainsequence-4.2.1 → mainsequence-4.2.2}/agent_scaffold/AGENTS.md +0 -0
  15. {mainsequence-4.2.1 → mainsequence-4.2.2}/agent_scaffold/skills/a2a_communication/SKILL.md +0 -0
  16. {mainsequence-4.2.1 → mainsequence-4.2.2}/agent_scaffold/skills/application_surfaces/api_surfaces/SKILL.md +0 -0
  17. {mainsequence-4.2.1 → mainsequence-4.2.2}/agent_scaffold/skills/command_center/adapter_from_api/SKILL.md +0 -0
  18. {mainsequence-4.2.1 → mainsequence-4.2.2}/agent_scaffold/skills/command_center/api_mock_prototyping/SKILL.md +0 -0
  19. {mainsequence-4.2.1 → mainsequence-4.2.2}/agent_scaffold/skills/command_center/app_components/SKILL.md +0 -0
  20. {mainsequence-4.2.1 → mainsequence-4.2.2}/agent_scaffold/skills/command_center/connections/SKILL.md +0 -0
  21. {mainsequence-4.2.1 → mainsequence-4.2.2}/agent_scaffold/skills/command_center/workspace_analysis/SKILL.md +0 -0
  22. {mainsequence-4.2.1 → mainsequence-4.2.2}/agent_scaffold/skills/command_center/workspace_builder/SKILL.md +0 -0
  23. {mainsequence-4.2.1 → mainsequence-4.2.2}/agent_scaffold/skills/command_center/workspace_design/SKILL.md +0 -0
  24. {mainsequence-4.2.1 → mainsequence-4.2.2}/agent_scaffold/skills/dashboards/streamlit/SKILL.md +0 -0
  25. {mainsequence-4.2.1 → mainsequence-4.2.2}/agent_scaffold/skills/data_access/exploration/SKILL.md +0 -0
  26. {mainsequence-4.2.1 → mainsequence-4.2.2}/agent_scaffold/skills/data_publishing/data_nodes/SKILL.md +0 -0
  27. {mainsequence-4.2.1 → mainsequence-4.2.2}/agent_scaffold/skills/maintenance/bug_auditor/SKILL.md +0 -0
  28. {mainsequence-4.2.1 → mainsequence-4.2.2}/agent_scaffold/skills/ms-markets/SKILL.md +0 -0
  29. {mainsequence-4.2.1 → mainsequence-4.2.2}/agent_scaffold/skills/platform_operations/access_control_and_sharing/SKILL.md +0 -0
  30. {mainsequence-4.2.1 → mainsequence-4.2.2}/agent_scaffold/skills/platform_operations/orchestration_and_releases/SKILL.md +0 -0
  31. {mainsequence-4.2.1 → mainsequence-4.2.2}/agent_scaffold/skills/project_builder/SKILL.md +0 -0
  32. {mainsequence-4.2.1 → mainsequence-4.2.2}/agent_scaffold/skills/project_to_agent/SKILL.md +0 -0
  33. {mainsequence-4.2.1 → mainsequence-4.2.2}/mainsequence/__init__.py +0 -0
  34. {mainsequence-4.2.1 → mainsequence-4.2.2}/mainsequence/__main__.py +0 -0
  35. {mainsequence-4.2.1 → mainsequence-4.2.2}/mainsequence/bootstrap.py +0 -0
  36. {mainsequence-4.2.1 → mainsequence-4.2.2}/mainsequence/cli/__init__.py +0 -0
  37. {mainsequence-4.2.1 → mainsequence-4.2.2}/mainsequence/cli/api.py +0 -0
  38. {mainsequence-4.2.1 → mainsequence-4.2.2}/mainsequence/cli/browser_auth.py +0 -0
  39. {mainsequence-4.2.1 → mainsequence-4.2.2}/mainsequence/cli/cli.py +0 -0
  40. {mainsequence-4.2.1 → mainsequence-4.2.2}/mainsequence/cli/config.py +0 -0
  41. {mainsequence-4.2.1 → mainsequence-4.2.2}/mainsequence/cli/docker_utils.py +0 -0
  42. {mainsequence-4.2.1 → mainsequence-4.2.2}/mainsequence/cli/doctor.py +0 -0
  43. {mainsequence-4.2.1 → mainsequence-4.2.2}/mainsequence/cli/local_ops.py +0 -0
  44. {mainsequence-4.2.1 → mainsequence-4.2.2}/mainsequence/cli/migrations.py +0 -0
  45. {mainsequence-4.2.1 → mainsequence-4.2.2}/mainsequence/cli/model_filters.py +0 -0
  46. {mainsequence-4.2.1 → mainsequence-4.2.2}/mainsequence/cli/project_status.py +0 -0
  47. {mainsequence-4.2.1 → mainsequence-4.2.2}/mainsequence/cli/pydantic_cli.py +0 -0
  48. {mainsequence-4.2.1 → mainsequence-4.2.2}/mainsequence/cli/sdk_utils.py +0 -0
  49. {mainsequence-4.2.1 → mainsequence-4.2.2}/mainsequence/cli/ssh_utils.py +0 -0
  50. {mainsequence-4.2.1 → mainsequence-4.2.2}/mainsequence/cli/ui.py +0 -0
  51. {mainsequence-4.2.1 → mainsequence-4.2.2}/mainsequence/client/__init__.py +0 -0
  52. {mainsequence-4.2.1 → mainsequence-4.2.2}/mainsequence/client/agent_runtime_models.py +0 -0
  53. {mainsequence-4.2.1 → mainsequence-4.2.2}/mainsequence/client/base.py +0 -0
  54. {mainsequence-4.2.1 → mainsequence-4.2.2}/mainsequence/client/client.py +0 -0
  55. {mainsequence-4.2.1 → mainsequence-4.2.2}/mainsequence/client/command_center/__init__.py +0 -0
  56. {mainsequence-4.2.1 → mainsequence-4.2.2}/mainsequence/client/command_center/app_component.py +0 -0
  57. {mainsequence-4.2.1 → mainsequence-4.2.2}/mainsequence/client/command_center/connections.py +0 -0
  58. {mainsequence-4.2.1 → mainsequence-4.2.2}/mainsequence/client/command_center/data_models.py +0 -0
  59. {mainsequence-4.2.1 → mainsequence-4.2.2}/mainsequence/client/command_center/workspace.py +0 -0
  60. {mainsequence-4.2.1 → mainsequence-4.2.2}/mainsequence/client/command_center/workspace_snapshot.py +0 -0
  61. {mainsequence-4.2.1 → mainsequence-4.2.2}/mainsequence/client/compute_validation.py +0 -0
  62. {mainsequence-4.2.1 → mainsequence-4.2.2}/mainsequence/client/data_sources_interfaces/__init__.py +0 -0
  63. {mainsequence-4.2.1 → mainsequence-4.2.2}/mainsequence/client/data_sources_interfaces/duckdb.py +0 -0
  64. {mainsequence-4.2.1 → mainsequence-4.2.2}/mainsequence/client/data_sources_interfaces/local_paths.py +0 -0
  65. {mainsequence-4.2.1 → mainsequence-4.2.2}/mainsequence/client/data_sources_interfaces/sqlite.py +0 -0
  66. {mainsequence-4.2.1 → mainsequence-4.2.2}/mainsequence/client/dtype_codec.py +0 -0
  67. {mainsequence-4.2.1 → mainsequence-4.2.2}/mainsequence/client/exceptions.py +0 -0
  68. {mainsequence-4.2.1 → mainsequence-4.2.2}/mainsequence/client/fastapi/__init__.py +0 -0
  69. {mainsequence-4.2.1 → mainsequence-4.2.2}/mainsequence/client/fastapi/auth.py +0 -0
  70. {mainsequence-4.2.1 → mainsequence-4.2.2}/mainsequence/client/metatables/__init__.py +0 -0
  71. {mainsequence-4.2.1 → mainsequence-4.2.2}/mainsequence/client/metatables/migrations.py +0 -0
  72. {mainsequence-4.2.1 → mainsequence-4.2.2}/mainsequence/client/models_foundry.py +0 -0
  73. {mainsequence-4.2.1 → mainsequence-4.2.2}/mainsequence/client/models_helpers.py +0 -0
  74. {mainsequence-4.2.1 → mainsequence-4.2.2}/mainsequence/client/models_user.py +0 -0
  75. {mainsequence-4.2.1 → mainsequence-4.2.2}/mainsequence/client/utils.py +0 -0
  76. {mainsequence-4.2.1 → mainsequence-4.2.2}/mainsequence/defaults.py +0 -0
  77. {mainsequence-4.2.1 → mainsequence-4.2.2}/mainsequence/instrumentation/__init__.py +0 -0
  78. {mainsequence-4.2.1 → mainsequence-4.2.2}/mainsequence/instrumentation/utils.py +0 -0
  79. {mainsequence-4.2.1 → mainsequence-4.2.2}/mainsequence/logconf.py +0 -0
  80. {mainsequence-4.2.1 → mainsequence-4.2.2}/mainsequence/meta_tables/__init__.py +0 -0
  81. {mainsequence-4.2.1 → mainsequence-4.2.2}/mainsequence/meta_tables/__main__.py +0 -0
  82. {mainsequence-4.2.1 → mainsequence-4.2.2}/mainsequence/meta_tables/compiled_sql/__init__.py +0 -0
  83. {mainsequence-4.2.1 → mainsequence-4.2.2}/mainsequence/meta_tables/compiled_sql/v1.py +0 -0
  84. {mainsequence-4.2.1 → mainsequence-4.2.2}/mainsequence/meta_tables/data_nodes/__init__.py +0 -0
  85. {mainsequence-4.2.1 → mainsequence-4.2.2}/mainsequence/meta_tables/data_nodes/build_operations.py +0 -0
  86. {mainsequence-4.2.1 → mainsequence-4.2.2}/mainsequence/meta_tables/data_nodes/data_nodes.py +0 -0
  87. {mainsequence-4.2.1 → mainsequence-4.2.2}/mainsequence/meta_tables/data_nodes/models.py +0 -0
  88. {mainsequence-4.2.1 → mainsequence-4.2.2}/mainsequence/meta_tables/data_nodes/namespacing.py +0 -0
  89. {mainsequence-4.2.1 → mainsequence-4.2.2}/mainsequence/meta_tables/data_nodes/run_operations.py +0 -0
  90. {mainsequence-4.2.1 → mainsequence-4.2.2}/mainsequence/meta_tables/data_nodes/utils.py +0 -0
  91. {mainsequence-4.2.1 → mainsequence-4.2.2}/mainsequence/meta_tables/future_registry.py +0 -0
  92. {mainsequence-4.2.1 → mainsequence-4.2.2}/mainsequence/meta_tables/hashing.py +0 -0
  93. {mainsequence-4.2.1 → mainsequence-4.2.2}/mainsequence/meta_tables/pydantic_metadata.py +0 -0
  94. {mainsequence-4.2.1 → mainsequence-4.2.2}/mainsequence/runtime_flags.py +0 -0
  95. {mainsequence-4.2.1 → mainsequence-4.2.2}/mainsequence.egg-info/SOURCES.txt +0 -0
  96. {mainsequence-4.2.1 → mainsequence-4.2.2}/mainsequence.egg-info/dependency_links.txt +0 -0
  97. {mainsequence-4.2.1 → mainsequence-4.2.2}/mainsequence.egg-info/entry_points.txt +0 -0
  98. {mainsequence-4.2.1 → mainsequence-4.2.2}/mainsequence.egg-info/requires.txt +0 -0
  99. {mainsequence-4.2.1 → mainsequence-4.2.2}/mainsequence.egg-info/top_level.txt +0 -0
  100. {mainsequence-4.2.1 → mainsequence-4.2.2}/setup.cfg +0 -0
  101. {mainsequence-4.2.1 → mainsequence-4.2.2}/tests/test_auth_precedence.py +0 -0
  102. {mainsequence-4.2.1 → mainsequence-4.2.2}/tests/test_build_operations_hashing.py +0 -0
  103. {mainsequence-4.2.1 → mainsequence-4.2.2}/tests/test_cli.py +0 -0
  104. {mainsequence-4.2.1 → mainsequence-4.2.2}/tests/test_cli_browser_auth.py +0 -0
  105. {mainsequence-4.2.1 → mainsequence-4.2.2}/tests/test_cli_migrations.py +0 -0
  106. {mainsequence-4.2.1 → mainsequence-4.2.2}/tests/test_client.py +0 -0
  107. {mainsequence-4.2.1 → mainsequence-4.2.2}/tests/test_command_center_app_component_models.py +0 -0
  108. {mainsequence-4.2.1 → mainsequence-4.2.2}/tests/test_command_center_data_models.py +0 -0
  109. {mainsequence-4.2.1 → mainsequence-4.2.2}/tests/test_command_center_models.py +0 -0
  110. {mainsequence-4.2.1 → mainsequence-4.2.2}/tests/test_data_access_mixin_dimension_audit.py +0 -0
  111. {mainsequence-4.2.1 → mainsequence-4.2.2}/tests/test_data_node_storage_dimension_queries.py +0 -0
  112. {mainsequence-4.2.1 → mainsequence-4.2.2}/tests/test_data_node_update_flow.py +0 -0
  113. {mainsequence-4.2.1 → mainsequence-4.2.2}/tests/test_dependency_extras.py +0 -0
  114. {mainsequence-4.2.1 → mainsequence-4.2.2}/tests/test_duckdb_interface_dimensions.py +0 -0
  115. {mainsequence-4.2.1 → mainsequence-4.2.2}/tests/test_filter_normalization.py +0 -0
  116. {mainsequence-4.2.1 → mainsequence-4.2.2}/tests/test_logconf.py +0 -0
  117. {mainsequence-4.2.1 → mainsequence-4.2.2}/tests/test_meta_tables_client_models.py +0 -0
  118. {mainsequence-4.2.1 → mainsequence-4.2.2}/tests/test_models_user_request_bound_auth.py +0 -0
  119. {mainsequence-4.2.1 → mainsequence-4.2.2}/tests/test_pod_project_resolution.py +0 -0
  120. {mainsequence-4.2.1 → mainsequence-4.2.2}/tests/test_project_batch_jobs_from_file.py +0 -0
  121. {mainsequence-4.2.1 → mainsequence-4.2.2}/tests/test_secret_client_model.py +0 -0
  122. {mainsequence-4.2.1 → mainsequence-4.2.2}/tests/test_source_table_configuration.py +0 -0
  123. {mainsequence-4.2.1 → mainsequence-4.2.2}/tests/test_sqlite_interface_dimensions.py +0 -0
  124. {mainsequence-4.2.1 → mainsequence-4.2.2}/tests/test_update_runner_uid_runtime.py +0 -0
  125. {mainsequence-4.2.1 → mainsequence-4.2.2}/tests/test_update_statistics.py +0 -0
  126. {mainsequence-4.2.1 → mainsequence-4.2.2}/tests/test_update_uid_guards.py +0 -0
  127. {mainsequence-4.2.1 → mainsequence-4.2.2}/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.1
3
+ Version: 4.2.2
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
@@ -95,6 +95,29 @@ For every non-trivial task, decide:
95
95
 
96
96
  ## Build Rules
97
97
 
98
+ ### 0. Creation And Deletion Are SDK-Managed
99
+
100
+ Do not build custom migrations for creating or deleting MetaTables.
101
+
102
+ MetaTable creation and deletion are handled through `mainsequence-sdk` and the
103
+ Main Sequence CLI. Assistants should define the SQLAlchemy MetaTable model,
104
+ select the SDK migration provider when schema evolution is needed, and use the
105
+ documented `mainsequence migrations ...` commands.
106
+
107
+ Do not hand-author create/delete migration files, backend migration payloads,
108
+ manifest rows, registry rows, or low-level SDK migration requests for MetaTable
109
+ creation or deletion.
110
+
111
+ The only migration workflow to recommend is the Main Sequence CLI lifecycle:
112
+
113
+ ```bash
114
+ mainsequence migrations current --provider mainsequence_migrations:migration
115
+ mainsequence migrations revision --provider mainsequence_migrations:migration
116
+ mainsequence migrations render --provider mainsequence_migrations:migration --to head
117
+ mainsequence migrations upgrade --provider mainsequence_migrations:migration --to head --dry-run
118
+ mainsequence migrations upgrade --provider mainsequence_migrations:migration --to head
119
+ ```
120
+
98
121
  ### 1. SQLAlchemy metadata is the authoring source
99
122
 
100
123
  Keep the application table model as the authoring source for the neutral table contract.
@@ -1102,6 +1102,25 @@ class MetaTable(BasePydanticModel, LabelableObjectMixin, ShareableObjectMixin, B
1102
1102
  *,
1103
1103
  timeout: int | float | tuple[float, float] | None = None,
1104
1104
  ) -> dict[str, Any]:
1105
+ """
1106
+ Refresh this MetaTable's physical database shape snapshot.
1107
+
1108
+ This calls the backend ``POST /meta_table/<uid>/introspect/`` action.
1109
+ The backend reads the real physical table through the MetaTable's data
1110
+ source, reflects columns, indexes, and constraints, stores that data on
1111
+ ``MetaTable.introspection_snapshot``, and returns the full response.
1112
+
1113
+ This method is intended for admin, debugging, and reconciliation
1114
+ workflows. Use it when a client needs to inspect what the database
1115
+ currently has, diagnose catalog/physical drift, or refresh metadata
1116
+ after an out-of-band DDL change. It is not required for normal reads,
1117
+ writes, registration, or migration-first application startup.
1118
+
1119
+ Returns:
1120
+ Backend response containing ``ok``, ``meta_table_uid``, and
1121
+ ``introspection_snapshot``. When the snapshot is an object, this
1122
+ instance's ``introspection_snapshot`` attribute is updated in place.
1123
+ """
1105
1124
  response_json = self._post_detail_action("introspect", timeout=timeout)
1106
1125
  snapshot = response_json.get("introspection_snapshot")
1107
1126
  if isinstance(snapshot, dict):
@@ -108,6 +108,13 @@ def ensure_registered_storage_table(
108
108
  raise ValueError(
109
109
  f"{context} storage_table is missing TimeIndexMetaData metadata."
110
110
  )
111
+ from mainsequence.client.metatables import TimeIndexMetaData
112
+
113
+ if not isinstance(storage_metadata, TimeIndexMetaData):
114
+ raise TypeError(
115
+ f"{context} storage_table must bind TimeIndexMetaData metadata; "
116
+ f"got {type(storage_metadata).__name__}."
117
+ )
111
118
  if storage_table.get_meta_table_uid() in (None, ""):
112
119
  raise ValueError(f"{context} storage_table must provide a MetaTable UID.")
113
120
  if storage_table.get_data_source_uid() in (None, ""):
@@ -18,10 +18,12 @@ from mainsequence.client.metatables import (
18
18
  MetaTableContract,
19
19
  MetaTablePhysicalContract,
20
20
  MetaTableRegistrationRequest,
21
+ TimeIndexMetaData,
21
22
  )
22
23
  from mainsequence.meta_tables.hashing import build_meta_table_storage_hash
23
24
  from mainsequence.meta_tables.sqlalchemy_contracts import (
24
25
  PlatformManagedMetaTable,
26
+ PlatformTimeIndexMetaData,
25
27
  _resolve_model_data_source_uid,
26
28
  platform_managed_migration_registration_context,
27
29
  resolve_metatable_identifier,
@@ -268,8 +270,8 @@ class AlembicMetaTableMigration:
268
270
  timeout: int | float | tuple[float, float] | None = None,
269
271
  ) -> list[Any]:
270
272
  registered: list[Any] = []
271
- existing_meta_tables = _get_metatables_by_identifier(
272
- [resolve_metatable_identifier(model) for model in self.metatable_models],
273
+ existing_meta_tables = _get_metatables_by_model_identifier(
274
+ self.metatable_models,
273
275
  timeout=timeout,
274
276
  )
275
277
  with platform_managed_migration_registration_context():
@@ -298,8 +300,8 @@ class AlembicMetaTableMigration:
298
300
  """
299
301
 
300
302
  resolved: list[Any] = []
301
- existing_meta_tables = _get_metatables_by_identifier(
302
- [resolve_metatable_identifier(model) for model in self.metatable_models],
303
+ existing_meta_tables = _get_metatables_by_model_identifier(
304
+ self.metatable_models,
303
305
  timeout=timeout,
304
306
  )
305
307
  with platform_managed_migration_registration_context():
@@ -326,7 +328,7 @@ class AlembicMetaTableMigration:
326
328
  existing_meta_table = (
327
329
  existing_meta_tables_by_identifier.get(identifier)
328
330
  if existing_meta_tables_by_identifier is not None
329
- else _get_metatable_by_identifier(identifier, timeout=timeout)
331
+ else _get_metatable_by_model_identifier(model, timeout=timeout)
330
332
  )
331
333
  if existing_meta_table is not None:
332
334
  _bind_model_to_existing_metatable(model, existing_meta_table)
@@ -603,25 +605,77 @@ def _get_metatable_by_identifier(
603
605
  identifier: str,
604
606
  *,
605
607
  timeout: int | float | tuple[float, float] | None = None,
608
+ meta_table_cls: type[Any] = MetaTable,
606
609
  ) -> MetaTable | None:
607
- matches = _get_metatables_by_identifier([identifier], timeout=timeout)
610
+ matches = _get_metatables_by_identifier(
611
+ [identifier],
612
+ timeout=timeout,
613
+ meta_table_cls=meta_table_cls,
614
+ )
608
615
  return matches.get(identifier)
609
616
 
610
617
 
618
+ def _get_metatable_by_model_identifier(
619
+ model: type[Any],
620
+ *,
621
+ timeout: int | float | tuple[float, float] | None = None,
622
+ ) -> Any | None:
623
+ identifier = resolve_metatable_identifier(model)
624
+ return _get_metatable_by_identifier(
625
+ identifier,
626
+ timeout=timeout,
627
+ meta_table_cls=_metatable_resource_class_for_model(model),
628
+ )
629
+
630
+
631
+ def _get_metatables_by_model_identifier(
632
+ models: Sequence[type[Any]],
633
+ *,
634
+ timeout: int | float | tuple[float, float] | None = None,
635
+ ) -> dict[str, Any]:
636
+ identifiers_by_resource: dict[type[Any], list[str]] = {}
637
+ for model in models:
638
+ identifiers_by_resource.setdefault(
639
+ _metatable_resource_class_for_model(model),
640
+ [],
641
+ ).append(resolve_metatable_identifier(model))
642
+
643
+ resolved: dict[str, Any] = {}
644
+ for meta_table_cls, identifiers in identifiers_by_resource.items():
645
+ matches = _get_metatables_by_identifier(
646
+ identifiers,
647
+ timeout=timeout,
648
+ meta_table_cls=meta_table_cls,
649
+ )
650
+ duplicate_identifiers = set(resolved).intersection(matches)
651
+ if duplicate_identifiers:
652
+ duplicate_list = ", ".join(sorted(duplicate_identifiers))
653
+ raise ValueError(
654
+ "MetaTable identifiers are not globally unique across provider models: "
655
+ f"{duplicate_list}."
656
+ )
657
+ resolved.update(matches)
658
+ return resolved
659
+
660
+
611
661
  def _get_metatables_by_identifier(
612
662
  identifiers: Sequence[str],
613
663
  *,
614
664
  timeout: int | float | tuple[float, float] | None = None,
615
- ) -> dict[str, MetaTable]:
665
+ meta_table_cls: type[Any] = MetaTable,
666
+ ) -> dict[str, Any]:
616
667
  unique_identifiers = list(dict.fromkeys(str(identifier) for identifier in identifiers))
617
668
  if not unique_identifiers:
618
669
  return {}
619
670
 
620
- matches = MetaTable.filter(identifier__in=unique_identifiers, timeout=timeout)
671
+ filter_kwargs: dict[str, Any] = {"identifier__in": unique_identifiers}
672
+ if issubclass(meta_table_cls, TimeIndexMetaData):
673
+ filter_kwargs["include_relations_detail"] = True
674
+ matches = meta_table_cls.filter(timeout=timeout, **filter_kwargs)
621
675
  if not matches:
622
676
  return {}
623
677
 
624
- matched_by_identifier: dict[str, MetaTable] = {}
678
+ matched_by_identifier: dict[str, Any] = {}
625
679
  for meta_table in matches:
626
680
  identifier = _meta_table_identifier(meta_table)
627
681
  if identifier is None:
@@ -629,8 +683,8 @@ def _get_metatables_by_identifier(
629
683
  identifier = unique_identifiers[0]
630
684
  else:
631
685
  raise ValueError(
632
- "Backend returned a MetaTable row without identifier while resolving "
633
- "migration provider models by identifier__in."
686
+ f"Backend returned a {meta_table_cls.__name__} row without identifier "
687
+ "while resolving migration provider models by identifier__in."
634
688
  )
635
689
  if identifier in matched_by_identifier:
636
690
  raise ValueError(
@@ -651,6 +705,12 @@ def _meta_table_identifier(meta_table: Any) -> str | None:
651
705
  return str(identifier)
652
706
 
653
707
 
708
+ def _metatable_resource_class_for_model(model: type[Any]) -> type[Any]:
709
+ if _is_platform_time_index_metatable_model(model):
710
+ return TimeIndexMetaData
711
+ return MetaTable
712
+
713
+
654
714
  def _bind_model_to_existing_metatable(model: Any, meta_table: MetaTable) -> None:
655
715
  bind = getattr(model, "_bind_meta_table", None)
656
716
  if not callable(bind):
@@ -665,6 +725,10 @@ def _is_platform_managed_metatable_model(model: Any) -> bool:
665
725
  return isinstance(model, type) and issubclass(model, PlatformManagedMetaTable)
666
726
 
667
727
 
728
+ def _is_platform_time_index_metatable_model(model: Any) -> bool:
729
+ return isinstance(model, type) and issubclass(model, PlatformTimeIndexMetaData)
730
+
731
+
668
732
  __all__ = [
669
733
  "AlembicMetaTableMigration",
670
734
  "AlembicVersionMetaTable",
@@ -506,6 +506,10 @@ class PlatformTimeIndexMetaData(PlatformManagedMetaTable):
506
506
 
507
507
  @classmethod
508
508
  def _bind_meta_table(cls, meta_table: TimeIndexMetaData) -> TimeIndexMetaData:
509
+ from mainsequence.client.metatables import TimeIndexMetaData
510
+
511
+ if not isinstance(meta_table, TimeIndexMetaData):
512
+ meta_table = cls._resolve_time_index_metadata_for_bind(meta_table)
509
513
  bound = super()._bind_meta_table(meta_table)
510
514
  cls.__time_index_metadata__ = bound
511
515
  return bound
@@ -514,6 +518,19 @@ class PlatformTimeIndexMetaData(PlatformManagedMetaTable):
514
518
  def get_time_index_metadata(cls) -> TimeIndexMetaData | None:
515
519
  return getattr(cls, "__time_index_metadata__", None)
516
520
 
521
+ @classmethod
522
+ def _resolve_time_index_metadata_for_bind(cls, meta_table: Any) -> TimeIndexMetaData:
523
+ from mainsequence.client.metatables import TimeIndexMetaData
524
+
525
+ meta_table_uid = _meta_table_uid(meta_table)
526
+ if meta_table_uid not in (None, "") and _meta_table_is_time_indexed(meta_table):
527
+ return TimeIndexMetaData.get_by_uid(str(meta_table_uid))
528
+ model_name = getattr(cls, "__qualname__", cls.__name__)
529
+ received_type = type(meta_table).__name__
530
+ raise TypeError(
531
+ f"{model_name} requires TimeIndexMetaData binding; got {received_type}."
532
+ )
533
+
517
534
  @classmethod
518
535
  def __table_cls__(cls, *args: Any, **kwargs: Any) -> Any:
519
536
  if len(args) < 2:
@@ -1355,6 +1372,29 @@ def _meta_table_physical_table_name(meta_table: Any) -> str | None:
1355
1372
  return str(physical_table_name)
1356
1373
 
1357
1374
 
1375
+ def _meta_table_is_time_indexed(meta_table: Any) -> bool:
1376
+ if isinstance(meta_table, Mapping):
1377
+ time_indexed = meta_table.get("time_indexed")
1378
+ table_kind = meta_table.get("table_kind")
1379
+ table_contract = meta_table.get("table_contract")
1380
+ time_indexed_profile = meta_table.get("time_indexed_profile")
1381
+ else:
1382
+ time_indexed = getattr(meta_table, "time_indexed", None)
1383
+ table_kind = getattr(meta_table, "table_kind", None)
1384
+ table_contract = getattr(meta_table, "table_contract", None)
1385
+ time_indexed_profile = getattr(meta_table, "time_indexed_profile", None)
1386
+
1387
+ if time_indexed is True or table_kind == "time_indexed" or time_indexed_profile is not None:
1388
+ return True
1389
+ if isinstance(table_contract, Mapping):
1390
+ return (
1391
+ table_contract.get("table_kind") == "time_indexed"
1392
+ or table_contract.get("dynamic_table") is not None
1393
+ or table_contract.get("time_indexed") is not None
1394
+ )
1395
+ return False
1396
+
1397
+
1358
1398
  def _model_bound_storage_hash(model_or_table: Any) -> str | None:
1359
1399
  storage_hash = getattr(model_or_table, "__metatable_storage_hash__", None)
1360
1400
  if storage_hash not in (None, ""):
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: mainsequence
3
- Version: 4.2.1
3
+ Version: 4.2.2
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
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "mainsequence"
7
- version = "4.2.1"
7
+ version = "4.2.2"
8
8
  description = "Main Sequence SDK "
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.11"
@@ -1,16 +1,17 @@
1
1
  from __future__ import annotations
2
2
 
3
+ import datetime
3
4
  import importlib
4
5
  import textwrap
5
6
  import types
6
7
  import uuid
7
8
 
8
9
  import pytest
9
- from sqlalchemy import Column, Integer, MetaData, Table, Uuid
10
+ from sqlalchemy import Column, DateTime, Integer, MetaData, String, Table, Uuid
10
11
  from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column
11
12
 
12
- from mainsequence.client.metatables import MetaTable
13
- from mainsequence.meta_tables import PlatformManagedMetaTable
13
+ from mainsequence.client.metatables import MetaTable, TimeIndexMetaData
14
+ from mainsequence.meta_tables import PlatformManagedMetaTable, PlatformTimeIndexMetaData
14
15
  from mainsequence.meta_tables.migrations import (
15
16
  DEFAULT_ALEMBIC_VERSION_COLUMN_NAME,
16
17
  DEFAULT_ALEMBIC_VERSION_IDENTIFIER,
@@ -404,6 +405,76 @@ def test_alembic_metatable_migration_bulk_resolves_existing_models(monkeypatch):
404
405
  assert Account.get_meta_table_uid() == "account-meta-table-uid"
405
406
 
406
407
 
408
+ def test_alembic_metatable_migration_bulk_resolves_time_index_models_with_dynamic_client(
409
+ monkeypatch,
410
+ ):
411
+ class Base(DeclarativeBase):
412
+ metadata = MetaData()
413
+
414
+ class ProjectAlembicVersion(AlembicVersionMetaTable):
415
+ __metatable_data_source_uid__ = "data-source-uid"
416
+
417
+ class AccountHoldings(PlatformTimeIndexMetaData, Base):
418
+ __metatable_namespace__ = "markets"
419
+ __metatable_identifier__ = "markets.AccountHoldings"
420
+ __metatable_data_source_uid__ = "data-source-uid"
421
+ __time_index_name__ = "time_index"
422
+ __index_names__ = ["time_index", "unique_identifier"]
423
+
424
+ time_index: Mapped[datetime.datetime] = mapped_column(
425
+ DateTime(timezone=True),
426
+ nullable=False,
427
+ )
428
+ unique_identifier: Mapped[str] = mapped_column(String(255), nullable=False)
429
+
430
+ migration = AlembicMetaTableMigration(
431
+ package="msm",
432
+ migration_namespace="markets",
433
+ script_location="msm:alembic",
434
+ target_metadata=Base.metadata,
435
+ alembic_registry=ProjectAlembicVersion,
436
+ metatable_models=[AccountHoldings],
437
+ )
438
+
439
+ filter_calls = []
440
+
441
+ def fake_time_index_filter(**kwargs):
442
+ filter_calls.append(kwargs)
443
+ return [
444
+ TimeIndexMetaData.model_construct(
445
+ identifier="markets.AccountHoldings",
446
+ uid="holdings-meta-table-uid",
447
+ data_source_uid="data-source-uid",
448
+ storage_hash="holdings-storage-hash",
449
+ physical_table_name="mt_holdings",
450
+ )
451
+ ]
452
+
453
+ monkeypatch.setattr(
454
+ TimeIndexMetaData,
455
+ "filter",
456
+ staticmethod(fake_time_index_filter),
457
+ )
458
+ monkeypatch.setattr(
459
+ MetaTable,
460
+ "filter",
461
+ staticmethod(lambda **kwargs: (_ for _ in ()).throw(AssertionError())),
462
+ )
463
+
464
+ resolved = migration.resolve_or_register_metatable_models(timeout=25)
465
+
466
+ assert filter_calls == [
467
+ {
468
+ "identifier__in": ["markets.AccountHoldings"],
469
+ "include_relations_detail": True,
470
+ "timeout": 25,
471
+ }
472
+ ]
473
+ assert [meta_table.uid for meta_table in resolved] == ["holdings-meta-table-uid"]
474
+ assert AccountHoldings.get_time_index_metadata() is resolved[0]
475
+ assert isinstance(AccountHoldings.get_time_index_metadata(), TimeIndexMetaData)
476
+
477
+
407
478
  def test_alembic_metatable_migration_does_not_call_hook_when_model_registration_fails(
408
479
  monkeypatch,
409
480
  ):
@@ -11,7 +11,9 @@ import mainsequence.meta_tables.sqlalchemy_contracts as sqlalchemy_contracts
11
11
  from mainsequence.client.metatables import (
12
12
  DataSource,
13
13
  DynamicTableDataSource,
14
+ MetaTable,
14
15
  MetaTableRegistrationRequest,
16
+ TimeIndexMetaData,
15
17
  TimeIndexMetaTableRegistrationRequest,
16
18
  )
17
19
  from mainsequence.meta_tables import (
@@ -1256,6 +1258,56 @@ def test_time_index_metadata_register_posts_to_dynamic_table_endpoint(monkeypatc
1256
1258
  ]
1257
1259
 
1258
1260
 
1261
+ def test_time_index_metadata_bind_rehydrates_flagged_generic_metatable(monkeypatch):
1262
+ class AccountHoldings(PlatformTimeIndexMetaData):
1263
+ pass
1264
+
1265
+ typed_meta_table = TimeIndexMetaData.model_construct(
1266
+ uid="aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa",
1267
+ data_source_uid="dddddddd-dddd-4ddd-8ddd-dddddddddddd",
1268
+ storage_hash="holdings-storage-hash",
1269
+ physical_table_name="mt_holdings",
1270
+ )
1271
+ generic_meta_table = MetaTable.model_construct(
1272
+ uid="aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa",
1273
+ data_source_uid="dddddddd-dddd-4ddd-8ddd-dddddddddddd",
1274
+ storage_hash="holdings-storage-hash",
1275
+ management_mode="platform_managed",
1276
+ physical_table_name="mt_holdings",
1277
+ time_indexed=True,
1278
+ table_kind="time_indexed",
1279
+ )
1280
+
1281
+ calls = []
1282
+
1283
+ def fake_get_by_uid(cls, uid, timeout=None, **filters):
1284
+ calls.append((uid, timeout, filters))
1285
+ return typed_meta_table
1286
+
1287
+ monkeypatch.setattr(TimeIndexMetaData, "get_by_uid", classmethod(fake_get_by_uid))
1288
+
1289
+ AccountHoldings._bind_meta_table(generic_meta_table)
1290
+
1291
+ assert calls == [("aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa", None, {})]
1292
+ assert AccountHoldings.get_time_index_metadata() is typed_meta_table
1293
+
1294
+
1295
+ def test_time_index_metadata_bind_rejects_unflagged_generic_metatable():
1296
+ class AccountHoldings(PlatformTimeIndexMetaData):
1297
+ pass
1298
+
1299
+ generic_meta_table = MetaTable.model_construct(
1300
+ uid="aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa",
1301
+ data_source_uid="dddddddd-dddd-4ddd-8ddd-dddddddddddd",
1302
+ storage_hash="holdings-storage-hash",
1303
+ management_mode="platform_managed",
1304
+ physical_table_name="mt_holdings",
1305
+ )
1306
+
1307
+ with pytest.raises(TypeError, match="requires TimeIndexMetaData binding"):
1308
+ AccountHoldings._bind_meta_table(generic_meta_table)
1309
+
1310
+
1259
1311
  def test_ensure_registered_storage_table_rejects_unbound_storage():
1260
1312
  columns = [
1261
1313
  FakeColumn("time_index", DateTime(timezone=True), nullable=False),
@@ -58,6 +58,8 @@ def _platform_storage_model(meta_table: MetaTable) -> type[PlatformTimeIndexMeta
58
58
  class RuntimeStorageTable(PlatformTimeIndexMetaData):
59
59
  pass
60
60
 
61
+ if not isinstance(meta_table, TimeIndexMetaData):
62
+ meta_table = TimeIndexMetaData.model_construct(**meta_table.model_dump())
61
63
  RuntimeStorageTable._bind_meta_table(meta_table)
62
64
  return RuntimeStorageTable
63
65
 
@@ -370,7 +372,8 @@ def test_persist_manager_validates_storage_table_without_creating_storage():
370
372
  }
371
373
  ]
372
374
  assert manager.storage_table is storage_table
373
- assert manager.storage_metadata is meta_table
375
+ assert manager.storage_metadata.uid == meta_table.uid
376
+ assert isinstance(manager.storage_metadata, TimeIndexMetaData)
374
377
 
375
378
 
376
379
  def test_persist_manager_rejects_unbound_platform_time_index_storage_table():
@@ -481,7 +484,8 @@ def test_persist_manager_preserves_storage_table_during_update_lookup():
481
484
 
482
485
  assert manager.data_node_update.data_node_storage is stale_response_storage
483
486
  assert manager.storage_table is storage_table
484
- assert manager.storage_metadata is meta_table
487
+ assert manager.storage_metadata.uid == meta_table.uid
488
+ assert isinstance(manager.storage_metadata, TimeIndexMetaData)
485
489
 
486
490
 
487
491
  def test_data_node_accepts_platform_time_index_storage_table_runtime_argument():
File without changes
File without changes
File without changes