mainsequence 4.3.19__tar.gz → 4.3.22__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.19/mainsequence.egg-info → mainsequence-4.3.22}/PKG-INFO +1 -1
  2. {mainsequence-4.3.19 → mainsequence-4.3.22}/agent_scaffold/skills/data_publishing/data_nodes/SKILL.md +82 -4
  3. {mainsequence-4.3.19 → mainsequence-4.3.22}/agent_scaffold/skills/data_publishing/meta_tables/SKILL.md +20 -0
  4. {mainsequence-4.3.19 → mainsequence-4.3.22}/mainsequence/cli/cli.py +60 -0
  5. {mainsequence-4.3.19 → mainsequence-4.3.22}/mainsequence/client/metatables/core.py +73 -4
  6. {mainsequence-4.3.19 → mainsequence-4.3.22}/mainsequence/client/models_foundry.py +10 -0
  7. {mainsequence-4.3.19 → mainsequence-4.3.22/mainsequence.egg-info}/PKG-INFO +1 -1
  8. {mainsequence-4.3.19 → mainsequence-4.3.22}/pyproject.toml +1 -1
  9. {mainsequence-4.3.19 → mainsequence-4.3.22}/tests/test_cli.py +53 -0
  10. {mainsequence-4.3.19 → mainsequence-4.3.22}/tests/test_meta_tables_client_models.py +138 -1
  11. {mainsequence-4.3.19 → mainsequence-4.3.22}/LICENSE +0 -0
  12. {mainsequence-4.3.19 → mainsequence-4.3.22}/README.md +0 -0
  13. {mainsequence-4.3.19 → mainsequence-4.3.22}/agent_scaffold/AGENTS.md +0 -0
  14. {mainsequence-4.3.19 → mainsequence-4.3.22}/agent_scaffold/skills/a2a_communication/SKILL.md +0 -0
  15. {mainsequence-4.3.19 → mainsequence-4.3.22}/agent_scaffold/skills/application_surfaces/api_surfaces/SKILL.md +0 -0
  16. {mainsequence-4.3.19 → mainsequence-4.3.22}/agent_scaffold/skills/command_center/adapter_from_api/SKILL.md +0 -0
  17. {mainsequence-4.3.19 → mainsequence-4.3.22}/agent_scaffold/skills/command_center/api_mock_prototyping/SKILL.md +0 -0
  18. {mainsequence-4.3.19 → mainsequence-4.3.22}/agent_scaffold/skills/command_center/app_components/SKILL.md +0 -0
  19. {mainsequence-4.3.19 → mainsequence-4.3.22}/agent_scaffold/skills/command_center/connections/SKILL.md +0 -0
  20. {mainsequence-4.3.19 → mainsequence-4.3.22}/agent_scaffold/skills/command_center/workspace_analysis/SKILL.md +0 -0
  21. {mainsequence-4.3.19 → mainsequence-4.3.22}/agent_scaffold/skills/command_center/workspace_builder/SKILL.md +0 -0
  22. {mainsequence-4.3.19 → mainsequence-4.3.22}/agent_scaffold/skills/command_center/workspace_design/SKILL.md +0 -0
  23. {mainsequence-4.3.19 → mainsequence-4.3.22}/agent_scaffold/skills/dashboards/streamlit/SKILL.md +0 -0
  24. {mainsequence-4.3.19 → mainsequence-4.3.22}/agent_scaffold/skills/data_access/exploration/SKILL.md +0 -0
  25. {mainsequence-4.3.19 → mainsequence-4.3.22}/agent_scaffold/skills/data_publishing/meta_table_migrations/SKILL.md +0 -0
  26. {mainsequence-4.3.19 → mainsequence-4.3.22}/agent_scaffold/skills/maintenance/bug_auditor/SKILL.md +0 -0
  27. {mainsequence-4.3.19 → mainsequence-4.3.22}/agent_scaffold/skills/ms-markets/SKILL.md +0 -0
  28. {mainsequence-4.3.19 → mainsequence-4.3.22}/agent_scaffold/skills/platform_operations/access_control_and_sharing/SKILL.md +0 -0
  29. {mainsequence-4.3.19 → mainsequence-4.3.22}/agent_scaffold/skills/platform_operations/orchestration_and_releases/SKILL.md +0 -0
  30. {mainsequence-4.3.19 → mainsequence-4.3.22}/agent_scaffold/skills/project_builder/SKILL.md +0 -0
  31. {mainsequence-4.3.19 → mainsequence-4.3.22}/agent_scaffold/skills/project_to_agent/SKILL.md +0 -0
  32. {mainsequence-4.3.19 → mainsequence-4.3.22}/mainsequence/__init__.py +0 -0
  33. {mainsequence-4.3.19 → mainsequence-4.3.22}/mainsequence/__main__.py +0 -0
  34. {mainsequence-4.3.19 → mainsequence-4.3.22}/mainsequence/bootstrap.py +0 -0
  35. {mainsequence-4.3.19 → mainsequence-4.3.22}/mainsequence/cli/__init__.py +0 -0
  36. {mainsequence-4.3.19 → mainsequence-4.3.22}/mainsequence/cli/api.py +0 -0
  37. {mainsequence-4.3.19 → mainsequence-4.3.22}/mainsequence/cli/browser_auth.py +0 -0
  38. {mainsequence-4.3.19 → mainsequence-4.3.22}/mainsequence/cli/config.py +0 -0
  39. {mainsequence-4.3.19 → mainsequence-4.3.22}/mainsequence/cli/docker_utils.py +0 -0
  40. {mainsequence-4.3.19 → mainsequence-4.3.22}/mainsequence/cli/doctor.py +0 -0
  41. {mainsequence-4.3.19 → mainsequence-4.3.22}/mainsequence/cli/local_ops.py +0 -0
  42. {mainsequence-4.3.19 → mainsequence-4.3.22}/mainsequence/cli/migrations.py +0 -0
  43. {mainsequence-4.3.19 → mainsequence-4.3.22}/mainsequence/cli/model_filters.py +0 -0
  44. {mainsequence-4.3.19 → mainsequence-4.3.22}/mainsequence/cli/project_status.py +0 -0
  45. {mainsequence-4.3.19 → mainsequence-4.3.22}/mainsequence/cli/pydantic_cli.py +0 -0
  46. {mainsequence-4.3.19 → mainsequence-4.3.22}/mainsequence/cli/sdk_utils.py +0 -0
  47. {mainsequence-4.3.19 → mainsequence-4.3.22}/mainsequence/cli/ssh_utils.py +0 -0
  48. {mainsequence-4.3.19 → mainsequence-4.3.22}/mainsequence/cli/ui.py +0 -0
  49. {mainsequence-4.3.19 → mainsequence-4.3.22}/mainsequence/client/__init__.py +0 -0
  50. {mainsequence-4.3.19 → mainsequence-4.3.22}/mainsequence/client/agent_runtime_models.py +0 -0
  51. {mainsequence-4.3.19 → mainsequence-4.3.22}/mainsequence/client/base.py +0 -0
  52. {mainsequence-4.3.19 → mainsequence-4.3.22}/mainsequence/client/client.py +0 -0
  53. {mainsequence-4.3.19 → mainsequence-4.3.22}/mainsequence/client/command_center/__init__.py +0 -0
  54. {mainsequence-4.3.19 → mainsequence-4.3.22}/mainsequence/client/command_center/app_component.py +0 -0
  55. {mainsequence-4.3.19 → mainsequence-4.3.22}/mainsequence/client/command_center/connections.py +0 -0
  56. {mainsequence-4.3.19 → mainsequence-4.3.22}/mainsequence/client/command_center/data_models.py +0 -0
  57. {mainsequence-4.3.19 → mainsequence-4.3.22}/mainsequence/client/command_center/workspace.py +0 -0
  58. {mainsequence-4.3.19 → mainsequence-4.3.22}/mainsequence/client/command_center/workspace_snapshot.py +0 -0
  59. {mainsequence-4.3.19 → mainsequence-4.3.22}/mainsequence/client/compute_validation.py +0 -0
  60. {mainsequence-4.3.19 → mainsequence-4.3.22}/mainsequence/client/data_sources_interfaces/__init__.py +0 -0
  61. {mainsequence-4.3.19 → mainsequence-4.3.22}/mainsequence/client/data_sources_interfaces/duckdb.py +0 -0
  62. {mainsequence-4.3.19 → mainsequence-4.3.22}/mainsequence/client/data_sources_interfaces/local_paths.py +0 -0
  63. {mainsequence-4.3.19 → mainsequence-4.3.22}/mainsequence/client/data_sources_interfaces/sqlite.py +0 -0
  64. {mainsequence-4.3.19 → mainsequence-4.3.22}/mainsequence/client/dtype_codec.py +0 -0
  65. {mainsequence-4.3.19 → mainsequence-4.3.22}/mainsequence/client/exceptions.py +0 -0
  66. {mainsequence-4.3.19 → mainsequence-4.3.22}/mainsequence/client/fastapi/__init__.py +0 -0
  67. {mainsequence-4.3.19 → mainsequence-4.3.22}/mainsequence/client/fastapi/auth.py +0 -0
  68. {mainsequence-4.3.19 → mainsequence-4.3.22}/mainsequence/client/metatables/__init__.py +0 -0
  69. {mainsequence-4.3.19 → mainsequence-4.3.22}/mainsequence/client/models_helpers.py +0 -0
  70. {mainsequence-4.3.19 → mainsequence-4.3.22}/mainsequence/client/models_user.py +0 -0
  71. {mainsequence-4.3.19 → mainsequence-4.3.22}/mainsequence/client/utils.py +0 -0
  72. {mainsequence-4.3.19 → mainsequence-4.3.22}/mainsequence/defaults.py +0 -0
  73. {mainsequence-4.3.19 → mainsequence-4.3.22}/mainsequence/instrumentation/__init__.py +0 -0
  74. {mainsequence-4.3.19 → mainsequence-4.3.22}/mainsequence/instrumentation/utils.py +0 -0
  75. {mainsequence-4.3.19 → mainsequence-4.3.22}/mainsequence/logconf.py +0 -0
  76. {mainsequence-4.3.19 → mainsequence-4.3.22}/mainsequence/meta_tables/__init__.py +0 -0
  77. {mainsequence-4.3.19 → mainsequence-4.3.22}/mainsequence/meta_tables/__main__.py +0 -0
  78. {mainsequence-4.3.19 → mainsequence-4.3.22}/mainsequence/meta_tables/compiled_sql/__init__.py +0 -0
  79. {mainsequence-4.3.19 → mainsequence-4.3.22}/mainsequence/meta_tables/compiled_sql/v1.py +0 -0
  80. {mainsequence-4.3.19 → mainsequence-4.3.22}/mainsequence/meta_tables/data_nodes/__init__.py +0 -0
  81. {mainsequence-4.3.19 → mainsequence-4.3.22}/mainsequence/meta_tables/data_nodes/build_operations.py +0 -0
  82. {mainsequence-4.3.19 → mainsequence-4.3.22}/mainsequence/meta_tables/data_nodes/data_nodes.py +0 -0
  83. {mainsequence-4.3.19 → mainsequence-4.3.22}/mainsequence/meta_tables/data_nodes/models.py +0 -0
  84. {mainsequence-4.3.19 → mainsequence-4.3.22}/mainsequence/meta_tables/data_nodes/namespacing.py +0 -0
  85. {mainsequence-4.3.19 → mainsequence-4.3.22}/mainsequence/meta_tables/data_nodes/persist_managers.py +0 -0
  86. {mainsequence-4.3.19 → mainsequence-4.3.22}/mainsequence/meta_tables/data_nodes/run_operations.py +0 -0
  87. {mainsequence-4.3.19 → mainsequence-4.3.22}/mainsequence/meta_tables/data_nodes/utils.py +0 -0
  88. {mainsequence-4.3.19 → mainsequence-4.3.22}/mainsequence/meta_tables/future_registry.py +0 -0
  89. {mainsequence-4.3.19 → mainsequence-4.3.22}/mainsequence/meta_tables/hashing.py +0 -0
  90. {mainsequence-4.3.19 → mainsequence-4.3.22}/mainsequence/meta_tables/migrations/__init__.py +0 -0
  91. {mainsequence-4.3.19 → mainsequence-4.3.22}/mainsequence/meta_tables/migrations/alembic.py +0 -0
  92. {mainsequence-4.3.19 → mainsequence-4.3.22}/mainsequence/meta_tables/migrations/env.py +0 -0
  93. {mainsequence-4.3.19 → mainsequence-4.3.22}/mainsequence/meta_tables/migrations/provider.py +0 -0
  94. {mainsequence-4.3.19 → mainsequence-4.3.22}/mainsequence/meta_tables/migrations/registry.py +0 -0
  95. {mainsequence-4.3.19 → mainsequence-4.3.22}/mainsequence/meta_tables/migrations/scaffold.py +0 -0
  96. {mainsequence-4.3.19 → mainsequence-4.3.22}/mainsequence/meta_tables/migrations/templates/__init__.py +0 -0
  97. {mainsequence-4.3.19 → mainsequence-4.3.22}/mainsequence/meta_tables/migrations/templates/env.py.mako +0 -0
  98. {mainsequence-4.3.19 → mainsequence-4.3.22}/mainsequence/meta_tables/migrations/templates/script.py.mako +0 -0
  99. {mainsequence-4.3.19 → mainsequence-4.3.22}/mainsequence/meta_tables/pydantic_metadata.py +0 -0
  100. {mainsequence-4.3.19 → mainsequence-4.3.22}/mainsequence/meta_tables/schema_names.py +0 -0
  101. {mainsequence-4.3.19 → mainsequence-4.3.22}/mainsequence/meta_tables/sqlalchemy_contracts.py +0 -0
  102. {mainsequence-4.3.19 → mainsequence-4.3.22}/mainsequence/runtime_flags.py +0 -0
  103. {mainsequence-4.3.19 → mainsequence-4.3.22}/mainsequence.egg-info/SOURCES.txt +0 -0
  104. {mainsequence-4.3.19 → mainsequence-4.3.22}/mainsequence.egg-info/dependency_links.txt +0 -0
  105. {mainsequence-4.3.19 → mainsequence-4.3.22}/mainsequence.egg-info/entry_points.txt +0 -0
  106. {mainsequence-4.3.19 → mainsequence-4.3.22}/mainsequence.egg-info/requires.txt +0 -0
  107. {mainsequence-4.3.19 → mainsequence-4.3.22}/mainsequence.egg-info/top_level.txt +0 -0
  108. {mainsequence-4.3.19 → mainsequence-4.3.22}/setup.cfg +0 -0
  109. {mainsequence-4.3.19 → mainsequence-4.3.22}/tests/test_auth_precedence.py +0 -0
  110. {mainsequence-4.3.19 → mainsequence-4.3.22}/tests/test_build_operations_hashing.py +0 -0
  111. {mainsequence-4.3.19 → mainsequence-4.3.22}/tests/test_cli_browser_auth.py +0 -0
  112. {mainsequence-4.3.19 → mainsequence-4.3.22}/tests/test_cli_migrations.py +0 -0
  113. {mainsequence-4.3.19 → mainsequence-4.3.22}/tests/test_client.py +0 -0
  114. {mainsequence-4.3.19 → mainsequence-4.3.22}/tests/test_command_center_app_component_models.py +0 -0
  115. {mainsequence-4.3.19 → mainsequence-4.3.22}/tests/test_command_center_data_models.py +0 -0
  116. {mainsequence-4.3.19 → mainsequence-4.3.22}/tests/test_command_center_models.py +0 -0
  117. {mainsequence-4.3.19 → mainsequence-4.3.22}/tests/test_data_access_mixin_dimension_audit.py +0 -0
  118. {mainsequence-4.3.19 → mainsequence-4.3.22}/tests/test_data_node_storage_dimension_queries.py +0 -0
  119. {mainsequence-4.3.19 → mainsequence-4.3.22}/tests/test_data_node_update_flow.py +0 -0
  120. {mainsequence-4.3.19 → mainsequence-4.3.22}/tests/test_dependency_extras.py +0 -0
  121. {mainsequence-4.3.19 → mainsequence-4.3.22}/tests/test_duckdb_interface_dimensions.py +0 -0
  122. {mainsequence-4.3.19 → mainsequence-4.3.22}/tests/test_filter_normalization.py +0 -0
  123. {mainsequence-4.3.19 → mainsequence-4.3.22}/tests/test_instrumentation.py +0 -0
  124. {mainsequence-4.3.19 → mainsequence-4.3.22}/tests/test_logconf.py +0 -0
  125. {mainsequence-4.3.19 → mainsequence-4.3.22}/tests/test_meta_table_migrations.py +0 -0
  126. {mainsequence-4.3.19 → mainsequence-4.3.22}/tests/test_meta_tables_sqlalchemy_contracts.py +0 -0
  127. {mainsequence-4.3.19 → mainsequence-4.3.22}/tests/test_models_user_request_bound_auth.py +0 -0
  128. {mainsequence-4.3.19 → mainsequence-4.3.22}/tests/test_pod_project_resolution.py +0 -0
  129. {mainsequence-4.3.19 → mainsequence-4.3.22}/tests/test_project_batch_jobs_from_file.py +0 -0
  130. {mainsequence-4.3.19 → mainsequence-4.3.22}/tests/test_run_configuration.py +0 -0
  131. {mainsequence-4.3.19 → mainsequence-4.3.22}/tests/test_schema_names.py +0 -0
  132. {mainsequence-4.3.19 → mainsequence-4.3.22}/tests/test_secret_client_model.py +0 -0
  133. {mainsequence-4.3.19 → mainsequence-4.3.22}/tests/test_source_table_configuration.py +0 -0
  134. {mainsequence-4.3.19 → mainsequence-4.3.22}/tests/test_sqlite_interface_dimensions.py +0 -0
  135. {mainsequence-4.3.19 → mainsequence-4.3.22}/tests/test_update_runner_uid_runtime.py +0 -0
  136. {mainsequence-4.3.19 → mainsequence-4.3.22}/tests/test_update_statistics.py +0 -0
  137. {mainsequence-4.3.19 → mainsequence-4.3.22}/tests/test_update_uid_guards.py +0 -0
  138. {mainsequence-4.3.19 → mainsequence-4.3.22}/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.19
3
+ Version: 4.3.22
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"),
@@ -500,9 +500,17 @@ class MetaTableOperationScope(BasePydanticModel):
500
500
  return self
501
501
 
502
502
 
503
+ DEFAULT_META_TABLE_OPERATION_MAX_ROWS = 10_000
504
+ DEFAULT_META_TABLE_OPERATION_STATEMENT_TIMEOUT_MS = 60_000
505
+
506
+
503
507
  class MetaTableOperationLimits(BasePydanticModel):
504
- max_rows: int | None = Field(default=None, ge=1)
505
- statement_timeout_ms: int | None = Field(default=None, ge=1)
508
+ max_rows: int = Field(default=DEFAULT_META_TABLE_OPERATION_MAX_ROWS, ge=1)
509
+ offset: int = Field(default=0, ge=0)
510
+ statement_timeout_ms: int = Field(
511
+ default=DEFAULT_META_TABLE_OPERATION_STATEMENT_TIMEOUT_MS,
512
+ ge=1,
513
+ )
506
514
 
507
515
 
508
516
  class MetaTableCompiledSQLOperation(BasePydanticModel):
@@ -511,7 +519,12 @@ class MetaTableCompiledSQLOperation(BasePydanticModel):
511
519
  dialect: MetaTableCompiledSQLDialect = "postgresql"
512
520
  statement: MetaTableStatementPayload
513
521
  scope: MetaTableOperationScope
514
- limits: MetaTableOperationLimits | None = None
522
+ limits: MetaTableOperationLimits = Field(default_factory=MetaTableOperationLimits)
523
+
524
+ @field_validator("limits", mode="before")
525
+ @classmethod
526
+ def default_limits(cls, value):
527
+ return {} if value is None else value
515
528
 
516
529
 
517
530
  class MetaTableRequestFields(BasePydanticModel):
@@ -1704,12 +1717,68 @@ class MetaTable(BasePydanticModel, LabelableObjectMixin, ShareableObjectMixin, B
1704
1717
  if isinstance(operation, MetaTableCompiledSQLOperation)
1705
1718
  else MetaTableCompiledSQLOperation(**operation)
1706
1719
  )
1707
- return cls._post_action(
1720
+ response = cls._post_action(
1708
1721
  "execute-operation",
1709
1722
  payload,
1710
1723
  timeout=timeout,
1711
1724
  expected_statuses=(200,),
1712
1725
  )
1726
+ if payload.operation != "select":
1727
+ return response
1728
+
1729
+ rows = response.get("rows")
1730
+ if not isinstance(rows, list):
1731
+ return response
1732
+
1733
+ requested_max_rows = payload.limits.max_rows
1734
+ if len(rows) >= requested_max_rows:
1735
+ return response
1736
+
1737
+ pagination = response.get("pagination") or {}
1738
+ has_more = bool(pagination.get("has_more"))
1739
+ next_offset = pagination.get("next_offset")
1740
+ if not has_more or next_offset in (None, ""):
1741
+ return response
1742
+
1743
+ accumulated_rows = list(rows)
1744
+ last_pagination = pagination
1745
+ while (
1746
+ has_more
1747
+ and next_offset not in (None, "")
1748
+ and len(accumulated_rows) < requested_max_rows
1749
+ ):
1750
+ remaining_rows = requested_max_rows - len(accumulated_rows)
1751
+ next_payload = payload.model_copy(deep=True)
1752
+ next_payload.limits.offset = int(next_offset)
1753
+ next_payload.limits.max_rows = remaining_rows
1754
+ page_response = cls._post_action(
1755
+ "execute-operation",
1756
+ next_payload,
1757
+ timeout=timeout,
1758
+ expected_statuses=(200,),
1759
+ )
1760
+ page_rows = page_response.get("rows") or []
1761
+ if not isinstance(page_rows, list) or not page_rows:
1762
+ last_pagination = page_response.get("pagination") or {}
1763
+ break
1764
+
1765
+ accumulated_rows.extend(page_rows[:remaining_rows])
1766
+ last_pagination = page_response.get("pagination") or {}
1767
+ has_more = bool(last_pagination.get("has_more"))
1768
+ next_offset = last_pagination.get("next_offset")
1769
+
1770
+ response["rows"] = accumulated_rows
1771
+ response["row_count"] = len(accumulated_rows)
1772
+ response["max_rows"] = requested_max_rows
1773
+ response["truncated"] = bool(last_pagination.get("has_more"))
1774
+ response["pagination"] = {
1775
+ "limit": requested_max_rows,
1776
+ "offset": payload.limits.offset,
1777
+ "returned_count": len(accumulated_rows),
1778
+ "has_more": bool(last_pagination.get("has_more")),
1779
+ "next_offset": last_pagination.get("next_offset"),
1780
+ }
1781
+ return response
1713
1782
 
1714
1783
 
1715
1784
  # Global executor (or you could define one on your class)
@@ -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.19
3
+ Version: 4.3.22
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.19"
7
+ version = "4.3.22"
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)
@@ -410,6 +410,118 @@ def test_meta_table_execute_operation_serializes_scope_uid(monkeypatch):
410
410
  assert captured["payload"]["json"]["statement"]["parameters"] == {
411
411
  "symbol_1": "%BTC%",
412
412
  }
413
+ assert captured["payload"]["json"]["limits"]["offset"] == 0
414
+
415
+
416
+ def test_meta_table_execute_operation_fetches_requested_rows_with_backend_pagination(
417
+ monkeypatch,
418
+ ):
419
+ calls = []
420
+
421
+ def fake_make_request(**kwargs):
422
+ payload = kwargs["payload"]["json"]
423
+ calls.append(payload)
424
+ offset = payload["limits"]["offset"]
425
+ if offset == 0:
426
+ return _Response(
427
+ {
428
+ "ok": True,
429
+ "operation": "select",
430
+ "dialect": "postgresql",
431
+ "row_count": 2,
432
+ "rows": [{"value": 1}, {"value": 2}],
433
+ "truncated": True,
434
+ "max_rows": 2,
435
+ "pagination": {
436
+ "limit": 2,
437
+ "offset": 0,
438
+ "returned_count": 2,
439
+ "has_more": True,
440
+ "next_offset": 2,
441
+ },
442
+ }
443
+ )
444
+ if offset == 2:
445
+ return _Response(
446
+ {
447
+ "ok": True,
448
+ "operation": "select",
449
+ "dialect": "postgresql",
450
+ "row_count": 2,
451
+ "rows": [{"value": 3}, {"value": 4}],
452
+ "truncated": True,
453
+ "max_rows": 2,
454
+ "pagination": {
455
+ "limit": 2,
456
+ "offset": 2,
457
+ "returned_count": 2,
458
+ "has_more": True,
459
+ "next_offset": 4,
460
+ },
461
+ }
462
+ )
463
+ return _Response(
464
+ {
465
+ "ok": True,
466
+ "operation": "select",
467
+ "dialect": "postgresql",
468
+ "row_count": 1,
469
+ "rows": [{"value": 5}],
470
+ "truncated": False,
471
+ "max_rows": 1,
472
+ "pagination": {
473
+ "limit": 1,
474
+ "offset": 4,
475
+ "returned_count": 1,
476
+ "has_more": False,
477
+ "next_offset": None,
478
+ },
479
+ }
480
+ )
481
+
482
+ monkeypatch.setattr(meta_table_models, "make_request", fake_make_request)
483
+ monkeypatch.setattr(
484
+ meta_table_models.MetaTable,
485
+ "build_session",
486
+ classmethod(lambda cls: SimpleNamespace(headers={})),
487
+ )
488
+
489
+ result = meta_table_models.MetaTable.execute_operation(
490
+ {
491
+ "operation": "select",
492
+ "statement": {
493
+ "sql": "SELECT value FROM public.asset ORDER BY value",
494
+ "parameters": {},
495
+ },
496
+ "scope": {
497
+ "data_source_uid": "dddddddd-dddd-4ddd-8ddd-dddddddddddd",
498
+ "tables": [
499
+ {
500
+ "meta_table_uid": "aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa",
501
+ "alias": "asset",
502
+ }
503
+ ],
504
+ },
505
+ "limits": {
506
+ "max_rows": 5,
507
+ "statement_timeout_ms": 15000,
508
+ },
509
+ }
510
+ )
511
+
512
+ assert [row["value"] for row in result["rows"]] == [1, 2, 3, 4, 5]
513
+ assert result["row_count"] == 5
514
+ assert result["max_rows"] == 5
515
+ assert result["truncated"] is False
516
+ assert result["pagination"] == {
517
+ "limit": 5,
518
+ "offset": 0,
519
+ "returned_count": 5,
520
+ "has_more": False,
521
+ "next_offset": None,
522
+ }
523
+ assert [call["limits"]["offset"] for call in calls] == [0, 2, 4]
524
+ assert [call["limits"]["max_rows"] for call in calls] == [5, 3, 1]
413
525
 
414
526
 
415
527
  def test_dynamic_table_data_source_issue_migration_connection_posts_scope(monkeypatch):
@@ -728,6 +840,31 @@ def test_compiled_sql_v1_protocol_is_validated_by_pydantic():
728
840
  )
729
841
 
730
842
 
843
+ def test_compiled_sql_v1_operation_uses_backend_limit_defaults():
844
+ operation = build_operation(
845
+ operation="select",
846
+ sql="SELECT asset.symbol FROM public.asset AS asset",
847
+ scope={
848
+ "dataSourceUid": "dddddddd-dddd-4ddd-8ddd-dddddddddddd",
849
+ "tables": [
850
+ {
851
+ "metaTableUid": "aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa",
852
+ "alias": "asset",
853
+ }
854
+ ]
855
+ },
856
+ )
857
+
858
+ assert operation.limits.max_rows == 10_000
859
+ assert operation.limits.offset == 0
860
+ assert operation.limits.statement_timeout_ms == 60_000
861
+ assert meta_table_models._payload_json(operation)["limits"] == {
862
+ "max_rows": 10_000,
863
+ "offset": 0,
864
+ "statement_timeout_ms": 60_000,
865
+ }
866
+
867
+
731
868
  def test_compiled_sql_v1_scope_resolves_session_data_source(monkeypatch):
732
869
  monkeypatch.setattr(
733
870
  meta_table_models,
@@ -774,7 +911,7 @@ def test_compiled_sql_v1_serializes_typed_temporal_parameters():
774
911
  "metaTableUid": "aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa",
775
912
  "alias": "asset",
776
913
  }
777
- ]
914
+ ],
778
915
  },
779
916
  )
780
917
 
File without changes
File without changes
File without changes