mainsequence 6.0.30__tar.gz → 6.0.32__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 (125) hide show
  1. {mainsequence-6.0.30/mainsequence.egg-info → mainsequence-6.0.32}/PKG-INFO +1 -1
  2. {mainsequence-6.0.30 → mainsequence-6.0.32}/agent_scaffold/skills/platform_operations/orchestration_and_releases/SKILL.md +39 -7
  3. {mainsequence-6.0.30 → mainsequence-6.0.32}/mainsequence/cli/api.py +10 -10
  4. {mainsequence-6.0.30 → mainsequence-6.0.32}/mainsequence/cli/cli.py +140 -55
  5. {mainsequence-6.0.30 → mainsequence-6.0.32}/mainsequence/client/metatables/core.py +4 -28
  6. {mainsequence-6.0.30 → mainsequence-6.0.32}/mainsequence/client/models_helpers.py +79 -32
  7. {mainsequence-6.0.30 → mainsequence-6.0.32}/mainsequence/meta_tables/compiled_sql/v1.py +7 -6
  8. {mainsequence-6.0.30 → mainsequence-6.0.32/mainsequence.egg-info}/PKG-INFO +1 -1
  9. {mainsequence-6.0.30 → mainsequence-6.0.32}/pyproject.toml +1 -1
  10. {mainsequence-6.0.30 → mainsequence-6.0.32}/tests/test_cli.py +169 -165
  11. {mainsequence-6.0.30 → mainsequence-6.0.32}/tests/test_filter_normalization.py +138 -23
  12. {mainsequence-6.0.30 → mainsequence-6.0.32}/tests/test_meta_tables_client_models.py +4 -35
  13. {mainsequence-6.0.30 → mainsequence-6.0.32}/LICENSE +0 -0
  14. {mainsequence-6.0.30 → mainsequence-6.0.32}/README.md +0 -0
  15. {mainsequence-6.0.30 → mainsequence-6.0.32}/agent_scaffold/AGENTS.md +0 -0
  16. {mainsequence-6.0.30 → mainsequence-6.0.32}/agent_scaffold/skills/a2a_sdk_execution/SKILL.md +0 -0
  17. {mainsequence-6.0.30 → mainsequence-6.0.32}/agent_scaffold/skills/application_surfaces/api_surfaces/SKILL.md +0 -0
  18. {mainsequence-6.0.30 → mainsequence-6.0.32}/agent_scaffold/skills/data_access/exploration/SKILL.md +0 -0
  19. {mainsequence-6.0.30 → mainsequence-6.0.32}/agent_scaffold/skills/data_publishing/data_nodes/SKILL.md +0 -0
  20. {mainsequence-6.0.30 → mainsequence-6.0.32}/agent_scaffold/skills/data_publishing/meta_table_migrations/SKILL.md +0 -0
  21. {mainsequence-6.0.30 → mainsequence-6.0.32}/agent_scaffold/skills/data_publishing/meta_tables/SKILL.md +0 -0
  22. {mainsequence-6.0.30 → mainsequence-6.0.32}/agent_scaffold/skills/maintenance/bug_auditor/SKILL.md +0 -0
  23. {mainsequence-6.0.30 → mainsequence-6.0.32}/agent_scaffold/skills/maintenance/project-maintenance/SKILL.md +0 -0
  24. {mainsequence-6.0.30 → mainsequence-6.0.32}/agent_scaffold/skills/platform_operations/access_control_and_sharing/SKILL.md +0 -0
  25. {mainsequence-6.0.30 → mainsequence-6.0.32}/agent_scaffold/skills/sdk_project_execution/SKILL.md +0 -0
  26. {mainsequence-6.0.30 → mainsequence-6.0.32}/mainsequence/__init__.py +0 -0
  27. {mainsequence-6.0.30 → mainsequence-6.0.32}/mainsequence/__main__.py +0 -0
  28. {mainsequence-6.0.30 → mainsequence-6.0.32}/mainsequence/bootstrap.py +0 -0
  29. {mainsequence-6.0.30 → mainsequence-6.0.32}/mainsequence/cli/__init__.py +0 -0
  30. {mainsequence-6.0.30 → mainsequence-6.0.32}/mainsequence/cli/browser_auth.py +0 -0
  31. {mainsequence-6.0.30 → mainsequence-6.0.32}/mainsequence/cli/config.py +0 -0
  32. {mainsequence-6.0.30 → mainsequence-6.0.32}/mainsequence/cli/docker_utils.py +0 -0
  33. {mainsequence-6.0.30 → mainsequence-6.0.32}/mainsequence/cli/doctor.py +0 -0
  34. {mainsequence-6.0.30 → mainsequence-6.0.32}/mainsequence/cli/local_ops.py +0 -0
  35. {mainsequence-6.0.30 → mainsequence-6.0.32}/mainsequence/cli/migrations.py +0 -0
  36. {mainsequence-6.0.30 → mainsequence-6.0.32}/mainsequence/cli/model_filters.py +0 -0
  37. {mainsequence-6.0.30 → mainsequence-6.0.32}/mainsequence/cli/project_status.py +0 -0
  38. {mainsequence-6.0.30 → mainsequence-6.0.32}/mainsequence/cli/pydantic_cli.py +0 -0
  39. {mainsequence-6.0.30 → mainsequence-6.0.32}/mainsequence/cli/sdk_utils.py +0 -0
  40. {mainsequence-6.0.30 → mainsequence-6.0.32}/mainsequence/cli/ssh_utils.py +0 -0
  41. {mainsequence-6.0.30 → mainsequence-6.0.32}/mainsequence/cli/ui.py +0 -0
  42. {mainsequence-6.0.30 → mainsequence-6.0.32}/mainsequence/client/__init__.py +0 -0
  43. {mainsequence-6.0.30 → mainsequence-6.0.32}/mainsequence/client/agent_runtime_models.py +0 -0
  44. {mainsequence-6.0.30 → mainsequence-6.0.32}/mainsequence/client/base.py +0 -0
  45. {mainsequence-6.0.30 → mainsequence-6.0.32}/mainsequence/client/client.py +0 -0
  46. {mainsequence-6.0.30 → mainsequence-6.0.32}/mainsequence/client/compute_validation.py +0 -0
  47. {mainsequence-6.0.30 → mainsequence-6.0.32}/mainsequence/client/data_sources_interfaces/__init__.py +0 -0
  48. {mainsequence-6.0.30 → mainsequence-6.0.32}/mainsequence/client/data_sources_interfaces/duckdb.py +0 -0
  49. {mainsequence-6.0.30 → mainsequence-6.0.32}/mainsequence/client/data_sources_interfaces/local_paths.py +0 -0
  50. {mainsequence-6.0.30 → mainsequence-6.0.32}/mainsequence/client/data_sources_interfaces/sqlite.py +0 -0
  51. {mainsequence-6.0.30 → mainsequence-6.0.32}/mainsequence/client/dtype_codec.py +0 -0
  52. {mainsequence-6.0.30 → mainsequence-6.0.32}/mainsequence/client/exceptions.py +0 -0
  53. {mainsequence-6.0.30 → mainsequence-6.0.32}/mainsequence/client/metatables/__init__.py +0 -0
  54. {mainsequence-6.0.30 → mainsequence-6.0.32}/mainsequence/client/models_foundry.py +0 -0
  55. {mainsequence-6.0.30 → mainsequence-6.0.32}/mainsequence/client/models_user.py +0 -0
  56. {mainsequence-6.0.30 → mainsequence-6.0.32}/mainsequence/client/utils.py +0 -0
  57. {mainsequence-6.0.30 → mainsequence-6.0.32}/mainsequence/defaults.py +0 -0
  58. {mainsequence-6.0.30 → mainsequence-6.0.32}/mainsequence/instrumentation/__init__.py +0 -0
  59. {mainsequence-6.0.30 → mainsequence-6.0.32}/mainsequence/instrumentation/utils.py +0 -0
  60. {mainsequence-6.0.30 → mainsequence-6.0.32}/mainsequence/logconf.py +0 -0
  61. {mainsequence-6.0.30 → mainsequence-6.0.32}/mainsequence/meta_tables/__init__.py +0 -0
  62. {mainsequence-6.0.30 → mainsequence-6.0.32}/mainsequence/meta_tables/__main__.py +0 -0
  63. {mainsequence-6.0.30 → mainsequence-6.0.32}/mainsequence/meta_tables/compiled_sql/__init__.py +0 -0
  64. {mainsequence-6.0.30 → mainsequence-6.0.32}/mainsequence/meta_tables/data_nodes/__init__.py +0 -0
  65. {mainsequence-6.0.30 → mainsequence-6.0.32}/mainsequence/meta_tables/data_nodes/build_operations.py +0 -0
  66. {mainsequence-6.0.30 → mainsequence-6.0.32}/mainsequence/meta_tables/data_nodes/data_nodes.py +0 -0
  67. {mainsequence-6.0.30 → mainsequence-6.0.32}/mainsequence/meta_tables/data_nodes/models.py +0 -0
  68. {mainsequence-6.0.30 → mainsequence-6.0.32}/mainsequence/meta_tables/data_nodes/namespacing.py +0 -0
  69. {mainsequence-6.0.30 → mainsequence-6.0.32}/mainsequence/meta_tables/data_nodes/persist_managers.py +0 -0
  70. {mainsequence-6.0.30 → mainsequence-6.0.32}/mainsequence/meta_tables/data_nodes/run_operations.py +0 -0
  71. {mainsequence-6.0.30 → mainsequence-6.0.32}/mainsequence/meta_tables/data_nodes/utils.py +0 -0
  72. {mainsequence-6.0.30 → mainsequence-6.0.32}/mainsequence/meta_tables/future_registry.py +0 -0
  73. {mainsequence-6.0.30 → mainsequence-6.0.32}/mainsequence/meta_tables/hashing.py +0 -0
  74. {mainsequence-6.0.30 → mainsequence-6.0.32}/mainsequence/meta_tables/migrations/__init__.py +0 -0
  75. {mainsequence-6.0.30 → mainsequence-6.0.32}/mainsequence/meta_tables/migrations/alembic.py +0 -0
  76. {mainsequence-6.0.30 → mainsequence-6.0.32}/mainsequence/meta_tables/migrations/env.py +0 -0
  77. {mainsequence-6.0.30 → mainsequence-6.0.32}/mainsequence/meta_tables/migrations/provider.py +0 -0
  78. {mainsequence-6.0.30 → mainsequence-6.0.32}/mainsequence/meta_tables/migrations/registry.py +0 -0
  79. {mainsequence-6.0.30 → mainsequence-6.0.32}/mainsequence/meta_tables/migrations/scaffold.py +0 -0
  80. {mainsequence-6.0.30 → mainsequence-6.0.32}/mainsequence/meta_tables/migrations/templates/__init__.py +0 -0
  81. {mainsequence-6.0.30 → mainsequence-6.0.32}/mainsequence/meta_tables/migrations/templates/env.py.mako +0 -0
  82. {mainsequence-6.0.30 → mainsequence-6.0.32}/mainsequence/meta_tables/migrations/templates/script.py.mako +0 -0
  83. {mainsequence-6.0.30 → mainsequence-6.0.32}/mainsequence/meta_tables/pydantic_metadata.py +0 -0
  84. {mainsequence-6.0.30 → mainsequence-6.0.32}/mainsequence/meta_tables/schema_names.py +0 -0
  85. {mainsequence-6.0.30 → mainsequence-6.0.32}/mainsequence/meta_tables/sqlalchemy_contracts.py +0 -0
  86. {mainsequence-6.0.30 → mainsequence-6.0.32}/mainsequence/project_skills.py +0 -0
  87. {mainsequence-6.0.30 → mainsequence-6.0.32}/mainsequence/runtime_context.py +0 -0
  88. {mainsequence-6.0.30 → mainsequence-6.0.32}/mainsequence/runtime_flags.py +0 -0
  89. {mainsequence-6.0.30 → mainsequence-6.0.32}/mainsequence/scaffold_skills.py +0 -0
  90. {mainsequence-6.0.30 → mainsequence-6.0.32}/mainsequence.egg-info/SOURCES.txt +0 -0
  91. {mainsequence-6.0.30 → mainsequence-6.0.32}/mainsequence.egg-info/dependency_links.txt +0 -0
  92. {mainsequence-6.0.30 → mainsequence-6.0.32}/mainsequence.egg-info/entry_points.txt +0 -0
  93. {mainsequence-6.0.30 → mainsequence-6.0.32}/mainsequence.egg-info/requires.txt +0 -0
  94. {mainsequence-6.0.30 → mainsequence-6.0.32}/mainsequence.egg-info/top_level.txt +0 -0
  95. {mainsequence-6.0.30 → mainsequence-6.0.32}/setup.cfg +0 -0
  96. {mainsequence-6.0.30 → mainsequence-6.0.32}/tests/test_auth_precedence.py +0 -0
  97. {mainsequence-6.0.30 → mainsequence-6.0.32}/tests/test_build_operations_hashing.py +0 -0
  98. {mainsequence-6.0.30 → mainsequence-6.0.32}/tests/test_canonical_api_route_contract.py +0 -0
  99. {mainsequence-6.0.30 → mainsequence-6.0.32}/tests/test_cli_browser_auth.py +0 -0
  100. {mainsequence-6.0.30 → mainsequence-6.0.32}/tests/test_cli_migrations.py +0 -0
  101. {mainsequence-6.0.30 → mainsequence-6.0.32}/tests/test_client.py +0 -0
  102. {mainsequence-6.0.30 → mainsequence-6.0.32}/tests/test_data_access_mixin_dimension_audit.py +0 -0
  103. {mainsequence-6.0.30 → mainsequence-6.0.32}/tests/test_data_node_storage_dimension_queries.py +0 -0
  104. {mainsequence-6.0.30 → mainsequence-6.0.32}/tests/test_data_node_update_flow.py +0 -0
  105. {mainsequence-6.0.30 → mainsequence-6.0.32}/tests/test_dependency_extras.py +0 -0
  106. {mainsequence-6.0.30 → mainsequence-6.0.32}/tests/test_duckdb_interface_dimensions.py +0 -0
  107. {mainsequence-6.0.30 → mainsequence-6.0.32}/tests/test_instrumentation.py +0 -0
  108. {mainsequence-6.0.30 → mainsequence-6.0.32}/tests/test_logconf.py +0 -0
  109. {mainsequence-6.0.30 → mainsequence-6.0.32}/tests/test_meta_table_migrations.py +0 -0
  110. {mainsequence-6.0.30 → mainsequence-6.0.32}/tests/test_meta_tables_sqlalchemy_contracts.py +0 -0
  111. {mainsequence-6.0.30 → mainsequence-6.0.32}/tests/test_models_user_request_bound_auth.py +0 -0
  112. {mainsequence-6.0.30 → mainsequence-6.0.32}/tests/test_notification_uid_contract.py +0 -0
  113. {mainsequence-6.0.30 → mainsequence-6.0.32}/tests/test_pod_project_resolution.py +0 -0
  114. {mainsequence-6.0.30 → mainsequence-6.0.32}/tests/test_project_batch_jobs_from_file.py +0 -0
  115. {mainsequence-6.0.30 → mainsequence-6.0.32}/tests/test_project_branch_contract.py +0 -0
  116. {mainsequence-6.0.30 → mainsequence-6.0.32}/tests/test_project_skills.py +0 -0
  117. {mainsequence-6.0.30 → mainsequence-6.0.32}/tests/test_run_configuration.py +0 -0
  118. {mainsequence-6.0.30 → mainsequence-6.0.32}/tests/test_scaffold_skills.py +0 -0
  119. {mainsequence-6.0.30 → mainsequence-6.0.32}/tests/test_schema_names.py +0 -0
  120. {mainsequence-6.0.30 → mainsequence-6.0.32}/tests/test_secret_client_model.py +0 -0
  121. {mainsequence-6.0.30 → mainsequence-6.0.32}/tests/test_source_table_configuration.py +0 -0
  122. {mainsequence-6.0.30 → mainsequence-6.0.32}/tests/test_sqlite_interface_dimensions.py +0 -0
  123. {mainsequence-6.0.30 → mainsequence-6.0.32}/tests/test_update_runner_uid_runtime.py +0 -0
  124. {mainsequence-6.0.30 → mainsequence-6.0.32}/tests/test_update_statistics.py +0 -0
  125. {mainsequence-6.0.30 → mainsequence-6.0.32}/tests/test_update_uid_guards.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: mainsequence
3
- Version: 6.0.30
3
+ Version: 6.0.32
4
4
  Summary: Main Sequence SDK
5
5
  Author-email: Main Sequence GmbH <dev@main-sequence.io>
6
6
  License-Expression: LicenseRef-MainSequence-Proprietary
@@ -27,7 +27,8 @@ This skill is for:
27
27
  - author backend-managed declarations under `.mainsequence/workflows/`
28
28
  - validate workflow files through the ProjectBranch workflow endpoints
29
29
  - create or select project images
30
- - freeze jobs to a project image
30
+ - bind every job to one exact project image
31
+ - configure standalone Job automatic deployment as future exact-image promotion
31
32
  - inspect job runs and logs
32
33
  - reason about project resources and resource releases
33
34
  - decide whether a `ResourceRelease` should opt into `automatic_deployment`
@@ -91,7 +92,9 @@ Before changing orchestration or release behavior, collect or infer:
91
92
  - Streamlit dashboard release creation
92
93
  - whether a `ResourceRelease` should be manually pinned or opted into repository-sync automatic deployment
93
94
  - whether Artifact inputs or outputs are part of the run
94
- - whether the job should be reproducible against a pinned image
95
+ - who owns the exact initial image: the caller for manual pinning or the
96
+ backend for automatic deployment
97
+ - whether future qualifying immutable repository events may promote a standalone Job
95
98
  - for Streamlit dashboard deployment, the `README.md` resource next to `app.py`
96
99
 
97
100
  If the execution target or image strategy is unclear, stop before scheduling anything.
@@ -102,7 +105,8 @@ For every non-trivial orchestration task, decide:
102
105
 
103
106
  1. Is this a one-off/manual workflow or a repository-managed recurring workflow?
104
107
  2. Should the jobs live in a `.mainsequence/workflows/*.yaml` declaration?
105
- 3. Which pinned project image should the job use?
108
+ 3. Is the Job manually pinned to a caller-selected exact ready image, or does
109
+ automatic deployment make the backend derive it from the synchronized commit?
106
110
  4. Does the workflow depend on Artifacts?
107
111
  5. Is the task actually a release/resource problem instead of only a job problem?
108
112
  6. For Streamlit dashboard deployment, do the selected app resource, README resource, and project image all refer to the intended pushed commit?
@@ -131,15 +135,37 @@ an existing backend resource.
131
135
 
132
136
  Do not hide important recurring schedules in ad hoc shell history or one-off manual commands.
133
137
 
134
- ### 2. Jobs should run against pinned images
138
+ ### 2. Every Job requires one exact image
135
139
 
136
- Jobs should be pinned to a project image.
140
+ Every persisted direct Job has one exact project image, but the creation mode
141
+ determines who supplies it.
137
142
 
138
143
  Remember:
139
144
 
140
145
  - images are built from pushed commits
141
146
  - if a commit is not on the remote, it cannot be used for an image
142
- - unpinned jobs are not an acceptable default in a managed Main Sequence project
147
+ - there is no dynamic, blank-image, branch-tip, or `latest` Job mode
148
+ - a manual Job requires the caller to select `related_image_uid`
149
+ - an automatic Job forbids caller image selection; the backend derives the
150
+ exact initial image from the ProjectBranch's persisted synchronized commit
151
+
152
+ When standalone Job automatic deployment is enabled during creation, the
153
+ backend prepares the exact initial image and later qualifying immutable
154
+ repository events may atomically promote another exact image. On an existing
155
+ Job, enabling or disabling it retains the current exact image. The backend owns
156
+ repository-event truth, policy evaluation, image preparation, and promotion;
157
+ project code and the SDK must not inspect Git or choose a runtime branch.
158
+
159
+ For direct creation use:
160
+
161
+ - `--automatic-deployment` to permit future exact-image promotion
162
+ - `--no-automatic-deployment` to keep future repository events from changing
163
+ the Job image
164
+ - `--automatic-redeployment-tag-regex` to require a full matching immutable
165
+ tag; omit the regex for every otherwise-eligible exact event
166
+
167
+ Use `--related-image-uid` only for manual pinning. Omit it with
168
+ `--automatic-deployment`; passing both is invalid.
143
169
 
144
170
  ### 3. Jobs must be verifiable after creation
145
171
 
@@ -263,6 +289,9 @@ When reviewing an orchestration task, look for:
263
289
  - direct job creation where a batch file should exist
264
290
  - missing or wrong `related_image_uid`
265
291
  - jobs tied to moving repository state instead of a pinned image
292
+ - manual Job creation that omits an exact ready image
293
+ - automatic Job creation that supplies a caller-selected image
294
+ - client code that interprets automatic deployment as a branch-tip or `latest` selector
266
295
  - no run/log verification after creation
267
296
  - unsafe use of `--strict`
268
297
  - workflows depending on laptop-specific file paths instead of Artifacts
@@ -282,7 +311,8 @@ Do not claim success until you have checked:
282
311
  - interval
283
312
  - crontab
284
313
  - one-off
285
- - the pinned image choice is intentional
314
+ - manual image selection or backend-owned automatic image preparation is intentional
315
+ - standalone Job automatic deployment and tag policy are intentionally enabled or disabled
286
316
  - the job exists after direct creation or repository workflow application
287
317
  - runs and logs were inspected when execution success matters
288
318
  - resources and releases were verified when deployment success matters
@@ -308,6 +338,8 @@ If the workflow uses Artifacts, also check:
308
338
  - the execution target is unclear
309
339
  - the image strategy is unclear but reproducibility matters
310
340
  - the backend rejects the workflow version, resource kind, or requested field
341
+ - a manually pinned Job has no exact ready initial image
342
+ - an automatically deployed Job has no persisted synchronized ProjectBranch commit
311
343
  - the workflow depends on local file paths that should be platform Artifacts
312
344
  - automatic deployment is requested but the deployment source branch/current synced project version is unclear
313
345
  - automatic deployment is requested but the resource path or required README is not stable
@@ -60,6 +60,7 @@ MCP_MAX_RESOURCE_PAGES = 100
60
60
  S = requests.Session()
61
61
  S.headers.update({"Content-Type": "application/json"})
62
62
 
63
+
63
64
  class ApiError(RuntimeError):
64
65
  """Base error for API failures."""
65
66
 
@@ -942,9 +943,7 @@ def render_project_branch_default_redeployment_tag(
942
943
  {"version": normalized_version},
943
944
  )
944
945
  if not r.ok:
945
- raise ApiError(
946
- f"Default redeployment tag rendering failed ({r.status_code})."
947
- )
946
+ raise ApiError(f"Default redeployment tag rendering failed ({r.status_code}).")
948
947
  payload = r.json()
949
948
  if not isinstance(payload, dict):
950
949
  raise ApiError("Default redeployment tag rendering returned an unexpected payload.")
@@ -1123,9 +1122,7 @@ def update_organization_team(
1123
1122
  class_name="Team",
1124
1123
  operation=lambda ClientTeam: ClientTeam.get_by_uid(
1125
1124
  str(team_uid), timeout=timeout
1126
- ).patch(
1127
- **updates
1128
- ),
1125
+ ).patch(**updates),
1129
1126
  )
1130
1127
  return _sdk_object_to_dict(team)
1131
1128
  except Exception as e:
@@ -2866,7 +2863,6 @@ def list_meta_tables(
2866
2863
  raise ApiError(f"MetaTables fetch failed: {e}") from e
2867
2864
 
2868
2865
 
2869
-
2870
2866
  def _serialize_sdk_search_response(payload: Any) -> dict[str, Any] | list[dict[str, Any]]:
2871
2867
  if isinstance(payload, list):
2872
2868
  return [_sdk_object_to_dict(item) for item in payload]
@@ -4372,6 +4368,8 @@ def create_project_job(
4372
4368
  spot: bool | None = None,
4373
4369
  max_runtime_seconds: int | None = None,
4374
4370
  related_image_uid: str | None = None,
4371
+ automatic_deployment: bool = False,
4372
+ automatic_redeployment_tag_regex: str | None = None,
4375
4373
  timeout: int | None = None,
4376
4374
  ) -> dict[str, Any]:
4377
4375
  """
@@ -4440,6 +4438,10 @@ def create_project_job(
4440
4438
  spot=spot,
4441
4439
  max_runtime_seconds=max_runtime_seconds,
4442
4440
  related_image_uid=related_image_uid,
4441
+ automatic_deployment=automatic_deployment,
4442
+ automatic_redeployment_policy={
4443
+ "tag_regex": automatic_redeployment_tag_regex,
4444
+ },
4443
4445
  timeout=timeout,
4444
4446
  )
4445
4447
  if isinstance(created, dict):
@@ -5031,9 +5033,7 @@ def create_project(
5031
5033
 
5032
5034
  if default_metatables_data_source_uid in (None, ""):
5033
5035
  raise ValueError("default_metatables_data_source_uid is required")
5034
- payload["default_metatables_data_source_uid"] = str(
5035
- default_metatables_data_source_uid
5036
- )
5036
+ payload["default_metatables_data_source_uid"] = str(default_metatables_data_source_uid)
5037
5037
  if default_base_image_uid not in (None, ""):
5038
5038
  payload["default_base_image_uid"] = str(default_base_image_uid)
5039
5039
  if github_org_uid not in (None, ""):
@@ -746,11 +746,14 @@ def _render_projects_table(items: list[dict], base_dir: str, org_slug: str) -> s
746
746
  for p in items:
747
747
  public_id = _project_identity_value(p)
748
748
  name = p.get("project_name") or "(unnamed)"
749
- branches = ", ".join(
750
- str(branch.get("repository_branch") or "-")
751
- for branch in list(p.get("branches") or [])
752
- if isinstance(branch, dict)
753
- ) or "-"
749
+ branches = (
750
+ ", ".join(
751
+ str(branch.get("repository_branch") or "-")
752
+ for branch in list(p.get("branches") or [])
753
+ if isinstance(branch, dict)
754
+ )
755
+ or "-"
756
+ )
754
757
 
755
758
  local_path = _find_local_project_dir(base_dir, org_slug, public_id, name)
756
759
  local = "Local" if local_path else "-"
@@ -2454,9 +2457,7 @@ def login(
2454
2457
  "tool": handoff["mcp_tool"],
2455
2458
  "arguments": handoff["mcp_arguments"],
2456
2459
  }
2457
- info(
2458
- "Authorize this CLI from the connected Main Sequence MCP session:"
2459
- )
2460
+ info("Authorize this CLI from the connected Main Sequence MCP session:")
2460
2461
  typer.echo(json.dumps(tool_call, separators=(",", ":")))
2461
2462
 
2462
2463
  flow = login_via_mcp_handoff(
@@ -2466,9 +2467,7 @@ def login(
2466
2467
  access = (flow.get("access") or "").strip()
2467
2468
  refresh = (flow.get("refresh") or "").strip()
2468
2469
  if not access or not refresh:
2469
- raise ApiError(
2470
- "MCP handoff login did not return access and refresh tokens."
2471
- )
2470
+ raise ApiError("MCP handoff login did not return access and refresh tokens.")
2472
2471
 
2473
2472
  persisted = cfg.save_tokens("", access, refresh)
2474
2473
  user_payload = flow.get("user")
@@ -2480,9 +2479,7 @@ def login(
2480
2479
  if isinstance(profile, dict):
2481
2480
  username = (profile.get("username") or "").strip()
2482
2481
  if username:
2483
- persisted = bool(
2484
- cfg.save_tokens(username, access, refresh) and persisted
2485
- )
2482
+ persisted = bool(cfg.save_tokens(username, access, refresh) and persisted)
2486
2483
 
2487
2484
  res = {
2488
2485
  "username": username,
@@ -2801,9 +2798,7 @@ def _organization_teams_list_impl(
2801
2798
  )
2802
2799
 
2803
2800
  if rows:
2804
- print_table(
2805
- "Organization Teams", ["UID", "Name", "Description", "Members", "Active"], rows
2806
- )
2801
+ print_table("Organization Teams", ["UID", "Name", "Description", "Members", "Active"], rows)
2807
2802
  else:
2808
2803
  info("No organization teams.")
2809
2804
  info(f"Total organization teams: {len(teams_payload)}")
@@ -4013,9 +4008,7 @@ def _agent_session_list_impl(
4013
4008
  command_label="Agent Sessions",
4014
4009
  )
4015
4010
  if agent_uid and any(key in filters for key in ("agent_uid", "agent_uid__in")):
4016
- error(
4017
- "Do not pass `--filter agent_uid=...` with `--agent-uid`. " "Use only one agent scope."
4018
- )
4011
+ error("Do not pass `--filter agent_uid=...` with `--agent-uid`. Use only one agent scope.")
4019
4012
  raise typer.Exit(1)
4020
4013
 
4021
4014
  _require_login()
@@ -4511,7 +4504,6 @@ def _parse_cli_csv_list(values: list[str] | None) -> list[str]:
4511
4504
  return items
4512
4505
 
4513
4506
 
4514
-
4515
4507
  def _data_node_storage_list_impl(
4516
4508
  timeout: int | None,
4517
4509
  filter_entries: list[str] | None,
@@ -4772,7 +4764,6 @@ def _print_data_node_storage_search_section(
4772
4764
  return len(storages)
4773
4765
 
4774
4766
 
4775
-
4776
4767
  @agent.command("list")
4777
4768
  def agent_list_cmd(
4778
4769
  filter_entries: list[str] | None = typer.Option(None, "--filter", help=LIST_FILTER_OPTION_HELP),
@@ -7515,11 +7506,7 @@ def project_create_cmd(
7515
7506
  ds_rows: list[list[str]] = []
7516
7507
  for item in ds_items:
7517
7508
  uid = _require_item_uid(item, prompt_label="data source uid")
7518
- ds_name = (
7519
- item.get("display_name")
7520
- or item.get("class_type")
7521
- or f"data-source-{uid}"
7522
- )
7509
+ ds_name = item.get("display_name") or item.get("class_type") or f"data-source-{uid}"
7523
7510
  ds_details = (
7524
7511
  f"class={item.get('class_type') or '-'}, status={item.get('status') or '-'}"
7525
7512
  )
@@ -8843,7 +8830,9 @@ def project_images_create_cmd(
8843
8830
  path: str | None = typer.Option(
8844
8831
  None, "--path", help="Project repository path (default: current project)"
8845
8832
  ),
8846
- base_image_uid: str | None = typer.Option(None, "--base-image-uid", help="Project base image UID"),
8833
+ base_image_uid: str | None = typer.Option(
8834
+ None, "--base-image-uid", help="Project base image UID"
8835
+ ),
8847
8836
  timeout: int = typer.Option(
8848
8837
  300, "--timeout", help="Maximum wait time in seconds for the image to become ready."
8849
8838
  ),
@@ -8905,7 +8894,9 @@ def project_create_image_cmd(
8905
8894
  path: str | None = typer.Option(
8906
8895
  None, "--path", help="Project repository path (default: current project)"
8907
8896
  ),
8908
- base_image_uid: str | None = typer.Option(None, "--base-image-uid", help="Project base image UID"),
8897
+ base_image_uid: str | None = typer.Option(
8898
+ None, "--base-image-uid", help="Project base image UID"
8899
+ ),
8909
8900
  timeout: int = typer.Option(
8910
8901
  300, "--timeout", help="Maximum wait time in seconds for the image to become ready."
8911
8902
  ),
@@ -8990,6 +8981,33 @@ def _project_jobs_list_impl(
8990
8981
  ],
8991
8982
  rows,
8992
8983
  )
8984
+
8985
+ deployment_rows = [
8986
+ [
8987
+ str(job.get("uid") or "-"),
8988
+ str(job.get("project_repo_hash") or "-"),
8989
+ str(job.get("related_image_uid") or job.get("related_image") or "-"),
8990
+ str(job.get("image_status") or "-"),
8991
+ str(bool(job.get("automatic_deployment"))).lower(),
8992
+ str(
8993
+ (job.get("automatic_redeployment_policy") or {}).get("tag_regex")
8994
+ or "every qualifying exact event"
8995
+ ),
8996
+ ]
8997
+ for job in jobs
8998
+ ]
8999
+ print_table(
9000
+ "Project Job Image And Promotion State",
9001
+ [
9002
+ "Job UID",
9003
+ "Exact Commit",
9004
+ "Image UID",
9005
+ "Image Status",
9006
+ "Automatic Deployment",
9007
+ "Promotion Tag Regex",
9008
+ ],
9009
+ deployment_rows,
9010
+ )
8993
9011
  else:
8994
9012
  info("No project jobs.")
8995
9013
  info(f"Total jobs: {len(jobs)}")
@@ -9048,6 +9066,21 @@ def _project_job_runs_list_impl(
9048
9066
  ],
9049
9067
  rows,
9050
9068
  )
9069
+
9070
+ snapshot_rows = [
9071
+ [
9072
+ str(run.get("uid") or "-"),
9073
+ str(run.get("runtime_image_uid") or "-"),
9074
+ str(run.get("runtime_image_digest") or "-"),
9075
+ str(run.get("commit_hash") or "-"),
9076
+ ]
9077
+ for run in runs
9078
+ ]
9079
+ print_table(
9080
+ "Immutable Job Run Image Snapshots",
9081
+ ["Job Run UID", "Runtime Image UID", "Runtime Image Digest", "Commit Hash"],
9082
+ snapshot_rows,
9083
+ )
9051
9084
  else:
9052
9085
  info("No job runs.")
9053
9086
  info(f"Total job runs: {len(runs)}")
@@ -9358,6 +9391,8 @@ def _project_jobs_create_impl(
9358
9391
  gpu_type: str | None,
9359
9392
  spot: bool | None,
9360
9393
  max_runtime_seconds: int | None,
9394
+ automatic_deployment: bool,
9395
+ automatic_redeployment_tag_regex: str | None,
9361
9396
  timeout: int | None,
9362
9397
  ) -> None:
9363
9398
  _require_login()
@@ -9384,34 +9419,46 @@ def _project_jobs_create_impl(
9384
9419
  error("Job name is required.")
9385
9420
  raise typer.Exit(1)
9386
9421
 
9387
- try:
9388
- project_images = list_project_images(
9389
- related_project_branch_uid=project_branch_uid,
9390
- timeout=timeout,
9422
+ if automatic_deployment:
9423
+ if related_image_uid is not None:
9424
+ error(
9425
+ "Do not provide --related-image-uid with --automatic-deployment. "
9426
+ "The backend derives and prepares the initial exact image."
9427
+ )
9428
+ raise typer.Exit(1)
9429
+ info(
9430
+ "Automatic deployment: the backend will derive the initial exact image "
9431
+ "from the ProjectBranch synchronized commit."
9391
9432
  )
9392
- except ApiError as e:
9393
- error(f"Project images fetch failed: {e}")
9394
- raise typer.Exit(1) from e
9433
+ else:
9434
+ try:
9435
+ project_images = list_project_images(
9436
+ related_project_branch_uid=project_branch_uid,
9437
+ timeout=timeout,
9438
+ )
9439
+ except ApiError as e:
9440
+ error(f"Project images fetch failed: {e}")
9441
+ raise typer.Exit(1) from e
9395
9442
 
9396
- if related_image_uid is None and project_images:
9397
- image_rows = [
9398
- [
9399
- str(img.get("uid") or "-"),
9400
- str(img.get("project_repo_hash") or "-"),
9401
- _format_base_image_label(img.get("base_image")),
9443
+ if related_image_uid is None and project_images:
9444
+ image_rows = [
9445
+ [
9446
+ str(img.get("uid") or "-"),
9447
+ str(img.get("project_repo_hash") or "-"),
9448
+ _format_base_image_label(img.get("base_image")),
9449
+ ]
9450
+ for img in project_images
9402
9451
  ]
9403
- for img in project_images
9404
- ]
9405
- related_image_uid = _prompt_select_uid(
9406
- title="Available Project Images",
9407
- prompt_label="Related image UID",
9408
- items=project_images,
9409
- rows=image_rows,
9410
- )
9452
+ related_image_uid = _prompt_select_uid(
9453
+ title="Available Project Images",
9454
+ prompt_label="Related image UID",
9455
+ items=project_images,
9456
+ rows=image_rows,
9457
+ )
9411
9458
 
9412
- if related_image_uid is None:
9413
- error("related_image_uid is required for jobs.")
9414
- raise typer.Exit(1)
9459
+ if related_image_uid is None:
9460
+ error("related_image_uid is required when automatic deployment is disabled.")
9461
+ raise typer.Exit(1)
9415
9462
 
9416
9463
  if execution_path is None and app_name is None:
9417
9464
  execution_path = (
@@ -9487,6 +9534,8 @@ def _project_jobs_create_impl(
9487
9534
  spot=spot,
9488
9535
  max_runtime_seconds=max_runtime_seconds,
9489
9536
  related_image_uid=related_image_uid,
9537
+ automatic_deployment=automatic_deployment,
9538
+ automatic_redeployment_tag_regex=automatic_redeployment_tag_regex,
9490
9539
  timeout=timeout,
9491
9540
  )
9492
9541
  except ApiError as e:
@@ -9523,6 +9572,19 @@ def _project_jobs_create_impl(
9523
9572
  "Max Runtime Seconds",
9524
9573
  str(created.get("max_runtime_seconds") or max_runtime_seconds),
9525
9574
  ),
9575
+ ("Image Status", str(created.get("image_status") or "-")),
9576
+ ("Exact Commit", str(created.get("project_repo_hash") or "-")),
9577
+ (
9578
+ "Automatic Deployment",
9579
+ str(created.get("automatic_deployment", automatic_deployment)).lower(),
9580
+ ),
9581
+ (
9582
+ "Promotion Tag Regex",
9583
+ str(
9584
+ (created.get("automatic_redeployment_policy") or {}).get("tag_regex")
9585
+ or "every qualifying exact event"
9586
+ ),
9587
+ ),
9526
9588
  ],
9527
9589
  )
9528
9590
 
@@ -9548,7 +9610,10 @@ def project_jobs_create_cmd(
9548
9610
  "related_image_uid",
9549
9611
  None,
9550
9612
  "--related-image-uid",
9551
- extra_help="Use the public project image UID.",
9613
+ extra_help=(
9614
+ "Required for manually pinned Jobs. Omit it with "
9615
+ "--automatic-deployment so the backend owns initial image preparation."
9616
+ ),
9552
9617
  ),
9553
9618
  schedule_type: str | None = pydantic_option(
9554
9619
  INTERVAL_SCHEDULE_MODEL_REF,
@@ -9620,12 +9685,30 @@ def project_jobs_create_cmd(
9620
9685
  "--max-runtime-seconds",
9621
9686
  extra_help="Defaults to 86400 when omitted.",
9622
9687
  ),
9688
+ automatic_deployment: bool = typer.Option(
9689
+ False,
9690
+ "--automatic-deployment/--no-automatic-deployment",
9691
+ help=(
9692
+ "Let the backend derive and prepare the initial exact image from the "
9693
+ "synchronized ProjectBranch commit, then promote future qualifying exact events."
9694
+ ),
9695
+ ),
9696
+ automatic_redeployment_tag_regex: str | None = typer.Option(
9697
+ None,
9698
+ "--automatic-redeployment-tag-regex",
9699
+ help=(
9700
+ "Full-match regex for qualifying immutable Git tags. Omit it to "
9701
+ "allow every qualifying exact event while automatic deployment is enabled."
9702
+ ),
9703
+ ),
9623
9704
  timeout: int | None = typer.Option(None, "--timeout", help="Request timeout in seconds"),
9624
9705
  ):
9625
9706
  """
9626
9707
  Create a job for a project.
9627
9708
 
9628
- Uses SDK client `Job.create()` as the single source of truth.
9709
+ Uses SDK client `Job.create()` as the single source of truth. Manual Jobs
9710
+ require `--related-image-uid`; automatically deployed Jobs forbid it and
9711
+ delegate initial exact-image preparation to the backend.
9629
9712
  When compute settings are omitted, the CLI applies safe defaults:
9630
9713
  `cpu_request=0.25`, `memory_request=0.5`, `spot=false`, `max_runtime_seconds=86400`.
9631
9714
  If schedule arguments are omitted, the CLI asks whether to build an interval or crontab schedule.
@@ -9657,6 +9740,8 @@ def project_jobs_create_cmd(
9657
9740
  gpu_type=gpu_type,
9658
9741
  spot=spot,
9659
9742
  max_runtime_seconds=max_runtime_seconds,
9743
+ automatic_deployment=automatic_deployment,
9744
+ automatic_redeployment_tag_regex=automatic_redeployment_tag_regex,
9660
9745
  timeout=timeout,
9661
9746
  )
9662
9747
 
@@ -533,40 +533,16 @@ class MetaTableOperationScope(BasePydanticModel):
533
533
  min_length=1,
534
534
  validation_alias=AliasChoices("data_source_uid", "dataSourceUid"),
535
535
  description=(
536
- "Public UID of the DataSource that owns the compiled "
537
- "SQL execution connection. If omitted, the SDK resolves the "
538
- "configured project/session default data source. Scoped MetaTables "
539
- "are the permission contract, not the source of execution routing."
536
+ "Optional public UID of the DataSource that owns the compiled "
537
+ "SQL execution connection. If omitted, the backend derives the "
538
+ "connection from the declared MetaTable scope and rejects mixed "
539
+ "data sources."
540
540
  ),
541
541
  )
542
542
  tables: list[MetaTableOperationScopeTable] = Field(..., min_length=1)
543
543
 
544
544
  model_config = ConfigDict(populate_by_name=True)
545
545
 
546
- @model_validator(mode="after")
547
- def resolve_default_data_source_uid(self):
548
- if self.data_source_uid not in (None, ""):
549
- self.data_source_uid = str(self.data_source_uid)
550
- return self
551
-
552
- try:
553
- data_source = get_session_data_source()
554
- except Exception as exc:
555
- raise ValueError(
556
- "MetaTable compiled SQL scope requires data_source_uid or a "
557
- "configured project/session default data source. "
558
- f"Default data source resolution failed: {exc}"
559
- ) from exc
560
-
561
- uid = getattr(data_source, "uid", None) or getattr(data_source, "data_source_uid", None)
562
- if uid in (None, ""):
563
- raise ValueError(
564
- "Configured project/session default data source does not expose a uid."
565
- )
566
- self.data_source_uid = str(uid)
567
- return self
568
-
569
-
570
546
  class MetaTableProjectContextRequest(BaseModel):
571
547
  model_config = ConfigDict(extra="forbid", frozen=True)
572
548