From 92469080d88ca273bd94e16e1637307cf5b30494 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20Ole=C5=9B?= Date: Fri, 2 Jan 2026 13:17:07 +0100 Subject: [PATCH 1/2] Add option to update LifeCycle --- koyeb/api/api/services_api.py | 78 +-------------------------- koyeb/api/docs/ServiceListItem.md | 1 + koyeb/api/docs/ServicesApi.md | 16 ++---- koyeb/api/models/service_list_item.py | 11 ++++ koyeb/sandbox/sandbox.py | 65 +++++++++++++++++++--- koyeb/sandbox/utils.py | 11 +++- spec/openapi.json | 31 ++--------- 7 files changed, 88 insertions(+), 125 deletions(-) diff --git a/koyeb/api/api/services_api.py b/koyeb/api/api/services_api.py index 31fc2fc1..7028ef40 100644 --- a/koyeb/api/api/services_api.py +++ b/koyeb/api/api/services_api.py @@ -16,7 +16,7 @@ from typing import Any, Dict, List, Optional, Tuple, Union from typing_extensions import Annotated -from pydantic import Field, StrictBool, StrictInt, StrictStr, field_validator +from pydantic import Field, StrictBool, StrictStr, field_validator from typing import Any, Dict, List, Optional from typing_extensions import Annotated from koyeb.api.models.autocomplete_reply import AutocompleteReply @@ -2711,8 +2711,6 @@ def update_service( description="If set, run validation and check that the service exists" ), ] = None, - life_cycle_delete_after_sleep: Optional[StrictInt] = None, - life_cycle_delete_after_create: Optional[StrictInt] = None, _request_timeout: Union[ None, Annotated[StrictFloat, Field(gt=0)], @@ -2734,10 +2732,6 @@ def update_service( :type service: UpdateService :param dry_run: If set, run validation and check that the service exists :type dry_run: bool - :param life_cycle_delete_after_sleep: - :type life_cycle_delete_after_sleep: int - :param life_cycle_delete_after_create: - :type life_cycle_delete_after_create: int :param _request_timeout: timeout setting for this request. If one number provided, it will be total request timeout. It can also be a pair (tuple) of @@ -2764,8 +2758,6 @@ def update_service( id=id, service=service, dry_run=dry_run, - life_cycle_delete_after_sleep=life_cycle_delete_after_sleep, - life_cycle_delete_after_create=life_cycle_delete_after_create, _request_auth=_request_auth, _content_type=_content_type, _headers=_headers, @@ -2801,8 +2793,6 @@ def update_service_with_http_info( description="If set, run validation and check that the service exists" ), ] = None, - life_cycle_delete_after_sleep: Optional[StrictInt] = None, - life_cycle_delete_after_create: Optional[StrictInt] = None, _request_timeout: Union[ None, Annotated[StrictFloat, Field(gt=0)], @@ -2824,10 +2814,6 @@ def update_service_with_http_info( :type service: UpdateService :param dry_run: If set, run validation and check that the service exists :type dry_run: bool - :param life_cycle_delete_after_sleep: - :type life_cycle_delete_after_sleep: int - :param life_cycle_delete_after_create: - :type life_cycle_delete_after_create: int :param _request_timeout: timeout setting for this request. If one number provided, it will be total request timeout. It can also be a pair (tuple) of @@ -2854,8 +2840,6 @@ def update_service_with_http_info( id=id, service=service, dry_run=dry_run, - life_cycle_delete_after_sleep=life_cycle_delete_after_sleep, - life_cycle_delete_after_create=life_cycle_delete_after_create, _request_auth=_request_auth, _content_type=_content_type, _headers=_headers, @@ -2891,8 +2875,6 @@ def update_service_without_preload_content( description="If set, run validation and check that the service exists" ), ] = None, - life_cycle_delete_after_sleep: Optional[StrictInt] = None, - life_cycle_delete_after_create: Optional[StrictInt] = None, _request_timeout: Union[ None, Annotated[StrictFloat, Field(gt=0)], @@ -2914,10 +2896,6 @@ def update_service_without_preload_content( :type service: UpdateService :param dry_run: If set, run validation and check that the service exists :type dry_run: bool - :param life_cycle_delete_after_sleep: - :type life_cycle_delete_after_sleep: int - :param life_cycle_delete_after_create: - :type life_cycle_delete_after_create: int :param _request_timeout: timeout setting for this request. If one number provided, it will be total request timeout. It can also be a pair (tuple) of @@ -2944,8 +2922,6 @@ def update_service_without_preload_content( id=id, service=service, dry_run=dry_run, - life_cycle_delete_after_sleep=life_cycle_delete_after_sleep, - life_cycle_delete_after_create=life_cycle_delete_after_create, _request_auth=_request_auth, _content_type=_content_type, _headers=_headers, @@ -2971,8 +2947,6 @@ def _update_service_serialize( id, service, dry_run, - life_cycle_delete_after_sleep, - life_cycle_delete_after_create, _request_auth, _content_type, _headers, @@ -3000,18 +2974,6 @@ def _update_service_serialize( _query_params.append(("dry_run", dry_run)) - if life_cycle_delete_after_sleep is not None: - - _query_params.append( - ("life_cycle.delete_after_sleep", life_cycle_delete_after_sleep) - ) - - if life_cycle_delete_after_create is not None: - - _query_params.append( - ("life_cycle.delete_after_create", life_cycle_delete_after_create) - ) - # process the header parameters # process the form parameters # process the body parameter @@ -3051,8 +3013,6 @@ def update_service2( description="If set, run validation and check that the service exists" ), ] = None, - life_cycle_delete_after_sleep: Optional[StrictInt] = None, - life_cycle_delete_after_create: Optional[StrictInt] = None, _request_timeout: Union[ None, Annotated[StrictFloat, Field(gt=0)], @@ -3074,10 +3034,6 @@ def update_service2( :type service: UpdateService :param dry_run: If set, run validation and check that the service exists :type dry_run: bool - :param life_cycle_delete_after_sleep: - :type life_cycle_delete_after_sleep: int - :param life_cycle_delete_after_create: - :type life_cycle_delete_after_create: int :param _request_timeout: timeout setting for this request. If one number provided, it will be total request timeout. It can also be a pair (tuple) of @@ -3104,8 +3060,6 @@ def update_service2( id=id, service=service, dry_run=dry_run, - life_cycle_delete_after_sleep=life_cycle_delete_after_sleep, - life_cycle_delete_after_create=life_cycle_delete_after_create, _request_auth=_request_auth, _content_type=_content_type, _headers=_headers, @@ -3141,8 +3095,6 @@ def update_service2_with_http_info( description="If set, run validation and check that the service exists" ), ] = None, - life_cycle_delete_after_sleep: Optional[StrictInt] = None, - life_cycle_delete_after_create: Optional[StrictInt] = None, _request_timeout: Union[ None, Annotated[StrictFloat, Field(gt=0)], @@ -3164,10 +3116,6 @@ def update_service2_with_http_info( :type service: UpdateService :param dry_run: If set, run validation and check that the service exists :type dry_run: bool - :param life_cycle_delete_after_sleep: - :type life_cycle_delete_after_sleep: int - :param life_cycle_delete_after_create: - :type life_cycle_delete_after_create: int :param _request_timeout: timeout setting for this request. If one number provided, it will be total request timeout. It can also be a pair (tuple) of @@ -3194,8 +3142,6 @@ def update_service2_with_http_info( id=id, service=service, dry_run=dry_run, - life_cycle_delete_after_sleep=life_cycle_delete_after_sleep, - life_cycle_delete_after_create=life_cycle_delete_after_create, _request_auth=_request_auth, _content_type=_content_type, _headers=_headers, @@ -3231,8 +3177,6 @@ def update_service2_without_preload_content( description="If set, run validation and check that the service exists" ), ] = None, - life_cycle_delete_after_sleep: Optional[StrictInt] = None, - life_cycle_delete_after_create: Optional[StrictInt] = None, _request_timeout: Union[ None, Annotated[StrictFloat, Field(gt=0)], @@ -3254,10 +3198,6 @@ def update_service2_without_preload_content( :type service: UpdateService :param dry_run: If set, run validation and check that the service exists :type dry_run: bool - :param life_cycle_delete_after_sleep: - :type life_cycle_delete_after_sleep: int - :param life_cycle_delete_after_create: - :type life_cycle_delete_after_create: int :param _request_timeout: timeout setting for this request. If one number provided, it will be total request timeout. It can also be a pair (tuple) of @@ -3284,8 +3224,6 @@ def update_service2_without_preload_content( id=id, service=service, dry_run=dry_run, - life_cycle_delete_after_sleep=life_cycle_delete_after_sleep, - life_cycle_delete_after_create=life_cycle_delete_after_create, _request_auth=_request_auth, _content_type=_content_type, _headers=_headers, @@ -3311,8 +3249,6 @@ def _update_service2_serialize( id, service, dry_run, - life_cycle_delete_after_sleep, - life_cycle_delete_after_create, _request_auth, _content_type, _headers, @@ -3340,18 +3276,6 @@ def _update_service2_serialize( _query_params.append(("dry_run", dry_run)) - if life_cycle_delete_after_sleep is not None: - - _query_params.append( - ("life_cycle.delete_after_sleep", life_cycle_delete_after_sleep) - ) - - if life_cycle_delete_after_create is not None: - - _query_params.append( - ("life_cycle.delete_after_create", life_cycle_delete_after_create) - ) - # process the header parameters # process the form parameters # process the body parameter diff --git a/koyeb/api/docs/ServiceListItem.md b/koyeb/api/docs/ServiceListItem.md index 9fbd2596..3c06997c 100644 --- a/koyeb/api/docs/ServiceListItem.md +++ b/koyeb/api/docs/ServiceListItem.md @@ -18,6 +18,7 @@ Name | Type | Description | Notes **state** | [**ServiceState**](ServiceState.md) | | [optional] **active_deployment_id** | **str** | | [optional] **latest_deployment_id** | **str** | | [optional] +**life_cycle** | [**ServiceLifeCycle**](ServiceLifeCycle.md) | | [optional] ## Example diff --git a/koyeb/api/docs/ServicesApi.md b/koyeb/api/docs/ServicesApi.md index 123d9854..1d10d33b 100644 --- a/koyeb/api/docs/ServicesApi.md +++ b/koyeb/api/docs/ServicesApi.md @@ -814,7 +814,7 @@ Name | Type | Description | Notes [[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md) # **update_service** -> UpdateServiceReply update_service(id, service, dry_run=dry_run, life_cycle_delete_after_sleep=life_cycle_delete_after_sleep, life_cycle_delete_after_create=life_cycle_delete_after_create) +> UpdateServiceReply update_service(id, service, dry_run=dry_run) Update Service @@ -853,12 +853,10 @@ with koyeb.api.ApiClient(configuration) as api_client: id = 'id_example' # str | The id of the entity to update service = koyeb.api.UpdateService() # UpdateService | dry_run = True # bool | If set, run validation and check that the service exists (optional) - life_cycle_delete_after_sleep = 56 # int | (optional) - life_cycle_delete_after_create = 56 # int | (optional) try: # Update Service - api_response = api_instance.update_service(id, service, dry_run=dry_run, life_cycle_delete_after_sleep=life_cycle_delete_after_sleep, life_cycle_delete_after_create=life_cycle_delete_after_create) + api_response = api_instance.update_service(id, service, dry_run=dry_run) print("The response of ServicesApi->update_service:\n") pprint(api_response) except Exception as e: @@ -875,8 +873,6 @@ Name | Type | Description | Notes **id** | **str**| The id of the entity to update | **service** | [**UpdateService**](UpdateService.md)| | **dry_run** | **bool**| If set, run validation and check that the service exists | [optional] - **life_cycle_delete_after_sleep** | **int**| | [optional] - **life_cycle_delete_after_create** | **int**| | [optional] ### Return type @@ -907,7 +903,7 @@ Name | Type | Description | Notes [[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md) # **update_service2** -> UpdateServiceReply update_service2(id, service, dry_run=dry_run, life_cycle_delete_after_sleep=life_cycle_delete_after_sleep, life_cycle_delete_after_create=life_cycle_delete_after_create) +> UpdateServiceReply update_service2(id, service, dry_run=dry_run) Update Service @@ -946,12 +942,10 @@ with koyeb.api.ApiClient(configuration) as api_client: id = 'id_example' # str | The id of the entity to update service = koyeb.api.UpdateService() # UpdateService | dry_run = True # bool | If set, run validation and check that the service exists (optional) - life_cycle_delete_after_sleep = 56 # int | (optional) - life_cycle_delete_after_create = 56 # int | (optional) try: # Update Service - api_response = api_instance.update_service2(id, service, dry_run=dry_run, life_cycle_delete_after_sleep=life_cycle_delete_after_sleep, life_cycle_delete_after_create=life_cycle_delete_after_create) + api_response = api_instance.update_service2(id, service, dry_run=dry_run) print("The response of ServicesApi->update_service2:\n") pprint(api_response) except Exception as e: @@ -968,8 +962,6 @@ Name | Type | Description | Notes **id** | **str**| The id of the entity to update | **service** | [**UpdateService**](UpdateService.md)| | **dry_run** | **bool**| If set, run validation and check that the service exists | [optional] - **life_cycle_delete_after_sleep** | **int**| | [optional] - **life_cycle_delete_after_create** | **int**| | [optional] ### Return type diff --git a/koyeb/api/models/service_list_item.py b/koyeb/api/models/service_list_item.py index 28084e8b..69c56d15 100644 --- a/koyeb/api/models/service_list_item.py +++ b/koyeb/api/models/service_list_item.py @@ -20,6 +20,7 @@ from datetime import datetime from pydantic import BaseModel, ConfigDict, StrictStr from typing import Any, ClassVar, Dict, List, Optional +from koyeb.api.models.service_life_cycle import ServiceLifeCycle from koyeb.api.models.service_state import ServiceState from koyeb.api.models.service_status import ServiceStatus from koyeb.api.models.service_type import ServiceType @@ -45,6 +46,7 @@ class ServiceListItem(BaseModel): state: Optional[ServiceState] = None active_deployment_id: Optional[StrictStr] = None latest_deployment_id: Optional[StrictStr] = None + life_cycle: Optional[ServiceLifeCycle] = None __properties: ClassVar[List[str]] = [ "id", "name", @@ -59,6 +61,7 @@ class ServiceListItem(BaseModel): "state", "active_deployment_id", "latest_deployment_id", + "life_cycle", ] model_config = ConfigDict( @@ -101,6 +104,9 @@ def to_dict(self) -> Dict[str, Any]: # override the default output from pydantic by calling `to_dict()` of state if self.state: _dict["state"] = self.state.to_dict() + # override the default output from pydantic by calling `to_dict()` of life_cycle + if self.life_cycle: + _dict["life_cycle"] = self.life_cycle.to_dict() return _dict @classmethod @@ -139,6 +145,11 @@ def from_dict(cls, obj: Optional[Dict[str, Any]]) -> Optional[Self]: ), "active_deployment_id": obj.get("active_deployment_id"), "latest_deployment_id": obj.get("latest_deployment_id"), + "life_cycle": ( + ServiceLifeCycle.from_dict(obj["life_cycle"]) + if obj.get("life_cycle") is not None + else None + ), } ) return _obj diff --git a/koyeb/sandbox/sandbox.py b/koyeb/sandbox/sandbox.py index c7941a1a..f74b8427 100644 --- a/koyeb/sandbox/sandbox.py +++ b/koyeb/sandbox/sandbox.py @@ -17,6 +17,7 @@ from koyeb.api.exceptions import ApiException, NotFoundException from koyeb.api.models.create_app import CreateApp, AppLifeCycle from koyeb.api.models.create_service import CreateService, ServiceLifeCycle +from koyeb.api.models.update_service import UpdateService from .utils import ( DEFAULT_INSTANCE_WAIT_TIMEOUT, @@ -225,7 +226,7 @@ def _create_sync( Subclasses can override to return their own type. """ - apps_api, services_api, _, catalog_instances_api = get_api_client(api_token) + apps_api, services_api, _, _, _ = get_api_client(api_token) # Always create routes (ports are always exposed, default to "http") routes = create_koyeb_sandbox_routes() @@ -315,7 +316,7 @@ def get_from_id( if not id: raise ValueError("id is required") - _, services_api, _, _ = get_api_client(api_token) + _, services_api, _, _, _ = get_api_client(api_token) deployments_api = DeploymentsApi(services_api.api_client) # Get service by ID @@ -429,7 +430,7 @@ def wait_tcp_proxy_ready( def delete(self) -> None: """Delete the sandbox instance.""" - apps_api, _, _, _ = get_api_client(self.api_token) + apps_api, _, _, _, _ = get_api_client(self.api_token) apps_api.delete_app(self.app_id) def get_domain(self) -> Optional[str]: @@ -447,12 +448,11 @@ def get_domain(self) -> Optional[str]: from .utils import get_api_client - _, services_api, _, _ = get_api_client(self.api_token) + apps_api, services_api, _, _, _ = get_api_client(self.api_token) service_response = services_api.get_service(self.service_id) service = service_response.service if service.app_id: - apps_api, _, _, _ = get_api_client(self.api_token) app_response = apps_api.get_app(service.app_id) app = app_response.app if hasattr(app, "domains") and app.domains: @@ -477,7 +477,7 @@ def get_tcp_proxy_info(self) -> Optional[tuple[str, int]]: from .utils import get_api_client - _, services_api, _, _ = get_api_client(self.api_token) + _, services_api, _, _, _ = get_api_client(self.api_token) service_response = services_api.get_service(self.service_id) service = service_response.service @@ -804,6 +804,59 @@ def kill_all_processes(self) -> int: pass return killed_count + def update_life_cycle( + self, + delete_after_delay: Optional[int] = None, + delete_after_inactivity: Optional[int] = None, + ) -> None: + """ + Update the sandbox's life cycle settings. + + Args: + delete_after_delay: If >0, automatically delete the sandbox if there was no activity + after this many seconds since creation. + delete_after_inactivity: If >0, automatically delete the sandbox if service sleeps due to inactivity + after this many seconds. + + Raises: + SandboxError: If updating life cycle fails + + Example: + >>> sandbox.update_life_cycle(delete_after_create=600, delete_after_sleep=300) + """ + try: + _, services_api, _, _, deployments_api = get_api_client(self.api_token) + service_response = services_api.get_service(self.service_id) + service = service_response.service + + deployment_response = deployments_api.get_deployment( + service.latest_deployment_id + ) + deployment = deployment_response.deployment + + if not service: + raise SandboxError("Sandbox service not found") + + # Update life cycle settings + life_cycle = service.life_cycle or ServiceLifeCycle() + if delete_after_delay is not None: + life_cycle.delete_after_create = delete_after_delay + if delete_after_inactivity is not None: + life_cycle.delete_after_sleep = delete_after_inactivity + + # Send update request + services_api.update_service( + id=self.service_id, + service=UpdateService( + definition=deployment.definition, + life_cycle=life_cycle, + ), + ) + except Exception as e: + if isinstance(e, SandboxError): + raise + raise SandboxError(f"Failed to update life cycle: {str(e)}") + def __enter__(self) -> "Sandbox": """Context manager entry - returns self.""" return self diff --git a/koyeb/sandbox/utils.py b/koyeb/sandbox/utils.py index 9448f8a9..2f86cc7a 100644 --- a/koyeb/sandbox/utils.py +++ b/koyeb/sandbox/utils.py @@ -11,7 +11,13 @@ from typing import Any, Callable, Dict, List, Optional from koyeb.api import ApiClient, Configuration -from koyeb.api.api import AppsApi, CatalogInstancesApi, InstancesApi, ServicesApi +from koyeb.api.api import ( + AppsApi, + CatalogInstancesApi, + InstancesApi, + ServicesApi, + DeploymentsApi, +) from koyeb.api.models.deployment_definition import DeploymentDefinition from koyeb.api.models.deployment_definition_type import DeploymentDefinitionType from koyeb.api.models.deployment_env import DeploymentEnv @@ -83,7 +89,7 @@ def _validate_port_protocol(protocol: str) -> str: def get_api_client( api_token: Optional[str] = None, host: Optional[str] = None -) -> tuple[AppsApi, ServicesApi, InstancesApi, CatalogInstancesApi]: +) -> tuple[AppsApi, ServicesApi, InstancesApi, CatalogInstancesApi, DeploymentsApi]: """ Get configured API clients for Koyeb operations. @@ -114,6 +120,7 @@ def get_api_client( ServicesApi(api_client), InstancesApi(api_client), CatalogInstancesApi(api_client), + DeploymentsApi(api_client), ) diff --git a/spec/openapi.json b/spec/openapi.json index 4abebe91..dd660aa7 100644 --- a/spec/openapi.json +++ b/spec/openapi.json @@ -4287,20 +4287,6 @@ "in": "query", "required": false, "type": "boolean" - }, - { - "name": "life_cycle.delete_after_sleep", - "in": "query", - "required": false, - "type": "integer", - "format": "int64" - }, - { - "name": "life_cycle.delete_after_create", - "in": "query", - "required": false, - "type": "integer", - "format": "int64" } ], "tags": [ @@ -4382,20 +4368,6 @@ "in": "query", "required": false, "type": "boolean" - }, - { - "name": "life_cycle.delete_after_sleep", - "in": "query", - "required": false, - "type": "integer", - "format": "int64" - }, - { - "name": "life_cycle.delete_after_create", - "in": "query", - "required": false, - "type": "integer", - "format": "int64" } ], "tags": [ @@ -16044,6 +16016,9 @@ }, "latest_deployment_id": { "type": "string" + }, + "life_cycle": { + "$ref": "#/definitions/ServiceLifeCycle" } } }, From 3aca98aebded42198ab2bc072695835a67d386aa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20Ole=C5=9B?= Date: Fri, 2 Jan 2026 14:11:09 +0100 Subject: [PATCH 2/2] Update koyeb/sandbox/sandbox.py Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- koyeb/sandbox/sandbox.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/koyeb/sandbox/sandbox.py b/koyeb/sandbox/sandbox.py index f74b8427..e5fa3034 100644 --- a/koyeb/sandbox/sandbox.py +++ b/koyeb/sandbox/sandbox.py @@ -822,7 +822,7 @@ def update_life_cycle( SandboxError: If updating life cycle fails Example: - >>> sandbox.update_life_cycle(delete_after_create=600, delete_after_sleep=300) + >>> sandbox.update_life_cycle(delete_after_delay=600, delete_after_inactivity=300) """ try: _, services_api, _, _, deployments_api = get_api_client(self.api_token)