diff --git a/.release-please-manifest.json b/.release-please-manifest.json index 3e9af1b3a..fbd9082d7 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,3 +1,3 @@ { - ".": "1.4.0" + ".": "1.5.0" } \ No newline at end of file diff --git a/.stats.yml b/.stats.yml index e2f90d495..26ecaabc0 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ -configured_endpoints: 111 +configured_endpoints: 112 openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/runloop-ai%2Frunloop-84e997ca5716b9378a58a1bdf3d6616cf3be80156a6aaed1bed469fe93ba2c95.yml openapi_spec_hash: b44a4ba1c2c3cb775c14545f2bab05a8 -config_hash: 22f65246be4646c23dde9f69f51252e7 +config_hash: 6c26299fd9ef01fb4713612a9a2ad17c diff --git a/CHANGELOG.md b/CHANGELOG.md index 6adca673a..eacbba506 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,13 @@ # Changelog +## 1.5.0 (2026-01-30) + +Full Changelog: [v1.4.0...v1.5.0](https://github.com/runloopai/api-client-python/compare/v1.4.0...v1.5.0) + +### Features + +* **devbox:** add enable_tunnel API ([#7236](https://github.com/runloopai/api-client-python/issues/7236)) ([bb58bfc](https://github.com/runloopai/api-client-python/commit/bb58bfc40c93c5832634ddb82c255ada7214c7f4)) + ## 1.4.0 (2026-01-30) Full Changelog: [v1.3.2...v1.4.0](https://github.com/runloopai/api-client-python/compare/v1.3.2...v1.4.0) diff --git a/api.md b/api.md index 44ba928b2..2720824ad 100644 --- a/api.md +++ b/api.md @@ -132,6 +132,7 @@ from runloop_api_client.types import ( DevboxSnapshotView, DevboxTunnelView, DevboxView, + TunnelView, DevboxCreateSSHKeyResponse, DevboxReadFileContentsResponse, ) @@ -148,6 +149,7 @@ Methods: - client.devboxes.create_tunnel(id, \*\*params) -> DevboxTunnelView - client.devboxes.delete_disk_snapshot(id) -> object - client.devboxes.download_file(id, \*\*params) -> BinaryAPIResponse +- client.devboxes.enable_tunnel(id, \*\*params) -> TunnelView - client.devboxes.execute(id, \*\*params) -> DevboxAsyncExecutionDetailView - client.devboxes.execute_async(id, \*\*params) -> DevboxAsyncExecutionDetailView - client.devboxes.execute_sync(id, \*\*params) -> DevboxExecutionDetailView diff --git a/pyproject.toml b/pyproject.toml index 2879cccda..2b4148401 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "runloop_api_client" -version = "1.4.0" +version = "1.5.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 0d6b7e275..cfd147a9e 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.4.0" # x-release-please-version +__version__ = "1.5.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 ea563fa59..524e0cc74 100644 --- a/src/runloop_api_client/resources/devboxes/devboxes.py +++ b/src/runloop_api_client/resources/devboxes/devboxes.py @@ -28,6 +28,7 @@ devbox_execute_sync_params, devbox_create_tunnel_params, devbox_download_file_params, + devbox_enable_tunnel_params, devbox_execute_async_params, devbox_remove_tunnel_params, devbox_snapshot_disk_params, @@ -99,6 +100,7 @@ ) from ...lib.polling_async import async_poll_until from ...types.devbox_view import DevboxView +from ...types.tunnel_view import TunnelView from ...types.devbox_tunnel_view import DevboxTunnelView from ...types.shared_params.mount import Mount from ...types.devbox_snapshot_view import DevboxSnapshotView @@ -787,6 +789,55 @@ def download_file( cast_to=BinaryAPIResponse, ) + def enable_tunnel( + self, + id: str, + *, + auth_mode: Optional[Literal["open", "authenticated"]] | Omit = omit, + # 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, + idempotency_key: str | None = None, + ) -> TunnelView: + """Create a V2 tunnel for an existing running Devbox. + + Tunnels provide encrypted + URL-based access to the Devbox without exposing internal IDs. The tunnel URL + format is: https://{port}-{tunnel_key}.tunnel.runloop.ai + + Each Devbox can have one tunnel. + + Args: + auth_mode: Authentication mode for the tunnel. Defaults to 'public' if not specified. + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + + idempotency_key: Specify a custom idempotency key for this request + """ + if not id: + raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") + return self._post( + f"/v1/devboxes/{id}/enable_tunnel", + body=maybe_transform({"auth_mode": auth_mode}, devbox_enable_tunnel_params.DevboxEnableTunnelParams), + options=make_request_options( + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + idempotency_key=idempotency_key, + ), + cast_to=TunnelView, + ) + def execute( self, id: str, @@ -2347,6 +2398,57 @@ async def download_file( cast_to=AsyncBinaryAPIResponse, ) + async def enable_tunnel( + self, + id: str, + *, + auth_mode: Optional[Literal["open", "authenticated"]] | Omit = omit, + # 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, + idempotency_key: str | None = None, + ) -> TunnelView: + """Create a V2 tunnel for an existing running Devbox. + + Tunnels provide encrypted + URL-based access to the Devbox without exposing internal IDs. The tunnel URL + format is: https://{port}-{tunnel_key}.tunnel.runloop.ai + + Each Devbox can have one tunnel. + + Args: + auth_mode: Authentication mode for the tunnel. Defaults to 'public' if not specified. + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + + idempotency_key: Specify a custom idempotency key for this request + """ + if not id: + raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") + return await self._post( + f"/v1/devboxes/{id}/enable_tunnel", + body=await async_maybe_transform( + {"auth_mode": auth_mode}, devbox_enable_tunnel_params.DevboxEnableTunnelParams + ), + options=make_request_options( + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + idempotency_key=idempotency_key, + ), + cast_to=TunnelView, + ) + async def execute( self, id: str, @@ -3293,6 +3395,9 @@ def __init__(self, devboxes: DevboxesResource) -> None: devboxes.download_file, BinaryAPIResponse, ) + self.enable_tunnel = to_raw_response_wrapper( + devboxes.enable_tunnel, + ) self.execute = to_raw_response_wrapper( devboxes.execute, ) @@ -3395,6 +3500,9 @@ def __init__(self, devboxes: AsyncDevboxesResource) -> None: devboxes.download_file, AsyncBinaryAPIResponse, ) + self.enable_tunnel = async_to_raw_response_wrapper( + devboxes.enable_tunnel, + ) self.execute = async_to_raw_response_wrapper( devboxes.execute, ) @@ -3497,6 +3605,9 @@ def __init__(self, devboxes: DevboxesResource) -> None: devboxes.download_file, StreamedBinaryAPIResponse, ) + self.enable_tunnel = to_streamed_response_wrapper( + devboxes.enable_tunnel, + ) self.execute = to_streamed_response_wrapper( devboxes.execute, ) @@ -3599,6 +3710,9 @@ def __init__(self, devboxes: AsyncDevboxesResource) -> None: devboxes.download_file, AsyncStreamedBinaryAPIResponse, ) + self.enable_tunnel = async_to_streamed_response_wrapper( + devboxes.enable_tunnel, + ) self.execute = async_to_streamed_response_wrapper( devboxes.execute, ) diff --git a/src/runloop_api_client/types/__init__.py b/src/runloop_api_client/types/__init__.py index be02c8312..8fb72317f 100644 --- a/src/runloop_api_client/types/__init__.py +++ b/src/runloop_api_client/types/__init__.py @@ -16,6 +16,7 @@ from .devbox_view import DevboxView as DevboxView from .object_view import ObjectView as ObjectView from .secret_view import SecretView as SecretView +from .tunnel_view import TunnelView as TunnelView from .input_context import InputContext as InputContext from .scenario_view import ScenarioView as ScenarioView from .benchmark_view import BenchmarkView as BenchmarkView @@ -91,6 +92,7 @@ from .benchmark_job_create_params import BenchmarkJobCreateParams as BenchmarkJobCreateParams from .devbox_create_tunnel_params import DevboxCreateTunnelParams as DevboxCreateTunnelParams from .devbox_download_file_params import DevboxDownloadFileParams as DevboxDownloadFileParams +from .devbox_enable_tunnel_params import DevboxEnableTunnelParams as DevboxEnableTunnelParams from .devbox_execute_async_params import DevboxExecuteAsyncParams as DevboxExecuteAsyncParams from .devbox_remove_tunnel_params import DevboxRemoveTunnelParams as DevboxRemoveTunnelParams from .devbox_snapshot_disk_params import DevboxSnapshotDiskParams as DevboxSnapshotDiskParams diff --git a/src/runloop_api_client/types/devbox_enable_tunnel_params.py b/src/runloop_api_client/types/devbox_enable_tunnel_params.py new file mode 100644 index 000000000..9ccc53e75 --- /dev/null +++ b/src/runloop_api_client/types/devbox_enable_tunnel_params.py @@ -0,0 +1,13 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from typing import Optional +from typing_extensions import Literal, TypedDict + +__all__ = ["DevboxEnableTunnelParams"] + + +class DevboxEnableTunnelParams(TypedDict, total=False): + auth_mode: Optional[Literal["open", "authenticated"]] + """Authentication mode for the tunnel. Defaults to 'public' if not specified.""" diff --git a/src/runloop_api_client/types/devbox_view.py b/src/runloop_api_client/types/devbox_view.py index 50422db1f..68a96dbd7 100644 --- a/src/runloop_api_client/types/devbox_view.py +++ b/src/runloop_api_client/types/devbox_view.py @@ -4,9 +4,10 @@ from typing_extensions import Literal from .._models import BaseModel +from .tunnel_view import TunnelView from .shared.launch_parameters import LaunchParameters -__all__ = ["DevboxView", "StateTransition", "GatewaySpecs", "Tunnel"] +__all__ = ["DevboxView", "StateTransition", "GatewaySpecs"] class StateTransition(BaseModel): @@ -38,30 +39,6 @@ class GatewaySpecs(BaseModel): """The ID of the secret containing the credential.""" -class Tunnel(BaseModel): - """ - V2 tunnel information if a tunnel was created at launch time or via the createTunnel API. - """ - - auth_mode: Literal["public_", "authenticated"] - """The authentication mode for the tunnel.""" - - create_time_ms: int - """Creation time of the tunnel (Unix timestamp milliseconds).""" - - tunnel_key: str - """The encrypted tunnel key used to construct the tunnel URL. - - URL format: https://{port}-{tunnel_key}.tunnel.runloop.{domain} - """ - - auth_token: Optional[str] = None - """Bearer token for tunnel authentication. - - Only present when auth_mode is 'authenticated'. - """ - - class DevboxView(BaseModel): """A Devbox represents a virtual development environment. @@ -137,7 +114,7 @@ class DevboxView(BaseModel): Snapshot. """ - tunnel: Optional[Tunnel] = None + tunnel: Optional[TunnelView] = None """ V2 tunnel information if a tunnel was created at launch time or via the createTunnel API. diff --git a/src/runloop_api_client/types/tunnel_view.py b/src/runloop_api_client/types/tunnel_view.py new file mode 100644 index 000000000..beb43daf9 --- /dev/null +++ b/src/runloop_api_client/types/tunnel_view.py @@ -0,0 +1,34 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from typing import Optional +from typing_extensions import Literal + +from .._models import BaseModel + +__all__ = ["TunnelView"] + + +class TunnelView(BaseModel): + """A V2 tunnel provides secure HTTP access to services running on a Devbox. + + Tunnels allow external clients to reach web servers, APIs, or other HTTP services running inside a Devbox without requiring direct network access. Each tunnel is uniquely identified by an encrypted tunnel_key and can be configured for either open (public) or authenticated access. + Usage: https://{port}-{tunnel_key}.tunnel.runloop.ai + """ + + auth_mode: Literal["public_", "authenticated"] + """The authentication mode for the tunnel.""" + + create_time_ms: int + """Creation time of the tunnel (Unix timestamp milliseconds).""" + + tunnel_key: str + """The encrypted tunnel key used to construct the tunnel URL. + + URL format: https://{port}-{tunnel_key}.tunnel.runloop.{domain} + """ + + auth_token: Optional[str] = None + """Bearer token for tunnel authentication. + + Only present when auth_mode is 'authenticated'. + """ diff --git a/tests/api_resources/test_devboxes.py b/tests/api_resources/test_devboxes.py index 8af777181..fcfc5c1de 100644 --- a/tests/api_resources/test_devboxes.py +++ b/tests/api_resources/test_devboxes.py @@ -14,6 +14,7 @@ from runloop_api_client import Runloop, AsyncRunloop from runloop_api_client.types import ( DevboxView, + TunnelView, DevboxTunnelView, DevboxSnapshotView, DevboxExecutionDetailView, @@ -427,6 +428,52 @@ def test_path_params_download_file(self, client: Runloop) -> None: path="path", ) + @parametrize + def test_method_enable_tunnel(self, client: Runloop) -> None: + devbox = client.devboxes.enable_tunnel( + id="id", + ) + assert_matches_type(TunnelView, devbox, path=["response"]) + + @parametrize + def test_method_enable_tunnel_with_all_params(self, client: Runloop) -> None: + devbox = client.devboxes.enable_tunnel( + id="id", + auth_mode="open", + ) + assert_matches_type(TunnelView, devbox, path=["response"]) + + @parametrize + def test_raw_response_enable_tunnel(self, client: Runloop) -> None: + response = client.devboxes.with_raw_response.enable_tunnel( + id="id", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + devbox = response.parse() + assert_matches_type(TunnelView, devbox, path=["response"]) + + @parametrize + def test_streaming_response_enable_tunnel(self, client: Runloop) -> None: + with client.devboxes.with_streaming_response.enable_tunnel( + id="id", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + devbox = response.parse() + assert_matches_type(TunnelView, devbox, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @parametrize + def test_path_params_enable_tunnel(self, client: Runloop) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): + client.devboxes.with_raw_response.enable_tunnel( + id="", + ) + @parametrize def test_method_execute(self, client: Runloop) -> None: devbox = client.devboxes.execute( @@ -1993,6 +2040,52 @@ async def test_path_params_download_file(self, async_client: AsyncRunloop) -> No path="path", ) + @parametrize + async def test_method_enable_tunnel(self, async_client: AsyncRunloop) -> None: + devbox = await async_client.devboxes.enable_tunnel( + id="id", + ) + assert_matches_type(TunnelView, devbox, path=["response"]) + + @parametrize + async def test_method_enable_tunnel_with_all_params(self, async_client: AsyncRunloop) -> None: + devbox = await async_client.devboxes.enable_tunnel( + id="id", + auth_mode="open", + ) + assert_matches_type(TunnelView, devbox, path=["response"]) + + @parametrize + async def test_raw_response_enable_tunnel(self, async_client: AsyncRunloop) -> None: + response = await async_client.devboxes.with_raw_response.enable_tunnel( + id="id", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + devbox = await response.parse() + assert_matches_type(TunnelView, devbox, path=["response"]) + + @parametrize + async def test_streaming_response_enable_tunnel(self, async_client: AsyncRunloop) -> None: + async with async_client.devboxes.with_streaming_response.enable_tunnel( + id="id", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + devbox = await response.parse() + assert_matches_type(TunnelView, devbox, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @parametrize + async def test_path_params_enable_tunnel(self, async_client: AsyncRunloop) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): + await async_client.devboxes.with_raw_response.enable_tunnel( + id="", + ) + @parametrize async def test_method_execute(self, async_client: AsyncRunloop) -> None: devbox = await async_client.devboxes.execute(