mainsequence 6.0.48__tar.gz → 6.0.49__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 (133) hide show
  1. {mainsequence-6.0.48 → mainsequence-6.0.49}/PKG-INFO +1 -1
  2. {mainsequence-6.0.48 → mainsequence-6.0.49}/agent_scaffold/skills/platform_operations/orchestration_and_releases/SKILL.md +10 -0
  3. {mainsequence-6.0.48 → mainsequence-6.0.49}/mainsequence/client/models_helpers.py +46 -0
  4. {mainsequence-6.0.48 → mainsequence-6.0.49}/mainsequence.egg-info/PKG-INFO +1 -1
  5. {mainsequence-6.0.48 → mainsequence-6.0.49}/pyproject.toml +1 -1
  6. {mainsequence-6.0.48 → mainsequence-6.0.49}/tests/test_filter_normalization.py +135 -0
  7. {mainsequence-6.0.48 → mainsequence-6.0.49}/LICENSE +0 -0
  8. {mainsequence-6.0.48 → mainsequence-6.0.49}/README.md +0 -0
  9. {mainsequence-6.0.48 → mainsequence-6.0.49}/agent_scaffold/AGENTS.md +0 -0
  10. {mainsequence-6.0.48 → mainsequence-6.0.49}/agent_scaffold/skills/a2a_sdk_execution/SKILL.md +0 -0
  11. {mainsequence-6.0.48 → mainsequence-6.0.49}/agent_scaffold/skills/application_surfaces/api_surfaces/SKILL.md +0 -0
  12. {mainsequence-6.0.48 → mainsequence-6.0.49}/agent_scaffold/skills/data_access/exploration/SKILL.md +0 -0
  13. {mainsequence-6.0.48 → mainsequence-6.0.49}/agent_scaffold/skills/data_publishing/data_nodes/SKILL.md +0 -0
  14. {mainsequence-6.0.48 → mainsequence-6.0.49}/agent_scaffold/skills/data_publishing/meta_table_migrations/SKILL.md +0 -0
  15. {mainsequence-6.0.48 → mainsequence-6.0.49}/agent_scaffold/skills/data_publishing/meta_tables/SKILL.md +0 -0
  16. {mainsequence-6.0.48 → mainsequence-6.0.49}/agent_scaffold/skills/maintenance/bug_auditor/SKILL.md +0 -0
  17. {mainsequence-6.0.48 → mainsequence-6.0.49}/agent_scaffold/skills/maintenance/project-maintenance/SKILL.md +0 -0
  18. {mainsequence-6.0.48 → mainsequence-6.0.49}/agent_scaffold/skills/platform_operations/access_control_and_sharing/SKILL.md +0 -0
  19. {mainsequence-6.0.48 → mainsequence-6.0.49}/agent_scaffold/skills/sdk_project_execution/SKILL.md +0 -0
  20. {mainsequence-6.0.48 → mainsequence-6.0.49}/mainsequence/__init__.py +0 -0
  21. {mainsequence-6.0.48 → mainsequence-6.0.49}/mainsequence/__main__.py +0 -0
  22. {mainsequence-6.0.48 → mainsequence-6.0.49}/mainsequence/bootstrap.py +0 -0
  23. {mainsequence-6.0.48 → mainsequence-6.0.49}/mainsequence/cli/__init__.py +0 -0
  24. {mainsequence-6.0.48 → mainsequence-6.0.49}/mainsequence/cli/api.py +0 -0
  25. {mainsequence-6.0.48 → mainsequence-6.0.49}/mainsequence/cli/browser_auth.py +0 -0
  26. {mainsequence-6.0.48 → mainsequence-6.0.49}/mainsequence/cli/cli.py +0 -0
  27. {mainsequence-6.0.48 → mainsequence-6.0.49}/mainsequence/cli/config.py +0 -0
  28. {mainsequence-6.0.48 → mainsequence-6.0.49}/mainsequence/cli/docker_utils.py +0 -0
  29. {mainsequence-6.0.48 → mainsequence-6.0.49}/mainsequence/cli/doctor.py +0 -0
  30. {mainsequence-6.0.48 → mainsequence-6.0.49}/mainsequence/cli/local_ops.py +0 -0
  31. {mainsequence-6.0.48 → mainsequence-6.0.49}/mainsequence/cli/migrations.py +0 -0
  32. {mainsequence-6.0.48 → mainsequence-6.0.49}/mainsequence/cli/model_filters.py +0 -0
  33. {mainsequence-6.0.48 → mainsequence-6.0.49}/mainsequence/cli/project_status.py +0 -0
  34. {mainsequence-6.0.48 → mainsequence-6.0.49}/mainsequence/cli/pydantic_cli.py +0 -0
  35. {mainsequence-6.0.48 → mainsequence-6.0.49}/mainsequence/cli/sdk_utils.py +0 -0
  36. {mainsequence-6.0.48 → mainsequence-6.0.49}/mainsequence/cli/ssh_utils.py +0 -0
  37. {mainsequence-6.0.48 → mainsequence-6.0.49}/mainsequence/cli/ui.py +0 -0
  38. {mainsequence-6.0.48 → mainsequence-6.0.49}/mainsequence/client/__init__.py +0 -0
  39. {mainsequence-6.0.48 → mainsequence-6.0.49}/mainsequence/client/agent_runtime_models.py +0 -0
  40. {mainsequence-6.0.48 → mainsequence-6.0.49}/mainsequence/client/base.py +0 -0
  41. {mainsequence-6.0.48 → mainsequence-6.0.49}/mainsequence/client/client.py +0 -0
  42. {mainsequence-6.0.48 → mainsequence-6.0.49}/mainsequence/client/compute_validation.py +0 -0
  43. {mainsequence-6.0.48 → mainsequence-6.0.49}/mainsequence/client/data_sources_interfaces/__init__.py +0 -0
  44. {mainsequence-6.0.48 → mainsequence-6.0.49}/mainsequence/client/data_sources_interfaces/duckdb.py +0 -0
  45. {mainsequence-6.0.48 → mainsequence-6.0.49}/mainsequence/client/data_sources_interfaces/local_paths.py +0 -0
  46. {mainsequence-6.0.48 → mainsequence-6.0.49}/mainsequence/client/data_sources_interfaces/sqlite.py +0 -0
  47. {mainsequence-6.0.48 → mainsequence-6.0.49}/mainsequence/client/dtype_codec.py +0 -0
  48. {mainsequence-6.0.48 → mainsequence-6.0.49}/mainsequence/client/exceptions.py +0 -0
  49. {mainsequence-6.0.48 → mainsequence-6.0.49}/mainsequence/client/metatables/__init__.py +0 -0
  50. {mainsequence-6.0.48 → mainsequence-6.0.49}/mainsequence/client/metatables/core.py +0 -0
  51. {mainsequence-6.0.48 → mainsequence-6.0.49}/mainsequence/client/models_foundry.py +0 -0
  52. {mainsequence-6.0.48 → mainsequence-6.0.49}/mainsequence/client/models_user.py +0 -0
  53. {mainsequence-6.0.48 → mainsequence-6.0.49}/mainsequence/client/observability.py +0 -0
  54. {mainsequence-6.0.48 → mainsequence-6.0.49}/mainsequence/client/utils.py +0 -0
  55. {mainsequence-6.0.48 → mainsequence-6.0.49}/mainsequence/defaults.py +0 -0
  56. {mainsequence-6.0.48 → mainsequence-6.0.49}/mainsequence/instrumentation/__init__.py +0 -0
  57. {mainsequence-6.0.48 → mainsequence-6.0.49}/mainsequence/instrumentation/utils.py +0 -0
  58. {mainsequence-6.0.48 → mainsequence-6.0.49}/mainsequence/logconf.py +0 -0
  59. {mainsequence-6.0.48 → mainsequence-6.0.49}/mainsequence/meta_tables/__init__.py +0 -0
  60. {mainsequence-6.0.48 → mainsequence-6.0.49}/mainsequence/meta_tables/__main__.py +0 -0
  61. {mainsequence-6.0.48 → mainsequence-6.0.49}/mainsequence/meta_tables/compiled_sql/__init__.py +0 -0
  62. {mainsequence-6.0.48 → mainsequence-6.0.49}/mainsequence/meta_tables/compiled_sql/v1.py +0 -0
  63. {mainsequence-6.0.48 → mainsequence-6.0.49}/mainsequence/meta_tables/data_nodes/__init__.py +0 -0
  64. {mainsequence-6.0.48 → mainsequence-6.0.49}/mainsequence/meta_tables/data_nodes/build_operations.py +0 -0
  65. {mainsequence-6.0.48 → mainsequence-6.0.49}/mainsequence/meta_tables/data_nodes/data_nodes.py +0 -0
  66. {mainsequence-6.0.48 → mainsequence-6.0.49}/mainsequence/meta_tables/data_nodes/models.py +0 -0
  67. {mainsequence-6.0.48 → mainsequence-6.0.49}/mainsequence/meta_tables/data_nodes/namespacing.py +0 -0
  68. {mainsequence-6.0.48 → mainsequence-6.0.49}/mainsequence/meta_tables/data_nodes/persist_managers.py +0 -0
  69. {mainsequence-6.0.48 → mainsequence-6.0.49}/mainsequence/meta_tables/data_nodes/run_operations.py +0 -0
  70. {mainsequence-6.0.48 → mainsequence-6.0.49}/mainsequence/meta_tables/data_nodes/utils.py +0 -0
  71. {mainsequence-6.0.48 → mainsequence-6.0.49}/mainsequence/meta_tables/future_registry.py +0 -0
  72. {mainsequence-6.0.48 → mainsequence-6.0.49}/mainsequence/meta_tables/hashing.py +0 -0
  73. {mainsequence-6.0.48 → mainsequence-6.0.49}/mainsequence/meta_tables/migrations/__init__.py +0 -0
  74. {mainsequence-6.0.48 → mainsequence-6.0.49}/mainsequence/meta_tables/migrations/alembic.py +0 -0
  75. {mainsequence-6.0.48 → mainsequence-6.0.49}/mainsequence/meta_tables/migrations/env.py +0 -0
  76. {mainsequence-6.0.48 → mainsequence-6.0.49}/mainsequence/meta_tables/migrations/provider.py +0 -0
  77. {mainsequence-6.0.48 → mainsequence-6.0.49}/mainsequence/meta_tables/migrations/registry.py +0 -0
  78. {mainsequence-6.0.48 → mainsequence-6.0.49}/mainsequence/meta_tables/migrations/scaffold.py +0 -0
  79. {mainsequence-6.0.48 → mainsequence-6.0.49}/mainsequence/meta_tables/migrations/templates/__init__.py +0 -0
  80. {mainsequence-6.0.48 → mainsequence-6.0.49}/mainsequence/meta_tables/migrations/templates/env.py.mako +0 -0
  81. {mainsequence-6.0.48 → mainsequence-6.0.49}/mainsequence/meta_tables/migrations/templates/script.py.mako +0 -0
  82. {mainsequence-6.0.48 → mainsequence-6.0.49}/mainsequence/meta_tables/pydantic_metadata.py +0 -0
  83. {mainsequence-6.0.48 → mainsequence-6.0.49}/mainsequence/meta_tables/schema_names.py +0 -0
  84. {mainsequence-6.0.48 → mainsequence-6.0.49}/mainsequence/meta_tables/sqlalchemy_contracts.py +0 -0
  85. {mainsequence-6.0.48 → mainsequence-6.0.49}/mainsequence/project_context.py +0 -0
  86. {mainsequence-6.0.48 → mainsequence-6.0.49}/mainsequence/project_skills.py +0 -0
  87. {mainsequence-6.0.48 → mainsequence-6.0.49}/mainsequence/runtime_flags.py +0 -0
  88. {mainsequence-6.0.48 → mainsequence-6.0.49}/mainsequence/scaffold_skills.py +0 -0
  89. {mainsequence-6.0.48 → mainsequence-6.0.49}/mainsequence.egg-info/SOURCES.txt +0 -0
  90. {mainsequence-6.0.48 → mainsequence-6.0.49}/mainsequence.egg-info/dependency_links.txt +0 -0
  91. {mainsequence-6.0.48 → mainsequence-6.0.49}/mainsequence.egg-info/entry_points.txt +0 -0
  92. {mainsequence-6.0.48 → mainsequence-6.0.49}/mainsequence.egg-info/requires.txt +0 -0
  93. {mainsequence-6.0.48 → mainsequence-6.0.49}/mainsequence.egg-info/top_level.txt +0 -0
  94. {mainsequence-6.0.48 → mainsequence-6.0.49}/setup.cfg +0 -0
  95. {mainsequence-6.0.48 → mainsequence-6.0.49}/tests/test_auth_precedence.py +0 -0
  96. {mainsequence-6.0.48 → mainsequence-6.0.49}/tests/test_build_operations_hashing.py +0 -0
  97. {mainsequence-6.0.48 → mainsequence-6.0.49}/tests/test_canonical_api_route_contract.py +0 -0
  98. {mainsequence-6.0.48 → mainsequence-6.0.49}/tests/test_cli.py +0 -0
  99. {mainsequence-6.0.48 → mainsequence-6.0.49}/tests/test_cli_browser_auth.py +0 -0
  100. {mainsequence-6.0.48 → mainsequence-6.0.49}/tests/test_cli_migrations.py +0 -0
  101. {mainsequence-6.0.48 → mainsequence-6.0.49}/tests/test_client.py +0 -0
  102. {mainsequence-6.0.48 → mainsequence-6.0.49}/tests/test_data_access_mixin_dimension_audit.py +0 -0
  103. {mainsequence-6.0.48 → mainsequence-6.0.49}/tests/test_data_node_storage_dimension_queries.py +0 -0
  104. {mainsequence-6.0.48 → mainsequence-6.0.49}/tests/test_data_node_update_flow.py +0 -0
  105. {mainsequence-6.0.48 → mainsequence-6.0.49}/tests/test_dependency_extras.py +0 -0
  106. {mainsequence-6.0.48 → mainsequence-6.0.49}/tests/test_duckdb_interface_dimensions.py +0 -0
  107. {mainsequence-6.0.48 → mainsequence-6.0.49}/tests/test_environment_resource_context.py +0 -0
  108. {mainsequence-6.0.48 → mainsequence-6.0.49}/tests/test_environment_response_contracts.py +0 -0
  109. {mainsequence-6.0.48 → mainsequence-6.0.49}/tests/test_instrumentation.py +0 -0
  110. {mainsequence-6.0.48 → mainsequence-6.0.49}/tests/test_local_ops.py +0 -0
  111. {mainsequence-6.0.48 → mainsequence-6.0.49}/tests/test_logconf.py +0 -0
  112. {mainsequence-6.0.48 → mainsequence-6.0.49}/tests/test_meta_table_migrations.py +0 -0
  113. {mainsequence-6.0.48 → mainsequence-6.0.49}/tests/test_meta_tables_client_models.py +0 -0
  114. {mainsequence-6.0.48 → mainsequence-6.0.49}/tests/test_meta_tables_sqlalchemy_contracts.py +0 -0
  115. {mainsequence-6.0.48 → mainsequence-6.0.49}/tests/test_models_user_request_bound_auth.py +0 -0
  116. {mainsequence-6.0.48 → mainsequence-6.0.49}/tests/test_notification_uid_contract.py +0 -0
  117. {mainsequence-6.0.48 → mainsequence-6.0.49}/tests/test_observability_client.py +0 -0
  118. {mainsequence-6.0.48 → mainsequence-6.0.49}/tests/test_pod_project_resolution.py +0 -0
  119. {mainsequence-6.0.48 → mainsequence-6.0.49}/tests/test_project_batch_jobs_from_file.py +0 -0
  120. {mainsequence-6.0.48 → mainsequence-6.0.49}/tests/test_project_branch_contract.py +0 -0
  121. {mainsequence-6.0.48 → mainsequence-6.0.49}/tests/test_project_runtime_context.py +0 -0
  122. {mainsequence-6.0.48 → mainsequence-6.0.49}/tests/test_project_skills.py +0 -0
  123. {mainsequence-6.0.48 → mainsequence-6.0.49}/tests/test_run_configuration.py +0 -0
  124. {mainsequence-6.0.48 → mainsequence-6.0.49}/tests/test_scaffold_skills.py +0 -0
  125. {mainsequence-6.0.48 → mainsequence-6.0.49}/tests/test_scheduler_client_model.py +0 -0
  126. {mainsequence-6.0.48 → mainsequence-6.0.49}/tests/test_schema_names.py +0 -0
  127. {mainsequence-6.0.48 → mainsequence-6.0.49}/tests/test_secret_client_model.py +0 -0
  128. {mainsequence-6.0.48 → mainsequence-6.0.49}/tests/test_source_table_configuration.py +0 -0
  129. {mainsequence-6.0.48 → mainsequence-6.0.49}/tests/test_sqlite_interface_dimensions.py +0 -0
  130. {mainsequence-6.0.48 → mainsequence-6.0.49}/tests/test_ssh_utils.py +0 -0
  131. {mainsequence-6.0.48 → mainsequence-6.0.49}/tests/test_update_runner_uid_runtime.py +0 -0
  132. {mainsequence-6.0.48 → mainsequence-6.0.49}/tests/test_update_statistics.py +0 -0
  133. {mainsequence-6.0.48 → mainsequence-6.0.49}/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.48
3
+ Version: 6.0.49
4
4
  Summary: Main Sequence SDK
5
5
  Author-email: Main Sequence GmbH <dev@main-sequence.io>
6
6
  License-Expression: LicenseRef-MainSequence-Proprietary
@@ -302,6 +302,16 @@ The SDK surface also accepts:
302
302
  - `ResourceRelease.deploy_current_version()` for an SDK-triggered manual deployment run; it returns `DeploymentRun`
303
303
  - `DeploymentRun.filter(target_type="resource_release", target_uid=release.uid)` to inspect runs for one release
304
304
 
305
+ Every release also exposes its immutable revision lifecycle:
306
+
307
+ - `desired_revision` is the public UID of the accepted revision being materialized
308
+ - `active_revision` is the public UID of the ready revision currently serving behind the stable release URL
309
+ - the two UIDs may differ while a deployment is running or after a deployment fails
310
+ - `revision_retention_count` is the positive release-owned retention setting; omission on create uses the backend default
311
+ - configure retention with `ResourceRelease.create(..., revision_retention_count=3)` or `release.patch(revision_retention_count=5)`
312
+
313
+ Treat `active_revision` and `desired_revision` as read-only public identities. Do not substitute provider revision names, deployment names, or internal database IDs.
314
+
305
315
  Do not claim there is a CLI command for `deploy_current_version` unless the local CLI actually exposes one. In this SDK, the manual detail action is available through the client model.
306
316
 
307
317
  Inspect the unified run's `state`, `phase`, `outcome`, `steps`, `logs`, and `error` fields. Do not use legacy resource-release deployment status fields or filters.
@@ -1199,6 +1199,33 @@ class ResourceRelease(
1199
1199
  "The revision is returned by the API and is not part of create requests."
1200
1200
  ),
1201
1201
  )
1202
+ revision_retention_count: PositiveInt = Field(
1203
+ default=3,
1204
+ title="Revision Retention Count",
1205
+ description=(
1206
+ "Number of immutable release revisions retained by the platform. "
1207
+ "This positive release setting can be supplied on create or patch."
1208
+ ),
1209
+ examples=[3],
1210
+ )
1211
+ active_revision: str | None = Field(
1212
+ default=None,
1213
+ title="Active Revision UID",
1214
+ description=(
1215
+ "Public UID of the ready immutable revision currently serving behind "
1216
+ "the stable release URL."
1217
+ ),
1218
+ examples=["19128ab6-d72f-460c-8525-d758fa92676a", None],
1219
+ )
1220
+ desired_revision: str | None = Field(
1221
+ default=None,
1222
+ title="Desired Revision UID",
1223
+ description=(
1224
+ "Public UID of the accepted immutable revision currently being "
1225
+ "materialized. It may differ from the active revision during deployment."
1226
+ ),
1227
+ examples=["2a9370a7-c07f-439c-bcd9-629e3e916699", None],
1228
+ )
1202
1229
  cors_allowed_origins: list[str] | None = Field(
1203
1230
  default=None,
1204
1231
  title="CORS Allowed Origins",
@@ -1222,6 +1249,7 @@ class ResourceRelease(
1222
1249
  gpu_type: str | None = None,
1223
1250
  spot: bool | None = None,
1224
1251
  automatic_deployment: bool | None = None,
1252
+ revision_retention_count: int | None = None,
1225
1253
  timeout: int | None = None,
1226
1254
  files=None,
1227
1255
  ) -> ResourceRelease:
@@ -1268,6 +1296,10 @@ class ResourceRelease(
1268
1296
  payload["spot"] = bool(spot)
1269
1297
  if automatic_deployment is not None:
1270
1298
  payload["automatic_deployment"] = bool(automatic_deployment)
1299
+ if revision_retention_count is not None:
1300
+ payload["revision_retention_count"] = cls._normalize_revision_retention_count(
1301
+ revision_retention_count
1302
+ )
1271
1303
 
1272
1304
  request_payload = {"json": cls.serialize_for_json(payload)}
1273
1305
  if files:
@@ -1287,6 +1319,20 @@ class ResourceRelease(
1287
1319
 
1288
1320
  return cls(**r.json())
1289
1321
 
1322
+ @staticmethod
1323
+ def _normalize_revision_retention_count(value: int) -> int:
1324
+ if isinstance(value, bool) or not isinstance(value, int) or value < 1:
1325
+ raise ValueError("revision_retention_count must be a positive integer.")
1326
+ return value
1327
+
1328
+ @classmethod
1329
+ def patch_by_uid(cls, uid: str, *args, _into=None, **kwargs):
1330
+ if "revision_retention_count" in kwargs:
1331
+ kwargs["revision_retention_count"] = cls._normalize_revision_retention_count(
1332
+ kwargs["revision_retention_count"]
1333
+ )
1334
+ return super().patch_by_uid(uid, *args, _into=_into, **kwargs)
1335
+
1290
1336
  def deploy_current_version(
1291
1337
  self,
1292
1338
  *,
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: mainsequence
3
- Version: 6.0.48
3
+ Version: 6.0.49
4
4
  Summary: Main Sequence SDK
5
5
  Author-email: Main Sequence GmbH <dev@main-sequence.io>
6
6
  License-Expression: LicenseRef-MainSequence-Proprietary
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "mainsequence"
7
- version = "6.0.48"
7
+ version = "6.0.49"
8
8
  description = "Main Sequence SDK "
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.13"
@@ -2538,6 +2538,9 @@ def test_resource_release_model_supports_automatic_deployment_payloads():
2538
2538
  "tag_regex": None,
2539
2539
  "policy_revision": 3,
2540
2540
  },
2541
+ "revision_retention_count": 3,
2542
+ "active_revision": None,
2543
+ "desired_revision": "2a9370a7-c07f-439c-bcd9-629e3e916699",
2541
2544
  }
2542
2545
  )
2543
2546
 
@@ -2547,6 +2550,45 @@ def test_resource_release_model_supports_automatic_deployment_payloads():
2547
2550
  assert release.automatic_redeployment_policy is not None
2548
2551
  assert release.automatic_redeployment_policy.tag_regex is None
2549
2552
  assert release.automatic_redeployment_policy.policy_revision == 3
2553
+ assert release.revision_retention_count == 3
2554
+ assert release.active_revision is None
2555
+ assert release.desired_revision == "2a9370a7-c07f-439c-bcd9-629e3e916699"
2556
+
2557
+
2558
+ def test_resource_release_filter_accepts_canonical_revision_lifecycle_projection(monkeypatch):
2559
+ release_uid = "2f4c4c3d-5669-4da5-9d86-b84633c1e6ed"
2560
+ desired_revision_uid = "2a9370a7-c07f-439c-bcd9-629e3e916699"
2561
+ response_payload = {
2562
+ "uid": release_uid,
2563
+ "project_branch_uid": PROJECT_BRANCH_UID,
2564
+ "name": "Pricing API",
2565
+ "release_kind": "fastapi",
2566
+ "automatic_deployment": True,
2567
+ "automatic_redeployment_policy": {
2568
+ "tag_regex": None,
2569
+ "policy_revision": 2,
2570
+ },
2571
+ "revision_retention_count": 3,
2572
+ "active_revision": None,
2573
+ "desired_revision": desired_revision_uid,
2574
+ }
2575
+
2576
+ class FakeResponse:
2577
+ status_code = 200
2578
+
2579
+ @staticmethod
2580
+ def json():
2581
+ return [dict(response_payload)]
2582
+
2583
+ monkeypatch.setattr(base_mod, "make_request", lambda **kwargs: FakeResponse())
2584
+
2585
+ releases = models_helpers_mod.ResourceRelease.filter(project_branch_uid=PROJECT_BRANCH_UID)
2586
+
2587
+ assert len(releases) == 1
2588
+ assert releases[0].uid == release_uid
2589
+ assert releases[0].revision_retention_count == 3
2590
+ assert releases[0].active_revision is None
2591
+ assert releases[0].desired_revision == desired_revision_uid
2550
2592
 
2551
2593
 
2552
2594
  def test_legacy_resource_release_deployment_run_model_is_not_exported():
@@ -2582,6 +2624,9 @@ def test_resource_release_create_sends_automatic_deployment(monkeypatch):
2582
2624
  "tag_regex": "^v[0-9]+\\.[0-9]+\\.[0-9]+$",
2583
2625
  "policy_revision": 1,
2584
2626
  },
2627
+ "revision_retention_count": 5,
2628
+ "active_revision": "19128ab6-d72f-460c-8525-d758fa92676a",
2629
+ "desired_revision": "2a9370a7-c07f-439c-bcd9-629e3e916699",
2585
2630
  }
2586
2631
 
2587
2632
  def _fake_make_request(*, s, loaders, r_type, url, payload, time_out=None):
@@ -2600,6 +2645,7 @@ def test_resource_release_create_sends_automatic_deployment(monkeypatch):
2600
2645
  cpu_request="500m",
2601
2646
  memory_request="1Gi",
2602
2647
  automatic_deployment=True,
2648
+ revision_retention_count=5,
2603
2649
  timeout=11,
2604
2650
  )
2605
2651
 
@@ -2607,14 +2653,42 @@ def test_resource_release_create_sends_automatic_deployment(monkeypatch):
2607
2653
  assert release.automatic_redeployment_policy is not None
2608
2654
  assert release.automatic_redeployment_policy.tag_regex == "^v[0-9]+\\.[0-9]+\\.[0-9]+$"
2609
2655
  assert release.automatic_redeployment_policy.policy_revision == 1
2656
+ assert release.revision_retention_count == 5
2657
+ assert release.active_revision == "19128ab6-d72f-460c-8525-d758fa92676a"
2658
+ assert release.desired_revision == "2a9370a7-c07f-439c-bcd9-629e3e916699"
2610
2659
  assert captured["r_type"] == "POST"
2611
2660
  assert str(captured["url"]).endswith("/api/v1/resource-releases/")
2612
2661
  assert captured["payload"]["json"]["automatic_deployment"] is True
2662
+ assert captured["payload"]["json"]["revision_retention_count"] == 5
2613
2663
  assert captured["payload"]["json"]["resource_uid"] == resource_uid
2614
2664
  assert captured["payload"]["json"]["related_image_uid"] == image_uid
2615
2665
  assert captured["timeout"] == 11
2616
2666
 
2617
2667
 
2668
+ @pytest.mark.parametrize("invalid_value", [0, -1, True, 1.5, "3"])
2669
+ def test_resource_release_create_rejects_invalid_revision_retention_count(
2670
+ monkeypatch, invalid_value
2671
+ ):
2672
+ monkeypatch.setattr(
2673
+ models_helpers_mod,
2674
+ "make_request",
2675
+ lambda **kwargs: pytest.fail("invalid retention must fail before the request"),
2676
+ )
2677
+
2678
+ with pytest.raises(
2679
+ ValueError,
2680
+ match="revision_retention_count must be a positive integer",
2681
+ ):
2682
+ models_helpers_mod.ResourceRelease.create(
2683
+ resource_uid="857bec7b-dd77-4272-aecd-13fc2138eacc",
2684
+ related_image_uid="6cfdb152-923e-45b9-a150-c4541c68b0d1",
2685
+ release_kind=models_helpers_mod.ResourceReleaseKind.FAST_API,
2686
+ cpu_request="500m",
2687
+ memory_request="1Gi",
2688
+ revision_retention_count=invalid_value,
2689
+ )
2690
+
2691
+
2618
2692
  def test_project_resource_create_release_uses_related_image_uid(monkeypatch):
2619
2693
  captured = {}
2620
2694
  resource_uid = "857bec7b-dd77-4272-aecd-13fc2138eacc"
@@ -2703,6 +2777,9 @@ def test_resource_release_get_accepts_fastapi_cors_allowed_origins(monkeypatch):
2703
2777
  "release_kind": "fastapi",
2704
2778
  "automatic_deployment": True,
2705
2779
  "automatic_redeployment_policy": None,
2780
+ "revision_retention_count": 4,
2781
+ "active_revision": "19128ab6-d72f-460c-8525-d758fa92676a",
2782
+ "desired_revision": None,
2706
2783
  "cors_allowed_origins": [
2707
2784
  "https://app.example.com",
2708
2785
  "https://*.site-dev.main-sequence.app",
@@ -2732,12 +2809,70 @@ def test_resource_release_get_accepts_fastapi_cors_allowed_origins(monkeypatch):
2732
2809
  release = models_helpers_mod.ResourceRelease.get(pk=release_uid, timeout=13)
2733
2810
 
2734
2811
  assert release.release_kind == models_helpers_mod.ResourceReleaseKind.FAST_API
2812
+ assert release.revision_retention_count == 4
2813
+ assert release.active_revision == "19128ab6-d72f-460c-8525-d758fa92676a"
2814
+ assert release.desired_revision is None
2735
2815
  assert release.cors_allowed_origins == response_payload["cors_allowed_origins"]
2736
2816
  assert captured["r_type"] == "GET"
2737
2817
  assert captured["url"].endswith(f"/resource-releases/{release_uid}/")
2738
2818
  assert captured["timeout"] == 13
2739
2819
 
2740
2820
 
2821
+ def test_resource_release_patch_supports_positive_revision_retention_count(monkeypatch):
2822
+ captured = {}
2823
+ release_uid = "2f4c4c3d-5669-4da5-9d86-b84633c1e6ed"
2824
+ release = models_helpers_mod.ResourceRelease(
2825
+ uid=release_uid,
2826
+ release_kind="fastapi",
2827
+ revision_retention_count=3,
2828
+ )
2829
+
2830
+ class FakeResponse:
2831
+ status_code = 200
2832
+
2833
+ @staticmethod
2834
+ def json():
2835
+ return {
2836
+ "revision_retention_count": 6,
2837
+ "active_revision": None,
2838
+ "desired_revision": None,
2839
+ }
2840
+
2841
+ def _fake_make_request(*, s, loaders, r_type, url, payload, time_out=None):
2842
+ captured.update({"r_type": r_type, "url": url, "payload": payload})
2843
+ return FakeResponse()
2844
+
2845
+ monkeypatch.setattr(base_mod, "make_request", _fake_make_request)
2846
+
2847
+ patched = release.patch(revision_retention_count=6)
2848
+
2849
+ assert patched is release
2850
+ assert patched.revision_retention_count == 6
2851
+ assert captured["r_type"] == "PATCH"
2852
+ assert captured["url"].endswith(f"/resource-releases/{release_uid}/")
2853
+ assert captured["payload"]["json"] == {"revision_retention_count": 6}
2854
+
2855
+
2856
+ @pytest.mark.parametrize("invalid_value", [None, 0, -1, True, 1.5, "3"])
2857
+ def test_resource_release_patch_rejects_invalid_revision_retention_count(
2858
+ monkeypatch, invalid_value
2859
+ ):
2860
+ monkeypatch.setattr(
2861
+ base_mod,
2862
+ "make_request",
2863
+ lambda **kwargs: pytest.fail("invalid retention must fail before the request"),
2864
+ )
2865
+
2866
+ with pytest.raises(
2867
+ ValueError,
2868
+ match="revision_retention_count must be a positive integer",
2869
+ ):
2870
+ models_helpers_mod.ResourceRelease.patch_by_uid(
2871
+ "2f4c4c3d-5669-4da5-9d86-b84633c1e6ed",
2872
+ revision_retention_count=invalid_value,
2873
+ )
2874
+
2875
+
2741
2876
  def _resource_release_pipeline_payload(*, running_step: str | None = None):
2742
2877
  keys = (
2743
2878
  "resolve_revision",
File without changes
File without changes
File without changes