diff --git a/.release-please-manifest.json b/.release-please-manifest.json index bfaab56f6..4eb89879b 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,3 +1,3 @@ { - ".": "1.24.0" + ".": "1.27.0" } \ No newline at end of file diff --git a/.stats.yml b/.stats.yml index 4e9c9d433..10c388023 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ -configured_endpoints: 119 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/runloop-ai/runloop-0b7cd0c2fc193b18189cd7f44cf45ece7726b5d485fb72577f7d235266432ea0.yml -openapi_spec_hash: 78c340dbfb9d3d58b24ef318fc2a657b -config_hash: 218b8d25038e627faab98532392ee9a0 +configured_endpoints: 120 +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/runloop-ai/runloop-6fb96a2de54d260cad2b76366cada4eda15335275dbab7f6c108c1bb29acf309.yml +openapi_spec_hash: 6044f4a4da13881c5f82fcc394c1f43b +config_hash: 92c04f0b12249cb2d2bcf9e503a9cbf9 diff --git a/CHANGELOG.md b/CHANGELOG.md index de0a6c03f..56f808909 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,23 @@ # Changelog +## 1.27.0 (2026-07-24) + +Full Changelog: [v1.24.0...v1.27.0](https://github.com/runloopai/api-client-python/compare/v1.24.0...v1.27.0) + +### Features + +* **mux:** wait_for_eviction endpoint for flex eviction notifications ([#10325](https://github.com/runloopai/api-client-python/issues/10325)) ([c06794d](https://github.com/runloopai/api-client-python/commit/c06794d413f37be16ee5955fe31b07b92b8adbc8)) + + +### Bug Fixes + +* **mux:** field-level merge on network policy egress updates ([#10338](https://github.com/runloopai/api-client-python/issues/10338)) ([cfc97ef](https://github.com/runloopai/api-client-python/commit/cfc97ef55a65e88abb0d76f733cf51dc33823fb0)) + + +### Chores + +* **stainless:** generate watch_evictions as an SSE stream ([#10404](https://github.com/runloopai/api-client-python/issues/10404)) ([0413e7b](https://github.com/runloopai/api-client-python/commit/0413e7ba4cd10b919a3fc50e67e0238dc95f4b18)) + ## 1.24.0 (2026-07-22) Full Changelog: [v1.23.3...v1.24.0](https://github.com/runloopai/api-client-python/compare/v1.23.3...v1.24.0) diff --git a/api.md b/api.md index b7db70b09..fc7f676a3 100644 --- a/api.md +++ b/api.md @@ -201,6 +201,7 @@ Types: ```python from runloop_api_client.types import ( DevboxAsyncExecutionDetailView, + DevboxEvictionEventView, DevboxExecutionDetailView, DevboxKillExecutionRequest, DevboxListView, @@ -244,6 +245,7 @@ Methods: - client.devboxes.suspend(id) -> DevboxView - client.devboxes.upload_file(id, \*\*params) -> object - client.devboxes.wait_for_command(execution_id, \*, devbox_id, \*\*params) -> DevboxAsyncExecutionDetailView +- client.devboxes.watch_evictions() -> DevboxEvictionEventView - client.devboxes.write_file_contents(id, \*\*params) -> DevboxExecutionDetailView diff --git a/pyproject.toml b/pyproject.toml index 77d099e9e..149da858b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "runloop_api_client" -version = "1.24.0" +version = "1.27.0" description = "The official Python library for the runloop API" dynamic = ["readme"] license = "MIT" diff --git a/src/runloop_api_client/_version.py b/src/runloop_api_client/_version.py index 5e32f9066..59ac2c1db 100644 --- a/src/runloop_api_client/_version.py +++ b/src/runloop_api_client/_version.py @@ -1,4 +1,4 @@ # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. __title__ = "runloop_api_client" -__version__ = "1.24.0" # x-release-please-version +__version__ = "1.27.0" # x-release-please-version diff --git a/src/runloop_api_client/resources/devboxes/devboxes.py b/src/runloop_api_client/resources/devboxes/devboxes.py index f100014f8..0a3fdade5 100644 --- a/src/runloop_api_client/resources/devboxes/devboxes.py +++ b/src/runloop_api_client/resources/devboxes/devboxes.py @@ -69,6 +69,7 @@ LONG_POLL_CLIENT_BUFFER_SECONDS, EXEC_LONG_POLL_SERVER_MAX_SECONDS, ) +from ..._streaming import Stream, AsyncStream from ...pagination import ( SyncDevboxesCursorIDPage, AsyncDevboxesCursorIDPage, @@ -94,6 +95,7 @@ from ...types.shared_params.mount import Mount from ...types.devbox_snapshot_view import DevboxSnapshotView from ...types.shared.launch_parameters import LaunchParameters as SharedLaunchParameters +from ...types.devbox_eviction_event_view import DevboxEvictionEventView from ...types.devbox_resource_usage_view import DevboxResourceUsageView from ...types.devbox_execution_detail_view import DevboxExecutionDetailView from ...types.devbox_create_ssh_key_response import DevboxCreateSSHKeyResponse @@ -1742,6 +1744,33 @@ def wait_for_command( cast_to=DevboxAsyncExecutionDetailView, ) + def watch_evictions( + self, + *, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> Stream[DevboxEvictionEventView]: + """ + Subscribe, via server-sent events, to pending infrastructure evictions for every + Devbox in the account. On connect the stream emits one event per Devbox that + currently has a pending eviction, then one event as each further eviction is + scheduled. Best-effort and advisory: a Devbox stays running until its deadline, + and delivery is not guaranteed. + """ + return self._get( + "/v1/devboxes/watch_evictions", + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=DevboxEvictionEventView, + stream=True, + stream_cls=Stream[DevboxEvictionEventView], + ) + def write_file_contents( self, id: str, @@ -3417,6 +3446,33 @@ async def wait_for_command( cast_to=DevboxAsyncExecutionDetailView, ) + async def watch_evictions( + self, + *, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> AsyncStream[DevboxEvictionEventView]: + """ + Subscribe, via server-sent events, to pending infrastructure evictions for every + Devbox in the account. On connect the stream emits one event per Devbox that + currently has a pending eviction, then one event as each further eviction is + scheduled. Best-effort and advisory: a Devbox stays running until its deadline, + and delivery is not guaranteed. + """ + return await self._get( + "/v1/devboxes/watch_evictions", + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=DevboxEvictionEventView, + stream=True, + stream_cls=AsyncStream[DevboxEvictionEventView], + ) + async def write_file_contents( self, id: str, @@ -3555,6 +3611,9 @@ def __init__(self, devboxes: DevboxesResource) -> None: self.wait_for_command = to_raw_response_wrapper( devboxes.wait_for_command, ) + self.watch_evictions = to_raw_response_wrapper( + devboxes.watch_evictions, + ) self.write_file_contents = to_raw_response_wrapper( devboxes.write_file_contents, ) @@ -3651,6 +3710,9 @@ def __init__(self, devboxes: AsyncDevboxesResource) -> None: self.wait_for_command = async_to_raw_response_wrapper( devboxes.wait_for_command, ) + self.watch_evictions = async_to_raw_response_wrapper( + devboxes.watch_evictions, + ) self.write_file_contents = async_to_raw_response_wrapper( devboxes.write_file_contents, ) @@ -3747,6 +3809,9 @@ def __init__(self, devboxes: DevboxesResource) -> None: self.wait_for_command = to_streamed_response_wrapper( devboxes.wait_for_command, ) + self.watch_evictions = to_streamed_response_wrapper( + devboxes.watch_evictions, + ) self.write_file_contents = to_streamed_response_wrapper( devboxes.write_file_contents, ) @@ -3843,6 +3908,9 @@ def __init__(self, devboxes: AsyncDevboxesResource) -> None: self.wait_for_command = async_to_streamed_response_wrapper( devboxes.wait_for_command, ) + self.watch_evictions = async_to_streamed_response_wrapper( + devboxes.watch_evictions, + ) self.write_file_contents = async_to_streamed_response_wrapper( devboxes.write_file_contents, ) diff --git a/src/runloop_api_client/resources/network_policies.py b/src/runloop_api_client/resources/network_policies.py index b02062311..135eabea6 100644 --- a/src/runloop_api_client/resources/network_policies.py +++ b/src/runloop_api_client/resources/network_policies.py @@ -191,7 +191,8 @@ def update( ) -> NetworkPolicyView: """Update an existing NetworkPolicy. - All fields are optional. + All fields are optional - null fields preserve + existing values, provided fields replace entirely. Args: allow_agent_gateway: If true, allows devbox egress to the agent gateway. @@ -525,7 +526,8 @@ async def update( ) -> NetworkPolicyView: """Update an existing NetworkPolicy. - All fields are optional. + All fields are optional - null fields preserve + existing values, provided fields replace entirely. Args: allow_agent_gateway: If true, allows devbox egress to the agent gateway. diff --git a/src/runloop_api_client/types/__init__.py b/src/runloop_api_client/types/__init__.py index 4abd94f92..0bda186a3 100644 --- a/src/runloop_api_client/types/__init__.py +++ b/src/runloop_api_client/types/__init__.py @@ -110,6 +110,7 @@ from .scenario_start_run_params import ScenarioStartRunParams as ScenarioStartRunParams from .benchmark_start_run_params import BenchmarkStartRunParams as BenchmarkStartRunParams from .blueprint_build_parameters import BlueprintBuildParameters as BlueprintBuildParameters +from .devbox_eviction_event_view import DevboxEvictionEventView as DevboxEvictionEventView from .devbox_execute_sync_params import DevboxExecuteSyncParams as DevboxExecuteSyncParams from .devbox_resource_usage_view import DevboxResourceUsageView as DevboxResourceUsageView from .gateway_config_list_params import GatewayConfigListParams as GatewayConfigListParams diff --git a/src/runloop_api_client/types/devbox_eviction_event_view.py b/src/runloop_api_client/types/devbox_eviction_event_view.py new file mode 100644 index 000000000..11d646c27 --- /dev/null +++ b/src/runloop_api_client/types/devbox_eviction_event_view.py @@ -0,0 +1,16 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from .._models import BaseModel + +__all__ = ["DevboxEvictionEventView"] + + +class DevboxEvictionEventView(BaseModel): + devbox_id: str + """The ID of the Devbox with a pending eviction.""" + + eviction_deadline_ms: int + """Unix timestamp (milliseconds) after which the Devbox will be suspended. + + Advisory and best-effort. + """ diff --git a/tests/api_resources/test_devboxes.py b/tests/api_resources/test_devboxes.py index 6c3e89c8f..0d9495f80 100644 --- a/tests/api_resources/test_devboxes.py +++ b/tests/api_resources/test_devboxes.py @@ -1190,6 +1190,30 @@ def test_path_params_wait_for_command(self, client: Runloop) -> None: statuses=["queued"], ) + @parametrize + def test_method_watch_evictions(self, client: Runloop) -> None: + devbox_stream = client.devboxes.watch_evictions() + devbox_stream.response.close() + + @parametrize + def test_raw_response_watch_evictions(self, client: Runloop) -> None: + response = client.devboxes.with_raw_response.watch_evictions() + + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + stream = response.parse() + stream.close() + + @parametrize + def test_streaming_response_watch_evictions(self, client: Runloop) -> None: + with client.devboxes.with_streaming_response.watch_evictions() as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + stream = response.parse() + stream.close() + + assert cast(Any, response.is_closed) is True + @parametrize def test_method_write_file_contents(self, client: Runloop) -> None: devbox = client.devboxes.write_file_contents( @@ -2859,6 +2883,30 @@ async def test_path_params_wait_for_command(self, async_client: AsyncRunloop) -> statuses=["queued"], ) + @parametrize + async def test_method_watch_evictions(self, async_client: AsyncRunloop) -> None: + devbox_stream = await async_client.devboxes.watch_evictions() + await devbox_stream.response.aclose() + + @parametrize + async def test_raw_response_watch_evictions(self, async_client: AsyncRunloop) -> None: + response = await async_client.devboxes.with_raw_response.watch_evictions() + + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + stream = await response.parse() + await stream.close() + + @parametrize + async def test_streaming_response_watch_evictions(self, async_client: AsyncRunloop) -> None: + async with async_client.devboxes.with_streaming_response.watch_evictions() as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + stream = await response.parse() + await stream.close() + + assert cast(Any, response.is_closed) is True + @parametrize async def test_method_write_file_contents(self, async_client: AsyncRunloop) -> None: devbox = await async_client.devboxes.write_file_contents(