mainsequence 4.1.9__tar.gz → 4.1.11__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 (121) hide show
  1. {mainsequence-4.1.9 → mainsequence-4.1.11}/PKG-INFO +1 -1
  2. {mainsequence-4.1.9 → mainsequence-4.1.11}/agent_scaffold/skills/data_publishing/data_nodes/SKILL.md +55 -18
  3. {mainsequence-4.1.9 → mainsequence-4.1.11}/agent_scaffold/skills/data_publishing/meta_tables/SKILL.md +37 -6
  4. {mainsequence-4.1.9 → mainsequence-4.1.11}/mainsequence/meta_tables/data_nodes/build_operations.py +7 -2
  5. {mainsequence-4.1.9 → mainsequence-4.1.11}/mainsequence/meta_tables/data_nodes/data_nodes.py +9 -24
  6. {mainsequence-4.1.9 → mainsequence-4.1.11}/mainsequence/meta_tables/data_nodes/persist_managers.py +38 -25
  7. {mainsequence-4.1.9 → mainsequence-4.1.11}/mainsequence/meta_tables/sqlalchemy_contracts.py +134 -92
  8. {mainsequence-4.1.9 → mainsequence-4.1.11}/mainsequence.egg-info/PKG-INFO +1 -1
  9. {mainsequence-4.1.9 → mainsequence-4.1.11}/pyproject.toml +1 -1
  10. {mainsequence-4.1.9 → mainsequence-4.1.11}/tests/test_build_operations_hashing.py +33 -0
  11. {mainsequence-4.1.9 → mainsequence-4.1.11}/tests/test_meta_tables_sqlalchemy_contracts.py +70 -29
  12. {mainsequence-4.1.9 → mainsequence-4.1.11}/LICENSE +0 -0
  13. {mainsequence-4.1.9 → mainsequence-4.1.11}/README.md +0 -0
  14. {mainsequence-4.1.9 → mainsequence-4.1.11}/agent_scaffold/AGENTS.md +0 -0
  15. {mainsequence-4.1.9 → mainsequence-4.1.11}/agent_scaffold/skills/a2a_communication/SKILL.md +0 -0
  16. {mainsequence-4.1.9 → mainsequence-4.1.11}/agent_scaffold/skills/application_surfaces/api_surfaces/SKILL.md +0 -0
  17. {mainsequence-4.1.9 → mainsequence-4.1.11}/agent_scaffold/skills/command_center/adapter_from_api/SKILL.md +0 -0
  18. {mainsequence-4.1.9 → mainsequence-4.1.11}/agent_scaffold/skills/command_center/api_mock_prototyping/SKILL.md +0 -0
  19. {mainsequence-4.1.9 → mainsequence-4.1.11}/agent_scaffold/skills/command_center/app_components/SKILL.md +0 -0
  20. {mainsequence-4.1.9 → mainsequence-4.1.11}/agent_scaffold/skills/command_center/connections/SKILL.md +0 -0
  21. {mainsequence-4.1.9 → mainsequence-4.1.11}/agent_scaffold/skills/command_center/workspace_analysis/SKILL.md +0 -0
  22. {mainsequence-4.1.9 → mainsequence-4.1.11}/agent_scaffold/skills/command_center/workspace_builder/SKILL.md +0 -0
  23. {mainsequence-4.1.9 → mainsequence-4.1.11}/agent_scaffold/skills/command_center/workspace_design/SKILL.md +0 -0
  24. {mainsequence-4.1.9 → mainsequence-4.1.11}/agent_scaffold/skills/dashboards/streamlit/SKILL.md +0 -0
  25. {mainsequence-4.1.9 → mainsequence-4.1.11}/agent_scaffold/skills/data_access/exploration/SKILL.md +0 -0
  26. {mainsequence-4.1.9 → mainsequence-4.1.11}/agent_scaffold/skills/maintenance/bug_auditor/SKILL.md +0 -0
  27. {mainsequence-4.1.9 → mainsequence-4.1.11}/agent_scaffold/skills/ms-markets/SKILL.md +0 -0
  28. {mainsequence-4.1.9 → mainsequence-4.1.11}/agent_scaffold/skills/platform_operations/access_control_and_sharing/SKILL.md +0 -0
  29. {mainsequence-4.1.9 → mainsequence-4.1.11}/agent_scaffold/skills/platform_operations/orchestration_and_releases/SKILL.md +0 -0
  30. {mainsequence-4.1.9 → mainsequence-4.1.11}/agent_scaffold/skills/project_builder/SKILL.md +0 -0
  31. {mainsequence-4.1.9 → mainsequence-4.1.11}/agent_scaffold/skills/project_to_agent/SKILL.md +0 -0
  32. {mainsequence-4.1.9 → mainsequence-4.1.11}/mainsequence/__init__.py +0 -0
  33. {mainsequence-4.1.9 → mainsequence-4.1.11}/mainsequence/__main__.py +0 -0
  34. {mainsequence-4.1.9 → mainsequence-4.1.11}/mainsequence/bootstrap.py +0 -0
  35. {mainsequence-4.1.9 → mainsequence-4.1.11}/mainsequence/cli/__init__.py +0 -0
  36. {mainsequence-4.1.9 → mainsequence-4.1.11}/mainsequence/cli/api.py +0 -0
  37. {mainsequence-4.1.9 → mainsequence-4.1.11}/mainsequence/cli/browser_auth.py +0 -0
  38. {mainsequence-4.1.9 → mainsequence-4.1.11}/mainsequence/cli/cli.py +0 -0
  39. {mainsequence-4.1.9 → mainsequence-4.1.11}/mainsequence/cli/config.py +0 -0
  40. {mainsequence-4.1.9 → mainsequence-4.1.11}/mainsequence/cli/docker_utils.py +0 -0
  41. {mainsequence-4.1.9 → mainsequence-4.1.11}/mainsequence/cli/doctor.py +0 -0
  42. {mainsequence-4.1.9 → mainsequence-4.1.11}/mainsequence/cli/local_ops.py +0 -0
  43. {mainsequence-4.1.9 → mainsequence-4.1.11}/mainsequence/cli/model_filters.py +0 -0
  44. {mainsequence-4.1.9 → mainsequence-4.1.11}/mainsequence/cli/project_status.py +0 -0
  45. {mainsequence-4.1.9 → mainsequence-4.1.11}/mainsequence/cli/pydantic_cli.py +0 -0
  46. {mainsequence-4.1.9 → mainsequence-4.1.11}/mainsequence/cli/sdk_utils.py +0 -0
  47. {mainsequence-4.1.9 → mainsequence-4.1.11}/mainsequence/cli/ssh_utils.py +0 -0
  48. {mainsequence-4.1.9 → mainsequence-4.1.11}/mainsequence/cli/ui.py +0 -0
  49. {mainsequence-4.1.9 → mainsequence-4.1.11}/mainsequence/client/__init__.py +0 -0
  50. {mainsequence-4.1.9 → mainsequence-4.1.11}/mainsequence/client/agent_runtime_models.py +0 -0
  51. {mainsequence-4.1.9 → mainsequence-4.1.11}/mainsequence/client/base.py +0 -0
  52. {mainsequence-4.1.9 → mainsequence-4.1.11}/mainsequence/client/client.py +0 -0
  53. {mainsequence-4.1.9 → mainsequence-4.1.11}/mainsequence/client/command_center/__init__.py +0 -0
  54. {mainsequence-4.1.9 → mainsequence-4.1.11}/mainsequence/client/command_center/app_component.py +0 -0
  55. {mainsequence-4.1.9 → mainsequence-4.1.11}/mainsequence/client/command_center/connections.py +0 -0
  56. {mainsequence-4.1.9 → mainsequence-4.1.11}/mainsequence/client/command_center/data_models.py +0 -0
  57. {mainsequence-4.1.9 → mainsequence-4.1.11}/mainsequence/client/command_center/workspace.py +0 -0
  58. {mainsequence-4.1.9 → mainsequence-4.1.11}/mainsequence/client/command_center/workspace_snapshot.py +0 -0
  59. {mainsequence-4.1.9 → mainsequence-4.1.11}/mainsequence/client/compute_validation.py +0 -0
  60. {mainsequence-4.1.9 → mainsequence-4.1.11}/mainsequence/client/data_sources_interfaces/__init__.py +0 -0
  61. {mainsequence-4.1.9 → mainsequence-4.1.11}/mainsequence/client/data_sources_interfaces/duckdb.py +0 -0
  62. {mainsequence-4.1.9 → mainsequence-4.1.11}/mainsequence/client/data_sources_interfaces/local_paths.py +0 -0
  63. {mainsequence-4.1.9 → mainsequence-4.1.11}/mainsequence/client/data_sources_interfaces/sqlite.py +0 -0
  64. {mainsequence-4.1.9 → mainsequence-4.1.11}/mainsequence/client/dtype_codec.py +0 -0
  65. {mainsequence-4.1.9 → mainsequence-4.1.11}/mainsequence/client/exceptions.py +0 -0
  66. {mainsequence-4.1.9 → mainsequence-4.1.11}/mainsequence/client/fastapi/__init__.py +0 -0
  67. {mainsequence-4.1.9 → mainsequence-4.1.11}/mainsequence/client/fastapi/auth.py +0 -0
  68. {mainsequence-4.1.9 → mainsequence-4.1.11}/mainsequence/client/models_foundry.py +0 -0
  69. {mainsequence-4.1.9 → mainsequence-4.1.11}/mainsequence/client/models_helpers.py +0 -0
  70. {mainsequence-4.1.9 → mainsequence-4.1.11}/mainsequence/client/models_metatables.py +0 -0
  71. {mainsequence-4.1.9 → mainsequence-4.1.11}/mainsequence/client/models_user.py +0 -0
  72. {mainsequence-4.1.9 → mainsequence-4.1.11}/mainsequence/client/utils.py +0 -0
  73. {mainsequence-4.1.9 → mainsequence-4.1.11}/mainsequence/defaults.py +0 -0
  74. {mainsequence-4.1.9 → mainsequence-4.1.11}/mainsequence/instrumentation/__init__.py +0 -0
  75. {mainsequence-4.1.9 → mainsequence-4.1.11}/mainsequence/instrumentation/utils.py +0 -0
  76. {mainsequence-4.1.9 → mainsequence-4.1.11}/mainsequence/logconf.py +0 -0
  77. {mainsequence-4.1.9 → mainsequence-4.1.11}/mainsequence/meta_tables/__init__.py +0 -0
  78. {mainsequence-4.1.9 → mainsequence-4.1.11}/mainsequence/meta_tables/__main__.py +0 -0
  79. {mainsequence-4.1.9 → mainsequence-4.1.11}/mainsequence/meta_tables/compiled_sql/__init__.py +0 -0
  80. {mainsequence-4.1.9 → mainsequence-4.1.11}/mainsequence/meta_tables/compiled_sql/v1.py +0 -0
  81. {mainsequence-4.1.9 → mainsequence-4.1.11}/mainsequence/meta_tables/data_nodes/__init__.py +0 -0
  82. {mainsequence-4.1.9 → mainsequence-4.1.11}/mainsequence/meta_tables/data_nodes/models.py +0 -0
  83. {mainsequence-4.1.9 → mainsequence-4.1.11}/mainsequence/meta_tables/data_nodes/namespacing.py +0 -0
  84. {mainsequence-4.1.9 → mainsequence-4.1.11}/mainsequence/meta_tables/data_nodes/run_operations.py +0 -0
  85. {mainsequence-4.1.9 → mainsequence-4.1.11}/mainsequence/meta_tables/data_nodes/utils.py +0 -0
  86. {mainsequence-4.1.9 → mainsequence-4.1.11}/mainsequence/meta_tables/future_registry.py +0 -0
  87. {mainsequence-4.1.9 → mainsequence-4.1.11}/mainsequence/meta_tables/hashing.py +0 -0
  88. {mainsequence-4.1.9 → mainsequence-4.1.11}/mainsequence/meta_tables/pydantic_metadata.py +0 -0
  89. {mainsequence-4.1.9 → mainsequence-4.1.11}/mainsequence/runtime_flags.py +0 -0
  90. {mainsequence-4.1.9 → mainsequence-4.1.11}/mainsequence.egg-info/SOURCES.txt +0 -0
  91. {mainsequence-4.1.9 → mainsequence-4.1.11}/mainsequence.egg-info/dependency_links.txt +0 -0
  92. {mainsequence-4.1.9 → mainsequence-4.1.11}/mainsequence.egg-info/entry_points.txt +0 -0
  93. {mainsequence-4.1.9 → mainsequence-4.1.11}/mainsequence.egg-info/requires.txt +0 -0
  94. {mainsequence-4.1.9 → mainsequence-4.1.11}/mainsequence.egg-info/top_level.txt +0 -0
  95. {mainsequence-4.1.9 → mainsequence-4.1.11}/setup.cfg +0 -0
  96. {mainsequence-4.1.9 → mainsequence-4.1.11}/tests/test_auth_precedence.py +0 -0
  97. {mainsequence-4.1.9 → mainsequence-4.1.11}/tests/test_cli.py +0 -0
  98. {mainsequence-4.1.9 → mainsequence-4.1.11}/tests/test_cli_browser_auth.py +0 -0
  99. {mainsequence-4.1.9 → mainsequence-4.1.11}/tests/test_client.py +0 -0
  100. {mainsequence-4.1.9 → mainsequence-4.1.11}/tests/test_command_center_app_component_models.py +0 -0
  101. {mainsequence-4.1.9 → mainsequence-4.1.11}/tests/test_command_center_data_models.py +0 -0
  102. {mainsequence-4.1.9 → mainsequence-4.1.11}/tests/test_command_center_models.py +0 -0
  103. {mainsequence-4.1.9 → mainsequence-4.1.11}/tests/test_data_access_mixin_dimension_audit.py +0 -0
  104. {mainsequence-4.1.9 → mainsequence-4.1.11}/tests/test_data_node_storage_dimension_queries.py +0 -0
  105. {mainsequence-4.1.9 → mainsequence-4.1.11}/tests/test_data_node_update_flow.py +0 -0
  106. {mainsequence-4.1.9 → mainsequence-4.1.11}/tests/test_dependency_extras.py +0 -0
  107. {mainsequence-4.1.9 → mainsequence-4.1.11}/tests/test_duckdb_interface_dimensions.py +0 -0
  108. {mainsequence-4.1.9 → mainsequence-4.1.11}/tests/test_filter_normalization.py +0 -0
  109. {mainsequence-4.1.9 → mainsequence-4.1.11}/tests/test_logconf.py +0 -0
  110. {mainsequence-4.1.9 → mainsequence-4.1.11}/tests/test_meta_tables_client_models.py +0 -0
  111. {mainsequence-4.1.9 → mainsequence-4.1.11}/tests/test_models_user_request_bound_auth.py +0 -0
  112. {mainsequence-4.1.9 → mainsequence-4.1.11}/tests/test_pod_project_resolution.py +0 -0
  113. {mainsequence-4.1.9 → mainsequence-4.1.11}/tests/test_project_batch_jobs_from_file.py +0 -0
  114. {mainsequence-4.1.9 → mainsequence-4.1.11}/tests/test_run_configuration.py +0 -0
  115. {mainsequence-4.1.9 → mainsequence-4.1.11}/tests/test_secret_client_model.py +0 -0
  116. {mainsequence-4.1.9 → mainsequence-4.1.11}/tests/test_source_table_configuration.py +0 -0
  117. {mainsequence-4.1.9 → mainsequence-4.1.11}/tests/test_sqlite_interface_dimensions.py +0 -0
  118. {mainsequence-4.1.9 → mainsequence-4.1.11}/tests/test_update_runner_uid_runtime.py +0 -0
  119. {mainsequence-4.1.9 → mainsequence-4.1.11}/tests/test_update_statistics.py +0 -0
  120. {mainsequence-4.1.9 → mainsequence-4.1.11}/tests/test_update_uid_guards.py +0 -0
  121. {mainsequence-4.1.9 → mainsequence-4.1.11}/tests/test_workspace_snapshot.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: mainsequence
3
- Version: 4.1.9
3
+ Version: 4.1.11
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
@@ -10,13 +10,13 @@ description: Use this skill when the task is about producing, changing, validati
10
10
  Use this skill when the task changes a DataNode producer.
11
11
 
12
12
  A DataNode is an update process. It is not the canonical storage model. Storage
13
- is defined by a registered `PlatformTimeIndexMetaData` SQLAlchemy model.
13
+ is defined by a `PlatformTimeIndexMetaData` SQLAlchemy model.
14
14
 
15
15
  Canonical workflow:
16
16
 
17
17
  1. Define a `PlatformTimeIndexMetaData` storage class.
18
- 2. Register that storage class before constructing the DataNode.
19
- 3. Construct the DataNode with `config=...` and `storage_table=StorageClass`.
18
+ 2. Construct the DataNode with `config=...` and `storage_table=StorageClass`.
19
+ 3. Let the SDK register the output storage class automatically when needed.
20
20
  4. Return a DataFrame from `update()` that matches the storage class contract.
21
21
 
22
22
  ## This Skill Can Do
@@ -41,12 +41,13 @@ Canonical workflow:
41
41
  This skill must not claim ownership of:
42
42
 
43
43
  - generic MetaTable registration or governed operation semantics
44
- - storage creation inside `DataNode` or `PersistManager`
44
+ - storage registration internals beyond the SDK lifecycle call
45
45
  - HTTP route design or FastAPI response contracts
46
46
  - workspace/widget layout payloads
47
47
  - job creation, scheduling, image pinning, or release creation
48
48
  - RBAC or sharing policy
49
49
  - domain strategy semantics
50
+ - ad hoc storage creation or registration inside `update()`
50
51
 
51
52
  If the task depends on one of those areas, route it explicitly instead of guessing.
52
53
 
@@ -75,7 +76,7 @@ If the task depends on one of those areas, route it explicitly instead of guessi
75
76
  Before changing code, collect or infer:
76
77
 
77
78
  - dataset meaning
78
- - registered `PlatformTimeIndexMetaData` storage class or the class to create
79
+ - `PlatformTimeIndexMetaData` output storage class or the class to create
79
80
  - expected time index and identity index shape
80
81
  - expected columns and dtypes from the storage class
81
82
  - upstream dependencies
@@ -91,7 +92,7 @@ For every non-trivial DataNode task, make these decisions explicitly:
91
92
 
92
93
  1. Is this a new dataset or the same dataset?
93
94
  2. Is this change storage-contract work or update-process work?
94
- 3. Is the storage class registered through its MetaTable path, or should the MetaTable skill handle it?
95
+ 3. Is the storage class contract correct, or should the MetaTable skill handle it?
95
96
  4. Is the node single-index or MultiIndex?
96
97
  5. Does the first validation run happen under an explicit `hash_namespace(...)`?
97
98
 
@@ -112,6 +113,11 @@ Every storage class must include `__metatable_description__`. The description
112
113
  should explain the table's intention, row grain, and downstream use, not only
113
114
  list columns or schema mechanics.
114
115
 
116
+ Every `mapped_column(...)` in a storage class must include
117
+ `info={"label": ..., "description": ...}`. The column description must explain
118
+ what the value means for this dataset and how downstream users should interpret
119
+ it; do not merely repeat the column name or dtype.
120
+
115
121
  Use `__metatable_extra_hash_components__` on storage classes when distinct
116
122
  DataNode storage tables could otherwise have the same storage-relevant shape.
117
123
  For example, two one-index daily tables with one float column need a stable
@@ -153,18 +159,38 @@ class PricesTable(PlatformTimeIndexMetaData, Base):
153
159
  time_index: Mapped[datetime.datetime] = mapped_column(
154
160
  DateTime(timezone=True),
155
161
  nullable=False,
162
+ info={
163
+ "label": "Time Index",
164
+ "description": "UTC timestamp for the close-price observation.",
165
+ },
166
+ )
167
+ unique_identifier: Mapped[str] = mapped_column(
168
+ nullable=False,
169
+ info={
170
+ "label": "Unique Identifier",
171
+ "description": "Stable asset identifier for the observed price.",
172
+ },
173
+ )
174
+ close: Mapped[float] = mapped_column(
175
+ Float,
176
+ nullable=False,
177
+ info={
178
+ "label": "Close",
179
+ "description": "Daily close price used by portfolio and risk analytics.",
180
+ },
156
181
  )
157
- unique_identifier: Mapped[str] = mapped_column(nullable=False)
158
- close: Mapped[float] = mapped_column(Float, nullable=False)
159
182
  ```
160
183
 
161
- Register storage before constructing the DataNode:
184
+ Storage registration is inferred from the class metadata and active Main
185
+ Sequence project/session. The DataNode constructor ensures the output
186
+ `storage_table` is registered when needed. You may still call it explicitly
187
+ during bootstrap when code needs the returned metadata object:
162
188
 
163
189
  ```python
164
- PricesTable.register(data_source_uid=data_source_uid)
190
+ PricesTable.register()
165
191
  ```
166
192
 
167
- `PlatformTimeIndexMetaData.register(...)` is the storage lifecycle path. Treat it
193
+ `PlatformTimeIndexMetaData.register()` is the storage lifecycle path. Treat it
168
194
  as an idempotent get-or-create operation: the platform returns the registered
169
195
  metadata and UID, and the SDK records that metadata on the class. Do not manually
170
196
  attach an existing UID, reconstruct a generic `MetaTable`, or use manual bind
@@ -175,18 +201,21 @@ helpers as an authoring step.
175
201
  The DataNode constructor should accept:
176
202
 
177
203
  - a `DataNodeConfiguration`
178
- - a registered `storage_table: type[PlatformTimeIndexMetaData]`
204
+ - a `storage_table: type[PlatformTimeIndexMetaData]`
179
205
  - optional `hash_namespace`
180
206
 
181
207
  The constructor `storage_table` is the output storage contract. Keep it out of
182
- `DataNodeConfiguration`.
208
+ `DataNodeConfiguration`. Do not pre-register this output storage class just to
209
+ construct the node; `DataNode` and `PersistManager` call the SDK registration
210
+ lifecycle automatically when the class is not yet bound.
183
211
 
184
212
  If the DataNode needs to select another DataNode's storage table as a
185
213
  dependency, put that dependency storage reference in the config as
186
214
  `type[PlatformTimeIndexMetaData]`. Do not add an extra constructor argument for
187
215
  dependency storage tables. Config values of this type are hashed by the bound
188
- `TimeIndexMetaData.uid` from `StorageClass.__time_index_metadata__`, so the class
189
- must be registered before DataNode construction.
216
+ `TimeIndexMetaData.uid` from `StorageClass.__time_index_metadata__`; if the
217
+ class is not yet bound, the config serializer calls `StorageClass.register()`
218
+ before reading the UID.
190
219
 
191
220
  Do not accept `test_node`. It has been removed. Use explicit
192
221
  `hash_namespace(...)` or `hash_namespace="..."`.
@@ -359,6 +388,10 @@ DataNode storage table needs a platform-managed FK, use
359
388
  `ForeignKey(Target.__table__.c.uid)`, table fullnames, or explicit target UID
360
389
  maps in DataNode examples.
361
390
 
391
+ Do not ask users to name these foreign keys. `MetaTableForeignKey(...)` derives
392
+ a stable contract name when `name` is omitted; `name=...` is only for deliberate
393
+ overrides.
394
+
362
395
  Registration of the storage class follows the MetaTable lifecycle:
363
396
  `register()` recursively registers unresolved FK target model classes, uses the
364
397
  local process registry keyed by `storage_hash`, and writes the target
@@ -379,11 +412,13 @@ When reviewing an existing DataNode, look for:
379
412
 
380
413
  - output storage contract hidden in `DataNodeConfiguration`
381
414
  - missing `__metatable_description__` on the storage table
415
+ - storage columns without `mapped_column(info={"label": ..., "description": ...})`
382
416
  - dependency storage table passed as an ad hoc constructor argument
383
417
  - schema or published table metadata hidden in DataNode configuration
384
418
  - `test_node=True`
385
419
  - missing explicit `storage_table`
386
- - accidental storage registration inside the DataNode
420
+ - manual pre-registration of the output `storage_table` when no returned
421
+ metadata object is needed
387
422
  - wrong split between hashed config fields and non-config class/runtime values
388
423
  - misuse of `hash_namespace`
389
424
  - non-incremental `update()` behavior
@@ -397,10 +432,12 @@ When reviewing an existing DataNode, look for:
397
432
  Do not claim success until you have checked:
398
433
 
399
434
  - the relevant docs were read first
400
- - storage is a registered `PlatformTimeIndexMetaData` class
435
+ - output storage is a `PlatformTimeIndexMetaData` class that the SDK can register
401
436
  - storage has an intention-rich `__metatable_description__`
437
+ - every storage column has an intention-rich `info.description`
402
438
  - the DataNode constructor requires `storage_table`
403
- - dependency storage-table references live in config and are registered
439
+ - dependency storage-table references live in config and rely on SDK
440
+ registration during config serialization
404
441
  - config fields are updater-scoped by default
405
442
  - no removed hash metadata markers remain
406
443
  - no `test_node` usage remains
@@ -102,6 +102,9 @@ Schema must come from SQLAlchemy table metadata, usually `__table_args__ = {"sch
102
102
  Always declare `__metatable_description__` on the model. The description must
103
103
  explain the table's business intention, row grain, and expected use, not only
104
104
  the schema. Column-level descriptions stay in `mapped_column(info={...})`.
105
+ Every mapped column must include `info={"label": ..., "description": ...}`.
106
+ The column description must explain what the value means in this table and how
107
+ it is used, not just restate the column name or dtype.
105
108
 
106
109
  Use `__metatable_extra_hash_components__` when two backend-managed tables could
107
110
  otherwise produce the same storage hash because their storage-relevant shape is
@@ -124,13 +127,32 @@ class Account(PlatformManagedMetaTable, Base):
124
127
  "Customer account master records used to scope balances, holdings, and "
125
128
  "account-level limits."
126
129
  )
130
+ __metatable_labels__ = ["sdk-example"]
131
+
132
+ uid: Mapped[uuid.UUID] = mapped_column(
133
+ Uuid,
134
+ primary_key=True,
135
+ info={
136
+ "label": "Account UID",
137
+ "description": "Stable account identifier referenced by dependent tables.",
138
+ },
139
+ )
140
+ name: Mapped[str] = mapped_column(
141
+ String(255),
142
+ nullable=False,
143
+ info={
144
+ "label": "Name",
145
+ "description": "Display name used to identify the account in examples.",
146
+ },
147
+ )
127
148
 
128
149
 
129
- account_meta_table = Account.register(labels=["sdk-example"])
150
+ account_meta_table = Account.register()
130
151
  ```
131
152
 
132
- Pass `description=...` only when the call intentionally overrides the class
133
- default.
153
+ Registration metadata belongs on the class. Do not pass description, labels,
154
+ provisioning, data-source UID, hash namespace, time-index fields, or storage
155
+ layout into `register()`.
134
156
 
135
157
  For platform-managed registration, the data source is resolved from the active Main Sequence project/session, the same way DataNode does. Do not require or thread a `data_source_uid` through normal platform-managed example code.
136
158
 
@@ -160,6 +182,11 @@ maps in the platform-managed path. Registration is the lifecycle path:
160
182
  returned `MetaTable` in a local process registry keyed by `storage_hash`, and
161
183
  uses the target `MetaTable.uid` in the child FK contract.
162
184
 
185
+ Do not require users to provide foreign-key names. `MetaTableForeignKey(...)`
186
+ accepts `name=...` only as an override; when omitted, the SDK derives a stable
187
+ PostgreSQL-safe contract name from the child table and source column after the
188
+ column is attached to the SQLAlchemy table.
189
+
163
190
  Use this pattern:
164
191
 
165
192
  ```python
@@ -176,7 +203,7 @@ both the schema and the table's intention.
176
203
  Example registration order:
177
204
 
178
205
  ```python
179
- asset_meta_table = Asset.register(...)
206
+ asset_meta_table = Asset.register()
180
207
  ```
181
208
 
182
209
  The child registration registers `Account` first if it has not already been
@@ -216,11 +243,13 @@ When reviewing an existing MetaTable workflow, look for:
216
243
 
217
244
  - missing namespace or identifier
218
245
  - missing `__metatable_description__`, or a description that only repeats column names instead of table intention
246
+ - mapped columns without `info.label` and `info.description`
219
247
  - backend-managed models that do not inherit `PlatformManagedMetaTable`
220
248
  - backend-managed examples that use namespace environment variables instead of a plain `sdk-examples` namespace
221
249
  - duplicate schema sources outside SQLAlchemy table metadata
222
250
  - external tables registered with unstable physical names
223
- - platform-managed child tables that are registered before parent tables
251
+ - platform-managed examples that manually sequence parent registration instead
252
+ of relying on `MetaTableForeignKey(...)` recursive registration
224
253
  - external child registrations that do not map foreign-key targets to registered parent `MetaTable.uid` values
225
254
  - compiled SQL operations without complete table scope
226
255
  - raw SQL that hardcodes stale physical names
@@ -232,6 +261,7 @@ Do not claim success until you have checked:
232
261
 
233
262
  - the table contract matches the intended row contract
234
263
  - the table has an intention-rich `__metatable_description__`
264
+ - every mapped column has an intention-rich `info.description`
235
265
  - indexes are intentional
236
266
  - foreign keys resolve to the correct dependency targets
237
267
  - management mode is correct
@@ -242,7 +272,8 @@ Do not claim success until you have checked:
242
272
  For related tables, also check:
243
273
 
244
274
  - aliases are readable
245
- - platform-managed parent tables are registered before child tables
275
+ - platform-managed child registration recursively resolves parent
276
+ `MetaTableForeignKey(...)` targets
246
277
  - external child registration requests map FK targets to the registered parent UIDs
247
278
  - query results still match the expected response contract
248
279
 
@@ -94,10 +94,15 @@ def _(value: type[Any]) -> Any:
94
94
 
95
95
  time_index_metadata = value.get_time_index_metadata()
96
96
  uid = getattr(time_index_metadata, "uid", None)
97
+ if uid in (None, ""):
98
+ value.register()
99
+ time_index_metadata = value.get_time_index_metadata()
100
+ uid = getattr(time_index_metadata, "uid", None)
101
+
97
102
  if uid in (None, ""):
98
103
  raise ValueError(
99
- "PlatformTimeIndexMetaData config values must be registered "
100
- "before they can be hashed."
104
+ "PlatformTimeIndexMetaData config value register() did not bind "
105
+ "TimeIndexMetaData metadata before hashing."
101
106
  )
102
107
  return {
103
108
  "__type__": "platform_time_index_metadata",
@@ -29,7 +29,11 @@ from mainsequence.client.utils import META_TABLES_CONSTANTS as CONSTANTS
29
29
  from mainsequence.instrumentation import tracer
30
30
  from mainsequence.logconf import logger
31
31
  from mainsequence.meta_tables import PlatformTimeIndexMetaData
32
- from mainsequence.meta_tables.data_nodes.persist_managers import APIPersistManager, PersistManager
32
+ from mainsequence.meta_tables.data_nodes.persist_managers import (
33
+ APIPersistManager,
34
+ PersistManager,
35
+ ensure_registered_storage_table,
36
+ )
33
37
 
34
38
  from .models import BaseConfiguration
35
39
  from .namespacing import current_hash_namespace
@@ -625,34 +629,15 @@ class DataNode(DataAccessMixin, ABC):
625
629
 
626
630
  @storage_table.setter
627
631
  def storage_table(self, value: type[PlatformTimeIndexMetaData]) -> None:
628
- if value is None:
629
- raise TypeError(
630
- "DataNode storage_table is required and must be a "
631
- "PlatformTimeIndexMetaData model class."
632
- )
633
- is_time_index_model_class = isinstance(value, type) and issubclass(
634
- value, PlatformTimeIndexMetaData
635
- )
636
- if not is_time_index_model_class:
637
- raise TypeError(
638
- "DataNode storage_table must be a PlatformTimeIndexMetaData model class; "
639
- f"got {type(value).__name__}."
640
- )
641
- if value.get_time_index_metadata() is None:
642
- raise ValueError(
643
- "DataNode storage_table must be registered before construction."
644
- )
645
- if value.get_meta_table_uid() in (None, ""):
646
- raise ValueError("DataNode storage_table must provide a MetaTable UID.")
647
- if value.get_data_source_uid() in (None, ""):
648
- raise ValueError("DataNode storage_table must provide a data-source UID.")
649
- self._storage_table = value
632
+ self._storage_table = ensure_registered_storage_table(value, context="DataNode")
650
633
 
651
634
  @property
652
635
  def storage_metadata(self) -> Any:
653
636
  storage_metadata = self.storage_table.get_time_index_metadata()
654
637
  if storage_metadata is None:
655
- raise ValueError("DataNode storage_table must be registered before use.")
638
+ raise ValueError(
639
+ "DataNode storage_table registration metadata is unavailable after register()."
640
+ )
656
641
  return storage_metadata
657
642
 
658
643
  def _initialize_configuration(self, init_kwargs: dict) -> None:
@@ -77,6 +77,40 @@ def get_data_node_source_code_git_hash(DataNodeClass: type[Any]) -> str:
77
77
  return hash_object.hexdigest()
78
78
 
79
79
 
80
+ def ensure_registered_storage_table(
81
+ storage_table: type[PlatformTimeIndexMetaData],
82
+ *,
83
+ context: str,
84
+ ) -> type[PlatformTimeIndexMetaData]:
85
+ if storage_table is None:
86
+ raise TypeError(
87
+ f"{context} storage_table is required and must be a "
88
+ "PlatformTimeIndexMetaData model class."
89
+ )
90
+ if not isinstance(storage_table, type) or not issubclass(
91
+ storage_table,
92
+ PlatformTimeIndexMetaData,
93
+ ):
94
+ raise TypeError(
95
+ f"{context} storage_table must be a PlatformTimeIndexMetaData "
96
+ f"model class; got {type(storage_table).__name__}."
97
+ )
98
+
99
+ if storage_table.get_time_index_metadata() is None:
100
+ storage_table.register()
101
+
102
+ storage_metadata = storage_table.get_time_index_metadata()
103
+ if storage_metadata is None:
104
+ raise ValueError(
105
+ f"{context} storage_table.register() did not bind TimeIndexMetaData metadata."
106
+ )
107
+ if storage_table.get_meta_table_uid() in (None, ""):
108
+ raise ValueError(f"{context} storage_table must provide a MetaTable UID.")
109
+ if storage_table.get_data_source_uid() in (None, ""):
110
+ raise ValueError(f"{context} storage_table must provide a data-source UID.")
111
+ return storage_table
112
+
113
+
80
114
  class BasePersistManager:
81
115
  UPDATE_CLASS: ClassVar[type[Any] | None] = None
82
116
  UPDATE_DETAILS_CLASS: ClassVar[type[Any] | None] = None
@@ -114,36 +148,15 @@ class BasePersistManager:
114
148
  def _validate_storage_table(
115
149
  storage_table: type[PlatformTimeIndexMetaData],
116
150
  ) -> type[PlatformTimeIndexMetaData]:
117
- if storage_table is None:
118
- raise TypeError(
119
- "PersistManager storage_table is required and must be a "
120
- "PlatformTimeIndexMetaData model class."
121
- )
122
- if not isinstance(storage_table, type) or not issubclass(
123
- storage_table,
124
- PlatformTimeIndexMetaData,
125
- ):
126
- raise TypeError(
127
- "PersistManager storage_table must be a PlatformTimeIndexMetaData "
128
- f"model class; got {type(storage_table).__name__}."
129
- )
130
- storage_metadata = storage_table.get_time_index_metadata()
131
- if storage_metadata is None:
132
- raise ValueError(
133
- "PersistManager storage_table must be registered before "
134
- "constructing the DataNode."
135
- )
136
- if storage_table.get_meta_table_uid() in (None, ""):
137
- raise ValueError("PersistManager storage_table must provide a MetaTable UID.")
138
- if storage_table.get_data_source_uid() in (None, ""):
139
- raise ValueError("PersistManager storage_table must provide a data-source UID.")
140
- return storage_table
151
+ return ensure_registered_storage_table(storage_table, context="PersistManager")
141
152
 
142
153
  @property
143
154
  def storage_metadata(self) -> Any:
144
155
  storage_metadata = self.storage_table.get_time_index_metadata()
145
156
  if storage_metadata is None:
146
- raise ValueError("PersistManager storage_table must be registered before use.")
157
+ raise ValueError(
158
+ "PersistManager storage_table registration metadata is unavailable after register()."
159
+ )
147
160
  return storage_metadata
148
161
 
149
162
  @property