mainsequence 4.3.0__tar.gz → 4.3.6__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 (129) hide show
  1. {mainsequence-4.3.0/mainsequence.egg-info → mainsequence-4.3.6}/PKG-INFO +1 -1
  2. {mainsequence-4.3.0 → mainsequence-4.3.6}/agent_scaffold/skills/data_publishing/data_nodes/SKILL.md +7 -0
  3. {mainsequence-4.3.0 → mainsequence-4.3.6}/agent_scaffold/skills/data_publishing/meta_tables/SKILL.md +5 -0
  4. {mainsequence-4.3.0 → mainsequence-4.3.6}/mainsequence/cli/migrations.py +25 -8
  5. {mainsequence-4.3.0 → mainsequence-4.3.6}/mainsequence/client/metatables/core.py +87 -9
  6. {mainsequence-4.3.0 → mainsequence-4.3.6}/mainsequence/meta_tables/compiled_sql/v1.py +9 -2
  7. {mainsequence-4.3.0 → mainsequence-4.3.6}/mainsequence/meta_tables/data_nodes/data_nodes.py +0 -1
  8. {mainsequence-4.3.0 → mainsequence-4.3.6}/mainsequence/meta_tables/data_nodes/persist_managers.py +0 -1
  9. {mainsequence-4.3.0 → mainsequence-4.3.6}/mainsequence/meta_tables/migrations.py +72 -0
  10. {mainsequence-4.3.0 → mainsequence-4.3.6}/mainsequence/meta_tables/sqlalchemy_contracts.py +37 -1
  11. {mainsequence-4.3.0 → mainsequence-4.3.6/mainsequence.egg-info}/PKG-INFO +1 -1
  12. {mainsequence-4.3.0 → mainsequence-4.3.6}/mainsequence.egg-info/entry_points.txt +1 -0
  13. {mainsequence-4.3.0 → mainsequence-4.3.6}/pyproject.toml +2 -1
  14. {mainsequence-4.3.0 → mainsequence-4.3.6}/tests/test_cli_migrations.py +102 -1
  15. {mainsequence-4.3.0 → mainsequence-4.3.6}/tests/test_data_access_mixin_dimension_audit.py +0 -2
  16. {mainsequence-4.3.0 → mainsequence-4.3.6}/tests/test_filter_normalization.py +16 -0
  17. {mainsequence-4.3.0 → mainsequence-4.3.6}/tests/test_meta_table_migrations.py +52 -0
  18. {mainsequence-4.3.0 → mainsequence-4.3.6}/tests/test_meta_tables_client_models.py +44 -1
  19. {mainsequence-4.3.0 → mainsequence-4.3.6}/tests/test_meta_tables_sqlalchemy_contracts.py +75 -0
  20. {mainsequence-4.3.0 → mainsequence-4.3.6}/LICENSE +0 -0
  21. {mainsequence-4.3.0 → mainsequence-4.3.6}/README.md +0 -0
  22. {mainsequence-4.3.0 → mainsequence-4.3.6}/agent_scaffold/AGENTS.md +0 -0
  23. {mainsequence-4.3.0 → mainsequence-4.3.6}/agent_scaffold/skills/a2a_communication/SKILL.md +0 -0
  24. {mainsequence-4.3.0 → mainsequence-4.3.6}/agent_scaffold/skills/application_surfaces/api_surfaces/SKILL.md +0 -0
  25. {mainsequence-4.3.0 → mainsequence-4.3.6}/agent_scaffold/skills/command_center/adapter_from_api/SKILL.md +0 -0
  26. {mainsequence-4.3.0 → mainsequence-4.3.6}/agent_scaffold/skills/command_center/api_mock_prototyping/SKILL.md +0 -0
  27. {mainsequence-4.3.0 → mainsequence-4.3.6}/agent_scaffold/skills/command_center/app_components/SKILL.md +0 -0
  28. {mainsequence-4.3.0 → mainsequence-4.3.6}/agent_scaffold/skills/command_center/connections/SKILL.md +0 -0
  29. {mainsequence-4.3.0 → mainsequence-4.3.6}/agent_scaffold/skills/command_center/workspace_analysis/SKILL.md +0 -0
  30. {mainsequence-4.3.0 → mainsequence-4.3.6}/agent_scaffold/skills/command_center/workspace_builder/SKILL.md +0 -0
  31. {mainsequence-4.3.0 → mainsequence-4.3.6}/agent_scaffold/skills/command_center/workspace_design/SKILL.md +0 -0
  32. {mainsequence-4.3.0 → mainsequence-4.3.6}/agent_scaffold/skills/dashboards/streamlit/SKILL.md +0 -0
  33. {mainsequence-4.3.0 → mainsequence-4.3.6}/agent_scaffold/skills/data_access/exploration/SKILL.md +0 -0
  34. {mainsequence-4.3.0 → mainsequence-4.3.6}/agent_scaffold/skills/maintenance/bug_auditor/SKILL.md +0 -0
  35. {mainsequence-4.3.0 → mainsequence-4.3.6}/agent_scaffold/skills/ms-markets/SKILL.md +0 -0
  36. {mainsequence-4.3.0 → mainsequence-4.3.6}/agent_scaffold/skills/platform_operations/access_control_and_sharing/SKILL.md +0 -0
  37. {mainsequence-4.3.0 → mainsequence-4.3.6}/agent_scaffold/skills/platform_operations/orchestration_and_releases/SKILL.md +0 -0
  38. {mainsequence-4.3.0 → mainsequence-4.3.6}/agent_scaffold/skills/project_builder/SKILL.md +0 -0
  39. {mainsequence-4.3.0 → mainsequence-4.3.6}/agent_scaffold/skills/project_to_agent/SKILL.md +0 -0
  40. {mainsequence-4.3.0 → mainsequence-4.3.6}/mainsequence/__init__.py +0 -0
  41. {mainsequence-4.3.0 → mainsequence-4.3.6}/mainsequence/__main__.py +0 -0
  42. {mainsequence-4.3.0 → mainsequence-4.3.6}/mainsequence/bootstrap.py +0 -0
  43. {mainsequence-4.3.0 → mainsequence-4.3.6}/mainsequence/cli/__init__.py +0 -0
  44. {mainsequence-4.3.0 → mainsequence-4.3.6}/mainsequence/cli/api.py +0 -0
  45. {mainsequence-4.3.0 → mainsequence-4.3.6}/mainsequence/cli/browser_auth.py +0 -0
  46. {mainsequence-4.3.0 → mainsequence-4.3.6}/mainsequence/cli/cli.py +0 -0
  47. {mainsequence-4.3.0 → mainsequence-4.3.6}/mainsequence/cli/config.py +0 -0
  48. {mainsequence-4.3.0 → mainsequence-4.3.6}/mainsequence/cli/docker_utils.py +0 -0
  49. {mainsequence-4.3.0 → mainsequence-4.3.6}/mainsequence/cli/doctor.py +0 -0
  50. {mainsequence-4.3.0 → mainsequence-4.3.6}/mainsequence/cli/local_ops.py +0 -0
  51. {mainsequence-4.3.0 → mainsequence-4.3.6}/mainsequence/cli/model_filters.py +0 -0
  52. {mainsequence-4.3.0 → mainsequence-4.3.6}/mainsequence/cli/project_status.py +0 -0
  53. {mainsequence-4.3.0 → mainsequence-4.3.6}/mainsequence/cli/pydantic_cli.py +0 -0
  54. {mainsequence-4.3.0 → mainsequence-4.3.6}/mainsequence/cli/sdk_utils.py +0 -0
  55. {mainsequence-4.3.0 → mainsequence-4.3.6}/mainsequence/cli/ssh_utils.py +0 -0
  56. {mainsequence-4.3.0 → mainsequence-4.3.6}/mainsequence/cli/ui.py +0 -0
  57. {mainsequence-4.3.0 → mainsequence-4.3.6}/mainsequence/client/__init__.py +0 -0
  58. {mainsequence-4.3.0 → mainsequence-4.3.6}/mainsequence/client/agent_runtime_models.py +0 -0
  59. {mainsequence-4.3.0 → mainsequence-4.3.6}/mainsequence/client/base.py +0 -0
  60. {mainsequence-4.3.0 → mainsequence-4.3.6}/mainsequence/client/client.py +0 -0
  61. {mainsequence-4.3.0 → mainsequence-4.3.6}/mainsequence/client/command_center/__init__.py +0 -0
  62. {mainsequence-4.3.0 → mainsequence-4.3.6}/mainsequence/client/command_center/app_component.py +0 -0
  63. {mainsequence-4.3.0 → mainsequence-4.3.6}/mainsequence/client/command_center/connections.py +0 -0
  64. {mainsequence-4.3.0 → mainsequence-4.3.6}/mainsequence/client/command_center/data_models.py +0 -0
  65. {mainsequence-4.3.0 → mainsequence-4.3.6}/mainsequence/client/command_center/workspace.py +0 -0
  66. {mainsequence-4.3.0 → mainsequence-4.3.6}/mainsequence/client/command_center/workspace_snapshot.py +0 -0
  67. {mainsequence-4.3.0 → mainsequence-4.3.6}/mainsequence/client/compute_validation.py +0 -0
  68. {mainsequence-4.3.0 → mainsequence-4.3.6}/mainsequence/client/data_sources_interfaces/__init__.py +0 -0
  69. {mainsequence-4.3.0 → mainsequence-4.3.6}/mainsequence/client/data_sources_interfaces/duckdb.py +0 -0
  70. {mainsequence-4.3.0 → mainsequence-4.3.6}/mainsequence/client/data_sources_interfaces/local_paths.py +0 -0
  71. {mainsequence-4.3.0 → mainsequence-4.3.6}/mainsequence/client/data_sources_interfaces/sqlite.py +0 -0
  72. {mainsequence-4.3.0 → mainsequence-4.3.6}/mainsequence/client/dtype_codec.py +0 -0
  73. {mainsequence-4.3.0 → mainsequence-4.3.6}/mainsequence/client/exceptions.py +0 -0
  74. {mainsequence-4.3.0 → mainsequence-4.3.6}/mainsequence/client/fastapi/__init__.py +0 -0
  75. {mainsequence-4.3.0 → mainsequence-4.3.6}/mainsequence/client/fastapi/auth.py +0 -0
  76. {mainsequence-4.3.0 → mainsequence-4.3.6}/mainsequence/client/metatables/__init__.py +0 -0
  77. {mainsequence-4.3.0 → mainsequence-4.3.6}/mainsequence/client/models_foundry.py +0 -0
  78. {mainsequence-4.3.0 → mainsequence-4.3.6}/mainsequence/client/models_helpers.py +0 -0
  79. {mainsequence-4.3.0 → mainsequence-4.3.6}/mainsequence/client/models_user.py +0 -0
  80. {mainsequence-4.3.0 → mainsequence-4.3.6}/mainsequence/client/utils.py +0 -0
  81. {mainsequence-4.3.0 → mainsequence-4.3.6}/mainsequence/defaults.py +0 -0
  82. {mainsequence-4.3.0 → mainsequence-4.3.6}/mainsequence/instrumentation/__init__.py +0 -0
  83. {mainsequence-4.3.0 → mainsequence-4.3.6}/mainsequence/instrumentation/utils.py +0 -0
  84. {mainsequence-4.3.0 → mainsequence-4.3.6}/mainsequence/logconf.py +0 -0
  85. {mainsequence-4.3.0 → mainsequence-4.3.6}/mainsequence/meta_tables/__init__.py +0 -0
  86. {mainsequence-4.3.0 → mainsequence-4.3.6}/mainsequence/meta_tables/__main__.py +0 -0
  87. {mainsequence-4.3.0 → mainsequence-4.3.6}/mainsequence/meta_tables/compiled_sql/__init__.py +0 -0
  88. {mainsequence-4.3.0 → mainsequence-4.3.6}/mainsequence/meta_tables/data_nodes/__init__.py +0 -0
  89. {mainsequence-4.3.0 → mainsequence-4.3.6}/mainsequence/meta_tables/data_nodes/build_operations.py +0 -0
  90. {mainsequence-4.3.0 → mainsequence-4.3.6}/mainsequence/meta_tables/data_nodes/models.py +0 -0
  91. {mainsequence-4.3.0 → mainsequence-4.3.6}/mainsequence/meta_tables/data_nodes/namespacing.py +0 -0
  92. {mainsequence-4.3.0 → mainsequence-4.3.6}/mainsequence/meta_tables/data_nodes/run_operations.py +0 -0
  93. {mainsequence-4.3.0 → mainsequence-4.3.6}/mainsequence/meta_tables/data_nodes/utils.py +0 -0
  94. {mainsequence-4.3.0 → mainsequence-4.3.6}/mainsequence/meta_tables/future_registry.py +0 -0
  95. {mainsequence-4.3.0 → mainsequence-4.3.6}/mainsequence/meta_tables/hashing.py +0 -0
  96. {mainsequence-4.3.0 → mainsequence-4.3.6}/mainsequence/meta_tables/pydantic_metadata.py +0 -0
  97. {mainsequence-4.3.0 → mainsequence-4.3.6}/mainsequence/meta_tables/schema_names.py +0 -0
  98. {mainsequence-4.3.0 → mainsequence-4.3.6}/mainsequence/runtime_flags.py +0 -0
  99. {mainsequence-4.3.0 → mainsequence-4.3.6}/mainsequence.egg-info/SOURCES.txt +0 -0
  100. {mainsequence-4.3.0 → mainsequence-4.3.6}/mainsequence.egg-info/dependency_links.txt +0 -0
  101. {mainsequence-4.3.0 → mainsequence-4.3.6}/mainsequence.egg-info/requires.txt +0 -0
  102. {mainsequence-4.3.0 → mainsequence-4.3.6}/mainsequence.egg-info/top_level.txt +0 -0
  103. {mainsequence-4.3.0 → mainsequence-4.3.6}/setup.cfg +0 -0
  104. {mainsequence-4.3.0 → mainsequence-4.3.6}/tests/test_auth_precedence.py +0 -0
  105. {mainsequence-4.3.0 → mainsequence-4.3.6}/tests/test_build_operations_hashing.py +0 -0
  106. {mainsequence-4.3.0 → mainsequence-4.3.6}/tests/test_cli.py +0 -0
  107. {mainsequence-4.3.0 → mainsequence-4.3.6}/tests/test_cli_browser_auth.py +0 -0
  108. {mainsequence-4.3.0 → mainsequence-4.3.6}/tests/test_client.py +0 -0
  109. {mainsequence-4.3.0 → mainsequence-4.3.6}/tests/test_command_center_app_component_models.py +0 -0
  110. {mainsequence-4.3.0 → mainsequence-4.3.6}/tests/test_command_center_data_models.py +0 -0
  111. {mainsequence-4.3.0 → mainsequence-4.3.6}/tests/test_command_center_models.py +0 -0
  112. {mainsequence-4.3.0 → mainsequence-4.3.6}/tests/test_data_node_storage_dimension_queries.py +0 -0
  113. {mainsequence-4.3.0 → mainsequence-4.3.6}/tests/test_data_node_update_flow.py +0 -0
  114. {mainsequence-4.3.0 → mainsequence-4.3.6}/tests/test_dependency_extras.py +0 -0
  115. {mainsequence-4.3.0 → mainsequence-4.3.6}/tests/test_duckdb_interface_dimensions.py +0 -0
  116. {mainsequence-4.3.0 → mainsequence-4.3.6}/tests/test_instrumentation.py +0 -0
  117. {mainsequence-4.3.0 → mainsequence-4.3.6}/tests/test_logconf.py +0 -0
  118. {mainsequence-4.3.0 → mainsequence-4.3.6}/tests/test_models_user_request_bound_auth.py +0 -0
  119. {mainsequence-4.3.0 → mainsequence-4.3.6}/tests/test_pod_project_resolution.py +0 -0
  120. {mainsequence-4.3.0 → mainsequence-4.3.6}/tests/test_project_batch_jobs_from_file.py +0 -0
  121. {mainsequence-4.3.0 → mainsequence-4.3.6}/tests/test_run_configuration.py +0 -0
  122. {mainsequence-4.3.0 → mainsequence-4.3.6}/tests/test_schema_names.py +0 -0
  123. {mainsequence-4.3.0 → mainsequence-4.3.6}/tests/test_secret_client_model.py +0 -0
  124. {mainsequence-4.3.0 → mainsequence-4.3.6}/tests/test_source_table_configuration.py +0 -0
  125. {mainsequence-4.3.0 → mainsequence-4.3.6}/tests/test_sqlite_interface_dimensions.py +0 -0
  126. {mainsequence-4.3.0 → mainsequence-4.3.6}/tests/test_update_runner_uid_runtime.py +0 -0
  127. {mainsequence-4.3.0 → mainsequence-4.3.6}/tests/test_update_statistics.py +0 -0
  128. {mainsequence-4.3.0 → mainsequence-4.3.6}/tests/test_update_uid_guards.py +0 -0
  129. {mainsequence-4.3.0 → mainsequence-4.3.6}/tests/test_workspace_snapshot.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: mainsequence
3
- Version: 4.3.0
3
+ Version: 4.3.6
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
@@ -159,6 +159,7 @@ class PricesTable(PlatformTimeIndexMetaTable, Base):
159
159
  "risk analytics."
160
160
  )
161
161
  __time_index_name__ = "time_index"
162
+ __cadence__ = "1d"
162
163
  __index_names__ = ["time_index", "unique_identifier"]
163
164
 
164
165
  time_index: Mapped[datetime.datetime] = mapped_column(
@@ -197,6 +198,12 @@ autogenerate runs. Do not manually repeat the full grain unique index in
197
198
  `__table_args__`; add ordinary SQLAlchemy `Index(...)` entries only for
198
199
  additional lookup/performance paths.
199
200
 
201
+ When the dataset has a known stable observation interval, declare `__cadence__`
202
+ on the `PlatformTimeIndexMetaTable` model, for example `1m`, `5m`, `1h`, `1d`,
203
+ `1w`, `1mo`, `1q`, or `1y`. Cadence is table metadata and should be included
204
+ whenever possible; do not make it a DataNode runtime configuration field unless
205
+ changing it actually changes the produced dataset identity.
206
+
200
207
  `PlatformTimeIndexMetaTable.register()` remains SDK plumbing for the migration
201
208
  workflow. Do not manually attach an existing UID, reconstruct a generic
202
209
  `MetaTable`, or use manual bind helpers as an authoring step.
@@ -137,6 +137,11 @@ first version, use Alembic. Keep the SDK model as a normal
137
137
  `PlatformManagedMetaTable` or `PlatformTimeIndexMetaTable` catalog contract, and
138
138
  apply physical schema changes through the Alembic migration workflow.
139
139
 
140
+ For `PlatformTimeIndexMetaTable`, declare `__cadence__` whenever the table has a
141
+ known stable observation interval, for example `1m`, `5m`, `1h`, `1d`, `1w`,
142
+ `1mo`, `1q`, or `1y`. Cadence is table metadata and belongs on the storage
143
+ model when possible.
144
+
140
145
  Default-schema tables must leave SQLAlchemy `Table.schema` unset; do not write
141
146
  `__table_args__ = {"schema": "public"}` for the default PostgreSQL schema. Set
142
147
  schema metadata only for non-default schemas, using `__table_args__ = {"schema":
@@ -122,12 +122,21 @@ def _load_alembic_command(command_name: str) -> Any:
122
122
  return command
123
123
 
124
124
 
125
+ def _coerce_alembic_version_path(version_path: str) -> str:
126
+ try:
127
+ from alembic import util
128
+ except ImportError as exc:
129
+ raise typer.BadParameter("Alembic is required for migration commands.") from exc
130
+ return str(util.coerce_resource_to_filename(version_path))
131
+
132
+
125
133
  def _emit_alembic_script_context(
126
134
  config: Any,
127
135
  *,
128
136
  target_revision: str | None = None,
129
137
  ) -> None:
130
138
  script_location = config.get_main_option("script_location")
139
+ version_locations = config.get_main_option("version_locations")
131
140
  version_table = config.get_main_option("version_table")
132
141
  version_table_schema = config.get_main_option("version_table_schema")
133
142
  version_table_label = (
@@ -135,9 +144,14 @@ def _emit_alembic_script_context(
135
144
  if version_table_schema not in (None, "")
136
145
  else version_table
137
146
  )
147
+ version_locations_label = (
148
+ version_locations.replace("\n", ", ") if version_locations else "<default>"
149
+ )
138
150
  _emit_status(
139
151
  "Alembic script context "
140
- f"script_location={script_location} version_table={version_table_label}"
152
+ f"script_location={script_location} "
153
+ f"version_locations={version_locations_label} "
154
+ f"version_table={version_table_label}"
141
155
  )
142
156
  try:
143
157
  from alembic.script import ScriptDirectory
@@ -745,14 +759,17 @@ def revision(
745
759
  )
746
760
  _emit_alembic_script_context(config, target_revision=head)
747
761
  _emit_status(f"Starting Alembic revision now rev_id={resolved_rev_id}...")
762
+ revision_kwargs = {
763
+ "message": resolved_message,
764
+ "autogenerate": autogenerate,
765
+ "rev_id": resolved_rev_id,
766
+ "head": head,
767
+ }
768
+ version_path = migration.resolved_version_path()
769
+ if version_path is not None:
770
+ revision_kwargs["version_path"] = _coerce_alembic_version_path(version_path)
748
771
  with _forward_alembic_logging():
749
- script = command.revision(
750
- config,
751
- message=resolved_message,
752
- autogenerate=autogenerate,
753
- rev_id=resolved_rev_id,
754
- head=head,
755
- )
772
+ script = command.revision(config, **revision_kwargs)
756
773
  _emit_status("Alembic revision finished.")
757
774
  _emit(
758
775
  {
@@ -8,6 +8,7 @@ import gzip
8
8
  import json
9
9
  import math
10
10
  import os
11
+ import re
11
12
  import time
12
13
  from collections.abc import Callable, Mapping, Sequence
13
14
  from dataclasses import dataclass
@@ -58,6 +59,24 @@ from ..utils import (
58
59
  DUCK_DB = "duck_db"
59
60
  SQLITE = "sqlite"
60
61
  LOCAL_DATA_SOURCE_CLASS_TYPES = {DUCK_DB, SQLITE}
62
+ _TIME_INDEXED_CADENCE_RE = re.compile(
63
+ r"^[1-9][0-9]*(mo|s|m|h|d|w|q|y)$",
64
+ re.IGNORECASE,
65
+ )
66
+
67
+
68
+ def _normalize_time_indexed_cadence(value: Any) -> str | None:
69
+ if value is None:
70
+ return None
71
+ normalized = str(value).strip().lower()
72
+ if not normalized:
73
+ return None
74
+ if not _TIME_INDEXED_CADENCE_RE.fullmatch(normalized):
75
+ raise ValueError(
76
+ "cadence must be an interval token such as 1m, 5m, 1h, 1d, 1w, "
77
+ "1mo, 1q, or 1y."
78
+ )
79
+ return normalized
61
80
 
62
81
 
63
82
  def _duckdb_interface():
@@ -452,8 +471,43 @@ class MetaTableOperationScopeTable(BasePydanticModel):
452
471
 
453
472
 
454
473
  class MetaTableOperationScope(BasePydanticModel):
474
+ data_source_uid: str | None = Field(
475
+ default=None,
476
+ min_length=1,
477
+ validation_alias=AliasChoices("data_source_uid", "dataSourceUid"),
478
+ description=(
479
+ "Public UID of the DynamicTableDataSource that owns the compiled "
480
+ "SQL execution connection. If omitted, the SDK resolves the "
481
+ "configured project/session default data source. Scoped MetaTables "
482
+ "are the permission contract, not the source of execution routing."
483
+ ),
484
+ )
455
485
  tables: list[MetaTableOperationScopeTable] = Field(..., min_length=1)
456
486
 
487
+ model_config = ConfigDict(populate_by_name=True)
488
+
489
+ @model_validator(mode="after")
490
+ def resolve_default_data_source_uid(self):
491
+ if self.data_source_uid not in (None, ""):
492
+ self.data_source_uid = str(self.data_source_uid)
493
+ return self
494
+
495
+ try:
496
+ data_source = get_session_data_source()
497
+ except Exception as exc:
498
+ raise ValueError(
499
+ "MetaTable compiled SQL scope requires data_source_uid or a "
500
+ "configured project/session default data source."
501
+ ) from exc
502
+
503
+ uid = getattr(data_source, "uid", None) or getattr(data_source, "data_source_uid", None)
504
+ if uid in (None, ""):
505
+ raise ValueError(
506
+ "Configured project/session default data source does not expose a uid."
507
+ )
508
+ self.data_source_uid = str(uid)
509
+ return self
510
+
457
511
 
458
512
  class MetaTableOperationLimits(BasePydanticModel):
459
513
  max_rows: int | None = Field(default=None, ge=1)
@@ -632,6 +686,11 @@ class ManagedMetaTableFinalizeTableResult(BasePydanticModel):
632
686
  )
633
687
  table_kind: str = Field(..., description="Backend table kind after reconciliation.")
634
688
  time_indexed: bool = Field(..., description="Whether the MetaTable is time-indexed.")
689
+ cadence: str | None = Field(
690
+ None,
691
+ max_length=32,
692
+ description="Optional declared cadence for time-indexed MetaTables.",
693
+ )
635
694
  finalized: bool = Field(
636
695
  ...,
637
696
  description="True when this row was reconciled and activated by this call.",
@@ -660,6 +719,11 @@ class ManagedMetaTableFinalizeTableResult(BasePydanticModel):
660
719
  description="Per-table structured error when finalization did not activate this row.",
661
720
  )
662
721
 
722
+ @field_validator("cadence")
723
+ @classmethod
724
+ def _normalize_cadence(cls, value: str | None) -> str | None:
725
+ return _normalize_time_indexed_cadence(value)
726
+
663
727
  model_config = ConfigDict(extra="ignore")
664
728
 
665
729
 
@@ -1888,6 +1952,14 @@ class TimeIndexMetaTableRegistrationRequest(BasePydanticModel):
1888
1952
  default_factory=lambda: {"create_table": True, "if_not_exists": True}
1889
1953
  )
1890
1954
  time_index_name: str = Field(..., description="Canonical timestamp column name")
1955
+ cadence: str | None = Field(
1956
+ None,
1957
+ max_length=32,
1958
+ description=(
1959
+ "Optional time-indexed cadence token such as 1m, 5m, 1h, 1d, "
1960
+ "1w, 1mo, 1q, or 1y."
1961
+ ),
1962
+ )
1891
1963
  partition_strategy: str = Field(
1892
1964
  default="backend_default",
1893
1965
  description="Time-indexed MetaTable physical partitioning strategy",
@@ -1938,6 +2010,11 @@ class TimeIndexMetaTableRegistrationRequest(BasePydanticModel):
1938
2010
  data["table_contract"] = normalized_contract
1939
2011
  return data
1940
2012
 
2013
+ @field_validator("cadence")
2014
+ @classmethod
2015
+ def _normalize_cadence(cls, value: str | None) -> str | None:
2016
+ return _normalize_time_indexed_cadence(value)
2017
+
1941
2018
 
1942
2019
  def _payload_get(obj: Any, key: str, default: Any = None) -> Any:
1943
2020
  if isinstance(obj, Mapping):
@@ -2078,6 +2155,11 @@ class TimeIndexedProfile(TimeIndexedProfileBase, BasePydanticModel):
2078
2155
  None, description="Public uid of the related TimeIndexMetaTable"
2079
2156
  )
2080
2157
  time_index_name: str = Field(..., max_length=100, description="Time index name")
2158
+ cadence: str | None = Field(
2159
+ None,
2160
+ max_length=32,
2161
+ description="Optional declared cadence for the time-indexed table.",
2162
+ )
2081
2163
  partition_strategy: str | None = None
2082
2164
  last_time_index_value: datetime.datetime | None = Field(
2083
2165
  None, description="Last time index value"
@@ -2089,6 +2171,11 @@ class TimeIndexedProfile(TimeIndexedProfileBase, BasePydanticModel):
2089
2171
  physical_index_plan: dict[str, Any] | None = Field(
2090
2172
  None, description="Server-rendered physical index plan"
2091
2173
  )
2174
+
2175
+ @field_validator("cadence")
2176
+ @classmethod
2177
+ def _normalize_cadence(cls, value: str | None) -> str | None:
2178
+ return _normalize_time_indexed_cadence(value)
2092
2179
  multi_index_stats: dict[str, Any] | None = Field(
2093
2180
  None, description="Canonical multi-index progress statistics"
2094
2181
  )
@@ -2928,15 +3015,6 @@ class TimeIndexMetaTable(MetaTable):
2928
3015
  "labels__in": "str",
2929
3016
  "labels__contains": "str",
2930
3017
  }
2931
- READ_QUERY_PARAMS: ClassVar[dict[str, str]] = {
2932
- "include_relations_detail": "bool",
2933
- }
2934
- READ_QUERY_PARAM_DESCRIPTIONS: ClassVar[dict[str, str]] = {
2935
- "include_relations_detail": (
2936
- "Expand related objects in the serializer response. "
2937
- "This changes response detail only and does not change which rows are returned."
2938
- ),
2939
- }
2940
3018
  build_configuration_json_schema: dict[str, Any] | None = Field(
2941
3019
  None,
2942
3020
  description="JSON schema describing the DataNode update build configuration.",
@@ -34,8 +34,8 @@ def build_operation(
34
34
  Build and validate the TS Manager compiled-sql.v1 operation contract.
35
35
 
36
36
  This is the client-side protocol object. It is intentionally plain SQL plus
37
- bound parameters and declared MetaTable scope, not a serialized SQLAlchemy
38
- object.
37
+ bound parameters, an explicit execution data source, and declared MetaTable
38
+ scope, not a serialized SQLAlchemy object.
39
39
  """
40
40
 
41
41
  if parameters is None:
@@ -72,6 +72,7 @@ def compile_sqlalchemy_statement(
72
72
  statement: Any,
73
73
  *,
74
74
  operation: MetaTableOperation,
75
+ data_source_uid: str | None = None,
75
76
  scope_tables: Sequence[MetaTableOperationScopeTable | Mapping[str, Any]],
76
77
  limits: MetaTableOperationLimits | Mapping[str, Any] | None = None,
77
78
  dialect: MetaTableCompiledSQLDialect = "postgresql",
@@ -80,6 +81,11 @@ def compile_sqlalchemy_statement(
80
81
  ) -> MetaTableCompiledSQLOperation:
81
82
  """
82
83
  Compile a SQLAlchemy/Core statement into the TS Manager compiled-sql.v1 payload.
84
+
85
+ ``data_source_uid`` selects the DynamicTableDataSource connection. If it is
86
+ omitted, the SDK resolves the configured project/session default data source.
87
+ ``scope_tables`` remains the declared MetaTable permission scope for the
88
+ operation.
83
89
  """
84
90
 
85
91
  if dialect != "postgresql":
@@ -97,6 +103,7 @@ def compile_sqlalchemy_statement(
97
103
  )
98
104
  parameter_types = _compiled_sqlalchemy_parameter_types(compiled)
99
105
  scope = MetaTableOperationScope(
106
+ data_source_uid=data_source_uid,
100
107
  tables=[
101
108
  (
102
109
  table
@@ -211,7 +211,6 @@ class APIDataNode(DataAccessMixin):
211
211
  def build_from_table_name(cls, table_name: str) -> "APIDataNode":
212
212
  storage_table = TimeIndexMetaTable.get_or_none(
213
213
  physical_table_name=table_name,
214
- include_relations_detail=True,
215
214
  )
216
215
  if storage_table is None:
217
216
  raise DoesNotExist(
@@ -567,7 +567,6 @@ class APIPersistManager:
567
567
  result = TimeIndexMetaTable.get_or_none(
568
568
  physical_table_name=self.storage_hash,
569
569
  data_source__uid=self.data_source_uid,
570
- include_relations_detail=True,
571
570
  )
572
571
  self._storage_table_future.set_result(result)
573
572
  except Exception as exc:
@@ -101,6 +101,49 @@ class AlembicProviderPhysicalStateError(RuntimeError):
101
101
  )
102
102
 
103
103
 
104
+ def _normalize_optional_alembic_location(value: Any, field_name: str) -> str | None:
105
+ if value is None:
106
+ return None
107
+ text = str(value).strip()
108
+ if not text:
109
+ raise ValueError(f"AlembicMetaTableMigration {field_name} cannot be empty.")
110
+ return text
111
+
112
+
113
+ def _normalize_alembic_version_locations(value: str | Sequence[str] | None) -> tuple[str, ...]:
114
+ if value is None:
115
+ return ()
116
+ raw_locations: Sequence[str]
117
+ if isinstance(value, str):
118
+ raw_locations = [value]
119
+ else:
120
+ raw_locations = value
121
+
122
+ locations: list[str] = []
123
+ seen: set[str] = set()
124
+ for raw_location in raw_locations:
125
+ location = _normalize_optional_alembic_location(raw_location, "version_locations")
126
+ if location is None or location in seen:
127
+ continue
128
+ seen.add(location)
129
+ locations.append(location)
130
+ if not locations:
131
+ raise ValueError("AlembicMetaTableMigration version_locations cannot be empty.")
132
+ return tuple(locations)
133
+
134
+
135
+ def _configure_alembic_version_locations(
136
+ config: Any,
137
+ version_locations: str | Sequence[str] | None,
138
+ ) -> tuple[str, ...]:
139
+ locations = _normalize_alembic_version_locations(version_locations)
140
+ if not locations:
141
+ return ()
142
+ config.set_main_option("version_locations", "\n".join(locations))
143
+ config.set_main_option("path_separator", "newline")
144
+ return locations
145
+
146
+
104
147
  class AlembicVersionMetaTable:
105
148
  """MetaTable catalog binding for Alembic's version table.
106
149
 
@@ -255,6 +298,8 @@ class AlembicMetaTableMigration:
255
298
  script_location: str
256
299
  target_metadata: Any
257
300
  alembic_registry: type[AlembicVersionMetaTable]
301
+ version_locations: str | Sequence[str] | None = None
302
+ version_path: str | None = None
258
303
  metatable_models: Sequence[type[Any]] = field(default_factory=tuple)
259
304
  after_register_metatables: Callable[[AlembicMetaTableCatalogRefreshContext], Any] | None = None
260
305
  include_name_hook: Any | None = None
@@ -273,6 +318,8 @@ class AlembicMetaTableMigration:
273
318
  self.after_register_metatables
274
319
  ):
275
320
  raise TypeError("after_register_metatables must be callable when provided.")
321
+ self.resolved_version_locations()
322
+ self.resolved_version_path()
276
323
  _normalize_provider_default_schemas(self.metatable_models)
277
324
  _ensure_provider_time_index_grain_indexes(self.metatable_models)
278
325
 
@@ -292,6 +339,23 @@ class AlembicMetaTableMigration:
292
339
  def version_table_schema(self) -> str | None:
293
340
  return self.alembic_registry.__alembic_version_schema__
294
341
 
342
+ def resolved_version_locations(self) -> tuple[str, ...]:
343
+ return _normalize_alembic_version_locations(self.version_locations)
344
+
345
+ def resolved_version_path(self) -> str | None:
346
+ explicit = _normalize_optional_alembic_location(self.version_path, "version_path")
347
+ if explicit is not None:
348
+ return explicit
349
+ locations = self.resolved_version_locations()
350
+ if len(locations) == 1:
351
+ return locations[0]
352
+ if len(locations) > 1:
353
+ raise ValueError(
354
+ "AlembicMetaTableMigration with multiple version_locations requires "
355
+ "an explicit version_path for revision generation."
356
+ )
357
+ return None
358
+
295
359
  def include_name(self, name: str | None, type_: str, parent_names: dict[str, Any]) -> bool:
296
360
  if self.include_name_hook is not None:
297
361
  return bool(self.include_name_hook(name, type_, parent_names))
@@ -750,6 +814,7 @@ def alembic_config_for_provider(
750
814
  if output_buffer is not None:
751
815
  config.output_buffer = output_buffer
752
816
  config.set_main_option("script_location", migration.script_location)
817
+ version_locations = _configure_alembic_version_locations(config, migration.version_locations)
753
818
  config.set_main_option("sqlalchemy.url", sqlalchemy_url.replace("%", "%%"))
754
819
  config.set_main_option("sqlalchemy.echo", "true")
755
820
  config.set_main_option("version_table", migration.version_table)
@@ -764,6 +829,8 @@ def alembic_config_for_provider(
764
829
  config.attributes["alembic_version_table"] = migration.alembic_version_table
765
830
  config.attributes["version_table"] = migration.version_table
766
831
  config.attributes["version_table_schema"] = migration.version_table_schema
832
+ config.attributes["version_locations"] = version_locations
833
+ config.attributes["version_path"] = migration.resolved_version_path()
767
834
  return config
768
835
 
769
836
 
@@ -805,6 +872,7 @@ def resolve_alembic_revision_metadata(
805
872
  *,
806
873
  script_location: str,
807
874
  revision: str,
875
+ version_locations: str | Sequence[str] | None = None,
808
876
  ) -> tuple[str, str | None]:
809
877
  try:
810
878
  from alembic.config import Config
@@ -814,6 +882,7 @@ def resolve_alembic_revision_metadata(
814
882
 
815
883
  config = Config()
816
884
  config.set_main_option("script_location", script_location)
885
+ _configure_alembic_version_locations(config, version_locations)
817
886
  script = ScriptDirectory.from_config(config)
818
887
  resolved = script.get_revision(revision)
819
888
  if resolved is None:
@@ -866,6 +935,9 @@ def _collection_create_row_from_registration_request(
866
935
  time_index_name = getattr(request, "time_index_name", None)
867
936
  if time_index_name not in (None, ""):
868
937
  row["time_index_name"] = str(time_index_name)
938
+ cadence = getattr(request, "cadence", None)
939
+ if cadence not in (None, ""):
940
+ row["cadence"] = str(cadence)
869
941
  partition_strategy = getattr(request, "partition_strategy", None)
870
942
  if partition_strategy not in (None, ""):
871
943
  row["partition_strategy"] = str(partition_strategy)
@@ -3,6 +3,7 @@ from __future__ import annotations
3
3
  import contextlib
4
4
  import contextvars
5
5
  import json
6
+ import re
6
7
  from collections.abc import Mapping, Sequence
7
8
  from dataclasses import dataclass
8
9
  from typing import Any, ClassVar
@@ -37,6 +38,10 @@ DEFAULT_PLATFORM_MANAGED_PROVISIONING = {
37
38
  }
38
39
  SERVER_GENERATED_UUID_DEFAULT = "gen_random_uuid()"
39
40
  DEFAULT_POSTGRES_SCHEMA = "public"
41
+ _TIME_INDEXED_CADENCE_RE = re.compile(
42
+ r"^[1-9][0-9]*(mo|s|m|h|d|w|q|y)$",
43
+ re.IGNORECASE,
44
+ )
40
45
  _PLATFORM_MANAGED_MIGRATION_REGISTRATION_CONTEXT: contextvars.ContextVar[bool] = (
41
46
  contextvars.ContextVar(
42
47
  "mainsequence_platform_managed_migration_registration_context",
@@ -292,6 +297,7 @@ class PlatformTimeIndexMetaTable(PlatformManagedMetaTable):
292
297
  """
293
298
 
294
299
  __time_index_meta_table__: ClassVar[TimeIndexMetaTable | None] = None
300
+ __cadence__: ClassVar[str | None] = None
295
301
 
296
302
  __mapper_args__ = _sqlalchemy_declared_attr.directive(_time_index_mapper_args)
297
303
 
@@ -505,6 +511,7 @@ def time_indexed_registration_request_from_sqlalchemy_model(
505
511
  model_or_table,
506
512
  storage_layout=storage_layout,
507
513
  )
514
+ resolved_cadence = _resolve_time_index_cadence(model_or_table)
508
515
  columns = _iter_columns(table)
509
516
  _validate_time_index_contract(
510
517
  columns=columns,
@@ -520,6 +527,7 @@ def time_indexed_registration_request_from_sqlalchemy_model(
520
527
  time_index_name=resolved_time_index_name,
521
528
  index_names=resolved_index_names,
522
529
  storage_layout=resolved_storage_layout,
530
+ cadence=resolved_cadence,
523
531
  ),
524
532
  hash_namespace=resolved_hash_namespace,
525
533
  extra_hash_components=resolved_extra_hash_components,
@@ -559,6 +567,7 @@ def time_indexed_registration_request_from_sqlalchemy_model(
559
567
  labels=_resolve_labels(model_or_table, labels=labels),
560
568
  provisioning=_resolve_provisioning(model_or_table, provisioning=provisioning),
561
569
  time_index_name=resolved_time_index_name,
570
+ cadence=resolved_cadence,
562
571
  table_contract={
563
572
  "version": "relational-table.v1",
564
573
  "table_kind": "time_indexed",
@@ -571,6 +580,7 @@ def time_indexed_registration_request_from_sqlalchemy_model(
571
580
  "time_indexed": {
572
581
  "time_index_name": resolved_time_index_name,
573
582
  "index_names": resolved_index_names,
583
+ **({"cadence": resolved_cadence} if resolved_cadence else {}),
574
584
  **(
575
585
  {"storage_layout": dict(resolved_storage_layout)}
576
586
  if resolved_storage_layout
@@ -1246,6 +1256,7 @@ def _configured_table_storage_identity(model_or_table: Any, *, table: Any) -> di
1246
1256
  time_index_name=time_index_name,
1247
1257
  index_names=index_names,
1248
1258
  storage_layout=_resolve_time_index_storage_layout(model_or_table),
1259
+ cadence=_resolve_time_index_cadence(model_or_table),
1249
1260
  )
1250
1261
 
1251
1262
 
@@ -1260,12 +1271,14 @@ def _time_index_table_storage_identity(
1260
1271
  time_index_name: str,
1261
1272
  index_names: Sequence[str],
1262
1273
  storage_layout: Mapping[str, Any] | None,
1274
+ cadence: str | None,
1263
1275
  ) -> dict[str, Any]:
1264
1276
  return _time_index_storage_identity(
1265
1277
  table_storage_identity=_table_storage_identity(table),
1266
1278
  time_index_name=time_index_name,
1267
1279
  index_names=index_names,
1268
1280
  storage_layout=storage_layout,
1281
+ cadence=cadence,
1269
1282
  )
1270
1283
 
1271
1284
 
@@ -1275,12 +1288,14 @@ def _time_index_table_items_storage_identity(
1275
1288
  time_index_name: str,
1276
1289
  index_names: Sequence[str],
1277
1290
  storage_layout: Mapping[str, Any] | None,
1291
+ cadence: str | None,
1278
1292
  ) -> dict[str, Any]:
1279
1293
  return _time_index_storage_identity(
1280
1294
  table_storage_identity=_table_items_storage_identity(table_items),
1281
1295
  time_index_name=time_index_name,
1282
1296
  index_names=index_names,
1283
1297
  storage_layout=storage_layout,
1298
+ cadence=cadence,
1284
1299
  )
1285
1300
 
1286
1301
 
@@ -1290,12 +1305,15 @@ def _time_index_storage_identity(
1290
1305
  time_index_name: str,
1291
1306
  index_names: Sequence[str],
1292
1307
  storage_layout: Mapping[str, Any] | None,
1308
+ cadence: str | None,
1293
1309
  ) -> dict[str, Any]:
1294
1310
  profile: dict[str, Any] = {
1295
1311
  "kind": "time_indexed",
1296
1312
  "time_index_name": str(time_index_name),
1297
1313
  "index_names": [str(name) for name in index_names],
1298
1314
  }
1315
+ if cadence:
1316
+ profile["cadence"] = cadence
1299
1317
  if storage_layout:
1300
1318
  profile["storage_layout"] = dict(storage_layout)
1301
1319
  return {
@@ -1399,10 +1417,28 @@ def _resolve_time_index_storage_layout(
1399
1417
  if not isinstance(resolved, Mapping):
1400
1418
  raise ValueError(
1401
1419
  "PlatformTimeIndexMetaTable storage_layout must be a mapping when provided."
1402
- )
1420
+ )
1403
1421
  return resolved
1404
1422
 
1405
1423
 
1424
+ def _resolve_time_index_cadence(model_or_table: Any) -> str | None:
1425
+ resolved = (
1426
+ getattr(model_or_table, "__cadence__", None)
1427
+ or getattr(model_or_table, "__dynamic_table_cadence__", None)
1428
+ )
1429
+ if resolved in (None, ""):
1430
+ return None
1431
+ normalized = str(resolved).strip().lower()
1432
+ if not normalized:
1433
+ return None
1434
+ if not _TIME_INDEXED_CADENCE_RE.fullmatch(normalized):
1435
+ raise ValueError(
1436
+ "PlatformTimeIndexMetaTable cadence must be an interval token such as "
1437
+ "1m, 5m, 1h, 1d, 1w, 1mo, 1q, or 1y."
1438
+ )
1439
+ return normalized
1440
+
1441
+
1406
1442
  def _dynamic_table_info_value(model_or_table: Any, key: str) -> Any:
1407
1443
  dynamic_table = _table_info_value(model_or_table, "dynamic_table")
1408
1444
  if isinstance(dynamic_table, Mapping):
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: mainsequence
3
- Version: 4.3.0
3
+ Version: 4.3.6
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
@@ -1,2 +1,3 @@
1
1
  [console_scripts]
2
2
  mainsequence = mainsequence.cli.cli:app
3
+ ms = mainsequence.cli.cli:app
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "mainsequence"
7
- version = "4.3.0"
7
+ version = "4.3.6"
8
8
  description = "Main Sequence SDK "
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.11"
@@ -60,6 +60,7 @@ Homepage = "https://github.com/mainsequence-sdk/mainsequence-sdk"
60
60
  Issues = "https://github.com/mainsequence-sdk/mainsequence-sdk/issues"
61
61
  [project.scripts]
62
62
  mainsequence = "mainsequence.cli.cli:app"
63
+ ms = "mainsequence.cli.cli:app"
63
64
 
64
65
  # PEP 621 dependencies (published requirements)
65
66
  [dependency-groups]