mainsequence 4.1.1__tar.gz → 4.1.2__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (122) hide show
  1. {mainsequence-4.1.1 → mainsequence-4.1.2}/PKG-INFO +1 -1
  2. {mainsequence-4.1.1 → mainsequence-4.1.2}/agent_scaffold/skills/data_publishing/data_nodes/SKILL.md +16 -1
  3. {mainsequence-4.1.1 → mainsequence-4.1.2}/mainsequence/meta_tables/data_nodes/build_operations.py +44 -98
  4. {mainsequence-4.1.1 → mainsequence-4.1.2}/mainsequence/meta_tables/data_nodes/run_operations.py +41 -63
  5. {mainsequence-4.1.1 → mainsequence-4.1.2}/mainsequence/meta_tables/sqlalchemy_contracts.py +7 -4
  6. {mainsequence-4.1.1 → mainsequence-4.1.2}/mainsequence.egg-info/PKG-INFO +1 -1
  7. {mainsequence-4.1.1 → mainsequence-4.1.2}/pyproject.toml +1 -1
  8. {mainsequence-4.1.1 → mainsequence-4.1.2}/tests/test_build_operations_hashing.py +41 -0
  9. {mainsequence-4.1.1 → mainsequence-4.1.2}/tests/test_pod_project_resolution.py +3 -55
  10. {mainsequence-4.1.1 → mainsequence-4.1.2}/tests/test_update_runner_uid_runtime.py +85 -4
  11. {mainsequence-4.1.1 → mainsequence-4.1.2}/LICENSE +0 -0
  12. {mainsequence-4.1.1 → mainsequence-4.1.2}/README.md +0 -0
  13. {mainsequence-4.1.1 → mainsequence-4.1.2}/agent_scaffold/AGENTS.md +0 -0
  14. {mainsequence-4.1.1 → mainsequence-4.1.2}/agent_scaffold/skills/a2a_communication/SKILL.md +0 -0
  15. {mainsequence-4.1.1 → mainsequence-4.1.2}/agent_scaffold/skills/application_surfaces/api_surfaces/SKILL.md +0 -0
  16. {mainsequence-4.1.1 → mainsequence-4.1.2}/agent_scaffold/skills/command_center/adapter_from_api/SKILL.md +0 -0
  17. {mainsequence-4.1.1 → mainsequence-4.1.2}/agent_scaffold/skills/command_center/api_mock_prototyping/SKILL.md +0 -0
  18. {mainsequence-4.1.1 → mainsequence-4.1.2}/agent_scaffold/skills/command_center/app_components/SKILL.md +0 -0
  19. {mainsequence-4.1.1 → mainsequence-4.1.2}/agent_scaffold/skills/command_center/connections/SKILL.md +0 -0
  20. {mainsequence-4.1.1 → mainsequence-4.1.2}/agent_scaffold/skills/command_center/workspace_analysis/SKILL.md +0 -0
  21. {mainsequence-4.1.1 → mainsequence-4.1.2}/agent_scaffold/skills/command_center/workspace_builder/SKILL.md +0 -0
  22. {mainsequence-4.1.1 → mainsequence-4.1.2}/agent_scaffold/skills/command_center/workspace_design/SKILL.md +0 -0
  23. {mainsequence-4.1.1 → mainsequence-4.1.2}/agent_scaffold/skills/dashboards/streamlit/SKILL.md +0 -0
  24. {mainsequence-4.1.1 → mainsequence-4.1.2}/agent_scaffold/skills/data_access/exploration/SKILL.md +0 -0
  25. {mainsequence-4.1.1 → mainsequence-4.1.2}/agent_scaffold/skills/data_publishing/meta_tables/SKILL.md +0 -0
  26. {mainsequence-4.1.1 → mainsequence-4.1.2}/agent_scaffold/skills/maintenance/bug_auditor/SKILL.md +0 -0
  27. {mainsequence-4.1.1 → mainsequence-4.1.2}/agent_scaffold/skills/ms-markets/SKILL.md +0 -0
  28. {mainsequence-4.1.1 → mainsequence-4.1.2}/agent_scaffold/skills/platform_operations/access_control_and_sharing/SKILL.md +0 -0
  29. {mainsequence-4.1.1 → mainsequence-4.1.2}/agent_scaffold/skills/platform_operations/orchestration_and_releases/SKILL.md +0 -0
  30. {mainsequence-4.1.1 → mainsequence-4.1.2}/agent_scaffold/skills/project_builder/SKILL.md +0 -0
  31. {mainsequence-4.1.1 → mainsequence-4.1.2}/agent_scaffold/skills/project_to_agent/SKILL.md +0 -0
  32. {mainsequence-4.1.1 → mainsequence-4.1.2}/mainsequence/__init__.py +0 -0
  33. {mainsequence-4.1.1 → mainsequence-4.1.2}/mainsequence/__main__.py +0 -0
  34. {mainsequence-4.1.1 → mainsequence-4.1.2}/mainsequence/bootstrap.py +0 -0
  35. {mainsequence-4.1.1 → mainsequence-4.1.2}/mainsequence/cli/__init__.py +0 -0
  36. {mainsequence-4.1.1 → mainsequence-4.1.2}/mainsequence/cli/api.py +0 -0
  37. {mainsequence-4.1.1 → mainsequence-4.1.2}/mainsequence/cli/browser_auth.py +0 -0
  38. {mainsequence-4.1.1 → mainsequence-4.1.2}/mainsequence/cli/cli.py +0 -0
  39. {mainsequence-4.1.1 → mainsequence-4.1.2}/mainsequence/cli/config.py +0 -0
  40. {mainsequence-4.1.1 → mainsequence-4.1.2}/mainsequence/cli/docker_utils.py +0 -0
  41. {mainsequence-4.1.1 → mainsequence-4.1.2}/mainsequence/cli/doctor.py +0 -0
  42. {mainsequence-4.1.1 → mainsequence-4.1.2}/mainsequence/cli/local_ops.py +0 -0
  43. {mainsequence-4.1.1 → mainsequence-4.1.2}/mainsequence/cli/model_filters.py +0 -0
  44. {mainsequence-4.1.1 → mainsequence-4.1.2}/mainsequence/cli/project_status.py +0 -0
  45. {mainsequence-4.1.1 → mainsequence-4.1.2}/mainsequence/cli/pydantic_cli.py +0 -0
  46. {mainsequence-4.1.1 → mainsequence-4.1.2}/mainsequence/cli/sdk_utils.py +0 -0
  47. {mainsequence-4.1.1 → mainsequence-4.1.2}/mainsequence/cli/ssh_utils.py +0 -0
  48. {mainsequence-4.1.1 → mainsequence-4.1.2}/mainsequence/cli/ui.py +0 -0
  49. {mainsequence-4.1.1 → mainsequence-4.1.2}/mainsequence/client/__init__.py +0 -0
  50. {mainsequence-4.1.1 → mainsequence-4.1.2}/mainsequence/client/agent_runtime_models.py +0 -0
  51. {mainsequence-4.1.1 → mainsequence-4.1.2}/mainsequence/client/base.py +0 -0
  52. {mainsequence-4.1.1 → mainsequence-4.1.2}/mainsequence/client/client.py +0 -0
  53. {mainsequence-4.1.1 → mainsequence-4.1.2}/mainsequence/client/command_center/__init__.py +0 -0
  54. {mainsequence-4.1.1 → mainsequence-4.1.2}/mainsequence/client/command_center/app_component.py +0 -0
  55. {mainsequence-4.1.1 → mainsequence-4.1.2}/mainsequence/client/command_center/connections.py +0 -0
  56. {mainsequence-4.1.1 → mainsequence-4.1.2}/mainsequence/client/command_center/data_models.py +0 -0
  57. {mainsequence-4.1.1 → mainsequence-4.1.2}/mainsequence/client/command_center/workspace.py +0 -0
  58. {mainsequence-4.1.1 → mainsequence-4.1.2}/mainsequence/client/command_center/workspace_snapshot.py +0 -0
  59. {mainsequence-4.1.1 → mainsequence-4.1.2}/mainsequence/client/data_sources_interfaces/__init__.py +0 -0
  60. {mainsequence-4.1.1 → mainsequence-4.1.2}/mainsequence/client/data_sources_interfaces/duckdb.py +0 -0
  61. {mainsequence-4.1.1 → mainsequence-4.1.2}/mainsequence/client/data_sources_interfaces/sqlite.py +0 -0
  62. {mainsequence-4.1.1 → mainsequence-4.1.2}/mainsequence/client/dtype_codec.py +0 -0
  63. {mainsequence-4.1.1 → mainsequence-4.1.2}/mainsequence/client/exceptions.py +0 -0
  64. {mainsequence-4.1.1 → mainsequence-4.1.2}/mainsequence/client/fastapi/__init__.py +0 -0
  65. {mainsequence-4.1.1 → mainsequence-4.1.2}/mainsequence/client/fastapi/auth.py +0 -0
  66. {mainsequence-4.1.1 → mainsequence-4.1.2}/mainsequence/client/models_foundry.py +0 -0
  67. {mainsequence-4.1.1 → mainsequence-4.1.2}/mainsequence/client/models_helpers.py +0 -0
  68. {mainsequence-4.1.1 → mainsequence-4.1.2}/mainsequence/client/models_metatables.py +0 -0
  69. {mainsequence-4.1.1 → mainsequence-4.1.2}/mainsequence/client/models_user.py +0 -0
  70. {mainsequence-4.1.1 → mainsequence-4.1.2}/mainsequence/client/utils.py +0 -0
  71. {mainsequence-4.1.1 → mainsequence-4.1.2}/mainsequence/compute_validation.py +0 -0
  72. {mainsequence-4.1.1 → mainsequence-4.1.2}/mainsequence/defaults.py +0 -0
  73. {mainsequence-4.1.1 → mainsequence-4.1.2}/mainsequence/instrumentation/__init__.py +0 -0
  74. {mainsequence-4.1.1 → mainsequence-4.1.2}/mainsequence/instrumentation/utils.py +0 -0
  75. {mainsequence-4.1.1 → mainsequence-4.1.2}/mainsequence/logconf.py +0 -0
  76. {mainsequence-4.1.1 → mainsequence-4.1.2}/mainsequence/meta_tables/__init__.py +0 -0
  77. {mainsequence-4.1.1 → mainsequence-4.1.2}/mainsequence/meta_tables/__main__.py +0 -0
  78. {mainsequence-4.1.1 → mainsequence-4.1.2}/mainsequence/meta_tables/compiled_sql.py +0 -0
  79. {mainsequence-4.1.1 → mainsequence-4.1.2}/mainsequence/meta_tables/config.py +0 -0
  80. {mainsequence-4.1.1 → mainsequence-4.1.2}/mainsequence/meta_tables/configuration_models.py +0 -0
  81. {mainsequence-4.1.1 → mainsequence-4.1.2}/mainsequence/meta_tables/data_nodes/__init__.py +0 -0
  82. {mainsequence-4.1.1 → mainsequence-4.1.2}/mainsequence/meta_tables/data_nodes/data_nodes.py +0 -0
  83. {mainsequence-4.1.1 → mainsequence-4.1.2}/mainsequence/meta_tables/data_nodes/models.py +0 -0
  84. {mainsequence-4.1.1 → mainsequence-4.1.2}/mainsequence/meta_tables/data_nodes/namespacing.py +0 -0
  85. {mainsequence-4.1.1 → mainsequence-4.1.2}/mainsequence/meta_tables/data_nodes/persist_managers.py +0 -0
  86. {mainsequence-4.1.1 → mainsequence-4.1.2}/mainsequence/meta_tables/data_nodes/utils.py +0 -0
  87. {mainsequence-4.1.1 → mainsequence-4.1.2}/mainsequence/meta_tables/future_registry.py +0 -0
  88. {mainsequence-4.1.1 → mainsequence-4.1.2}/mainsequence/meta_tables/hashing.py +0 -0
  89. {mainsequence-4.1.1 → mainsequence-4.1.2}/mainsequence/meta_tables/pydantic_metadata.py +0 -0
  90. {mainsequence-4.1.1 → mainsequence-4.1.2}/mainsequence/meta_tables/utils.py +0 -0
  91. {mainsequence-4.1.1 → mainsequence-4.1.2}/mainsequence/runtime_flags.py +0 -0
  92. {mainsequence-4.1.1 → mainsequence-4.1.2}/mainsequence.egg-info/SOURCES.txt +0 -0
  93. {mainsequence-4.1.1 → mainsequence-4.1.2}/mainsequence.egg-info/dependency_links.txt +0 -0
  94. {mainsequence-4.1.1 → mainsequence-4.1.2}/mainsequence.egg-info/entry_points.txt +0 -0
  95. {mainsequence-4.1.1 → mainsequence-4.1.2}/mainsequence.egg-info/requires.txt +0 -0
  96. {mainsequence-4.1.1 → mainsequence-4.1.2}/mainsequence.egg-info/top_level.txt +0 -0
  97. {mainsequence-4.1.1 → mainsequence-4.1.2}/setup.cfg +0 -0
  98. {mainsequence-4.1.1 → mainsequence-4.1.2}/tests/test_auth_precedence.py +0 -0
  99. {mainsequence-4.1.1 → mainsequence-4.1.2}/tests/test_cli.py +0 -0
  100. {mainsequence-4.1.1 → mainsequence-4.1.2}/tests/test_cli_browser_auth.py +0 -0
  101. {mainsequence-4.1.1 → mainsequence-4.1.2}/tests/test_client.py +0 -0
  102. {mainsequence-4.1.1 → mainsequence-4.1.2}/tests/test_command_center_app_component_models.py +0 -0
  103. {mainsequence-4.1.1 → mainsequence-4.1.2}/tests/test_command_center_data_models.py +0 -0
  104. {mainsequence-4.1.1 → mainsequence-4.1.2}/tests/test_command_center_models.py +0 -0
  105. {mainsequence-4.1.1 → mainsequence-4.1.2}/tests/test_data_access_mixin_dimension_audit.py +0 -0
  106. {mainsequence-4.1.1 → mainsequence-4.1.2}/tests/test_data_node_storage_dimension_queries.py +0 -0
  107. {mainsequence-4.1.1 → mainsequence-4.1.2}/tests/test_data_node_update_flow.py +0 -0
  108. {mainsequence-4.1.1 → mainsequence-4.1.2}/tests/test_dependency_extras.py +0 -0
  109. {mainsequence-4.1.1 → mainsequence-4.1.2}/tests/test_duckdb_interface_dimensions.py +0 -0
  110. {mainsequence-4.1.1 → mainsequence-4.1.2}/tests/test_filter_normalization.py +0 -0
  111. {mainsequence-4.1.1 → mainsequence-4.1.2}/tests/test_logconf.py +0 -0
  112. {mainsequence-4.1.1 → mainsequence-4.1.2}/tests/test_meta_tables_client_models.py +0 -0
  113. {mainsequence-4.1.1 → mainsequence-4.1.2}/tests/test_meta_tables_sqlalchemy_contracts.py +0 -0
  114. {mainsequence-4.1.1 → mainsequence-4.1.2}/tests/test_models_user_request_bound_auth.py +0 -0
  115. {mainsequence-4.1.1 → mainsequence-4.1.2}/tests/test_project_batch_jobs_from_file.py +0 -0
  116. {mainsequence-4.1.1 → mainsequence-4.1.2}/tests/test_run_configuration.py +0 -0
  117. {mainsequence-4.1.1 → mainsequence-4.1.2}/tests/test_secret_client_model.py +0 -0
  118. {mainsequence-4.1.1 → mainsequence-4.1.2}/tests/test_source_table_configuration.py +0 -0
  119. {mainsequence-4.1.1 → mainsequence-4.1.2}/tests/test_sqlite_interface_dimensions.py +0 -0
  120. {mainsequence-4.1.1 → mainsequence-4.1.2}/tests/test_update_statistics.py +0 -0
  121. {mainsequence-4.1.1 → mainsequence-4.1.2}/tests/test_update_uid_guards.py +0 -0
  122. {mainsequence-4.1.1 → mainsequence-4.1.2}/tests/test_workspace_snapshot.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: mainsequence
3
- Version: 4.1.1
3
+ Version: 4.1.2
4
4
  Summary: Main Sequence SDK
5
5
  Author-email: Main Sequence GmbH <dev@main-sequence.io>
6
6
  License: MainSequence GmbH SDK License Agreement
@@ -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
@@ -19,16 +19,12 @@ from pydantic import BaseModel
19
19
  from mainsequence.client import BaseObjectOrm
20
20
  from mainsequence.client.models_helpers import get_model_class
21
21
  from mainsequence.client.models_metatables import _resolve_local_pod_project
22
- from mainsequence.instrumentation import tracer, tracer_instrumentator
23
22
  from mainsequence.meta_tables.pydantic_metadata import (
24
23
  is_serialized_pydantic_model,
25
24
  serialize_pydantic_model,
26
25
  strip_pydantic_hash_exclusions,
27
26
  )
28
27
 
29
- from .namespacing import disable_hash_namespace
30
- from .persist_managers import PersistManager
31
-
32
28
  if TYPE_CHECKING:
33
29
  from .data_nodes import APIDataNode, DataNode
34
30
 
@@ -70,6 +66,46 @@ def _serialize_api_timeserie(value: APIDataNode) -> dict[str, Any]:
70
66
  }
71
67
 
72
68
 
69
+ def _import_qualified_name(module_name: str, qualname: str) -> Any:
70
+ module = importlib.import_module(module_name)
71
+ value: Any = module
72
+ for part in qualname.split("."):
73
+ value = getattr(value, part)
74
+ return value
75
+
76
+
77
+ def _is_platform_time_index_metadata_class(value: Any) -> bool:
78
+ try:
79
+ from mainsequence.meta_tables.sqlalchemy_contracts import PlatformTimeIndexMetaData
80
+ except ImportError:
81
+ return False
82
+
83
+ try:
84
+ return isinstance(value, type) and issubclass(value, PlatformTimeIndexMetaData)
85
+ except TypeError:
86
+ return False
87
+
88
+
89
+ @serialize_argument.register(type)
90
+ def _(value: type[Any]) -> Any:
91
+ if not _is_platform_time_index_metadata_class(value):
92
+ return value
93
+
94
+ time_index_metadata = value.get_time_index_metadata()
95
+ uid = getattr(time_index_metadata, "uid", None)
96
+ if uid in (None, ""):
97
+ raise ValueError(
98
+ "PlatformTimeIndexMetaData config values must be registered or bound "
99
+ "before they can be hashed."
100
+ )
101
+ return {
102
+ "__type__": "platform_time_index_metadata",
103
+ "uid": str(uid),
104
+ "module": value.__module__,
105
+ "qualname": value.__qualname__,
106
+ }
107
+
108
+
73
109
  @serialize_argument.register(datetime.datetime)
74
110
  def _(value: datetime.datetime) -> str:
75
111
  return value.isoformat()
@@ -180,6 +216,8 @@ def parse_dictionary_before_hashing(dictionary: dict[str, Any]) -> dict[str, Any
180
216
  # The value["items"] are already serialized dicts
181
217
 
182
218
  local_ts_dict_to_hash[key] = [v["unique_identifier"] for v in value["items"]]
219
+ elif value.get("__type__") == "platform_time_index_metadata":
220
+ local_ts_dict_to_hash[key] = value["uid"]
183
221
  else:
184
222
  # recursively apply hash signature
185
223
  local_ts_dict_to_hash[key] = parse_dictionary_before_hashing(value)
@@ -327,6 +365,8 @@ class ConfigRebuilder(BaseRebuilder):
327
365
  return build_model(value)
328
366
 
329
367
  def _handle_complex_type(self, value: dict, **kwargs) -> Any:
368
+ if value.get("__type__") == "platform_time_index_metadata":
369
+ return _import_qualified_name(value["module"], value["qualname"])
330
370
  # Special case for ORM lists within the generic complex type handler
331
371
  if value.get("__type__") == "orm_model_list":
332
372
  return [build_model(item) for item in value["items"]]
@@ -434,97 +474,3 @@ def create_config(
434
474
  remote_initial_configuration=remote_config,
435
475
  build_configuration_json_schema=build_configuration_json_schema,
436
476
  )
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
@@ -31,9 +31,6 @@ from mainsequence.client.dtype_codec import (
31
31
  # Instrumentation and Logging
32
32
  from mainsequence.instrumentation import TracerInstrumentator, tracer
33
33
 
34
- # MetaTable DataNode core components and helpers
35
- from mainsequence.meta_tables.data_nodes import build_operations
36
-
37
34
  if TYPE_CHECKING:
38
35
  from .data_nodes import DataNode
39
36
 
@@ -377,7 +374,6 @@ class UpdateRunner:
377
374
 
378
375
  def _start_update(
379
376
  self,
380
- reuse_declared_dependency_instances: bool,
381
377
  override_update_stats: BaseUpdateStatistics | None = None,
382
378
  ) -> tuple[bool, LocalUpdateResult]:
383
379
  """Orchestrates a single DataNode update, including pre/post routines."""
@@ -404,7 +400,6 @@ class UpdateRunner:
404
400
  self.logger.debug(f"Update required for {self.ts}.")
405
401
  update_result = self._update_local(
406
402
  historical_update=historical_update,
407
- reuse_declared_dependency_instances=reuse_declared_dependency_instances,
408
403
  )
409
404
  else:
410
405
  self.logger.debug(f"Already up-to-date. Skipping update for {self.ts}.")
@@ -509,7 +504,6 @@ class UpdateRunner:
509
504
  def _update_local(
510
505
  self,
511
506
  historical_update: Any,
512
- reuse_declared_dependency_instances: bool,
513
507
  ) -> LocalUpdateResult:
514
508
  """
515
509
  Calculates, validates, and persists the node update result.
@@ -519,14 +513,11 @@ class UpdateRunner:
519
513
  ``set_start_of_execution()`` for this run. The node-specific
520
514
  ``_execute_local_update(...)`` implementation is responsible for
521
515
  interpreting any fields on this object.
522
- reuse_declared_dependency_instances: When True in debug mode, dependency
523
- updates reuse already-instantiated dependency nodes from the current
524
- in-memory dependency graph instead of rebuilding them from backend
525
- metadata.
516
+ Dependencies are executed from the source-declared in-memory graph.
526
517
  """
527
518
  # 1. Handle dependency tree update first
528
519
  if self.update_tree:
529
- self._verify_tree_is_updated(reuse_declared_dependency_instances)
520
+ self._verify_tree_is_updated()
530
521
  if self.update_only_tree:
531
522
  self.logger.info(
532
523
  f"Dependency tree for {self.ts} updated. Halting run as requested."
@@ -552,10 +543,7 @@ class UpdateRunner:
552
543
  self.ts.update_statistics = us
553
544
 
554
545
  @tracer.start_as_current_span("UpdateRunner._verify_tree_is_updated")
555
- def _verify_tree_is_updated(
556
- self,
557
- reuse_declared_dependency_instances: bool,
558
- ) -> None:
546
+ def _verify_tree_is_updated(self) -> None:
559
547
  """
560
548
  Ensures all dependencies in the tree are updated before the head node.
561
549
 
@@ -563,10 +551,9 @@ class UpdateRunner:
563
551
  then delegates the update execution to either a sequential (debug) or
564
552
  parallel (production) helper method.
565
553
 
566
- Args:
567
- reuse_declared_dependency_instances: If True in debug mode, reuse the
568
- already-declared in-memory dependency instances instead of rebuilding
569
- them from backend metadata.
554
+ Dependencies are executed from the currently declared DataNode graph.
555
+ Backend dependency metadata is ordering/state only; it is not used to
556
+ cold-rebuild executable DataNode instances.
570
557
  """
571
558
  # 1. Ensure the dependency graph is built in the backend
572
559
  declared_dependencies = self.ts.dependencies() or {}
@@ -611,10 +598,8 @@ class UpdateRunner:
611
598
  self.logger.debug("No dependencies to update.")
612
599
  return
613
600
 
614
- # 3. Build a map of dependency instances if needed for debug mode
615
- update_map = {}
616
- if self.debug_mode and reuse_declared_dependency_instances:
617
- update_map = self._get_update_map(declared_dependencies, logger=self.logger)
601
+ # 3. Build the executable dependency map from source declarations.
602
+ update_map = self._get_update_map(declared_dependencies, logger=self.logger)
618
603
 
619
604
  # 4. Delegate to the appropriate execution method
620
605
  self.logger.debug(f"Starting update for {len(dependencies_df)} dependencies...")
@@ -638,7 +623,7 @@ class UpdateRunner:
638
623
  declared_dependencies: dict[str, DataNode],
639
624
  logger: object,
640
625
  dependecy_map: dict | None = None,
641
- ) -> dict[tuple[str, int], dict[str, Any]]:
626
+ ) -> dict[str, dict[str, Any]]:
642
627
  """
643
628
  Obtains all DataNode objects in the dependency graph by recursively
644
629
  calling the dependencies() method.
@@ -651,7 +636,7 @@ class UpdateRunner:
651
636
  dependecy_map: An optional dictionary to store the dependency map, used for recursion.
652
637
 
653
638
  Returns:
654
- A dictionary mapping (update_hash, data_source_uid) to DataNode info.
639
+ A dictionary mapping update node uid to DataNode info.
655
640
  """
656
641
  # Initialize the map on the first call
657
642
  if dependecy_map is None:
@@ -660,16 +645,16 @@ class UpdateRunner:
660
645
  # Get the explicitly declared dependencies, just like set_relation_tree
661
646
 
662
647
  for name, dependency_ts in declared_dependencies.items():
663
- key = (dependency_ts.update_hash, dependency_ts.data_source_uid)
664
-
665
- # If we have already processed this node, skip it to prevent infinite loops
666
- if key in dependecy_map:
667
- continue
668
648
  if dependency_ts.is_api:
669
649
  continue
670
650
 
671
- # Ensure the dependency is initialized in the persistence layer
651
+ # Ensure the dependency is initialized in the persistence layer.
672
652
  _ = dependency_ts.local_persist_manager
653
+ key = _require_uid(dependency_ts.data_node_update, "DataNodeUpdate")
654
+
655
+ # If we have already processed this node, skip it to prevent infinite loops
656
+ if key in dependecy_map:
657
+ continue
673
658
 
674
659
  logger.debug(f"Adding dependency '{name}' to update map.")
675
660
  dependecy_map[key] = {"ts": dependency_ts}
@@ -686,7 +671,7 @@ class UpdateRunner:
686
671
  def _execute_sequential_debug_update(
687
672
  self,
688
673
  dependencies_df: pd.DataFrame,
689
- update_map: dict[tuple[str, str], dict],
674
+ update_map: dict[str, dict],
690
675
  ) -> None:
691
676
  """Runs dependency updates sequentially in the same process for debugging."""
692
677
  self.logger.info("Executing dependency updates in sequential debug mode.")
@@ -707,40 +692,34 @@ class UpdateRunner:
707
692
  sorted_deps = priority_df.sort_values("number_of_upstreams", ascending=False)
708
693
 
709
694
  for _, ts_row in sorted_deps.iterrows():
710
- key = (ts_row["update_hash"], ts_row["data_source_uid"])
711
- ts_to_update = None
695
+ update_node_uid = str(ts_row["update_node_uid"])
712
696
  try:
713
- if key in update_map:
714
- ts_to_update = update_map[key]["ts"]
715
-
716
- # update the update_statistics of the dependencies
717
- refresh_update_statistics_of_deps(ts_to_update)
718
-
719
- else:
720
- # If not in the map, it must be rebuilt from storage
721
- ts_to_update = build_operations.rebuild_and_set_from_update_hash(
722
- update_hash=key[0], data_source_uid=key[1]
697
+ if update_node_uid not in update_map:
698
+ raise DependencyUpdateError(
699
+ "Backend dependency metadata includes an update node that "
700
+ "is not declared by the current DataNode.dependencies() graph: "
701
+ f"update_node_uid={update_node_uid!r}."
723
702
  )
724
703
 
725
- if ts_to_update:
726
- self.logger.debug(
727
- f"Running debug update for dependency: {ts_to_update.update_hash}"
728
- )
729
- # Each dependency gets its own clean runner
730
- dep_runner = UpdateRunner(
731
- time_serie=ts_to_update,
732
- debug_mode=True,
733
- update_tree=False, # We only update one node at a time
734
- force_update=self.force_update,
735
- remote_scheduler=self.scheduler,
736
- )
737
- dep_runner._setup_scheduler()
738
-
739
- dep_runner._start_update(
740
- reuse_declared_dependency_instances=False,
741
- )
704
+ ts_to_update = update_map[update_node_uid]["ts"]
705
+ refresh_update_statistics_of_deps(ts_to_update)
706
+
707
+ self.logger.debug(
708
+ f"Running debug update for dependency: {ts_to_update.update_hash}"
709
+ )
710
+ # Each dependency gets its own clean runner.
711
+ dep_runner = UpdateRunner(
712
+ time_serie=ts_to_update,
713
+ debug_mode=True,
714
+ update_tree=False,
715
+ force_update=self.force_update,
716
+ remote_scheduler=self.scheduler,
717
+ )
718
+ dep_runner._setup_scheduler()
719
+
720
+ dep_runner._start_update()
742
721
  except Exception as e:
743
- self.logger.exception(f"Failed to update dependency {key[0]}")
722
+ self.logger.exception(f"Failed to update dependency {update_node_uid}")
744
723
  raise e # Re-raise to halt the entire process on failure
745
724
 
746
725
  # refresh update statistics of direct dependencies
@@ -803,7 +782,6 @@ class UpdateRunner:
803
782
 
804
783
  # 5. Trigger the core update process
805
784
  error_on_last_update, update_result = self._start_update(
806
- reuse_declared_dependency_instances=True,
807
785
  override_update_stats=self.override_update_stats,
808
786
  )
809
787
 
@@ -1,7 +1,7 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  from collections.abc import Mapping, Sequence
4
- from typing import Any, ClassVar
4
+ from typing import TYPE_CHECKING, Any, ClassVar
5
5
  from uuid import UUID
6
6
 
7
7
  from mainsequence.client.dtype_codec import (
@@ -21,6 +21,9 @@ from mainsequence.client.models_metatables import (
21
21
 
22
22
  from .hashing import build_meta_table_configured_storage_hash, build_meta_table_storage_hash
23
23
 
24
+ if TYPE_CHECKING:
25
+ from mainsequence.client.models_metatables import TimeIndexMetaData
26
+
24
27
  DEFAULT_PLATFORM_MANAGED_PROVISIONING = {
25
28
  "create_table": True,
26
29
  "if_not_exists": True,
@@ -351,19 +354,19 @@ class PlatformTimeIndexMetaData(PlatformManagedMetaTable):
351
354
  ordinary non-null table columns.
352
355
  """
353
356
 
354
- __time_index_metadata__: ClassVar[Any | None] = None
357
+ __time_index_metadata__: ClassVar[TimeIndexMetaData | None] = None
355
358
 
356
359
  if _sqlalchemy_declared_attr is not None:
357
360
  __mapper_args__ = _sqlalchemy_declared_attr.directive(_time_index_mapper_args)
358
361
 
359
362
  @classmethod
360
- def bind_meta_table(cls, meta_table: Any) -> Any:
363
+ def bind_meta_table(cls, meta_table: TimeIndexMetaData) -> TimeIndexMetaData:
361
364
  bound = super().bind_meta_table(meta_table)
362
365
  cls.__time_index_metadata__ = bound
363
366
  return bound
364
367
 
365
368
  @classmethod
366
- def get_time_index_metadata(cls) -> Any | None:
369
+ def get_time_index_metadata(cls) -> TimeIndexMetaData | None:
367
370
  return getattr(cls, "__time_index_metadata__", None)
368
371
 
369
372
  @classmethod
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: mainsequence
3
- Version: 4.1.1
3
+ Version: 4.1.2
4
4
  Summary: Main Sequence SDK
5
5
  Author-email: Main Sequence GmbH <dev@main-sequence.io>
6
6
  License: MainSequence GmbH SDK License Agreement
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "mainsequence"
7
- version = "4.1.1"
7
+ version = "4.1.2"
8
8
  description = "Main Sequence SDK "
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.11"
@@ -10,8 +10,11 @@ os.environ.setdefault("MAINSEQUENCE_ACCESS_TOKEN", "test-access-token")
10
10
  os.environ.setdefault("MAINSEQUENCE_REFRESH_TOKEN", "test-refresh-token")
11
11
 
12
12
  import mainsequence.meta_tables.data_nodes.build_operations as build_operations
13
+ from mainsequence.client.models_metatables import TimeIndexMetaData
13
14
  from mainsequence.meta_tables import (
15
+ DataNode,
14
16
  DataNodeConfiguration,
17
+ PlatformTimeIndexMetaData,
15
18
  RecordDefinition,
16
19
  )
17
20
 
@@ -170,6 +173,44 @@ def test_offset_start_changes_update_hash(monkeypatch):
170
173
  assert storage_hash_a != storage_hash_b
171
174
 
172
175
 
176
+ def test_platform_time_index_metadata_config_hashes_by_bound_metadata_uid(monkeypatch):
177
+ monkeypatch.setattr(build_operations, "POD_PROJECT", None, raising=False)
178
+
179
+ class StorageA(PlatformTimeIndexMetaData):
180
+ pass
181
+
182
+ class StorageB(PlatformTimeIndexMetaData):
183
+ pass
184
+
185
+ class StorageC(PlatformTimeIndexMetaData):
186
+ pass
187
+
188
+ StorageA.bind_meta_table(TimeIndexMetaData.model_construct(uid="storage-uid-a"))
189
+ StorageB.bind_meta_table(TimeIndexMetaData.model_construct(uid="storage-uid-a"))
190
+ StorageC.bind_meta_table(TimeIndexMetaData.model_construct(uid="storage-uid-c"))
191
+
192
+ class NodeConfig(BaseModel):
193
+ dependency_storage: type[PlatformTimeIndexMetaData]
194
+
195
+ hashes_a = _hashes(NodeConfig(dependency_storage=StorageA))
196
+ hashes_b = _hashes(NodeConfig(dependency_storage=StorageB))
197
+ hashes_c = _hashes(NodeConfig(dependency_storage=StorageC))
198
+
199
+ assert hashes_a == hashes_b
200
+ assert hashes_a != hashes_c
201
+
202
+ config = build_operations.create_config(
203
+ ts_class_name="StorageConfigNode",
204
+ kwargs={"config": NodeConfig(dependency_storage=StorageA)},
205
+ )
206
+ assert (
207
+ config.local_initial_configuration["config"]["serialized_model"]["dependency_storage"][
208
+ "uid"
209
+ ]
210
+ == "storage-uid-a"
211
+ )
212
+
213
+
173
214
  def test_plain_dict_with_pydantic_model_import_path_key_is_not_treated_as_wrapper(monkeypatch):
174
215
  monkeypatch.setattr(build_operations, "POD_PROJECT", None, raising=False)
175
216
 
@@ -486,58 +486,6 @@ def test_build_operations_api_node_reference_serialization_uses_data_source_uid(
486
486
  assert "is_api_time_serie_pickled" not in payload
487
487
 
488
488
 
489
- def test_build_operations_rebuilds_from_configuration_without_pickle(monkeypatch):
490
- calls = []
491
-
492
- class FakeLogger:
493
- def debug(self, message):
494
- calls.append(("debug", message))
495
-
496
- class FakeDataNode:
497
- logger = FakeLogger()
498
-
499
- def set_relation_tree(self):
500
- calls.append(("set_relation_tree",))
501
-
502
- def _set_state_with_sessions(
503
- self,
504
- *,
505
- graph_depth,
506
- graph_depth_limit,
507
- include_client_objects,
508
- ):
509
- calls.append(
510
- (
511
- "set_state_with_sessions",
512
- graph_depth,
513
- graph_depth_limit,
514
- include_client_objects,
515
- )
516
- )
517
-
518
- fake_ts = FakeDataNode()
519
-
520
- def fake_rebuild_from_configuration(update_hash, data_source):
521
- calls.append(("rebuild_from_configuration", update_hash, data_source))
522
- return fake_ts
523
-
524
- monkeypatch.setattr(
525
- build_operations,
526
- "rebuild_from_configuration",
527
- fake_rebuild_from_configuration,
528
- )
529
-
530
- result = build_operations.rebuild_and_set_from_update_hash(
531
- update_hash="update-hash-1",
532
- data_source_uid=DATA_SOURCE_UID,
533
- set_dependencies_df=True,
534
- graph_depth_limit=3,
535
- )
536
-
537
- assert result is fake_ts
538
- assert calls == [
539
- ("rebuild_from_configuration", "update-hash-1", DATA_SOURCE_UID),
540
- ("set_relation_tree",),
541
- ("set_state_with_sessions", 0, 3, False),
542
- ("debug", "ts update-hash-1 rebuilt from configuration"),
543
- ]
489
+ def test_build_operations_does_not_expose_data_node_cold_rebuild_helpers():
490
+ assert not hasattr(build_operations, "rebuild_from_configuration")
491
+ assert not hasattr(build_operations, "rebuild_and_set_from_update_hash")
@@ -16,6 +16,9 @@ class _Logger:
16
16
  def info(self, *_args, **_kwargs):
17
17
  return None
18
18
 
19
+ def exception(self, *_args, **_kwargs):
20
+ return None
21
+
19
22
 
20
23
  class _PersistManager:
21
24
  def __init__(self, data_node_update):
@@ -30,6 +33,9 @@ class _PersistManager:
30
33
  def set_data_node_update_lazy(self, include_relations_detail=False):
31
34
  self.include_relations_detail = include_relations_detail
32
35
 
36
+ def get_update_statistics_for_table(self):
37
+ return {"updated": self.data_node_update.uid}
38
+
33
39
 
34
40
  class _Scheduler:
35
41
  uid = "scheduler-uid"
@@ -172,9 +178,13 @@ def test_update_runner_verify_tree_uses_dependency_uids(monkeypatch):
172
178
  depth_df=depth_df,
173
179
  dependencies_df=depth_df.copy(),
174
180
  )
175
- ts.dependencies = lambda: {
176
- "dependency": SimpleNamespace(is_api=False, data_node_update=dependency_update)
177
- }
181
+ dependency = SimpleNamespace(
182
+ is_api=False,
183
+ data_node_update=dependency_update,
184
+ local_persist_manager=_PersistManager(dependency_update),
185
+ dependencies=lambda: {},
186
+ )
187
+ ts.dependencies = lambda: {"dependency": dependency}
178
188
  runner = run_operations.UpdateRunner(ts, debug_mode=True)
179
189
  executed = {}
180
190
  monkeypatch.setattr(
@@ -188,10 +198,81 @@ def test_update_runner_verify_tree_uses_dependency_uids(monkeypatch):
188
198
  ),
189
199
  )
190
200
 
191
- runner._verify_tree_is_updated(reuse_declared_dependency_instances=False)
201
+ runner._verify_tree_is_updated()
192
202
 
193
203
  assert patch_calls == []
194
204
  assert executed["update_node_uids"] == ["dep-uid"]
205
+ assert list(executed["update_map"]) == ["dep-uid"]
206
+
207
+
208
+ def test_sequential_debug_update_uses_update_node_uid_without_data_source_uid(monkeypatch):
209
+ dependency_update = _update("dep-uid")
210
+ dependency = SimpleNamespace(
211
+ is_api=False,
212
+ data_node_update=dependency_update,
213
+ update_hash="dep-hash",
214
+ local_persist_manager=_PersistManager(dependency_update),
215
+ dependencies=lambda: {},
216
+ logger=_Logger(),
217
+ )
218
+ dependencies_df = pd.DataFrame(
219
+ [
220
+ {
221
+ "update_node_uid": "dep-uid",
222
+ "update_hash": "dep-hash",
223
+ "update_priority": 0,
224
+ "number_of_upstreams": 0,
225
+ }
226
+ ]
227
+ )
228
+ ts = _time_series()
229
+ ts.dependencies = lambda: {"dependency": dependency}
230
+ runner = run_operations.UpdateRunner(ts, debug_mode=True)
231
+ runner.scheduler = _Scheduler()
232
+ started = []
233
+
234
+ monkeypatch.setattr(
235
+ run_operations.UpdateRunner,
236
+ "_setup_scheduler",
237
+ lambda self: started.append(("scheduler", self.ts.data_node_update.uid)),
238
+ )
239
+ monkeypatch.setattr(
240
+ run_operations.UpdateRunner,
241
+ "_start_update",
242
+ lambda self, **_kwargs: started.append(("update", self.ts.data_node_update.uid)),
243
+ )
244
+
245
+ runner._execute_sequential_debug_update(
246
+ dependencies_df=dependencies_df,
247
+ update_map={"dep-uid": {"ts": dependency}},
248
+ )
249
+
250
+ assert started == [("scheduler", "dep-uid"), ("update", "dep-uid")]
251
+
252
+
253
+ def test_sequential_debug_update_rejects_backend_dependency_not_declared():
254
+ dependencies_df = pd.DataFrame(
255
+ [
256
+ {
257
+ "update_node_uid": "stale-dep-uid",
258
+ "update_hash": "stale-dep-hash",
259
+ "update_priority": 0,
260
+ "number_of_upstreams": 0,
261
+ }
262
+ ]
263
+ )
264
+ runner = run_operations.UpdateRunner(_time_series(), debug_mode=True)
265
+
266
+ try:
267
+ runner._execute_sequential_debug_update(
268
+ dependencies_df=dependencies_df,
269
+ update_map={},
270
+ )
271
+ except run_operations.DependencyUpdateError as exc:
272
+ assert "not declared by the current DataNode.dependencies() graph" in str(exc)
273
+ assert "stale-dep-uid" in str(exc)
274
+ else:
275
+ raise AssertionError("Expected DependencyUpdateError")
195
276
 
196
277
 
197
278
  def test_data_node_update_dependency_priority_normalizes_uid_columns(monkeypatch):
File without changes
File without changes
File without changes