mainsequence 4.3.18__tar.gz → 4.3.20__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 (138) hide show
  1. {mainsequence-4.3.18/mainsequence.egg-info → mainsequence-4.3.20}/PKG-INFO +1 -1
  2. {mainsequence-4.3.18 → mainsequence-4.3.20}/agent_scaffold/skills/data_publishing/data_nodes/SKILL.md +82 -4
  3. {mainsequence-4.3.18 → mainsequence-4.3.20}/agent_scaffold/skills/data_publishing/meta_tables/SKILL.md +20 -0
  4. {mainsequence-4.3.18 → mainsequence-4.3.20}/mainsequence/cli/cli.py +60 -0
  5. {mainsequence-4.3.18 → mainsequence-4.3.20}/mainsequence/client/metatables/core.py +52 -1
  6. {mainsequence-4.3.18 → mainsequence-4.3.20}/mainsequence/client/models_foundry.py +10 -0
  7. {mainsequence-4.3.18 → mainsequence-4.3.20/mainsequence.egg-info}/PKG-INFO +1 -1
  8. {mainsequence-4.3.18 → mainsequence-4.3.20}/pyproject.toml +1 -1
  9. {mainsequence-4.3.18 → mainsequence-4.3.20}/tests/test_cli.py +53 -0
  10. {mainsequence-4.3.18 → mainsequence-4.3.20}/tests/test_data_node_storage_dimension_queries.py +75 -0
  11. {mainsequence-4.3.18 → mainsequence-4.3.20}/LICENSE +0 -0
  12. {mainsequence-4.3.18 → mainsequence-4.3.20}/README.md +0 -0
  13. {mainsequence-4.3.18 → mainsequence-4.3.20}/agent_scaffold/AGENTS.md +0 -0
  14. {mainsequence-4.3.18 → mainsequence-4.3.20}/agent_scaffold/skills/a2a_communication/SKILL.md +0 -0
  15. {mainsequence-4.3.18 → mainsequence-4.3.20}/agent_scaffold/skills/application_surfaces/api_surfaces/SKILL.md +0 -0
  16. {mainsequence-4.3.18 → mainsequence-4.3.20}/agent_scaffold/skills/command_center/adapter_from_api/SKILL.md +0 -0
  17. {mainsequence-4.3.18 → mainsequence-4.3.20}/agent_scaffold/skills/command_center/api_mock_prototyping/SKILL.md +0 -0
  18. {mainsequence-4.3.18 → mainsequence-4.3.20}/agent_scaffold/skills/command_center/app_components/SKILL.md +0 -0
  19. {mainsequence-4.3.18 → mainsequence-4.3.20}/agent_scaffold/skills/command_center/connections/SKILL.md +0 -0
  20. {mainsequence-4.3.18 → mainsequence-4.3.20}/agent_scaffold/skills/command_center/workspace_analysis/SKILL.md +0 -0
  21. {mainsequence-4.3.18 → mainsequence-4.3.20}/agent_scaffold/skills/command_center/workspace_builder/SKILL.md +0 -0
  22. {mainsequence-4.3.18 → mainsequence-4.3.20}/agent_scaffold/skills/command_center/workspace_design/SKILL.md +0 -0
  23. {mainsequence-4.3.18 → mainsequence-4.3.20}/agent_scaffold/skills/dashboards/streamlit/SKILL.md +0 -0
  24. {mainsequence-4.3.18 → mainsequence-4.3.20}/agent_scaffold/skills/data_access/exploration/SKILL.md +0 -0
  25. {mainsequence-4.3.18 → mainsequence-4.3.20}/agent_scaffold/skills/data_publishing/meta_table_migrations/SKILL.md +0 -0
  26. {mainsequence-4.3.18 → mainsequence-4.3.20}/agent_scaffold/skills/maintenance/bug_auditor/SKILL.md +0 -0
  27. {mainsequence-4.3.18 → mainsequence-4.3.20}/agent_scaffold/skills/ms-markets/SKILL.md +0 -0
  28. {mainsequence-4.3.18 → mainsequence-4.3.20}/agent_scaffold/skills/platform_operations/access_control_and_sharing/SKILL.md +0 -0
  29. {mainsequence-4.3.18 → mainsequence-4.3.20}/agent_scaffold/skills/platform_operations/orchestration_and_releases/SKILL.md +0 -0
  30. {mainsequence-4.3.18 → mainsequence-4.3.20}/agent_scaffold/skills/project_builder/SKILL.md +0 -0
  31. {mainsequence-4.3.18 → mainsequence-4.3.20}/agent_scaffold/skills/project_to_agent/SKILL.md +0 -0
  32. {mainsequence-4.3.18 → mainsequence-4.3.20}/mainsequence/__init__.py +0 -0
  33. {mainsequence-4.3.18 → mainsequence-4.3.20}/mainsequence/__main__.py +0 -0
  34. {mainsequence-4.3.18 → mainsequence-4.3.20}/mainsequence/bootstrap.py +0 -0
  35. {mainsequence-4.3.18 → mainsequence-4.3.20}/mainsequence/cli/__init__.py +0 -0
  36. {mainsequence-4.3.18 → mainsequence-4.3.20}/mainsequence/cli/api.py +0 -0
  37. {mainsequence-4.3.18 → mainsequence-4.3.20}/mainsequence/cli/browser_auth.py +0 -0
  38. {mainsequence-4.3.18 → mainsequence-4.3.20}/mainsequence/cli/config.py +0 -0
  39. {mainsequence-4.3.18 → mainsequence-4.3.20}/mainsequence/cli/docker_utils.py +0 -0
  40. {mainsequence-4.3.18 → mainsequence-4.3.20}/mainsequence/cli/doctor.py +0 -0
  41. {mainsequence-4.3.18 → mainsequence-4.3.20}/mainsequence/cli/local_ops.py +0 -0
  42. {mainsequence-4.3.18 → mainsequence-4.3.20}/mainsequence/cli/migrations.py +0 -0
  43. {mainsequence-4.3.18 → mainsequence-4.3.20}/mainsequence/cli/model_filters.py +0 -0
  44. {mainsequence-4.3.18 → mainsequence-4.3.20}/mainsequence/cli/project_status.py +0 -0
  45. {mainsequence-4.3.18 → mainsequence-4.3.20}/mainsequence/cli/pydantic_cli.py +0 -0
  46. {mainsequence-4.3.18 → mainsequence-4.3.20}/mainsequence/cli/sdk_utils.py +0 -0
  47. {mainsequence-4.3.18 → mainsequence-4.3.20}/mainsequence/cli/ssh_utils.py +0 -0
  48. {mainsequence-4.3.18 → mainsequence-4.3.20}/mainsequence/cli/ui.py +0 -0
  49. {mainsequence-4.3.18 → mainsequence-4.3.20}/mainsequence/client/__init__.py +0 -0
  50. {mainsequence-4.3.18 → mainsequence-4.3.20}/mainsequence/client/agent_runtime_models.py +0 -0
  51. {mainsequence-4.3.18 → mainsequence-4.3.20}/mainsequence/client/base.py +0 -0
  52. {mainsequence-4.3.18 → mainsequence-4.3.20}/mainsequence/client/client.py +0 -0
  53. {mainsequence-4.3.18 → mainsequence-4.3.20}/mainsequence/client/command_center/__init__.py +0 -0
  54. {mainsequence-4.3.18 → mainsequence-4.3.20}/mainsequence/client/command_center/app_component.py +0 -0
  55. {mainsequence-4.3.18 → mainsequence-4.3.20}/mainsequence/client/command_center/connections.py +0 -0
  56. {mainsequence-4.3.18 → mainsequence-4.3.20}/mainsequence/client/command_center/data_models.py +0 -0
  57. {mainsequence-4.3.18 → mainsequence-4.3.20}/mainsequence/client/command_center/workspace.py +0 -0
  58. {mainsequence-4.3.18 → mainsequence-4.3.20}/mainsequence/client/command_center/workspace_snapshot.py +0 -0
  59. {mainsequence-4.3.18 → mainsequence-4.3.20}/mainsequence/client/compute_validation.py +0 -0
  60. {mainsequence-4.3.18 → mainsequence-4.3.20}/mainsequence/client/data_sources_interfaces/__init__.py +0 -0
  61. {mainsequence-4.3.18 → mainsequence-4.3.20}/mainsequence/client/data_sources_interfaces/duckdb.py +0 -0
  62. {mainsequence-4.3.18 → mainsequence-4.3.20}/mainsequence/client/data_sources_interfaces/local_paths.py +0 -0
  63. {mainsequence-4.3.18 → mainsequence-4.3.20}/mainsequence/client/data_sources_interfaces/sqlite.py +0 -0
  64. {mainsequence-4.3.18 → mainsequence-4.3.20}/mainsequence/client/dtype_codec.py +0 -0
  65. {mainsequence-4.3.18 → mainsequence-4.3.20}/mainsequence/client/exceptions.py +0 -0
  66. {mainsequence-4.3.18 → mainsequence-4.3.20}/mainsequence/client/fastapi/__init__.py +0 -0
  67. {mainsequence-4.3.18 → mainsequence-4.3.20}/mainsequence/client/fastapi/auth.py +0 -0
  68. {mainsequence-4.3.18 → mainsequence-4.3.20}/mainsequence/client/metatables/__init__.py +0 -0
  69. {mainsequence-4.3.18 → mainsequence-4.3.20}/mainsequence/client/models_helpers.py +0 -0
  70. {mainsequence-4.3.18 → mainsequence-4.3.20}/mainsequence/client/models_user.py +0 -0
  71. {mainsequence-4.3.18 → mainsequence-4.3.20}/mainsequence/client/utils.py +0 -0
  72. {mainsequence-4.3.18 → mainsequence-4.3.20}/mainsequence/defaults.py +0 -0
  73. {mainsequence-4.3.18 → mainsequence-4.3.20}/mainsequence/instrumentation/__init__.py +0 -0
  74. {mainsequence-4.3.18 → mainsequence-4.3.20}/mainsequence/instrumentation/utils.py +0 -0
  75. {mainsequence-4.3.18 → mainsequence-4.3.20}/mainsequence/logconf.py +0 -0
  76. {mainsequence-4.3.18 → mainsequence-4.3.20}/mainsequence/meta_tables/__init__.py +0 -0
  77. {mainsequence-4.3.18 → mainsequence-4.3.20}/mainsequence/meta_tables/__main__.py +0 -0
  78. {mainsequence-4.3.18 → mainsequence-4.3.20}/mainsequence/meta_tables/compiled_sql/__init__.py +0 -0
  79. {mainsequence-4.3.18 → mainsequence-4.3.20}/mainsequence/meta_tables/compiled_sql/v1.py +0 -0
  80. {mainsequence-4.3.18 → mainsequence-4.3.20}/mainsequence/meta_tables/data_nodes/__init__.py +0 -0
  81. {mainsequence-4.3.18 → mainsequence-4.3.20}/mainsequence/meta_tables/data_nodes/build_operations.py +0 -0
  82. {mainsequence-4.3.18 → mainsequence-4.3.20}/mainsequence/meta_tables/data_nodes/data_nodes.py +0 -0
  83. {mainsequence-4.3.18 → mainsequence-4.3.20}/mainsequence/meta_tables/data_nodes/models.py +0 -0
  84. {mainsequence-4.3.18 → mainsequence-4.3.20}/mainsequence/meta_tables/data_nodes/namespacing.py +0 -0
  85. {mainsequence-4.3.18 → mainsequence-4.3.20}/mainsequence/meta_tables/data_nodes/persist_managers.py +0 -0
  86. {mainsequence-4.3.18 → mainsequence-4.3.20}/mainsequence/meta_tables/data_nodes/run_operations.py +0 -0
  87. {mainsequence-4.3.18 → mainsequence-4.3.20}/mainsequence/meta_tables/data_nodes/utils.py +0 -0
  88. {mainsequence-4.3.18 → mainsequence-4.3.20}/mainsequence/meta_tables/future_registry.py +0 -0
  89. {mainsequence-4.3.18 → mainsequence-4.3.20}/mainsequence/meta_tables/hashing.py +0 -0
  90. {mainsequence-4.3.18 → mainsequence-4.3.20}/mainsequence/meta_tables/migrations/__init__.py +0 -0
  91. {mainsequence-4.3.18 → mainsequence-4.3.20}/mainsequence/meta_tables/migrations/alembic.py +0 -0
  92. {mainsequence-4.3.18 → mainsequence-4.3.20}/mainsequence/meta_tables/migrations/env.py +0 -0
  93. {mainsequence-4.3.18 → mainsequence-4.3.20}/mainsequence/meta_tables/migrations/provider.py +0 -0
  94. {mainsequence-4.3.18 → mainsequence-4.3.20}/mainsequence/meta_tables/migrations/registry.py +0 -0
  95. {mainsequence-4.3.18 → mainsequence-4.3.20}/mainsequence/meta_tables/migrations/scaffold.py +0 -0
  96. {mainsequence-4.3.18 → mainsequence-4.3.20}/mainsequence/meta_tables/migrations/templates/__init__.py +0 -0
  97. {mainsequence-4.3.18 → mainsequence-4.3.20}/mainsequence/meta_tables/migrations/templates/env.py.mako +0 -0
  98. {mainsequence-4.3.18 → mainsequence-4.3.20}/mainsequence/meta_tables/migrations/templates/script.py.mako +0 -0
  99. {mainsequence-4.3.18 → mainsequence-4.3.20}/mainsequence/meta_tables/pydantic_metadata.py +0 -0
  100. {mainsequence-4.3.18 → mainsequence-4.3.20}/mainsequence/meta_tables/schema_names.py +0 -0
  101. {mainsequence-4.3.18 → mainsequence-4.3.20}/mainsequence/meta_tables/sqlalchemy_contracts.py +0 -0
  102. {mainsequence-4.3.18 → mainsequence-4.3.20}/mainsequence/runtime_flags.py +0 -0
  103. {mainsequence-4.3.18 → mainsequence-4.3.20}/mainsequence.egg-info/SOURCES.txt +0 -0
  104. {mainsequence-4.3.18 → mainsequence-4.3.20}/mainsequence.egg-info/dependency_links.txt +0 -0
  105. {mainsequence-4.3.18 → mainsequence-4.3.20}/mainsequence.egg-info/entry_points.txt +0 -0
  106. {mainsequence-4.3.18 → mainsequence-4.3.20}/mainsequence.egg-info/requires.txt +0 -0
  107. {mainsequence-4.3.18 → mainsequence-4.3.20}/mainsequence.egg-info/top_level.txt +0 -0
  108. {mainsequence-4.3.18 → mainsequence-4.3.20}/setup.cfg +0 -0
  109. {mainsequence-4.3.18 → mainsequence-4.3.20}/tests/test_auth_precedence.py +0 -0
  110. {mainsequence-4.3.18 → mainsequence-4.3.20}/tests/test_build_operations_hashing.py +0 -0
  111. {mainsequence-4.3.18 → mainsequence-4.3.20}/tests/test_cli_browser_auth.py +0 -0
  112. {mainsequence-4.3.18 → mainsequence-4.3.20}/tests/test_cli_migrations.py +0 -0
  113. {mainsequence-4.3.18 → mainsequence-4.3.20}/tests/test_client.py +0 -0
  114. {mainsequence-4.3.18 → mainsequence-4.3.20}/tests/test_command_center_app_component_models.py +0 -0
  115. {mainsequence-4.3.18 → mainsequence-4.3.20}/tests/test_command_center_data_models.py +0 -0
  116. {mainsequence-4.3.18 → mainsequence-4.3.20}/tests/test_command_center_models.py +0 -0
  117. {mainsequence-4.3.18 → mainsequence-4.3.20}/tests/test_data_access_mixin_dimension_audit.py +0 -0
  118. {mainsequence-4.3.18 → mainsequence-4.3.20}/tests/test_data_node_update_flow.py +0 -0
  119. {mainsequence-4.3.18 → mainsequence-4.3.20}/tests/test_dependency_extras.py +0 -0
  120. {mainsequence-4.3.18 → mainsequence-4.3.20}/tests/test_duckdb_interface_dimensions.py +0 -0
  121. {mainsequence-4.3.18 → mainsequence-4.3.20}/tests/test_filter_normalization.py +0 -0
  122. {mainsequence-4.3.18 → mainsequence-4.3.20}/tests/test_instrumentation.py +0 -0
  123. {mainsequence-4.3.18 → mainsequence-4.3.20}/tests/test_logconf.py +0 -0
  124. {mainsequence-4.3.18 → mainsequence-4.3.20}/tests/test_meta_table_migrations.py +0 -0
  125. {mainsequence-4.3.18 → mainsequence-4.3.20}/tests/test_meta_tables_client_models.py +0 -0
  126. {mainsequence-4.3.18 → mainsequence-4.3.20}/tests/test_meta_tables_sqlalchemy_contracts.py +0 -0
  127. {mainsequence-4.3.18 → mainsequence-4.3.20}/tests/test_models_user_request_bound_auth.py +0 -0
  128. {mainsequence-4.3.18 → mainsequence-4.3.20}/tests/test_pod_project_resolution.py +0 -0
  129. {mainsequence-4.3.18 → mainsequence-4.3.20}/tests/test_project_batch_jobs_from_file.py +0 -0
  130. {mainsequence-4.3.18 → mainsequence-4.3.20}/tests/test_run_configuration.py +0 -0
  131. {mainsequence-4.3.18 → mainsequence-4.3.20}/tests/test_schema_names.py +0 -0
  132. {mainsequence-4.3.18 → mainsequence-4.3.20}/tests/test_secret_client_model.py +0 -0
  133. {mainsequence-4.3.18 → mainsequence-4.3.20}/tests/test_source_table_configuration.py +0 -0
  134. {mainsequence-4.3.18 → mainsequence-4.3.20}/tests/test_sqlite_interface_dimensions.py +0 -0
  135. {mainsequence-4.3.18 → mainsequence-4.3.20}/tests/test_update_runner_uid_runtime.py +0 -0
  136. {mainsequence-4.3.18 → mainsequence-4.3.20}/tests/test_update_statistics.py +0 -0
  137. {mainsequence-4.3.18 → mainsequence-4.3.20}/tests/test_update_uid_guards.py +0 -0
  138. {mainsequence-4.3.18 → mainsequence-4.3.20}/tests/test_workspace_snapshot.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: mainsequence
3
- Version: 4.3.18
3
+ Version: 4.3.20
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
@@ -335,7 +335,80 @@ Use `UpdateStatistics`.
335
335
  Do not fetch or return full history every run unless there is a documented
336
336
  reason.
337
337
 
338
- ### 6. `time_index` Must Be Nanosecond UTC
338
+ ### 6. DataNode Tail Deletes Must Use `delete_after_date(...)`
339
+
340
+ When a DataNode workflow needs to remove persisted rows from its
341
+ `PlatformTimeIndexMetaTable` storage table, use
342
+ `TimeIndexMetaTable.delete_after_date(...)`. This is the only normal DataNode
343
+ storage deletion path.
344
+
345
+ Do not use raw SQL, compiled SQL operations, direct database clients,
346
+ `run_query(...)`, backend-private endpoints, table truncation, or ad hoc
347
+ delete helpers to clean DataNode storage rows.
348
+
349
+ The SDK call targets:
350
+
351
+ ```text
352
+ POST /orm/api/ts_manager/dynamic_table/<dynamic_table_uid>/delete_after_date/
353
+ ```
354
+
355
+ Use a global tail delete only when all streams in the table should be rolled
356
+ back from the same inclusive cutoff:
357
+
358
+ ```python
359
+ storage = MyStorageTable.get_time_index_meta_table()
360
+ storage.delete_after_date("2026-04-01T00:00:00Z")
361
+ ```
362
+
363
+ For multidimensional storage, prefer an explicit scope. Use
364
+ `dimension_filters` when deleting all rows for one or more dimension values:
365
+
366
+ ```python
367
+ storage.delete_after_date(
368
+ "2026-04-01T00:00:00Z",
369
+ dimension_filters={
370
+ "asset_identifier": ["example-asset-btc", "example-asset-eth"],
371
+ },
372
+ )
373
+ ```
374
+
375
+ Use `index_coordinates` when deleting exact coordinate streams:
376
+
377
+ ```python
378
+ storage.delete_after_date(
379
+ "2026-04-01T00:00:00Z",
380
+ index_coordinates=[
381
+ {
382
+ "account_uid": "account-a",
383
+ "asset_identifier": "example-asset-btc",
384
+ }
385
+ ],
386
+ )
387
+ ```
388
+
389
+ Deleting all rows for scoped streams is allowed only with an explicit scope:
390
+
391
+ ```python
392
+ storage.delete_after_date(
393
+ None,
394
+ dimension_filters={"asset_identifier": ["example-asset-btc"]},
395
+ )
396
+
397
+ storage.delete_after_date(
398
+ None,
399
+ index_coordinates=[
400
+ {
401
+ "account_uid": "account-a",
402
+ "asset_identifier": "example-asset-btc",
403
+ }
404
+ ],
405
+ )
406
+ ```
407
+
408
+ Never send `after_date=None` without `dimension_filters` or
409
+ `index_coordinates`. That is an unbounded table delete and must be rejected.
410
+
411
+ ### 7. `time_index` Must Be Nanosecond UTC
339
412
 
340
413
  Every non-empty DataFrame returned by `update()` must have its first index
341
414
  level named `time_index` with dtype exactly `datetime64[ns, UTC]`.
@@ -386,7 +459,7 @@ physical schema evolution. Keep the `PlatformTimeIndexMetaTable` catalog model a
386
459
  the SDK storage contract, apply Alembic-rendered SQL through the migration
387
460
  workflow, then register or refresh the MetaTable catalog binding separately.
388
461
 
389
- ### 7. Dependencies Must Be Deterministic
462
+ ### 8. Dependencies Must Be Deterministic
390
463
 
391
464
  Dependencies belong in constructor setup and `dependencies()`.
392
465
 
@@ -395,7 +468,7 @@ changing it changes the dependency graph and update identity.
395
468
 
396
469
  Do not construct dependency graphs dynamically inside `update()`.
397
470
 
398
- ### 8. Foreign Keys Belong To SQLAlchemy And Alembic
471
+ ### 9. Foreign Keys Belong To SQLAlchemy And Alembic
399
472
 
400
473
  For new code, model foreign keys on the `PlatformTimeIndexMetaTable` storage
401
474
  class, or route the storage work to the MetaTable skill. When a DataNode storage
@@ -416,7 +489,7 @@ binding after upgrade.
416
489
 
417
490
  Do not add DataNode configuration fields just to mutate storage metadata.
418
491
 
419
- ### 9. Metadata Belongs To Storage
492
+ ### 10. Metadata Belongs To Storage
420
493
 
421
494
  Production-quality table identifiers, descriptions, labels, column docs, and
422
495
  foreign-key metadata belong to the storage class/MetaTable registration path.
@@ -444,6 +517,8 @@ When reviewing an existing DataNode, look for:
444
517
  - misuse of `hash_namespace`
445
518
  - non-incremental `update()` behavior
446
519
  - hidden dependency creation inside `update()`
520
+ - DataNode storage cleanup that bypasses `TimeIndexMetaTable.delete_after_date(...)`
521
+ - `delete_after_date(None)` without explicit `dimension_filters` or `index_coordinates`
447
522
  - invalid identity-indexed output shape
448
523
  - `time_index` dtype that is not exactly `datetime64[ns, UTC]`
449
524
  - DataFrame columns that do not match the `PlatformTimeIndexMetaTable` class
@@ -464,6 +539,9 @@ Do not claim success until you have checked:
464
539
  - no `test_node` usage remains
465
540
  - `dependencies()` is deterministic
466
541
  - `update()` is incremental
542
+ - DataNode storage deletion, rollback, and repair paths use
543
+ `TimeIndexMetaTable.delete_after_date(...)`
544
+ - unbounded deletes with `after_date=None` and no dimension/coordinate scope are absent
467
545
  - the DataFrame shape matches the storage class
468
546
  - non-empty outputs have first index level `time_index` with dtype `datetime64[ns, UTC]`
469
547
  - the first validation run uses explicit `hash_namespace(...)` when it touches a shared backend
@@ -387,6 +387,24 @@ Only use physical table names returned by registered `MetaTable` objects when co
387
387
 
388
388
  Do not hardcode platform-managed physical names manually.
389
389
 
390
+ ### 7. DataNode storage deletes use the DataNode tail-delete API
391
+
392
+ For `PlatformTimeIndexMetaTable` storage owned by DataNodes, do not design raw
393
+ SQL delete operations or compiled SQL delete operations for rollback, repair, or
394
+ stream cleanup. Route that work to the DataNode skill and use
395
+ `TimeIndexMetaTable.delete_after_date(...)`.
396
+
397
+ The DataNode delete path is:
398
+
399
+ ```text
400
+ POST /orm/api/ts_manager/dynamic_table/<dynamic_table_uid>/delete_after_date/
401
+ ```
402
+
403
+ Use `after_date` for global tail rollback. Use `dimension_filters` or
404
+ `index_coordinates` for scoped deletes, including scoped full-stream deletes
405
+ where `after_date=None`. Never allow `after_date=None` without an explicit
406
+ dimension or coordinate scope.
407
+
390
408
  ## Review Rules
391
409
 
392
410
  When reviewing an existing MetaTable workflow, look for:
@@ -406,6 +424,8 @@ When reviewing an existing MetaTable workflow, look for:
406
424
  - migration work that asks users to define backend payloads, artifact rows, or SDK request objects
407
425
  - compiled SQL operations without complete table scope
408
426
  - raw SQL that hardcodes stale physical names
427
+ - raw SQL or compiled SQL deletes against DataNode-owned
428
+ `PlatformTimeIndexMetaTable` storage instead of `delete_after_date(...)`
409
429
  - a table that should really be modeled as a DataNode instead
410
430
 
411
431
  ## Validation Checklist
@@ -11450,6 +11450,66 @@ def project_sync(
11450
11450
  success(f"Synced: {repo_name}")
11451
11451
 
11452
11452
 
11453
+ @project.command("sync-after-commit")
11454
+ def project_sync_after_commit(
11455
+ project_uid: str | None = typer.Argument(
11456
+ None,
11457
+ help="Project UID. If omitted, read MAIN_SEQUENCE_PROJECT_UID from local .env.",
11458
+ ),
11459
+ path: str | None = typer.Option(
11460
+ None,
11461
+ "--path",
11462
+ help="Project directory used to resolve MAIN_SEQUENCE_PROJECT_UID when PROJECT_UID is omitted.",
11463
+ ),
11464
+ timeout: int | None = typer.Option(None, "--timeout", help="Request timeout in seconds."),
11465
+ ):
11466
+ """
11467
+ Trigger backend post-commit project sync.
11468
+
11469
+ This directly calls:
11470
+
11471
+ POST /orm/api/pods/projects/<project_uid>/sync_project_after_commit/
11472
+
11473
+ Examples
11474
+ --------
11475
+ ```bash
11476
+ mainsequence project sync-after-commit project-uid-123
11477
+ mainsequence project sync-after-commit --path .
11478
+ ```
11479
+ """
11480
+ _require_login()
11481
+
11482
+ resolved_project_uid = project_uid
11483
+ if resolved_project_uid is None:
11484
+ project_dir = normalize_path(path) if path else pathlib.Path.cwd()
11485
+ if path and not project_dir.exists():
11486
+ error(f"Folder does not exist: {project_dir}")
11487
+ raise typer.Exit(1)
11488
+ resolved_project_uid = _read_project_ref_from_env_file(project_dir)
11489
+
11490
+ if resolved_project_uid is None:
11491
+ error(
11492
+ "Could not determine project uid. Pass PROJECT_UID or ensure "
11493
+ "MAIN_SEQUENCE_PROJECT_UID is present in local .env."
11494
+ )
11495
+ raise typer.Exit(1)
11496
+
11497
+ try:
11498
+ payload = sync_project_after_commit(resolved_project_uid, timeout=timeout)
11499
+ except ApiError as e:
11500
+ error(f"Backend post-commit sync failed: {e}")
11501
+ raise typer.Exit(1) from e
11502
+
11503
+ result = payload or {
11504
+ "project_uid": resolved_project_uid,
11505
+ "detail": "sync_project_after_commit triggered",
11506
+ }
11507
+ if _emit_json(result):
11508
+ return
11509
+
11510
+ success(f"Triggered backend sync for project {resolved_project_uid}.")
11511
+
11512
+
11453
11513
  @project.command("sync_project", hidden=True)
11454
11514
  def project_sync_project(
11455
11515
  message: str = typer.Argument(..., help="Git commit message"),
@@ -3027,6 +3027,56 @@ class TimeIndexMetaTable(MetaTable):
3027
3027
  descriptor[key] = cls._date_for_payload(descriptor[key])
3028
3028
  return normalized
3029
3029
 
3030
+ def _identity_dimensions_from_time_indexed_profile(self) -> list[str]:
3031
+ profile = self.time_indexed_profile
3032
+ if profile is None:
3033
+ raise ValueError(
3034
+ "Cannot validate dimension_range_map because TimeIndexMetaTable "
3035
+ "is missing time_indexed_profile."
3036
+ )
3037
+
3038
+ time_index_name = str(profile.time_index_name)
3039
+ index_names = [str(name) for name in profile.index_names]
3040
+ if not time_index_name or not index_names:
3041
+ raise ValueError(
3042
+ "Cannot validate dimension_range_map because TimeIndexMetaTable "
3043
+ "time_indexed_profile is missing time_index_name or index_names."
3044
+ )
3045
+ return [name for name in index_names if name != time_index_name]
3046
+
3047
+ def _validate_dimension_range_map_coordinates(
3048
+ self,
3049
+ dimension_range_map: list[dict[str, Any]] | None,
3050
+ ) -> None:
3051
+ if not dimension_range_map:
3052
+ return
3053
+
3054
+ expected_dimensions = self._identity_dimensions_from_time_indexed_profile()
3055
+ if not expected_dimensions:
3056
+ return
3057
+
3058
+ for position, descriptor in enumerate(dimension_range_map):
3059
+ coordinate = descriptor.get("coordinate") if isinstance(descriptor, Mapping) else None
3060
+ provided_dimensions = (
3061
+ [str(name) for name in coordinate.keys()]
3062
+ if isinstance(coordinate, Mapping)
3063
+ else []
3064
+ )
3065
+ provided_set = set(provided_dimensions)
3066
+ missing_dimensions = [
3067
+ dimension
3068
+ for dimension in expected_dimensions
3069
+ if dimension not in provided_set
3070
+ ]
3071
+ if missing_dimensions:
3072
+ raise ValueError(
3073
+ "dimension_range_map coordinate is incomplete for "
3074
+ "TimeIndexMetaTable read. "
3075
+ f"Entry {position} expected identity dimensions "
3076
+ f"{expected_dimensions!r}; provided {provided_dimensions!r}; "
3077
+ f"missing {missing_dimensions!r}."
3078
+ )
3079
+
3030
3080
  def _build_dimension_payload(
3031
3081
  self,
3032
3082
  *,
@@ -3040,6 +3090,7 @@ class TimeIndexMetaTable(MetaTable):
3040
3090
  if index_coordinates is not None:
3041
3091
  payload["index_coordinates"] = index_coordinates
3042
3092
  if dimension_range_map is not None:
3093
+ self._validate_dimension_range_map_coordinates(dimension_range_map)
3043
3094
  payload["dimension_range_map"] = self._normalize_dimension_range_map(
3044
3095
  dimension_range_map
3045
3096
  )
@@ -3326,7 +3377,7 @@ class TimeIndexMetaTable(MetaTable):
3326
3377
  )
3327
3378
  if r.status_code != 200:
3328
3379
  logger.warning(f"Error in request: {r.text}")
3329
- return [], None
3380
+ raise_for_response(r, payload=payload)
3330
3381
 
3331
3382
  response_data = r.json()
3332
3383
  # Accumulate results
@@ -183,6 +183,16 @@ class Project(LabelableObjectMixin, ShareableObjectMixin, BasePydanticModel, Bas
183
183
  examples=["git@github.com:mainsequence/data-pipeline.git"],
184
184
  json_schema_extra={"label": "Git SSH URL"},
185
185
  )
186
+ latest_git_version: str = Field(
187
+ "",
188
+ title="Latest Git Version",
189
+ description=(
190
+ "Normalized highest valid version extracted from repository tags for the "
191
+ "project's configured branch. Empty when the backend has not found a valid version."
192
+ ),
193
+ examples=["1.2.3"],
194
+ json_schema_extra={"label": "Latest Git Version"},
195
+ )
186
196
  created_by: str | int | dict[str, Any] | None = Field(
187
197
  None,
188
198
  title="Created By",
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: mainsequence
3
- Version: 4.3.18
3
+ Version: 4.3.20
4
4
  Summary: Main Sequence SDK
5
5
  Author-email: Main Sequence GmbH <dev@main-sequence.io>
6
6
  License: MainSequence GmbH SDK License Agreement
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "mainsequence"
7
- version = "4.3.18"
7
+ version = "4.3.20"
8
8
  description = "Main Sequence SDK "
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.11"
@@ -10294,6 +10294,59 @@ def test_project_sync_triggers_backend_sync_after_push(cli_mod, runner, monkeypa
10294
10294
  assert "Triggered backend sync for project project-uid-123." in result.output
10295
10295
 
10296
10296
 
10297
+ def test_project_sync_after_commit_uses_project_uid(cli_mod, runner, monkeypatch):
10298
+ captured = {}
10299
+
10300
+ monkeypatch.setattr(cli_mod, "_require_login", lambda: {"username": "u"})
10301
+ monkeypatch.setattr(
10302
+ cli_mod,
10303
+ "sync_project_after_commit",
10304
+ lambda project_uid, timeout=None: captured.update(
10305
+ project_uid=project_uid,
10306
+ timeout=timeout,
10307
+ )
10308
+ or {"uid": project_uid},
10309
+ )
10310
+
10311
+ result = runner.invoke(
10312
+ cli_mod.app,
10313
+ ["project", "sync-after-commit", "project-uid-123", "--timeout", "30"],
10314
+ )
10315
+
10316
+ assert result.exit_code == 0
10317
+ assert captured == {"project_uid": "project-uid-123", "timeout": 30}
10318
+ assert "Triggered backend sync for project project-uid-123." in result.output
10319
+
10320
+
10321
+ def test_project_sync_after_commit_reads_project_uid_from_env_file(
10322
+ cli_mod, runner, monkeypatch, tmp_path
10323
+ ):
10324
+ target = tmp_path / "project"
10325
+ target.mkdir()
10326
+ (target / ".env").write_text("MAIN_SEQUENCE_PROJECT_UID=project-uid-123\n", encoding="utf-8")
10327
+
10328
+ monkeypatch.setattr(cli_mod, "_require_login", lambda: {"username": "u"})
10329
+ monkeypatch.setattr(
10330
+ cli_mod,
10331
+ "sync_project_after_commit",
10332
+ lambda project_uid, timeout=None: {
10333
+ "project_uid": project_uid,
10334
+ "status": "queued",
10335
+ },
10336
+ )
10337
+
10338
+ result = runner.invoke(
10339
+ cli_mod.app,
10340
+ ["project", "sync-after-commit", "--path", str(target), "--json"],
10341
+ )
10342
+
10343
+ assert result.exit_code == 0
10344
+ assert json.loads(result.output) == {
10345
+ "project_uid": "project-uid-123",
10346
+ "status": "queued",
10347
+ }
10348
+
10349
+
10297
10350
  def test_project_sync_project(cli_mod, runner, monkeypatch, tmp_path):
10298
10351
  target = tmp_path / "project"
10299
10352
  target.mkdir(parents=True, exist_ok=True)
@@ -1,8 +1,10 @@
1
1
  import datetime
2
2
 
3
3
  import pandas as pd
4
+ import pytest
4
5
 
5
6
  from mainsequence.client import metatables as models_metatables
7
+ from mainsequence.client.exceptions import BadRequestError
6
8
 
7
9
 
8
10
  def _source_config(index_names: list[str]) -> models_metatables.TimeIndexedProfile:
@@ -173,6 +175,79 @@ def test_get_data_between_dates_from_api_sends_dimension_range_map(monkeypatch):
173
175
  ]
174
176
 
175
177
 
178
+ def test_get_data_between_dates_rejects_incomplete_dimension_range_map(monkeypatch):
179
+ def _fake_make_request(**_kwargs):
180
+ raise AssertionError("request should not be sent with an incomplete coordinate")
181
+
182
+ monkeypatch.setattr(models_metatables, "make_request", _fake_make_request)
183
+
184
+ start = datetime.datetime(2026, 5, 1, 0, tzinfo=datetime.UTC)
185
+
186
+ with pytest.raises(ValueError) as exc:
187
+ _storage(
188
+ ["time_index", "account_uid", "asset_identifier"]
189
+ ).get_data_between_dates_from_api(
190
+ start_date=start,
191
+ dimension_range_map=[
192
+ {
193
+ "coordinate": {"account_uid": "account-a"},
194
+ "start_date": start,
195
+ }
196
+ ],
197
+ )
198
+
199
+ message = str(exc.value)
200
+ assert "dimension_range_map coordinate is incomplete" in message
201
+ assert "asset_identifier" in message
202
+ assert "account_uid" in message
203
+
204
+
205
+ def test_get_data_between_dates_raises_backend_error(monkeypatch):
206
+ class FakeResponse:
207
+ status_code = 400
208
+ text = '{"error": {"dimension_range_map": ["Index coordinate missing dimension(s): asset_identifier"]}}'
209
+
210
+ @staticmethod
211
+ def json():
212
+ return {
213
+ "error": {
214
+ "dimension_range_map": [
215
+ "Index coordinate missing dimension(s): asset_identifier"
216
+ ]
217
+ }
218
+ }
219
+
220
+ def _fake_make_request(*, s, loaders, payload, r_type, url):
221
+ return FakeResponse()
222
+
223
+ monkeypatch.setattr(models_metatables, "make_request", _fake_make_request)
224
+ monkeypatch.setattr(
225
+ models_metatables.TimeIndexMetaTable,
226
+ "build_session",
227
+ classmethod(lambda cls: object()),
228
+ )
229
+
230
+ start = datetime.datetime(2026, 5, 1, 0, tzinfo=datetime.UTC)
231
+
232
+ with pytest.raises(BadRequestError) as exc:
233
+ _storage(
234
+ ["time_index", "account_uid", "asset_identifier"]
235
+ ).get_data_between_dates_from_api(
236
+ start_date=start,
237
+ dimension_range_map=[
238
+ {
239
+ "coordinate": {
240
+ "account_uid": "account-a",
241
+ "asset_identifier": "BTC",
242
+ },
243
+ "start_date": start,
244
+ }
245
+ ],
246
+ )
247
+
248
+ assert "asset_identifier" in str(exc.value)
249
+
250
+
176
251
  def test_get_data_between_dates_from_node_identifier_sends_canonical_dimensions(monkeypatch):
177
252
  captured = {}
178
253
 
File without changes
File without changes
File without changes