mainsequence 4.3.5__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.5/mainsequence.egg-info → mainsequence-4.3.6}/PKG-INFO +1 -1
  2. {mainsequence-4.3.5 → mainsequence-4.3.6}/agent_scaffold/skills/data_publishing/data_nodes/SKILL.md +7 -0
  3. {mainsequence-4.3.5 → mainsequence-4.3.6}/agent_scaffold/skills/data_publishing/meta_tables/SKILL.md +5 -0
  4. {mainsequence-4.3.5 → mainsequence-4.3.6}/mainsequence/client/metatables/core.py +52 -0
  5. {mainsequence-4.3.5 → mainsequence-4.3.6}/mainsequence/meta_tables/migrations.py +3 -0
  6. {mainsequence-4.3.5 → mainsequence-4.3.6}/mainsequence/meta_tables/sqlalchemy_contracts.py +37 -1
  7. {mainsequence-4.3.5 → mainsequence-4.3.6/mainsequence.egg-info}/PKG-INFO +1 -1
  8. {mainsequence-4.3.5 → mainsequence-4.3.6}/pyproject.toml +1 -1
  9. {mainsequence-4.3.5 → mainsequence-4.3.6}/tests/test_meta_tables_sqlalchemy_contracts.py +75 -0
  10. {mainsequence-4.3.5 → mainsequence-4.3.6}/LICENSE +0 -0
  11. {mainsequence-4.3.5 → mainsequence-4.3.6}/README.md +0 -0
  12. {mainsequence-4.3.5 → mainsequence-4.3.6}/agent_scaffold/AGENTS.md +0 -0
  13. {mainsequence-4.3.5 → mainsequence-4.3.6}/agent_scaffold/skills/a2a_communication/SKILL.md +0 -0
  14. {mainsequence-4.3.5 → mainsequence-4.3.6}/agent_scaffold/skills/application_surfaces/api_surfaces/SKILL.md +0 -0
  15. {mainsequence-4.3.5 → mainsequence-4.3.6}/agent_scaffold/skills/command_center/adapter_from_api/SKILL.md +0 -0
  16. {mainsequence-4.3.5 → mainsequence-4.3.6}/agent_scaffold/skills/command_center/api_mock_prototyping/SKILL.md +0 -0
  17. {mainsequence-4.3.5 → mainsequence-4.3.6}/agent_scaffold/skills/command_center/app_components/SKILL.md +0 -0
  18. {mainsequence-4.3.5 → mainsequence-4.3.6}/agent_scaffold/skills/command_center/connections/SKILL.md +0 -0
  19. {mainsequence-4.3.5 → mainsequence-4.3.6}/agent_scaffold/skills/command_center/workspace_analysis/SKILL.md +0 -0
  20. {mainsequence-4.3.5 → mainsequence-4.3.6}/agent_scaffold/skills/command_center/workspace_builder/SKILL.md +0 -0
  21. {mainsequence-4.3.5 → mainsequence-4.3.6}/agent_scaffold/skills/command_center/workspace_design/SKILL.md +0 -0
  22. {mainsequence-4.3.5 → mainsequence-4.3.6}/agent_scaffold/skills/dashboards/streamlit/SKILL.md +0 -0
  23. {mainsequence-4.3.5 → mainsequence-4.3.6}/agent_scaffold/skills/data_access/exploration/SKILL.md +0 -0
  24. {mainsequence-4.3.5 → mainsequence-4.3.6}/agent_scaffold/skills/maintenance/bug_auditor/SKILL.md +0 -0
  25. {mainsequence-4.3.5 → mainsequence-4.3.6}/agent_scaffold/skills/ms-markets/SKILL.md +0 -0
  26. {mainsequence-4.3.5 → mainsequence-4.3.6}/agent_scaffold/skills/platform_operations/access_control_and_sharing/SKILL.md +0 -0
  27. {mainsequence-4.3.5 → mainsequence-4.3.6}/agent_scaffold/skills/platform_operations/orchestration_and_releases/SKILL.md +0 -0
  28. {mainsequence-4.3.5 → mainsequence-4.3.6}/agent_scaffold/skills/project_builder/SKILL.md +0 -0
  29. {mainsequence-4.3.5 → mainsequence-4.3.6}/agent_scaffold/skills/project_to_agent/SKILL.md +0 -0
  30. {mainsequence-4.3.5 → mainsequence-4.3.6}/mainsequence/__init__.py +0 -0
  31. {mainsequence-4.3.5 → mainsequence-4.3.6}/mainsequence/__main__.py +0 -0
  32. {mainsequence-4.3.5 → mainsequence-4.3.6}/mainsequence/bootstrap.py +0 -0
  33. {mainsequence-4.3.5 → mainsequence-4.3.6}/mainsequence/cli/__init__.py +0 -0
  34. {mainsequence-4.3.5 → mainsequence-4.3.6}/mainsequence/cli/api.py +0 -0
  35. {mainsequence-4.3.5 → mainsequence-4.3.6}/mainsequence/cli/browser_auth.py +0 -0
  36. {mainsequence-4.3.5 → mainsequence-4.3.6}/mainsequence/cli/cli.py +0 -0
  37. {mainsequence-4.3.5 → mainsequence-4.3.6}/mainsequence/cli/config.py +0 -0
  38. {mainsequence-4.3.5 → mainsequence-4.3.6}/mainsequence/cli/docker_utils.py +0 -0
  39. {mainsequence-4.3.5 → mainsequence-4.3.6}/mainsequence/cli/doctor.py +0 -0
  40. {mainsequence-4.3.5 → mainsequence-4.3.6}/mainsequence/cli/local_ops.py +0 -0
  41. {mainsequence-4.3.5 → mainsequence-4.3.6}/mainsequence/cli/migrations.py +0 -0
  42. {mainsequence-4.3.5 → mainsequence-4.3.6}/mainsequence/cli/model_filters.py +0 -0
  43. {mainsequence-4.3.5 → mainsequence-4.3.6}/mainsequence/cli/project_status.py +0 -0
  44. {mainsequence-4.3.5 → mainsequence-4.3.6}/mainsequence/cli/pydantic_cli.py +0 -0
  45. {mainsequence-4.3.5 → mainsequence-4.3.6}/mainsequence/cli/sdk_utils.py +0 -0
  46. {mainsequence-4.3.5 → mainsequence-4.3.6}/mainsequence/cli/ssh_utils.py +0 -0
  47. {mainsequence-4.3.5 → mainsequence-4.3.6}/mainsequence/cli/ui.py +0 -0
  48. {mainsequence-4.3.5 → mainsequence-4.3.6}/mainsequence/client/__init__.py +0 -0
  49. {mainsequence-4.3.5 → mainsequence-4.3.6}/mainsequence/client/agent_runtime_models.py +0 -0
  50. {mainsequence-4.3.5 → mainsequence-4.3.6}/mainsequence/client/base.py +0 -0
  51. {mainsequence-4.3.5 → mainsequence-4.3.6}/mainsequence/client/client.py +0 -0
  52. {mainsequence-4.3.5 → mainsequence-4.3.6}/mainsequence/client/command_center/__init__.py +0 -0
  53. {mainsequence-4.3.5 → mainsequence-4.3.6}/mainsequence/client/command_center/app_component.py +0 -0
  54. {mainsequence-4.3.5 → mainsequence-4.3.6}/mainsequence/client/command_center/connections.py +0 -0
  55. {mainsequence-4.3.5 → mainsequence-4.3.6}/mainsequence/client/command_center/data_models.py +0 -0
  56. {mainsequence-4.3.5 → mainsequence-4.3.6}/mainsequence/client/command_center/workspace.py +0 -0
  57. {mainsequence-4.3.5 → mainsequence-4.3.6}/mainsequence/client/command_center/workspace_snapshot.py +0 -0
  58. {mainsequence-4.3.5 → mainsequence-4.3.6}/mainsequence/client/compute_validation.py +0 -0
  59. {mainsequence-4.3.5 → mainsequence-4.3.6}/mainsequence/client/data_sources_interfaces/__init__.py +0 -0
  60. {mainsequence-4.3.5 → mainsequence-4.3.6}/mainsequence/client/data_sources_interfaces/duckdb.py +0 -0
  61. {mainsequence-4.3.5 → mainsequence-4.3.6}/mainsequence/client/data_sources_interfaces/local_paths.py +0 -0
  62. {mainsequence-4.3.5 → mainsequence-4.3.6}/mainsequence/client/data_sources_interfaces/sqlite.py +0 -0
  63. {mainsequence-4.3.5 → mainsequence-4.3.6}/mainsequence/client/dtype_codec.py +0 -0
  64. {mainsequence-4.3.5 → mainsequence-4.3.6}/mainsequence/client/exceptions.py +0 -0
  65. {mainsequence-4.3.5 → mainsequence-4.3.6}/mainsequence/client/fastapi/__init__.py +0 -0
  66. {mainsequence-4.3.5 → mainsequence-4.3.6}/mainsequence/client/fastapi/auth.py +0 -0
  67. {mainsequence-4.3.5 → mainsequence-4.3.6}/mainsequence/client/metatables/__init__.py +0 -0
  68. {mainsequence-4.3.5 → mainsequence-4.3.6}/mainsequence/client/models_foundry.py +0 -0
  69. {mainsequence-4.3.5 → mainsequence-4.3.6}/mainsequence/client/models_helpers.py +0 -0
  70. {mainsequence-4.3.5 → mainsequence-4.3.6}/mainsequence/client/models_user.py +0 -0
  71. {mainsequence-4.3.5 → mainsequence-4.3.6}/mainsequence/client/utils.py +0 -0
  72. {mainsequence-4.3.5 → mainsequence-4.3.6}/mainsequence/defaults.py +0 -0
  73. {mainsequence-4.3.5 → mainsequence-4.3.6}/mainsequence/instrumentation/__init__.py +0 -0
  74. {mainsequence-4.3.5 → mainsequence-4.3.6}/mainsequence/instrumentation/utils.py +0 -0
  75. {mainsequence-4.3.5 → mainsequence-4.3.6}/mainsequence/logconf.py +0 -0
  76. {mainsequence-4.3.5 → mainsequence-4.3.6}/mainsequence/meta_tables/__init__.py +0 -0
  77. {mainsequence-4.3.5 → mainsequence-4.3.6}/mainsequence/meta_tables/__main__.py +0 -0
  78. {mainsequence-4.3.5 → mainsequence-4.3.6}/mainsequence/meta_tables/compiled_sql/__init__.py +0 -0
  79. {mainsequence-4.3.5 → mainsequence-4.3.6}/mainsequence/meta_tables/compiled_sql/v1.py +0 -0
  80. {mainsequence-4.3.5 → mainsequence-4.3.6}/mainsequence/meta_tables/data_nodes/__init__.py +0 -0
  81. {mainsequence-4.3.5 → mainsequence-4.3.6}/mainsequence/meta_tables/data_nodes/build_operations.py +0 -0
  82. {mainsequence-4.3.5 → mainsequence-4.3.6}/mainsequence/meta_tables/data_nodes/data_nodes.py +0 -0
  83. {mainsequence-4.3.5 → mainsequence-4.3.6}/mainsequence/meta_tables/data_nodes/models.py +0 -0
  84. {mainsequence-4.3.5 → mainsequence-4.3.6}/mainsequence/meta_tables/data_nodes/namespacing.py +0 -0
  85. {mainsequence-4.3.5 → mainsequence-4.3.6}/mainsequence/meta_tables/data_nodes/persist_managers.py +0 -0
  86. {mainsequence-4.3.5 → mainsequence-4.3.6}/mainsequence/meta_tables/data_nodes/run_operations.py +0 -0
  87. {mainsequence-4.3.5 → mainsequence-4.3.6}/mainsequence/meta_tables/data_nodes/utils.py +0 -0
  88. {mainsequence-4.3.5 → mainsequence-4.3.6}/mainsequence/meta_tables/future_registry.py +0 -0
  89. {mainsequence-4.3.5 → mainsequence-4.3.6}/mainsequence/meta_tables/hashing.py +0 -0
  90. {mainsequence-4.3.5 → mainsequence-4.3.6}/mainsequence/meta_tables/pydantic_metadata.py +0 -0
  91. {mainsequence-4.3.5 → mainsequence-4.3.6}/mainsequence/meta_tables/schema_names.py +0 -0
  92. {mainsequence-4.3.5 → mainsequence-4.3.6}/mainsequence/runtime_flags.py +0 -0
  93. {mainsequence-4.3.5 → mainsequence-4.3.6}/mainsequence.egg-info/SOURCES.txt +0 -0
  94. {mainsequence-4.3.5 → mainsequence-4.3.6}/mainsequence.egg-info/dependency_links.txt +0 -0
  95. {mainsequence-4.3.5 → mainsequence-4.3.6}/mainsequence.egg-info/entry_points.txt +0 -0
  96. {mainsequence-4.3.5 → mainsequence-4.3.6}/mainsequence.egg-info/requires.txt +0 -0
  97. {mainsequence-4.3.5 → mainsequence-4.3.6}/mainsequence.egg-info/top_level.txt +0 -0
  98. {mainsequence-4.3.5 → mainsequence-4.3.6}/setup.cfg +0 -0
  99. {mainsequence-4.3.5 → mainsequence-4.3.6}/tests/test_auth_precedence.py +0 -0
  100. {mainsequence-4.3.5 → mainsequence-4.3.6}/tests/test_build_operations_hashing.py +0 -0
  101. {mainsequence-4.3.5 → mainsequence-4.3.6}/tests/test_cli.py +0 -0
  102. {mainsequence-4.3.5 → mainsequence-4.3.6}/tests/test_cli_browser_auth.py +0 -0
  103. {mainsequence-4.3.5 → mainsequence-4.3.6}/tests/test_cli_migrations.py +0 -0
  104. {mainsequence-4.3.5 → mainsequence-4.3.6}/tests/test_client.py +0 -0
  105. {mainsequence-4.3.5 → mainsequence-4.3.6}/tests/test_command_center_app_component_models.py +0 -0
  106. {mainsequence-4.3.5 → mainsequence-4.3.6}/tests/test_command_center_data_models.py +0 -0
  107. {mainsequence-4.3.5 → mainsequence-4.3.6}/tests/test_command_center_models.py +0 -0
  108. {mainsequence-4.3.5 → mainsequence-4.3.6}/tests/test_data_access_mixin_dimension_audit.py +0 -0
  109. {mainsequence-4.3.5 → mainsequence-4.3.6}/tests/test_data_node_storage_dimension_queries.py +0 -0
  110. {mainsequence-4.3.5 → mainsequence-4.3.6}/tests/test_data_node_update_flow.py +0 -0
  111. {mainsequence-4.3.5 → mainsequence-4.3.6}/tests/test_dependency_extras.py +0 -0
  112. {mainsequence-4.3.5 → mainsequence-4.3.6}/tests/test_duckdb_interface_dimensions.py +0 -0
  113. {mainsequence-4.3.5 → mainsequence-4.3.6}/tests/test_filter_normalization.py +0 -0
  114. {mainsequence-4.3.5 → mainsequence-4.3.6}/tests/test_instrumentation.py +0 -0
  115. {mainsequence-4.3.5 → mainsequence-4.3.6}/tests/test_logconf.py +0 -0
  116. {mainsequence-4.3.5 → mainsequence-4.3.6}/tests/test_meta_table_migrations.py +0 -0
  117. {mainsequence-4.3.5 → mainsequence-4.3.6}/tests/test_meta_tables_client_models.py +0 -0
  118. {mainsequence-4.3.5 → mainsequence-4.3.6}/tests/test_models_user_request_bound_auth.py +0 -0
  119. {mainsequence-4.3.5 → mainsequence-4.3.6}/tests/test_pod_project_resolution.py +0 -0
  120. {mainsequence-4.3.5 → mainsequence-4.3.6}/tests/test_project_batch_jobs_from_file.py +0 -0
  121. {mainsequence-4.3.5 → mainsequence-4.3.6}/tests/test_run_configuration.py +0 -0
  122. {mainsequence-4.3.5 → mainsequence-4.3.6}/tests/test_schema_names.py +0 -0
  123. {mainsequence-4.3.5 → mainsequence-4.3.6}/tests/test_secret_client_model.py +0 -0
  124. {mainsequence-4.3.5 → mainsequence-4.3.6}/tests/test_source_table_configuration.py +0 -0
  125. {mainsequence-4.3.5 → mainsequence-4.3.6}/tests/test_sqlite_interface_dimensions.py +0 -0
  126. {mainsequence-4.3.5 → mainsequence-4.3.6}/tests/test_update_runner_uid_runtime.py +0 -0
  127. {mainsequence-4.3.5 → mainsequence-4.3.6}/tests/test_update_statistics.py +0 -0
  128. {mainsequence-4.3.5 → mainsequence-4.3.6}/tests/test_update_uid_guards.py +0 -0
  129. {mainsequence-4.3.5 → 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.5
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":
@@ -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():
@@ -667,6 +686,11 @@ class ManagedMetaTableFinalizeTableResult(BasePydanticModel):
667
686
  )
668
687
  table_kind: str = Field(..., description="Backend table kind after reconciliation.")
669
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
+ )
670
694
  finalized: bool = Field(
671
695
  ...,
672
696
  description="True when this row was reconciled and activated by this call.",
@@ -695,6 +719,11 @@ class ManagedMetaTableFinalizeTableResult(BasePydanticModel):
695
719
  description="Per-table structured error when finalization did not activate this row.",
696
720
  )
697
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
+
698
727
  model_config = ConfigDict(extra="ignore")
699
728
 
700
729
 
@@ -1923,6 +1952,14 @@ class TimeIndexMetaTableRegistrationRequest(BasePydanticModel):
1923
1952
  default_factory=lambda: {"create_table": True, "if_not_exists": True}
1924
1953
  )
1925
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
+ )
1926
1963
  partition_strategy: str = Field(
1927
1964
  default="backend_default",
1928
1965
  description="Time-indexed MetaTable physical partitioning strategy",
@@ -1973,6 +2010,11 @@ class TimeIndexMetaTableRegistrationRequest(BasePydanticModel):
1973
2010
  data["table_contract"] = normalized_contract
1974
2011
  return data
1975
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
+
1976
2018
 
1977
2019
  def _payload_get(obj: Any, key: str, default: Any = None) -> Any:
1978
2020
  if isinstance(obj, Mapping):
@@ -2113,6 +2155,11 @@ class TimeIndexedProfile(TimeIndexedProfileBase, BasePydanticModel):
2113
2155
  None, description="Public uid of the related TimeIndexMetaTable"
2114
2156
  )
2115
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
+ )
2116
2163
  partition_strategy: str | None = None
2117
2164
  last_time_index_value: datetime.datetime | None = Field(
2118
2165
  None, description="Last time index value"
@@ -2124,6 +2171,11 @@ class TimeIndexedProfile(TimeIndexedProfileBase, BasePydanticModel):
2124
2171
  physical_index_plan: dict[str, Any] | None = Field(
2125
2172
  None, description="Server-rendered physical index plan"
2126
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)
2127
2179
  multi_index_stats: dict[str, Any] | None = Field(
2128
2180
  None, description="Canonical multi-index progress statistics"
2129
2181
  )
@@ -935,6 +935,9 @@ def _collection_create_row_from_registration_request(
935
935
  time_index_name = getattr(request, "time_index_name", None)
936
936
  if time_index_name not in (None, ""):
937
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)
938
941
  partition_strategy = getattr(request, "partition_strategy", None)
939
942
  if partition_strategy not in (None, ""):
940
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.5
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
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "mainsequence"
7
- version = "4.3.5"
7
+ version = "4.3.6"
8
8
  description = "Main Sequence SDK "
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.11"
@@ -180,6 +180,7 @@ def _time_index_model_class(
180
180
  identifier=None,
181
181
  time_index_name="time_index",
182
182
  index_names=None,
183
+ cadence=None,
183
184
  ):
184
185
  attrs = _model_attrs(
185
186
  name,
@@ -193,6 +194,8 @@ def _time_index_model_class(
193
194
  "__index_names__": list(index_names or [time_index_name]),
194
195
  }
195
196
  )
197
+ if cadence is not None:
198
+ attrs["__cadence__"] = cadence
196
199
  return type(
197
200
  name,
198
201
  (PlatformTimeIndexMetaTable,),
@@ -1247,6 +1250,78 @@ def test_time_index_meta_table_registration_request_uses_dynamic_contract():
1247
1250
  assert "physical_index_plan" not in payload
1248
1251
 
1249
1252
 
1253
+ def test_time_index_meta_table_registration_request_uses_class_cadence():
1254
+ table = FakeTable(
1255
+ "example_assets__prices",
1256
+ columns=[
1257
+ FakeColumn("time_index", DateTime(timezone=True), nullable=False),
1258
+ FakeColumn("asset_uid", Uuid(), nullable=False),
1259
+ FakeColumn("price", String(64), nullable=False),
1260
+ ],
1261
+ )
1262
+ DailyPrices = _time_index_model_class(
1263
+ "DailyPrices",
1264
+ table,
1265
+ index_names=["time_index", "asset_uid"],
1266
+ cadence="1D",
1267
+ )
1268
+
1269
+ request = DailyPrices.build_registration_request(
1270
+ data_source_uid="dddddddd-dddd-4ddd-8ddd-dddddddddddd",
1271
+ )
1272
+
1273
+ assert request.cadence == "1d"
1274
+ assert request.table_contract["authoring"]["time_indexed"]["cadence"] == "1d"
1275
+ payload = request.model_dump(mode="json", exclude_none=True)
1276
+ assert payload["cadence"] == "1d"
1277
+
1278
+
1279
+ def test_time_index_meta_table_cadence_changes_configured_storage_hash():
1280
+ table = FakeTable(
1281
+ "example_assets__prices",
1282
+ columns=[
1283
+ FakeColumn("time_index", DateTime(timezone=True), nullable=False),
1284
+ FakeColumn("asset_uid", Uuid(), nullable=False),
1285
+ FakeColumn("price", String(64), nullable=False),
1286
+ ],
1287
+ )
1288
+ DailyPrices = _time_index_model_class(
1289
+ "DailyPrices",
1290
+ table,
1291
+ index_names=["time_index", "asset_uid"],
1292
+ cadence="1d",
1293
+ )
1294
+ IntradayPrices = _time_index_model_class(
1295
+ "IntradayPrices",
1296
+ table,
1297
+ index_names=["time_index", "asset_uid"],
1298
+ cadence="5m",
1299
+ )
1300
+
1301
+ assert _configured_storage_hash(DailyPrices) != _configured_storage_hash(IntradayPrices)
1302
+
1303
+
1304
+ def test_time_index_meta_table_rejects_invalid_class_cadence():
1305
+ table = FakeTable(
1306
+ "example_assets__prices",
1307
+ columns=[
1308
+ FakeColumn("time_index", DateTime(timezone=True), nullable=False),
1309
+ FakeColumn("asset_uid", Uuid(), nullable=False),
1310
+ ],
1311
+ )
1312
+ Prices = _time_index_model_class(
1313
+ "Prices",
1314
+ table,
1315
+ index_names=["time_index", "asset_uid"],
1316
+ cadence="daily",
1317
+ )
1318
+
1319
+ with pytest.raises(ValueError, match="cadence"):
1320
+ Prices.build_registration_request(
1321
+ data_source_uid="dddddddd-dddd-4ddd-8ddd-dddddddddddd",
1322
+ )
1323
+
1324
+
1250
1325
  def test_time_index_meta_table_registration_request_uses_class_metatable_description():
1251
1326
  table = FakeTable(
1252
1327
  "placeholder",
File without changes
File without changes
File without changes