mainsequence 4.1.1__tar.gz → 4.1.3__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 (122) hide show
  1. {mainsequence-4.1.1 → mainsequence-4.1.3}/PKG-INFO +1 -1
  2. {mainsequence-4.1.1 → mainsequence-4.1.3}/agent_scaffold/skills/data_publishing/data_nodes/SKILL.md +16 -1
  3. {mainsequence-4.1.1 → mainsequence-4.1.3}/mainsequence/client/dtype_codec.py +20 -19
  4. {mainsequence-4.1.1 → mainsequence-4.1.3}/mainsequence/client/models_metatables.py +75 -42
  5. {mainsequence-4.1.1 → mainsequence-4.1.3}/mainsequence/client/utils.py +13 -4
  6. {mainsequence-4.1.1 → mainsequence-4.1.3}/mainsequence/meta_tables/data_nodes/build_operations.py +50 -98
  7. {mainsequence-4.1.1 → mainsequence-4.1.3}/mainsequence/meta_tables/data_nodes/persist_managers.py +11 -0
  8. {mainsequence-4.1.1 → mainsequence-4.1.3}/mainsequence/meta_tables/data_nodes/run_operations.py +41 -63
  9. {mainsequence-4.1.1 → mainsequence-4.1.3}/mainsequence/meta_tables/sqlalchemy_contracts.py +7 -4
  10. {mainsequence-4.1.1 → mainsequence-4.1.3}/mainsequence.egg-info/PKG-INFO +1 -1
  11. {mainsequence-4.1.1 → mainsequence-4.1.3}/pyproject.toml +1 -1
  12. {mainsequence-4.1.1 → mainsequence-4.1.3}/tests/test_build_operations_hashing.py +75 -0
  13. {mainsequence-4.1.1 → mainsequence-4.1.3}/tests/test_data_node_update_flow.py +133 -0
  14. {mainsequence-4.1.1 → mainsequence-4.1.3}/tests/test_pod_project_resolution.py +3 -55
  15. {mainsequence-4.1.1 → mainsequence-4.1.3}/tests/test_run_configuration.py +64 -0
  16. {mainsequence-4.1.1 → mainsequence-4.1.3}/tests/test_update_runner_uid_runtime.py +85 -4
  17. {mainsequence-4.1.1 → mainsequence-4.1.3}/LICENSE +0 -0
  18. {mainsequence-4.1.1 → mainsequence-4.1.3}/README.md +0 -0
  19. {mainsequence-4.1.1 → mainsequence-4.1.3}/agent_scaffold/AGENTS.md +0 -0
  20. {mainsequence-4.1.1 → mainsequence-4.1.3}/agent_scaffold/skills/a2a_communication/SKILL.md +0 -0
  21. {mainsequence-4.1.1 → mainsequence-4.1.3}/agent_scaffold/skills/application_surfaces/api_surfaces/SKILL.md +0 -0
  22. {mainsequence-4.1.1 → mainsequence-4.1.3}/agent_scaffold/skills/command_center/adapter_from_api/SKILL.md +0 -0
  23. {mainsequence-4.1.1 → mainsequence-4.1.3}/agent_scaffold/skills/command_center/api_mock_prototyping/SKILL.md +0 -0
  24. {mainsequence-4.1.1 → mainsequence-4.1.3}/agent_scaffold/skills/command_center/app_components/SKILL.md +0 -0
  25. {mainsequence-4.1.1 → mainsequence-4.1.3}/agent_scaffold/skills/command_center/connections/SKILL.md +0 -0
  26. {mainsequence-4.1.1 → mainsequence-4.1.3}/agent_scaffold/skills/command_center/workspace_analysis/SKILL.md +0 -0
  27. {mainsequence-4.1.1 → mainsequence-4.1.3}/agent_scaffold/skills/command_center/workspace_builder/SKILL.md +0 -0
  28. {mainsequence-4.1.1 → mainsequence-4.1.3}/agent_scaffold/skills/command_center/workspace_design/SKILL.md +0 -0
  29. {mainsequence-4.1.1 → mainsequence-4.1.3}/agent_scaffold/skills/dashboards/streamlit/SKILL.md +0 -0
  30. {mainsequence-4.1.1 → mainsequence-4.1.3}/agent_scaffold/skills/data_access/exploration/SKILL.md +0 -0
  31. {mainsequence-4.1.1 → mainsequence-4.1.3}/agent_scaffold/skills/data_publishing/meta_tables/SKILL.md +0 -0
  32. {mainsequence-4.1.1 → mainsequence-4.1.3}/agent_scaffold/skills/maintenance/bug_auditor/SKILL.md +0 -0
  33. {mainsequence-4.1.1 → mainsequence-4.1.3}/agent_scaffold/skills/ms-markets/SKILL.md +0 -0
  34. {mainsequence-4.1.1 → mainsequence-4.1.3}/agent_scaffold/skills/platform_operations/access_control_and_sharing/SKILL.md +0 -0
  35. {mainsequence-4.1.1 → mainsequence-4.1.3}/agent_scaffold/skills/platform_operations/orchestration_and_releases/SKILL.md +0 -0
  36. {mainsequence-4.1.1 → mainsequence-4.1.3}/agent_scaffold/skills/project_builder/SKILL.md +0 -0
  37. {mainsequence-4.1.1 → mainsequence-4.1.3}/agent_scaffold/skills/project_to_agent/SKILL.md +0 -0
  38. {mainsequence-4.1.1 → mainsequence-4.1.3}/mainsequence/__init__.py +0 -0
  39. {mainsequence-4.1.1 → mainsequence-4.1.3}/mainsequence/__main__.py +0 -0
  40. {mainsequence-4.1.1 → mainsequence-4.1.3}/mainsequence/bootstrap.py +0 -0
  41. {mainsequence-4.1.1 → mainsequence-4.1.3}/mainsequence/cli/__init__.py +0 -0
  42. {mainsequence-4.1.1 → mainsequence-4.1.3}/mainsequence/cli/api.py +0 -0
  43. {mainsequence-4.1.1 → mainsequence-4.1.3}/mainsequence/cli/browser_auth.py +0 -0
  44. {mainsequence-4.1.1 → mainsequence-4.1.3}/mainsequence/cli/cli.py +0 -0
  45. {mainsequence-4.1.1 → mainsequence-4.1.3}/mainsequence/cli/config.py +0 -0
  46. {mainsequence-4.1.1 → mainsequence-4.1.3}/mainsequence/cli/docker_utils.py +0 -0
  47. {mainsequence-4.1.1 → mainsequence-4.1.3}/mainsequence/cli/doctor.py +0 -0
  48. {mainsequence-4.1.1 → mainsequence-4.1.3}/mainsequence/cli/local_ops.py +0 -0
  49. {mainsequence-4.1.1 → mainsequence-4.1.3}/mainsequence/cli/model_filters.py +0 -0
  50. {mainsequence-4.1.1 → mainsequence-4.1.3}/mainsequence/cli/project_status.py +0 -0
  51. {mainsequence-4.1.1 → mainsequence-4.1.3}/mainsequence/cli/pydantic_cli.py +0 -0
  52. {mainsequence-4.1.1 → mainsequence-4.1.3}/mainsequence/cli/sdk_utils.py +0 -0
  53. {mainsequence-4.1.1 → mainsequence-4.1.3}/mainsequence/cli/ssh_utils.py +0 -0
  54. {mainsequence-4.1.1 → mainsequence-4.1.3}/mainsequence/cli/ui.py +0 -0
  55. {mainsequence-4.1.1 → mainsequence-4.1.3}/mainsequence/client/__init__.py +0 -0
  56. {mainsequence-4.1.1 → mainsequence-4.1.3}/mainsequence/client/agent_runtime_models.py +0 -0
  57. {mainsequence-4.1.1 → mainsequence-4.1.3}/mainsequence/client/base.py +0 -0
  58. {mainsequence-4.1.1 → mainsequence-4.1.3}/mainsequence/client/client.py +0 -0
  59. {mainsequence-4.1.1 → mainsequence-4.1.3}/mainsequence/client/command_center/__init__.py +0 -0
  60. {mainsequence-4.1.1 → mainsequence-4.1.3}/mainsequence/client/command_center/app_component.py +0 -0
  61. {mainsequence-4.1.1 → mainsequence-4.1.3}/mainsequence/client/command_center/connections.py +0 -0
  62. {mainsequence-4.1.1 → mainsequence-4.1.3}/mainsequence/client/command_center/data_models.py +0 -0
  63. {mainsequence-4.1.1 → mainsequence-4.1.3}/mainsequence/client/command_center/workspace.py +0 -0
  64. {mainsequence-4.1.1 → mainsequence-4.1.3}/mainsequence/client/command_center/workspace_snapshot.py +0 -0
  65. {mainsequence-4.1.1 → mainsequence-4.1.3}/mainsequence/client/data_sources_interfaces/__init__.py +0 -0
  66. {mainsequence-4.1.1 → mainsequence-4.1.3}/mainsequence/client/data_sources_interfaces/duckdb.py +0 -0
  67. {mainsequence-4.1.1 → mainsequence-4.1.3}/mainsequence/client/data_sources_interfaces/sqlite.py +0 -0
  68. {mainsequence-4.1.1 → mainsequence-4.1.3}/mainsequence/client/exceptions.py +0 -0
  69. {mainsequence-4.1.1 → mainsequence-4.1.3}/mainsequence/client/fastapi/__init__.py +0 -0
  70. {mainsequence-4.1.1 → mainsequence-4.1.3}/mainsequence/client/fastapi/auth.py +0 -0
  71. {mainsequence-4.1.1 → mainsequence-4.1.3}/mainsequence/client/models_foundry.py +0 -0
  72. {mainsequence-4.1.1 → mainsequence-4.1.3}/mainsequence/client/models_helpers.py +0 -0
  73. {mainsequence-4.1.1 → mainsequence-4.1.3}/mainsequence/client/models_user.py +0 -0
  74. {mainsequence-4.1.1 → mainsequence-4.1.3}/mainsequence/compute_validation.py +0 -0
  75. {mainsequence-4.1.1 → mainsequence-4.1.3}/mainsequence/defaults.py +0 -0
  76. {mainsequence-4.1.1 → mainsequence-4.1.3}/mainsequence/instrumentation/__init__.py +0 -0
  77. {mainsequence-4.1.1 → mainsequence-4.1.3}/mainsequence/instrumentation/utils.py +0 -0
  78. {mainsequence-4.1.1 → mainsequence-4.1.3}/mainsequence/logconf.py +0 -0
  79. {mainsequence-4.1.1 → mainsequence-4.1.3}/mainsequence/meta_tables/__init__.py +0 -0
  80. {mainsequence-4.1.1 → mainsequence-4.1.3}/mainsequence/meta_tables/__main__.py +0 -0
  81. {mainsequence-4.1.1 → mainsequence-4.1.3}/mainsequence/meta_tables/compiled_sql.py +0 -0
  82. {mainsequence-4.1.1 → mainsequence-4.1.3}/mainsequence/meta_tables/config.py +0 -0
  83. {mainsequence-4.1.1 → mainsequence-4.1.3}/mainsequence/meta_tables/configuration_models.py +0 -0
  84. {mainsequence-4.1.1 → mainsequence-4.1.3}/mainsequence/meta_tables/data_nodes/__init__.py +0 -0
  85. {mainsequence-4.1.1 → mainsequence-4.1.3}/mainsequence/meta_tables/data_nodes/data_nodes.py +0 -0
  86. {mainsequence-4.1.1 → mainsequence-4.1.3}/mainsequence/meta_tables/data_nodes/models.py +0 -0
  87. {mainsequence-4.1.1 → mainsequence-4.1.3}/mainsequence/meta_tables/data_nodes/namespacing.py +0 -0
  88. {mainsequence-4.1.1 → mainsequence-4.1.3}/mainsequence/meta_tables/data_nodes/utils.py +0 -0
  89. {mainsequence-4.1.1 → mainsequence-4.1.3}/mainsequence/meta_tables/future_registry.py +0 -0
  90. {mainsequence-4.1.1 → mainsequence-4.1.3}/mainsequence/meta_tables/hashing.py +0 -0
  91. {mainsequence-4.1.1 → mainsequence-4.1.3}/mainsequence/meta_tables/pydantic_metadata.py +0 -0
  92. {mainsequence-4.1.1 → mainsequence-4.1.3}/mainsequence/meta_tables/utils.py +0 -0
  93. {mainsequence-4.1.1 → mainsequence-4.1.3}/mainsequence/runtime_flags.py +0 -0
  94. {mainsequence-4.1.1 → mainsequence-4.1.3}/mainsequence.egg-info/SOURCES.txt +0 -0
  95. {mainsequence-4.1.1 → mainsequence-4.1.3}/mainsequence.egg-info/dependency_links.txt +0 -0
  96. {mainsequence-4.1.1 → mainsequence-4.1.3}/mainsequence.egg-info/entry_points.txt +0 -0
  97. {mainsequence-4.1.1 → mainsequence-4.1.3}/mainsequence.egg-info/requires.txt +0 -0
  98. {mainsequence-4.1.1 → mainsequence-4.1.3}/mainsequence.egg-info/top_level.txt +0 -0
  99. {mainsequence-4.1.1 → mainsequence-4.1.3}/setup.cfg +0 -0
  100. {mainsequence-4.1.1 → mainsequence-4.1.3}/tests/test_auth_precedence.py +0 -0
  101. {mainsequence-4.1.1 → mainsequence-4.1.3}/tests/test_cli.py +0 -0
  102. {mainsequence-4.1.1 → mainsequence-4.1.3}/tests/test_cli_browser_auth.py +0 -0
  103. {mainsequence-4.1.1 → mainsequence-4.1.3}/tests/test_client.py +0 -0
  104. {mainsequence-4.1.1 → mainsequence-4.1.3}/tests/test_command_center_app_component_models.py +0 -0
  105. {mainsequence-4.1.1 → mainsequence-4.1.3}/tests/test_command_center_data_models.py +0 -0
  106. {mainsequence-4.1.1 → mainsequence-4.1.3}/tests/test_command_center_models.py +0 -0
  107. {mainsequence-4.1.1 → mainsequence-4.1.3}/tests/test_data_access_mixin_dimension_audit.py +0 -0
  108. {mainsequence-4.1.1 → mainsequence-4.1.3}/tests/test_data_node_storage_dimension_queries.py +0 -0
  109. {mainsequence-4.1.1 → mainsequence-4.1.3}/tests/test_dependency_extras.py +0 -0
  110. {mainsequence-4.1.1 → mainsequence-4.1.3}/tests/test_duckdb_interface_dimensions.py +0 -0
  111. {mainsequence-4.1.1 → mainsequence-4.1.3}/tests/test_filter_normalization.py +0 -0
  112. {mainsequence-4.1.1 → mainsequence-4.1.3}/tests/test_logconf.py +0 -0
  113. {mainsequence-4.1.1 → mainsequence-4.1.3}/tests/test_meta_tables_client_models.py +0 -0
  114. {mainsequence-4.1.1 → mainsequence-4.1.3}/tests/test_meta_tables_sqlalchemy_contracts.py +0 -0
  115. {mainsequence-4.1.1 → mainsequence-4.1.3}/tests/test_models_user_request_bound_auth.py +0 -0
  116. {mainsequence-4.1.1 → mainsequence-4.1.3}/tests/test_project_batch_jobs_from_file.py +0 -0
  117. {mainsequence-4.1.1 → mainsequence-4.1.3}/tests/test_secret_client_model.py +0 -0
  118. {mainsequence-4.1.1 → mainsequence-4.1.3}/tests/test_source_table_configuration.py +0 -0
  119. {mainsequence-4.1.1 → mainsequence-4.1.3}/tests/test_sqlite_interface_dimensions.py +0 -0
  120. {mainsequence-4.1.1 → mainsequence-4.1.3}/tests/test_update_statistics.py +0 -0
  121. {mainsequence-4.1.1 → mainsequence-4.1.3}/tests/test_update_uid_guards.py +0 -0
  122. {mainsequence-4.1.1 → mainsequence-4.1.3}/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.1
3
+ Version: 4.1.3
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
@@ -161,6 +161,16 @@ The DataNode constructor should accept:
161
161
  - a registered `storage_table: type[PlatformTimeIndexMetaData]`
162
162
  - optional `hash_namespace`
163
163
 
164
+ The constructor `storage_table` is the output storage contract. Keep it out of
165
+ `DataNodeConfiguration`.
166
+
167
+ If the DataNode needs to select another DataNode's storage table as a
168
+ dependency, put that dependency storage reference in the config as
169
+ `type[PlatformTimeIndexMetaData]`. Do not add an extra constructor argument for
170
+ dependency storage tables. Config values of this type are hashed by the bound
171
+ `TimeIndexMetaData.uid` from `StorageClass.__time_index_metadata__`, so the class
172
+ must be registered or bound before DataNode construction.
173
+
164
174
  Do not accept `test_node`. It has been removed. Use explicit
165
175
  `hash_namespace(...)` or `hash_namespace="..."`.
166
176
 
@@ -287,6 +297,9 @@ Time index must be datetime64[ns, UTC]
287
297
 
288
298
  Dependencies belong in constructor setup and `dependencies()`.
289
299
 
300
+ Dependency storage-table selection belongs in `DataNodeConfiguration`, because
301
+ changing it changes the dependency graph and update identity.
302
+
290
303
  Do not construct dependency graphs dynamically inside `update()`.
291
304
 
292
305
  ### 8. Asset-Scoped Updates Must Be Explicit
@@ -318,7 +331,8 @@ surface for new DataNode work.
318
331
 
319
332
  When reviewing an existing DataNode, look for:
320
333
 
321
- - storage contract hidden in `DataNodeConfiguration`
334
+ - output storage contract hidden in `DataNodeConfiguration`
335
+ - dependency storage table passed as an ad hoc constructor argument
322
336
  - old `RecordDefinition` or `DataNodeMetaData` schema patterns
323
337
  - `update_only`, `runtime_only`, or `ignore_from_storage_hash`
324
338
  - `test_node=True`
@@ -339,6 +353,7 @@ Do not claim success until you have checked:
339
353
  - the relevant docs were read first
340
354
  - storage is a registered or bound `PlatformTimeIndexMetaData` class
341
355
  - the DataNode constructor requires `storage_table`
356
+ - dependency storage-table references live in config and are registered or bound
342
357
  - config fields are updater-scoped by default
343
358
  - no removed hash metadata markers remain
344
359
  - no `test_node` usage remains
@@ -163,9 +163,7 @@ def record_definitions_to_column_dtypes_map(
163
163
  )
164
164
 
165
165
  if duplicate_columns:
166
- raise ValueError(
167
- f"Duplicate DataNode record column names: {sorted(duplicate_columns)}"
168
- )
166
+ raise ValueError(f"Duplicate DataNode record column names: {sorted(duplicate_columns)}")
169
167
 
170
168
  return column_dtypes_map
171
169
 
@@ -247,10 +245,14 @@ def token_to_pandas_series(
247
245
  is_time_index: bool = False,
248
246
  nullable: bool = True,
249
247
  ) -> pd.Series:
250
- normalized = TIMESTAMP_TZ if is_time_index else normalize_dtype_token(
251
- token,
252
- remote=False,
253
- allow_naive_datetime=True,
248
+ normalized = (
249
+ TIMESTAMP_TZ
250
+ if is_time_index
251
+ else normalize_dtype_token(
252
+ token,
253
+ remote=False,
254
+ allow_naive_datetime=True,
255
+ )
254
256
  )
255
257
  if normalized == TIMESTAMP_TZ:
256
258
  return pd.to_datetime(series, errors="coerce", utc=True)
@@ -319,17 +321,15 @@ def _serialize_timestamp_tz_value(value: Any) -> str | None:
319
321
  raise ValueError(
320
322
  f"Remote datetime value {value!r} must include an explicit timezone offset or Z."
321
323
  )
322
- if isinstance(value, (pd.Timestamp, datetime.datetime)) and not _timestamp_is_timezone_aware(value):
323
- raise ValueError(
324
- f"Remote datetime value {value!r} must be timezone-aware."
325
- )
324
+ if isinstance(value, (pd.Timestamp, datetime.datetime)) and not _timestamp_is_timezone_aware(
325
+ value
326
+ ):
327
+ raise ValueError(f"Remote datetime value {value!r} must be timezone-aware.")
326
328
  timestamp = pd.to_datetime(value, errors="raise", utc=True)
327
329
  if not isinstance(timestamp, pd.Timestamp):
328
330
  timestamp = pd.Timestamp(timestamp).tz_convert("UTC")
329
331
  if timestamp.tzinfo is None:
330
- raise ValueError(
331
- f"Remote datetime value {value!r} must be timezone-aware."
332
- )
332
+ raise ValueError(f"Remote datetime value {value!r} must be timezone-aware.")
333
333
  timestamp = timestamp.tz_convert("UTC")
334
334
  return timestamp.isoformat().replace("+00:00", "Z")
335
335
 
@@ -375,11 +375,8 @@ def prepare_dataframe_for_remote_write(
375
375
  if column_name not in prepared.columns:
376
376
  continue
377
377
  normalized = normalize_dtype_token(token, remote=True)
378
- if normalized not in {DATE, TIMESTAMP_TZ}:
379
- continue
380
378
  prepared[column_name] = [
381
- serialize_remote_value(value, normalized)
382
- for value in prepared[column_name].tolist()
379
+ serialize_remote_value(value, normalized) for value in prepared[column_name].tolist()
383
380
  ]
384
381
  return prepared
385
382
 
@@ -459,7 +456,11 @@ def sqlalchemy_type_to_token(column_type: Any, *, remote: bool = True) -> str:
459
456
  "by the remote TS Manager contract."
460
457
  )
461
458
  return LOCAL_DATETIME_NAIVE
462
- if timezone is True or "with time zone" in normalized_backend or "timestamptz" in normalized_backend:
459
+ if (
460
+ timezone is True
461
+ or "with time zone" in normalized_backend
462
+ or "timestamptz" in normalized_backend
463
+ ):
463
464
  return TIMESTAMP_TZ
464
465
  if remote:
465
466
  raise ValueError(
@@ -1244,9 +1244,6 @@ class MetaTable(BasePydanticModel, LabelableObjectMixin, ShareableObjectMixin, B
1244
1244
  )
1245
1245
 
1246
1246
 
1247
- LOGICAL_COLUMN_DTYPES_ATTR = "mainsequence_column_dtypes_map"
1248
-
1249
-
1250
1247
  def _warn_legacy_compat(message: str, *, stacklevel: int = 3) -> None:
1251
1248
  warnings.warn(
1252
1249
  f"Deprecated TDAG compatibility path: {message}",
@@ -2143,7 +2140,6 @@ class DataNodeUpdate(TableUpdateNode, BaseObjectOrm):
2143
2140
  remote_dtypes: bool = True,
2144
2141
  allow_naive_datetime: bool = False,
2145
2142
  ):
2146
- logical_column_dtypes_map = data_frame.attrs.get(LOGICAL_COLUMN_DTYPES_ATTR)
2147
2143
  record_column_dtypes_map = record_definitions_to_column_dtypes_map(
2148
2144
  records,
2149
2145
  remote=remote_dtypes,
@@ -2178,40 +2174,6 @@ class DataNodeUpdate(TableUpdateNode, BaseObjectOrm):
2178
2174
  "DataNode records declare columns not present in the DataFrame: "
2179
2175
  f"{missing_record_columns}"
2180
2176
  )
2181
- if logical_column_dtypes_map is not None:
2182
- logical_column_dtypes_map = normalize_column_dtypes_map(
2183
- logical_column_dtypes_map,
2184
- remote=remote_dtypes,
2185
- allow_naive_datetime=allow_naive_datetime,
2186
- )
2187
- missing_logical_columns = [
2188
- column_name
2189
- for column_name in logical_column_dtypes_map
2190
- if column_name not in data_frame.columns
2191
- ]
2192
- if missing_logical_columns:
2193
- raise ValueError(
2194
- "Logical column dtype contract contains columns not present "
2195
- f"in the DataFrame: {missing_logical_columns}"
2196
- )
2197
- conflicting_declared_dtypes = {
2198
- column_name: {
2199
- "record_dtype": record_column_dtypes_map[column_name],
2200
- "logical_dtype": logical_column_dtypes_map[column_name],
2201
- }
2202
- for column_name in logical_column_dtypes_map
2203
- if (
2204
- column_name in record_column_dtypes_map
2205
- and logical_column_dtypes_map[column_name]
2206
- != record_column_dtypes_map[column_name]
2207
- )
2208
- }
2209
- if conflicting_declared_dtypes:
2210
- raise ValueError(
2211
- "Logical column dtype contract conflicts with DataNode records: "
2212
- f"{conflicting_declared_dtypes}"
2213
- )
2214
- column_dtypes_map.update(logical_column_dtypes_map)
2215
2177
  column_dtypes_map.update(record_column_dtypes_map)
2216
2178
 
2217
2179
  data_frame = data_frame.replace({np.nan: None})
@@ -3970,10 +3932,70 @@ class UpdateStatistics(BaseUpdateStatistics):
3970
3932
  def _assign_nested_coordinate(root: dict[Any, Any], keys: list[Any], value: Any) -> None:
3971
3933
  if not keys:
3972
3934
  return
3935
+ normalized_keys = [_normalize_update_stat_key(key) for key in keys]
3973
3936
  sub = root
3974
- for key in keys[:-1]:
3975
- sub = sub.setdefault(key, {})
3976
- sub[keys[-1]] = value
3937
+ for key in normalized_keys[:-1]:
3938
+ existing = sub.setdefault(key, {})
3939
+ if not isinstance(existing, dict):
3940
+ raise ValueError("Update statistics coordinate keys collide after JSON normalization.")
3941
+ sub = existing
3942
+ final_key = normalized_keys[-1]
3943
+ if final_key in sub:
3944
+ raise ValueError("Update statistics coordinate keys collide after JSON normalization.")
3945
+ sub[final_key] = value
3946
+
3947
+
3948
+ def _normalize_update_stat_key(key: Any) -> str:
3949
+ if isinstance(key, str):
3950
+ return key
3951
+ if isinstance(key, np.generic):
3952
+ key = key.item()
3953
+ if isinstance(key, datetime.datetime):
3954
+ value = serialize_to_json({"_": key})["_"]
3955
+ return str(value)
3956
+ if isinstance(key, datetime.date):
3957
+ return key.isoformat()
3958
+ if key is None:
3959
+ return "null"
3960
+ if isinstance(key, bool):
3961
+ return "true" if key else "false"
3962
+ if isinstance(key, int | float):
3963
+ try:
3964
+ return json.dumps(key, allow_nan=False)
3965
+ except (TypeError, ValueError):
3966
+ return str(key)
3967
+
3968
+ normalized = serialize_to_json({key: None})
3969
+ normalized_key = next(iter(normalized.keys()))
3970
+ if normalized_key is None:
3971
+ return "null"
3972
+ if isinstance(normalized_key, bool):
3973
+ return "true" if normalized_key else "false"
3974
+ if isinstance(normalized_key, int | float):
3975
+ try:
3976
+ return json.dumps(normalized_key, allow_nan=False)
3977
+ except (TypeError, ValueError):
3978
+ return str(normalized_key)
3979
+ return str(normalized_key)
3980
+
3981
+
3982
+ def _normalize_update_stat_mapping_keys(value: Any) -> Any:
3983
+ if isinstance(value, Mapping):
3984
+ normalized: dict[str, Any] = {}
3985
+ for key, item in value.items():
3986
+ normalized_key = _normalize_update_stat_key(key)
3987
+ if normalized_key in normalized:
3988
+ raise ValueError(
3989
+ "Update statistics coordinate keys collide after JSON normalization: "
3990
+ f"{normalized_key!r}."
3991
+ )
3992
+ normalized[normalized_key] = _normalize_update_stat_mapping_keys(item)
3993
+ return normalized
3994
+ if isinstance(value, list):
3995
+ return [_normalize_update_stat_mapping_keys(item) for item in value]
3996
+ if isinstance(value, tuple):
3997
+ return [_normalize_update_stat_mapping_keys(item) for item in value]
3998
+ return value
3977
3999
 
3978
4000
 
3979
4001
  def get_index_progress_chunk_stats(chunk_df, time_index_name, index_names):
@@ -4081,6 +4103,11 @@ class LastUpdateMultiIndexStatsPayload(BaseModel):
4081
4103
  index_progress: dict[str, Any] = Field(default_factory=dict)
4082
4104
  index_min: dict[str, Any] = Field(default_factory=dict)
4083
4105
 
4106
+ @model_validator(mode="before")
4107
+ @classmethod
4108
+ def _normalize_mapping_keys(cls, value: Any) -> Any:
4109
+ return _normalize_update_stat_mapping_keys(value)
4110
+
4084
4111
  def to_payload(self) -> dict[str, Any]:
4085
4112
  return {
4086
4113
  "_GLOBAL_": self.global_stats,
@@ -4099,6 +4126,11 @@ class LastUpdateIndexTimePayload(BaseModel):
4099
4126
  multi_index_stats: LastUpdateMultiIndexStatsPayload | None = None
4100
4127
  multi_index_column_stats: dict[str, Any] | None = Field(default_factory=dict)
4101
4128
 
4129
+ @model_validator(mode="before")
4130
+ @classmethod
4131
+ def _normalize_mapping_keys(cls, value: Any) -> Any:
4132
+ return _normalize_update_stat_mapping_keys(value)
4133
+
4102
4134
  @model_validator(mode="after")
4103
4135
  def _validate_shape(self):
4104
4136
  top_level_progress_keys = [
@@ -4164,7 +4196,8 @@ def build_last_update_index_time_payload(
4164
4196
  }
4165
4197
  )
4166
4198
 
4167
- return LastUpdateIndexTimePayload.model_validate(raw_payload).to_nested_payload()
4199
+ normalized_payload = _normalize_update_stat_mapping_keys(raw_payload)
4200
+ return LastUpdateIndexTimePayload.model_validate(normalized_payload).to_nested_payload()
4168
4201
 
4169
4202
 
4170
4203
  class HistoricalUpdateRecord:
@@ -9,11 +9,11 @@ import socket
9
9
  import subprocess
10
10
  import threading
11
11
  import time
12
- import uuid
13
12
  from dataclasses import dataclass, field
14
13
  from decimal import Decimal
15
14
  from enum import Enum
16
15
  from typing import TypedDict
16
+ from uuid import UUID, getnode
17
17
 
18
18
  import psutil
19
19
  import pytz
@@ -761,6 +761,9 @@ def serialize_to_json(kwargs):
761
761
  if isinstance(v, Decimal):
762
762
  return str(v)
763
763
 
764
+ if isinstance(v, UUID):
765
+ return str(v)
766
+
764
767
  if isinstance(v, datetime.datetime):
765
768
  dt = v
766
769
  if dt.tzinfo is None:
@@ -776,13 +779,19 @@ def serialize_to_json(kwargs):
776
779
  return v.model_dump()
777
780
 
778
781
  if isinstance(v, dict):
779
- return {k: to_jsonable(x) for k, x in v.items()}
782
+ return {to_json_key(k): to_jsonable(x) for k, x in v.items()}
780
783
  if isinstance(v, (list, tuple)):
781
784
  return [to_jsonable(x) for x in v]
782
785
 
783
786
  return v
784
787
 
785
- return {k: to_jsonable(v) for k, v in kwargs.items()}
788
+ def to_json_key(value):
789
+ key = to_jsonable(value)
790
+ if key is None or isinstance(key, str | int | float | bool):
791
+ return key
792
+ return str(key)
793
+
794
+ return {to_json_key(k): to_jsonable(v) for k, v in kwargs.items()}
786
795
 
787
796
 
788
797
  def _linux_machine_id() -> str | None:
@@ -839,7 +848,7 @@ def bios_uuid() -> str:
839
848
  return mid
840
849
 
841
850
  # Tier 4 – MAC address (uuid.getnode). Always available.
842
- return f"{uuid.getnode():012x}"
851
+ return f"{getnode():012x}"
843
852
 
844
853
 
845
854
  def _install_retry_adapters_in_place(
@@ -13,22 +13,19 @@ from enum import Enum
13
13
  from functools import singledispatch
14
14
  from types import SimpleNamespace
15
15
  from typing import TYPE_CHECKING, Any
16
+ from uuid import UUID
16
17
 
17
18
  from pydantic import BaseModel
18
19
 
19
20
  from mainsequence.client import BaseObjectOrm
20
21
  from mainsequence.client.models_helpers import get_model_class
21
22
  from mainsequence.client.models_metatables import _resolve_local_pod_project
22
- from mainsequence.instrumentation import tracer, tracer_instrumentator
23
23
  from mainsequence.meta_tables.pydantic_metadata import (
24
24
  is_serialized_pydantic_model,
25
25
  serialize_pydantic_model,
26
26
  strip_pydantic_hash_exclusions,
27
27
  )
28
28
 
29
- from .namespacing import disable_hash_namespace
30
- from .persist_managers import PersistManager
31
-
32
29
  if TYPE_CHECKING:
33
30
  from .data_nodes import APIDataNode, DataNode
34
31
 
@@ -70,11 +67,56 @@ def _serialize_api_timeserie(value: APIDataNode) -> dict[str, Any]:
70
67
  }
71
68
 
72
69
 
70
+ def _import_qualified_name(module_name: str, qualname: str) -> Any:
71
+ module = importlib.import_module(module_name)
72
+ value: Any = module
73
+ for part in qualname.split("."):
74
+ value = getattr(value, part)
75
+ return value
76
+
77
+
78
+ def _is_platform_time_index_metadata_class(value: Any) -> bool:
79
+ try:
80
+ from mainsequence.meta_tables.sqlalchemy_contracts import PlatformTimeIndexMetaData
81
+ except ImportError:
82
+ return False
83
+
84
+ try:
85
+ return isinstance(value, type) and issubclass(value, PlatformTimeIndexMetaData)
86
+ except TypeError:
87
+ return False
88
+
89
+
90
+ @serialize_argument.register(type)
91
+ def _(value: type[Any]) -> Any:
92
+ if not _is_platform_time_index_metadata_class(value):
93
+ return value
94
+
95
+ time_index_metadata = value.get_time_index_metadata()
96
+ uid = getattr(time_index_metadata, "uid", None)
97
+ if uid in (None, ""):
98
+ raise ValueError(
99
+ "PlatformTimeIndexMetaData config values must be registered or bound "
100
+ "before they can be hashed."
101
+ )
102
+ return {
103
+ "__type__": "platform_time_index_metadata",
104
+ "uid": str(uid),
105
+ "module": value.__module__,
106
+ "qualname": value.__qualname__,
107
+ }
108
+
109
+
73
110
  @serialize_argument.register(datetime.datetime)
74
111
  def _(value: datetime.datetime) -> str:
75
112
  return value.isoformat()
76
113
 
77
114
 
115
+ @serialize_argument.register(UUID)
116
+ def _(value: UUID) -> str:
117
+ return str(value)
118
+
119
+
78
120
  @serialize_argument.register(BaseModel)
79
121
  def _(value: BaseModel) -> dict[str, Any]:
80
122
  """Serialization logic for any Pydantic BaseModel."""
@@ -180,6 +222,8 @@ def parse_dictionary_before_hashing(dictionary: dict[str, Any]) -> dict[str, Any
180
222
  # The value["items"] are already serialized dicts
181
223
 
182
224
  local_ts_dict_to_hash[key] = [v["unique_identifier"] for v in value["items"]]
225
+ elif value.get("__type__") == "platform_time_index_metadata":
226
+ local_ts_dict_to_hash[key] = value["uid"]
183
227
  else:
184
228
  # recursively apply hash signature
185
229
  local_ts_dict_to_hash[key] = parse_dictionary_before_hashing(value)
@@ -327,6 +371,8 @@ class ConfigRebuilder(BaseRebuilder):
327
371
  return build_model(value)
328
372
 
329
373
  def _handle_complex_type(self, value: dict, **kwargs) -> Any:
374
+ if value.get("__type__") == "platform_time_index_metadata":
375
+ return _import_qualified_name(value["module"], value["qualname"])
330
376
  # Special case for ORM lists within the generic complex type handler
331
377
  if value.get("__type__") == "orm_model_list":
332
378
  return [build_model(item) for item in value["items"]]
@@ -434,97 +480,3 @@ def create_config(
434
480
  remote_initial_configuration=remote_config,
435
481
  build_configuration_json_schema=build_configuration_json_schema,
436
482
  )
437
-
438
-
439
- def rebuild_and_set_from_update_hash(
440
- update_hash: str,
441
- data_source_uid: str,
442
- set_dependencies_df: bool = False,
443
- graph_depth_limit: int = 1,
444
- ) -> DataNode:
445
- """
446
- Rebuilds a DataNode from stored configuration and attaches runtime sessions.
447
-
448
- Args:
449
- update_hash: The local hash ID of the DataNode.
450
- data_source_uid: The data source UID.
451
- set_dependencies_df: Whether to set the dependencies DataFrame.
452
- graph_depth_limit: The depth limit for graph traversal.
453
-
454
- Returns:
455
- The rebuilt DataNode object.
456
- """
457
- ts = rebuild_from_configuration(
458
- update_hash=update_hash,
459
- data_source=data_source_uid,
460
- )
461
- if set_dependencies_df:
462
- ts.set_relation_tree()
463
- ts._set_state_with_sessions(
464
- graph_depth=0,
465
- graph_depth_limit=graph_depth_limit,
466
- include_client_objects=False,
467
- )
468
- ts.logger.debug(f"ts {update_hash} rebuilt from configuration")
469
- return ts
470
-
471
-
472
- @tracer.start_as_current_span("TS: Rebuild From Configuration")
473
- def rebuild_from_configuration(update_hash: str, data_source: str | object) -> DataNode:
474
- """
475
- Rebuilds a DataNode instance from its configuration.
476
-
477
- Args:
478
- update_hash: The local hash ID of the DataNode.
479
- data_source: The data source UID or object.
480
-
481
- Returns:
482
- The rebuilt DataNode instance.
483
- """
484
- import importlib
485
-
486
- tracer_instrumentator.append_attribute_to_current_span("update_hash", update_hash)
487
-
488
- data_source_uid = getattr(data_source, "uid", data_source)
489
- data_node_update = PersistManager.UPDATE_CLASS.get_or_none(
490
- update_hash=update_hash,
491
- remote_table__data_source__uid=str(data_source_uid),
492
- include_relations_detail=True,
493
- )
494
- if data_node_update is None:
495
- raise ValueError(
496
- f"DataNodeUpdate {update_hash!r} with data source {data_source_uid!r} was not found."
497
- )
498
- storage_table = data_node_update.data_node_storage
499
- persist_manager = PersistManager.get_from_storage_table(
500
- update_hash=update_hash,
501
- storage_table=storage_table,
502
- data_node_update=data_node_update,
503
- )
504
- try:
505
- time_serie_config = persist_manager.local_build_configuration
506
- except Exception as e:
507
- raise e
508
-
509
- try:
510
- mod = importlib.import_module(time_serie_config["time_series_class_import_path"]["module"])
511
- TimeSerieClass = getattr(
512
- mod, time_serie_config["time_series_class_import_path"]["qualname"]
513
- )
514
- except Exception as e:
515
- raise e
516
-
517
- time_serie_class_name = time_serie_config["time_series_class_import_path"]["qualname"]
518
-
519
- time_serie_config.pop("time_series_class_import_path")
520
- time_serie_config = DeserializerManager().rebuild_serialized_config(
521
- time_serie_config, time_serie_class_name=time_serie_class_name
522
- )
523
-
524
- # IMPORTANT:
525
- # When rebuilding from stored config, ignore any ambient test namespace.
526
- # If the stored config includes 'hash_namespace' (test tables), it will still be passed explicitly.
527
- with disable_hash_namespace():
528
- re_build_ts = TimeSerieClass(**time_serie_config)
529
-
530
- return re_build_ts
@@ -438,11 +438,22 @@ class BasePersistManager:
438
438
  data=temp_df,
439
439
  data_source=self.data_source,
440
440
  overwrite=overwrite,
441
+ source_table_schema=self._source_table_schema(),
441
442
  )
442
443
 
443
444
  persisted = True
444
445
  return persisted
445
446
 
447
+ def _source_table_schema(self) -> dict[str, Any]:
448
+ time_index_name, index_names, column_dtypes_map = (
449
+ self.storage_metadata._require_time_indexed_table_contract()
450
+ )
451
+ return {
452
+ "time_index_name": time_index_name,
453
+ "index_names": list(index_names),
454
+ "column_dtypes_map": dict(column_dtypes_map),
455
+ }
456
+
446
457
  def get_update_statistics_for_table(self) -> UpdateStatistics:
447
458
  return self.storage_metadata.get_data_updates()
448
459