From 840addd8884fd93ede0c9bbc2a16cf399d6372d0 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Fri, 30 Jan 2026 03:54:01 +0000 Subject: [PATCH 1/7] feat(client): add custom JSON encoder for extended type support --- src/runloop_api_client/_base_client.py | 7 +- src/runloop_api_client/_compat.py | 6 +- src/runloop_api_client/_utils/_json.py | 35 +++++++ tests/test_utils/test_json.py | 126 +++++++++++++++++++++++++ 4 files changed, 169 insertions(+), 5 deletions(-) create mode 100644 src/runloop_api_client/_utils/_json.py create mode 100644 tests/test_utils/test_json.py diff --git a/src/runloop_api_client/_base_client.py b/src/runloop_api_client/_base_client.py index bd2a51553..945a6d520 100644 --- a/src/runloop_api_client/_base_client.py +++ b/src/runloop_api_client/_base_client.py @@ -86,6 +86,7 @@ APIConnectionError, APIResponseValidationError, ) +from ._utils._json import openapi_dumps log: logging.Logger = logging.getLogger(__name__) @@ -554,8 +555,10 @@ def _build_request( kwargs["content"] = options.content elif isinstance(json_data, bytes): kwargs["content"] = json_data - else: - kwargs["json"] = json_data if is_given(json_data) else None + elif not files: + # Don't set content when JSON is sent as multipart/form-data, + # since httpx's content param overrides other body arguments + kwargs["content"] = openapi_dumps(json_data) if is_given(json_data) and json_data is not None else None kwargs["files"] = files else: headers.pop("Content-Type", None) diff --git a/src/runloop_api_client/_compat.py b/src/runloop_api_client/_compat.py index bdef67f04..786ff42ad 100644 --- a/src/runloop_api_client/_compat.py +++ b/src/runloop_api_client/_compat.py @@ -139,6 +139,7 @@ def model_dump( exclude_defaults: bool = False, warnings: bool = True, mode: Literal["json", "python"] = "python", + by_alias: bool | None = None, ) -> dict[str, Any]: if (not PYDANTIC_V1) or hasattr(model, "model_dump"): return model.model_dump( @@ -148,13 +149,12 @@ def model_dump( exclude_defaults=exclude_defaults, # warnings are not supported in Pydantic v1 warnings=True if PYDANTIC_V1 else warnings, + by_alias=by_alias, ) return cast( "dict[str, Any]", model.dict( # pyright: ignore[reportDeprecated, reportUnnecessaryCast] - exclude=exclude, - exclude_unset=exclude_unset, - exclude_defaults=exclude_defaults, + exclude=exclude, exclude_unset=exclude_unset, exclude_defaults=exclude_defaults, by_alias=bool(by_alias) ), ) diff --git a/src/runloop_api_client/_utils/_json.py b/src/runloop_api_client/_utils/_json.py new file mode 100644 index 000000000..60584214a --- /dev/null +++ b/src/runloop_api_client/_utils/_json.py @@ -0,0 +1,35 @@ +import json +from typing import Any +from datetime import datetime +from typing_extensions import override + +import pydantic + +from .._compat import model_dump + + +def openapi_dumps(obj: Any) -> bytes: + """ + Serialize an object to UTF-8 encoded JSON bytes. + + Extends the standard json.dumps with support for additional types + commonly used in the SDK, such as `datetime`, `pydantic.BaseModel`, etc. + """ + return json.dumps( + obj, + cls=_CustomEncoder, + # Uses the same defaults as httpx's JSON serialization + ensure_ascii=False, + separators=(",", ":"), + allow_nan=False, + ).encode() + + +class _CustomEncoder(json.JSONEncoder): + @override + def default(self, o: Any) -> Any: + if isinstance(o, datetime): + return o.isoformat() + if isinstance(o, pydantic.BaseModel): + return model_dump(o, exclude_unset=True, mode="json", by_alias=True) + return super().default(o) diff --git a/tests/test_utils/test_json.py b/tests/test_utils/test_json.py new file mode 100644 index 000000000..7e9390783 --- /dev/null +++ b/tests/test_utils/test_json.py @@ -0,0 +1,126 @@ +from __future__ import annotations + +import datetime +from typing import Union + +import pydantic + +from runloop_api_client import _compat +from runloop_api_client._utils._json import openapi_dumps + + +class TestOpenapiDumps: + def test_basic(self) -> None: + data = {"key": "value", "number": 42} + json_bytes = openapi_dumps(data) + assert json_bytes == b'{"key":"value","number":42}' + + def test_datetime_serialization(self) -> None: + dt = datetime.datetime(2023, 1, 1, 12, 0, 0) + data = {"datetime": dt} + json_bytes = openapi_dumps(data) + assert json_bytes == b'{"datetime":"2023-01-01T12:00:00"}' + + def test_pydantic_model_serialization(self) -> None: + class User(pydantic.BaseModel): + first_name: str + last_name: str + age: int + + model_instance = User(first_name="John", last_name="Kramer", age=83) + data = {"model": model_instance} + json_bytes = openapi_dumps(data) + assert json_bytes == b'{"model":{"first_name":"John","last_name":"Kramer","age":83}}' + + def test_pydantic_model_with_default_values(self) -> None: + class User(pydantic.BaseModel): + name: str + role: str = "user" + active: bool = True + score: int = 0 + + model_instance = User(name="Alice") + data = {"model": model_instance} + json_bytes = openapi_dumps(data) + assert json_bytes == b'{"model":{"name":"Alice"}}' + + def test_pydantic_model_with_default_values_overridden(self) -> None: + class User(pydantic.BaseModel): + name: str + role: str = "user" + active: bool = True + + model_instance = User(name="Bob", role="admin", active=False) + data = {"model": model_instance} + json_bytes = openapi_dumps(data) + assert json_bytes == b'{"model":{"name":"Bob","role":"admin","active":false}}' + + def test_pydantic_model_with_alias(self) -> None: + class User(pydantic.BaseModel): + first_name: str = pydantic.Field(alias="firstName") + last_name: str = pydantic.Field(alias="lastName") + + model_instance = User(firstName="John", lastName="Doe") + data = {"model": model_instance} + json_bytes = openapi_dumps(data) + assert json_bytes == b'{"model":{"firstName":"John","lastName":"Doe"}}' + + def test_pydantic_model_with_alias_and_default(self) -> None: + class User(pydantic.BaseModel): + user_name: str = pydantic.Field(alias="userName") + user_role: str = pydantic.Field(default="member", alias="userRole") + is_active: bool = pydantic.Field(default=True, alias="isActive") + + model_instance = User(userName="charlie") + data = {"model": model_instance} + json_bytes = openapi_dumps(data) + assert json_bytes == b'{"model":{"userName":"charlie"}}' + + model_with_overrides = User(userName="diana", userRole="admin", isActive=False) + data = {"model": model_with_overrides} + json_bytes = openapi_dumps(data) + assert json_bytes == b'{"model":{"userName":"diana","userRole":"admin","isActive":false}}' + + def test_pydantic_model_with_nested_models_and_defaults(self) -> None: + class Address(pydantic.BaseModel): + street: str + city: str = "Unknown" + + class User(pydantic.BaseModel): + name: str + address: Address + verified: bool = False + + if _compat.PYDANTIC_V1: + # to handle forward references in Pydantic v1 + User.update_forward_refs(**locals()) # type: ignore[reportDeprecated] + + address = Address(street="123 Main St") + user = User(name="Diana", address=address) + data = {"user": user} + json_bytes = openapi_dumps(data) + assert json_bytes == b'{"user":{"name":"Diana","address":{"street":"123 Main St"}}}' + + address_with_city = Address(street="456 Oak Ave", city="Boston") + user_verified = User(name="Eve", address=address_with_city, verified=True) + data = {"user": user_verified} + json_bytes = openapi_dumps(data) + assert ( + json_bytes == b'{"user":{"name":"Eve","address":{"street":"456 Oak Ave","city":"Boston"},"verified":true}}' + ) + + def test_pydantic_model_with_optional_fields(self) -> None: + class User(pydantic.BaseModel): + name: str + email: Union[str, None] + phone: Union[str, None] + + model_with_none = User(name="Eve", email=None, phone=None) + data = {"model": model_with_none} + json_bytes = openapi_dumps(data) + assert json_bytes == b'{"model":{"name":"Eve","email":null,"phone":null}}' + + model_with_values = User(name="Frank", email="frank@example.com", phone=None) + data = {"model": model_with_values} + json_bytes = openapi_dumps(data) + assert json_bytes == b'{"model":{"name":"Frank","email":"frank@example.com","phone":null}}' From 2527bb7714f9ce9114964286bfbb35582ceaa976 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Fri, 30 Jan 2026 04:27:13 +0000 Subject: [PATCH 2/7] feat(devbox): add gateway routes (#7212) --- .stats.yml | 8 +- api.md | 21 + src/runloop_api_client/_client.py | 38 + src/runloop_api_client/pagination.py | 75 ++ src/runloop_api_client/resources/__init__.py | 14 + .../resources/devboxes/devboxes.py | 16 + .../resources/gateway_configs.py | 658 ++++++++++++++++++ src/runloop_api_client/types/__init__.py | 5 + .../types/devbox_create_params.py | 25 +- src/runloop_api_client/types/devbox_view.py | 16 +- .../types/gateway_config_create_params.py | 41 ++ .../types/gateway_config_list_params.py | 21 + .../types/gateway_config_list_view.py | 21 + .../types/gateway_config_update_params.py | 32 + .../types/gateway_config_view.py | 47 ++ tests/api_resources/test_devboxes.py | 12 + tests/api_resources/test_gateway_configs.py | 453 ++++++++++++ 17 files changed, 1496 insertions(+), 7 deletions(-) create mode 100644 src/runloop_api_client/resources/gateway_configs.py create mode 100644 src/runloop_api_client/types/gateway_config_create_params.py create mode 100644 src/runloop_api_client/types/gateway_config_list_params.py create mode 100644 src/runloop_api_client/types/gateway_config_list_view.py create mode 100644 src/runloop_api_client/types/gateway_config_update_params.py create mode 100644 src/runloop_api_client/types/gateway_config_view.py create mode 100644 tests/api_resources/test_gateway_configs.py diff --git a/.stats.yml b/.stats.yml index f28b394ab..cc6ae0f60 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ -configured_endpoints: 106 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/runloop-ai%2Frunloop-63dab7833d6670810c4f4882df560ebbfe2de8e8e1a98d51422368607b5335ae.yml -openapi_spec_hash: ebb5068064f7469f9239b18a51a6fe44 -config_hash: fd168de77f219e46a1427bbec2eecfb9 +configured_endpoints: 111 +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/runloop-ai%2Frunloop-294ebcf6886a5ddbebeaa70923b7674757459e73ef08cd2fbc63fb70e1932eac.yml +openapi_spec_hash: 3a2a14e7ddd646f53d9f21bef2e84ec5 +config_hash: 22f65246be4646c23dde9f69f51252e7 diff --git a/api.md b/api.md index 45d93e973..44ba928b2 100644 --- a/api.md +++ b/api.md @@ -397,3 +397,24 @@ Methods: - client.network_policies.update(id, \*\*params) -> NetworkPolicyView - client.network_policies.list(\*\*params) -> SyncNetworkPoliciesCursorIDPage[NetworkPolicyView] - client.network_policies.delete(id) -> NetworkPolicyView + +# GatewayConfigs + +Types: + +```python +from runloop_api_client.types import ( + GatewayConfigCreateParameters, + GatewayConfigListView, + GatewayConfigUpdateParameters, + GatewayConfigView, +) +``` + +Methods: + +- client.gateway_configs.create(\*\*params) -> GatewayConfigView +- client.gateway_configs.retrieve(id) -> GatewayConfigView +- client.gateway_configs.update(id, \*\*params) -> GatewayConfigView +- client.gateway_configs.list(\*\*params) -> SyncGatewayConfigsCursorIDPage[GatewayConfigView] +- client.gateway_configs.delete(id) -> GatewayConfigView diff --git a/src/runloop_api_client/_client.py b/src/runloop_api_client/_client.py index 3b469e7ec..32561bc0f 100644 --- a/src/runloop_api_client/_client.py +++ b/src/runloop_api_client/_client.py @@ -42,6 +42,7 @@ repositories, benchmark_jobs, benchmark_runs, + gateway_configs, network_policies, ) from .resources.agents import AgentsResource, AsyncAgentsResource @@ -52,6 +53,7 @@ from .resources.repositories import RepositoriesResource, AsyncRepositoriesResource from .resources.benchmark_jobs import BenchmarkJobsResource, AsyncBenchmarkJobsResource from .resources.benchmark_runs import BenchmarkRunsResource, AsyncBenchmarkRunsResource + from .resources.gateway_configs import GatewayConfigsResource, AsyncGatewayConfigsResource from .resources.network_policies import NetworkPoliciesResource, AsyncNetworkPoliciesResource from .resources.devboxes.devboxes import DevboxesResource, AsyncDevboxesResource from .resources.scenarios.scenarios import ScenariosResource, AsyncScenariosResource @@ -182,6 +184,12 @@ def network_policies(self) -> NetworkPoliciesResource: return NetworkPoliciesResource(self) + @cached_property + def gateway_configs(self) -> GatewayConfigsResource: + from .resources.gateway_configs import GatewayConfigsResource + + return GatewayConfigsResource(self) + @cached_property def with_raw_response(self) -> RunloopWithRawResponse: return RunloopWithRawResponse(self) @@ -418,6 +426,12 @@ def network_policies(self) -> AsyncNetworkPoliciesResource: return AsyncNetworkPoliciesResource(self) + @cached_property + def gateway_configs(self) -> AsyncGatewayConfigsResource: + from .resources.gateway_configs import AsyncGatewayConfigsResource + + return AsyncGatewayConfigsResource(self) + @cached_property def with_raw_response(self) -> AsyncRunloopWithRawResponse: return AsyncRunloopWithRawResponse(self) @@ -603,6 +617,12 @@ def network_policies(self) -> network_policies.NetworkPoliciesResourceWithRawRes return NetworkPoliciesResourceWithRawResponse(self._client.network_policies) + @cached_property + def gateway_configs(self) -> gateway_configs.GatewayConfigsResourceWithRawResponse: + from .resources.gateway_configs import GatewayConfigsResourceWithRawResponse + + return GatewayConfigsResourceWithRawResponse(self._client.gateway_configs) + class AsyncRunloopWithRawResponse: _client: AsyncRunloop @@ -676,6 +696,12 @@ def network_policies(self) -> network_policies.AsyncNetworkPoliciesResourceWithR return AsyncNetworkPoliciesResourceWithRawResponse(self._client.network_policies) + @cached_property + def gateway_configs(self) -> gateway_configs.AsyncGatewayConfigsResourceWithRawResponse: + from .resources.gateway_configs import AsyncGatewayConfigsResourceWithRawResponse + + return AsyncGatewayConfigsResourceWithRawResponse(self._client.gateway_configs) + class RunloopWithStreamedResponse: _client: Runloop @@ -749,6 +775,12 @@ def network_policies(self) -> network_policies.NetworkPoliciesResourceWithStream return NetworkPoliciesResourceWithStreamingResponse(self._client.network_policies) + @cached_property + def gateway_configs(self) -> gateway_configs.GatewayConfigsResourceWithStreamingResponse: + from .resources.gateway_configs import GatewayConfigsResourceWithStreamingResponse + + return GatewayConfigsResourceWithStreamingResponse(self._client.gateway_configs) + class AsyncRunloopWithStreamedResponse: _client: AsyncRunloop @@ -822,6 +854,12 @@ def network_policies(self) -> network_policies.AsyncNetworkPoliciesResourceWithS return AsyncNetworkPoliciesResourceWithStreamingResponse(self._client.network_policies) + @cached_property + def gateway_configs(self) -> gateway_configs.AsyncGatewayConfigsResourceWithStreamingResponse: + from .resources.gateway_configs import AsyncGatewayConfigsResourceWithStreamingResponse + + return AsyncGatewayConfigsResourceWithStreamingResponse(self._client.gateway_configs) + Client = Runloop diff --git a/src/runloop_api_client/pagination.py b/src/runloop_api_client/pagination.py index c6e696214..e937259d7 100644 --- a/src/runloop_api_client/pagination.py +++ b/src/runloop_api_client/pagination.py @@ -30,6 +30,8 @@ "AsyncObjectsCursorIDPage", "SyncNetworkPoliciesCursorIDPage", "AsyncNetworkPoliciesCursorIDPage", + "SyncGatewayConfigsCursorIDPage", + "AsyncGatewayConfigsCursorIDPage", ] _T = TypeVar("_T") @@ -95,6 +97,11 @@ class NetworkPoliciesCursorIDPageItem(Protocol): id: str +@runtime_checkable +class GatewayConfigsCursorIDPageItem(Protocol): + id: str + + class SyncBlueprintsCursorIDPage(BaseSyncPage[_T], BasePage[_T], Generic[_T]): blueprints: List[_T] has_more: Optional[bool] = None @@ -909,3 +916,71 @@ def next_page_info(self) -> Optional[PageInfo]: return None return PageInfo(params={"starting_after": item.id}) + + +class SyncGatewayConfigsCursorIDPage(BaseSyncPage[_T], BasePage[_T], Generic[_T]): + gateway_configs: List[_T] + has_more: Optional[bool] = None + total_count: Optional[int] = None + + @override + def _get_page_items(self) -> List[_T]: + gateway_configs = self.gateway_configs + if not gateway_configs: + return [] + return gateway_configs + + @override + def has_next_page(self) -> bool: + has_more = self.has_more + if has_more is not None and has_more is False: + return False + + return super().has_next_page() + + @override + def next_page_info(self) -> Optional[PageInfo]: + gateway_configs = self.gateway_configs + if not gateway_configs: + return None + + item = cast(Any, gateway_configs[-1]) + if not isinstance(item, GatewayConfigsCursorIDPageItem) or item.id is None: # pyright: ignore[reportUnnecessaryComparison] + # TODO emit warning log + return None + + return PageInfo(params={"starting_after": item.id}) + + +class AsyncGatewayConfigsCursorIDPage(BaseAsyncPage[_T], BasePage[_T], Generic[_T]): + gateway_configs: List[_T] + has_more: Optional[bool] = None + total_count: Optional[int] = None + + @override + def _get_page_items(self) -> List[_T]: + gateway_configs = self.gateway_configs + if not gateway_configs: + return [] + return gateway_configs + + @override + def has_next_page(self) -> bool: + has_more = self.has_more + if has_more is not None and has_more is False: + return False + + return super().has_next_page() + + @override + def next_page_info(self) -> Optional[PageInfo]: + gateway_configs = self.gateway_configs + if not gateway_configs: + return None + + item = cast(Any, gateway_configs[-1]) + if not isinstance(item, GatewayConfigsCursorIDPageItem) or item.id is None: # pyright: ignore[reportUnnecessaryComparison] + # TODO emit warning log + return None + + return PageInfo(params={"starting_after": item.id}) diff --git a/src/runloop_api_client/resources/__init__.py b/src/runloop_api_client/resources/__init__.py index 325158535..877822444 100644 --- a/src/runloop_api_client/resources/__init__.py +++ b/src/runloop_api_client/resources/__init__.py @@ -80,6 +80,14 @@ BenchmarkRunsResourceWithStreamingResponse, AsyncBenchmarkRunsResourceWithStreamingResponse, ) +from .gateway_configs import ( + GatewayConfigsResource, + AsyncGatewayConfigsResource, + GatewayConfigsResourceWithRawResponse, + AsyncGatewayConfigsResourceWithRawResponse, + GatewayConfigsResourceWithStreamingResponse, + AsyncGatewayConfigsResourceWithStreamingResponse, +) from .network_policies import ( NetworkPoliciesResource, AsyncNetworkPoliciesResource, @@ -156,4 +164,10 @@ "AsyncNetworkPoliciesResourceWithRawResponse", "NetworkPoliciesResourceWithStreamingResponse", "AsyncNetworkPoliciesResourceWithStreamingResponse", + "GatewayConfigsResource", + "AsyncGatewayConfigsResource", + "GatewayConfigsResourceWithRawResponse", + "AsyncGatewayConfigsResourceWithRawResponse", + "GatewayConfigsResourceWithStreamingResponse", + "AsyncGatewayConfigsResourceWithStreamingResponse", ] diff --git a/src/runloop_api_client/resources/devboxes/devboxes.py b/src/runloop_api_client/resources/devboxes/devboxes.py index 429918891..544dfd7d0 100644 --- a/src/runloop_api_client/resources/devboxes/devboxes.py +++ b/src/runloop_api_client/resources/devboxes/devboxes.py @@ -185,6 +185,7 @@ def create( entrypoint: Optional[str] | Omit = omit, environment_variables: Optional[Dict[str, str]] | Omit = omit, file_mounts: Optional[Dict[str, str]] | Omit = omit, + gateways: Optional[Dict[str, devbox_create_params.Gateways]] | Omit = omit, launch_parameters: Optional[LaunchParameters] | Omit = omit, metadata: Optional[Dict[str, str]] | Omit = omit, mounts: Optional[Iterable[Mount]] | Omit = omit, @@ -227,6 +228,12 @@ def create( file_mounts: Map of paths and file contents to write before setup. Use mounts instead. + gateways: [Beta] (Optional) Gateway specifications for credential proxying. Map key is the + environment variable prefix (e.g., 'GWS_ANTHROPIC'). The gateway will proxy + requests to external APIs using the specified credential without exposing the + real API key. Example: {'GWS_ANTHROPIC': {'gateway': 'anthropic', 'secret': + 'my_claude_key'}} + launch_parameters: Parameters to configure the resources and launch time behavior of the Devbox. metadata: User defined metadata to attach to the devbox for organization. @@ -265,6 +272,7 @@ def create( "entrypoint": entrypoint, "environment_variables": environment_variables, "file_mounts": file_mounts, + "gateways": gateways, "launch_parameters": launch_parameters, "metadata": metadata, "mounts": mounts, @@ -1723,6 +1731,7 @@ async def create( entrypoint: Optional[str] | Omit = omit, environment_variables: Optional[Dict[str, str]] | Omit = omit, file_mounts: Optional[Dict[str, str]] | Omit = omit, + gateways: Optional[Dict[str, devbox_create_params.Gateways]] | Omit = omit, launch_parameters: Optional[LaunchParameters] | Omit = omit, metadata: Optional[Dict[str, str]] | Omit = omit, mounts: Optional[Iterable[Mount]] | Omit = omit, @@ -1765,6 +1774,12 @@ async def create( file_mounts: Map of paths and file contents to write before setup. Use mounts instead. + gateways: [Beta] (Optional) Gateway specifications for credential proxying. Map key is the + environment variable prefix (e.g., 'GWS_ANTHROPIC'). The gateway will proxy + requests to external APIs using the specified credential without exposing the + real API key. Example: {'GWS_ANTHROPIC': {'gateway': 'anthropic', 'secret': + 'my_claude_key'}} + launch_parameters: Parameters to configure the resources and launch time behavior of the Devbox. metadata: User defined metadata to attach to the devbox for organization. @@ -1803,6 +1818,7 @@ async def create( "entrypoint": entrypoint, "environment_variables": environment_variables, "file_mounts": file_mounts, + "gateways": gateways, "launch_parameters": launch_parameters, "metadata": metadata, "mounts": mounts, diff --git a/src/runloop_api_client/resources/gateway_configs.py b/src/runloop_api_client/resources/gateway_configs.py new file mode 100644 index 000000000..38c921fcd --- /dev/null +++ b/src/runloop_api_client/resources/gateway_configs.py @@ -0,0 +1,658 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from typing import Optional + +import httpx + +from ..types import gateway_config_list_params, gateway_config_create_params, gateway_config_update_params +from .._types import Body, Omit, Query, Headers, NotGiven, omit, not_given +from .._utils import maybe_transform, async_maybe_transform +from .._compat import cached_property +from .._resource import SyncAPIResource, AsyncAPIResource +from .._response import ( + to_raw_response_wrapper, + to_streamed_response_wrapper, + async_to_raw_response_wrapper, + async_to_streamed_response_wrapper, +) +from ..pagination import SyncGatewayConfigsCursorIDPage, AsyncGatewayConfigsCursorIDPage +from .._base_client import AsyncPaginator, make_request_options +from ..types.gateway_config_view import GatewayConfigView + +__all__ = ["GatewayConfigsResource", "AsyncGatewayConfigsResource"] + + +class GatewayConfigsResource(SyncAPIResource): + @cached_property + def with_raw_response(self) -> GatewayConfigsResourceWithRawResponse: + """ + This property can be used as a prefix for any HTTP method call to return + the raw response object instead of the parsed content. + + For more information, see https://www.github.com/runloopai/api-client-python#accessing-raw-response-data-eg-headers + """ + return GatewayConfigsResourceWithRawResponse(self) + + @cached_property + def with_streaming_response(self) -> GatewayConfigsResourceWithStreamingResponse: + """ + An alternative to `.with_raw_response` that doesn't eagerly read the response body. + + For more information, see https://www.github.com/runloopai/api-client-python#with_streaming_response + """ + return GatewayConfigsResourceWithStreamingResponse(self) + + def create( + self, + *, + auth_mechanism: gateway_config_create_params.AuthMechanism, + endpoint: str, + name: str, + description: Optional[str] | 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, + ) -> GatewayConfigView: + """ + [Beta] Create a new GatewayConfig to proxy API requests through the credential + gateway. The config specifies the target endpoint and how credentials should be + applied. + + Args: + auth_mechanism: How credentials should be applied to proxied requests. Specify the type + ('header', 'bearer') and optional key field. + + endpoint: The target endpoint URL (e.g., 'https://api.anthropic.com'). + + name: The human-readable name for the GatewayConfig. Must be unique within your + account. + + description: Optional description for this gateway configuration. + + 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 + """ + return self._post( + "/v1/gateway-configs", + body=maybe_transform( + { + "auth_mechanism": auth_mechanism, + "endpoint": endpoint, + "name": name, + "description": description, + }, + gateway_config_create_params.GatewayConfigCreateParams, + ), + options=make_request_options( + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + idempotency_key=idempotency_key, + ), + cast_to=GatewayConfigView, + ) + + def retrieve( + self, + id: str, + *, + # 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, + ) -> GatewayConfigView: + """ + [Beta] Get a specific GatewayConfig by its unique identifier. + + Args: + 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 + """ + if not id: + raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") + return self._get( + f"/v1/gateway-configs/{id}", + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=GatewayConfigView, + ) + + def update( + self, + id: str, + *, + auth_mechanism: Optional[gateway_config_update_params.AuthMechanism] | Omit = omit, + description: Optional[str] | Omit = omit, + endpoint: Optional[str] | Omit = omit, + name: Optional[str] | 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, + ) -> GatewayConfigView: + """[Beta] Update an existing GatewayConfig. + + All fields are optional. + + Args: + auth_mechanism: New authentication mechanism for applying credentials to proxied requests. + + description: New description for this gateway configuration. + + endpoint: New target endpoint URL (e.g., 'https://api.anthropic.com'). + + name: New name for the GatewayConfig. Must be unique within your account. + + 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/gateway-configs/{id}", + body=maybe_transform( + { + "auth_mechanism": auth_mechanism, + "description": description, + "endpoint": endpoint, + "name": name, + }, + gateway_config_update_params.GatewayConfigUpdateParams, + ), + options=make_request_options( + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + idempotency_key=idempotency_key, + ), + cast_to=GatewayConfigView, + ) + + def list( + self, + *, + id: str | Omit = omit, + limit: int | Omit = omit, + name: str | Omit = omit, + starting_after: str | 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, + ) -> SyncGatewayConfigsCursorIDPage[GatewayConfigView]: + """ + [Beta] List all GatewayConfigs for the authenticated account, including + system-provided configs like 'anthropic' and 'openai'. + + Args: + id: Filter by ID. + + limit: The limit of items to return. Default is 20. Max is 5000. + + name: Filter by name (partial match supported). + + starting_after: Load the next page of data starting after the item with the given ID. + + 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 + """ + return self._get_api_list( + "/v1/gateway-configs", + page=SyncGatewayConfigsCursorIDPage[GatewayConfigView], + options=make_request_options( + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + query=maybe_transform( + { + "id": id, + "limit": limit, + "name": name, + "starting_after": starting_after, + }, + gateway_config_list_params.GatewayConfigListParams, + ), + ), + model=GatewayConfigView, + ) + + def delete( + self, + id: str, + *, + # 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, + ) -> GatewayConfigView: + """[Beta] Delete an existing GatewayConfig. + + This action is irreversible. + + Args: + 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/gateway-configs/{id}/delete", + options=make_request_options( + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + idempotency_key=idempotency_key, + ), + cast_to=GatewayConfigView, + ) + + +class AsyncGatewayConfigsResource(AsyncAPIResource): + @cached_property + def with_raw_response(self) -> AsyncGatewayConfigsResourceWithRawResponse: + """ + This property can be used as a prefix for any HTTP method call to return + the raw response object instead of the parsed content. + + For more information, see https://www.github.com/runloopai/api-client-python#accessing-raw-response-data-eg-headers + """ + return AsyncGatewayConfigsResourceWithRawResponse(self) + + @cached_property + def with_streaming_response(self) -> AsyncGatewayConfigsResourceWithStreamingResponse: + """ + An alternative to `.with_raw_response` that doesn't eagerly read the response body. + + For more information, see https://www.github.com/runloopai/api-client-python#with_streaming_response + """ + return AsyncGatewayConfigsResourceWithStreamingResponse(self) + + async def create( + self, + *, + auth_mechanism: gateway_config_create_params.AuthMechanism, + endpoint: str, + name: str, + description: Optional[str] | 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, + ) -> GatewayConfigView: + """ + [Beta] Create a new GatewayConfig to proxy API requests through the credential + gateway. The config specifies the target endpoint and how credentials should be + applied. + + Args: + auth_mechanism: How credentials should be applied to proxied requests. Specify the type + ('header', 'bearer') and optional key field. + + endpoint: The target endpoint URL (e.g., 'https://api.anthropic.com'). + + name: The human-readable name for the GatewayConfig. Must be unique within your + account. + + description: Optional description for this gateway configuration. + + 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 + """ + return await self._post( + "/v1/gateway-configs", + body=await async_maybe_transform( + { + "auth_mechanism": auth_mechanism, + "endpoint": endpoint, + "name": name, + "description": description, + }, + gateway_config_create_params.GatewayConfigCreateParams, + ), + options=make_request_options( + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + idempotency_key=idempotency_key, + ), + cast_to=GatewayConfigView, + ) + + async def retrieve( + self, + id: str, + *, + # 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, + ) -> GatewayConfigView: + """ + [Beta] Get a specific GatewayConfig by its unique identifier. + + Args: + 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 + """ + if not id: + raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") + return await self._get( + f"/v1/gateway-configs/{id}", + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=GatewayConfigView, + ) + + async def update( + self, + id: str, + *, + auth_mechanism: Optional[gateway_config_update_params.AuthMechanism] | Omit = omit, + description: Optional[str] | Omit = omit, + endpoint: Optional[str] | Omit = omit, + name: Optional[str] | 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, + ) -> GatewayConfigView: + """[Beta] Update an existing GatewayConfig. + + All fields are optional. + + Args: + auth_mechanism: New authentication mechanism for applying credentials to proxied requests. + + description: New description for this gateway configuration. + + endpoint: New target endpoint URL (e.g., 'https://api.anthropic.com'). + + name: New name for the GatewayConfig. Must be unique within your account. + + 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/gateway-configs/{id}", + body=await async_maybe_transform( + { + "auth_mechanism": auth_mechanism, + "description": description, + "endpoint": endpoint, + "name": name, + }, + gateway_config_update_params.GatewayConfigUpdateParams, + ), + options=make_request_options( + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + idempotency_key=idempotency_key, + ), + cast_to=GatewayConfigView, + ) + + def list( + self, + *, + id: str | Omit = omit, + limit: int | Omit = omit, + name: str | Omit = omit, + starting_after: str | 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, + ) -> AsyncPaginator[GatewayConfigView, AsyncGatewayConfigsCursorIDPage[GatewayConfigView]]: + """ + [Beta] List all GatewayConfigs for the authenticated account, including + system-provided configs like 'anthropic' and 'openai'. + + Args: + id: Filter by ID. + + limit: The limit of items to return. Default is 20. Max is 5000. + + name: Filter by name (partial match supported). + + starting_after: Load the next page of data starting after the item with the given ID. + + 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 + """ + return self._get_api_list( + "/v1/gateway-configs", + page=AsyncGatewayConfigsCursorIDPage[GatewayConfigView], + options=make_request_options( + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + query=maybe_transform( + { + "id": id, + "limit": limit, + "name": name, + "starting_after": starting_after, + }, + gateway_config_list_params.GatewayConfigListParams, + ), + ), + model=GatewayConfigView, + ) + + async def delete( + self, + id: str, + *, + # 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, + ) -> GatewayConfigView: + """[Beta] Delete an existing GatewayConfig. + + This action is irreversible. + + Args: + 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/gateway-configs/{id}/delete", + options=make_request_options( + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + idempotency_key=idempotency_key, + ), + cast_to=GatewayConfigView, + ) + + +class GatewayConfigsResourceWithRawResponse: + def __init__(self, gateway_configs: GatewayConfigsResource) -> None: + self._gateway_configs = gateway_configs + + self.create = to_raw_response_wrapper( + gateway_configs.create, + ) + self.retrieve = to_raw_response_wrapper( + gateway_configs.retrieve, + ) + self.update = to_raw_response_wrapper( + gateway_configs.update, + ) + self.list = to_raw_response_wrapper( + gateway_configs.list, + ) + self.delete = to_raw_response_wrapper( + gateway_configs.delete, + ) + + +class AsyncGatewayConfigsResourceWithRawResponse: + def __init__(self, gateway_configs: AsyncGatewayConfigsResource) -> None: + self._gateway_configs = gateway_configs + + self.create = async_to_raw_response_wrapper( + gateway_configs.create, + ) + self.retrieve = async_to_raw_response_wrapper( + gateway_configs.retrieve, + ) + self.update = async_to_raw_response_wrapper( + gateway_configs.update, + ) + self.list = async_to_raw_response_wrapper( + gateway_configs.list, + ) + self.delete = async_to_raw_response_wrapper( + gateway_configs.delete, + ) + + +class GatewayConfigsResourceWithStreamingResponse: + def __init__(self, gateway_configs: GatewayConfigsResource) -> None: + self._gateway_configs = gateway_configs + + self.create = to_streamed_response_wrapper( + gateway_configs.create, + ) + self.retrieve = to_streamed_response_wrapper( + gateway_configs.retrieve, + ) + self.update = to_streamed_response_wrapper( + gateway_configs.update, + ) + self.list = to_streamed_response_wrapper( + gateway_configs.list, + ) + self.delete = to_streamed_response_wrapper( + gateway_configs.delete, + ) + + +class AsyncGatewayConfigsResourceWithStreamingResponse: + def __init__(self, gateway_configs: AsyncGatewayConfigsResource) -> None: + self._gateway_configs = gateway_configs + + self.create = async_to_streamed_response_wrapper( + gateway_configs.create, + ) + self.retrieve = async_to_streamed_response_wrapper( + gateway_configs.retrieve, + ) + self.update = async_to_streamed_response_wrapper( + gateway_configs.update, + ) + self.list = async_to_streamed_response_wrapper( + gateway_configs.list, + ) + self.delete = async_to_streamed_response_wrapper( + gateway_configs.delete, + ) diff --git a/src/runloop_api_client/types/__init__.py b/src/runloop_api_client/types/__init__.py index 59130e662..be02c8312 100644 --- a/src/runloop_api_client/types/__init__.py +++ b/src/runloop_api_client/types/__init__.py @@ -37,6 +37,7 @@ from .agent_create_params import AgentCreateParams as AgentCreateParams from .blueprint_build_log import BlueprintBuildLog as BlueprintBuildLog from .blueprint_list_view import BlueprintListView as BlueprintListView +from .gateway_config_view import GatewayConfigView as GatewayConfigView from .input_context_param import InputContextParam as InputContextParam from .network_policy_view import NetworkPolicyView as NetworkPolicyView from .devbox_create_params import DevboxCreateParams as DevboxCreateParams @@ -65,6 +66,7 @@ from .blueprint_create_params import BlueprintCreateParams as BlueprintCreateParams from .inspection_source_param import InspectionSourceParam as InspectionSourceParam from .blueprint_preview_params import BlueprintPreviewParams as BlueprintPreviewParams +from .gateway_config_list_view import GatewayConfigListView as GatewayConfigListView from .network_policy_list_view import NetworkPolicyListView as NetworkPolicyListView from .object_download_url_view import ObjectDownloadURLView as ObjectDownloadURLView from .repository_create_params import RepositoryCreateParams as RepositoryCreateParams @@ -81,6 +83,7 @@ from .benchmark_start_run_params import BenchmarkStartRunParams as BenchmarkStartRunParams from .blueprint_build_parameters import BlueprintBuildParameters as BlueprintBuildParameters from .devbox_execute_sync_params import DevboxExecuteSyncParams as DevboxExecuteSyncParams +from .gateway_config_list_params import GatewayConfigListParams as GatewayConfigListParams from .input_context_update_param import InputContextUpdateParam as InputContextUpdateParam from .network_policy_list_params import NetworkPolicyListParams as NetworkPolicyListParams from .repository_connection_view import RepositoryConnectionView as RepositoryConnectionView @@ -96,6 +99,8 @@ from .benchmark_list_public_params import BenchmarkListPublicParams as BenchmarkListPublicParams from .blueprint_list_public_params import BlueprintListPublicParams as BlueprintListPublicParams from .devbox_execution_detail_view import DevboxExecutionDetailView as DevboxExecutionDetailView +from .gateway_config_create_params import GatewayConfigCreateParams as GatewayConfigCreateParams +from .gateway_config_update_params import GatewayConfigUpdateParams as GatewayConfigUpdateParams from .network_policy_create_params import NetworkPolicyCreateParams as NetworkPolicyCreateParams from .network_policy_update_params import NetworkPolicyUpdateParams as NetworkPolicyUpdateParams from .scoring_contract_result_view import ScoringContractResultView as ScoringContractResultView diff --git a/src/runloop_api_client/types/devbox_create_params.py b/src/runloop_api_client/types/devbox_create_params.py index 91651ad87..b877861eb 100644 --- a/src/runloop_api_client/types/devbox_create_params.py +++ b/src/runloop_api_client/types/devbox_create_params.py @@ -3,13 +3,13 @@ from __future__ import annotations from typing import Dict, Iterable, Optional -from typing_extensions import TypedDict +from typing_extensions import Required, TypedDict from .shared_params.mount import Mount from .shared_params.launch_parameters import LaunchParameters from .shared_params.code_mount_parameters import CodeMountParameters -__all__ = ["DevboxCreateParams"] +__all__ = ["DevboxCreateParams", "Gateways"] # We split up the original DevboxCreateParams into two nested types to enable us to # omit blueprint_id, blueprint_name, and snapshot_id when we unpack the TypedDict @@ -37,6 +37,15 @@ class DevboxBaseCreateParams(TypedDict, total=False): file_mounts: Optional[Dict[str, str]] """Map of paths and file contents to write before setup. Use mounts instead.""" + gateways: Optional[Dict[str, Gateways]] + """[Beta] (Optional) Gateway specifications for credential proxying. + + Map key is the environment variable prefix (e.g., 'GWS_ANTHROPIC'). The gateway + will proxy requests to external APIs using the specified credential without + exposing the real API key. Example: {'GWS_ANTHROPIC': {'gateway': 'anthropic', + 'secret': 'my_claude_key'}} + """ + launch_parameters: Optional[LaunchParameters] """Parameters to configure the resources and launch time behavior of the Devbox.""" @@ -84,3 +93,15 @@ class DevboxCreateParams(DevboxBaseCreateParams, total=False): Only one of (Snapshot ID, Blueprint ID, Blueprint name) should be specified. """ + + +class Gateways(TypedDict, total=False): + """ + [Beta] GatewaySpec links a gateway configuration to a secret for credential proxying in a devbox. The gateway will proxy requests to external APIs using the specified credential without exposing the real API key. + """ + + gateway: Required[str] + """The gateway config to use. Can be a gateway config ID (gwc_xxx) or name.""" + + secret: Required[str] + """The secret containing the credential. Can be a secret ID or name.""" diff --git a/src/runloop_api_client/types/devbox_view.py b/src/runloop_api_client/types/devbox_view.py index cf9dc2383..8db65da9b 100644 --- a/src/runloop_api_client/types/devbox_view.py +++ b/src/runloop_api_client/types/devbox_view.py @@ -6,7 +6,7 @@ from .._models import BaseModel from .shared.launch_parameters import LaunchParameters -__all__ = ["DevboxView", "StateTransition"] +__all__ = ["DevboxView", "StateTransition", "GatewaySpecs"] class StateTransition(BaseModel): @@ -30,6 +30,14 @@ class StateTransition(BaseModel): """The time the status change occurred""" +class GatewaySpecs(BaseModel): + gateway_config_id: str + """The ID of the gateway config (e.g., gwc_123abc).""" + + secret_id: str + """The ID of the secret containing the credential.""" + + class DevboxView(BaseModel): """A Devbox represents a virtual development environment. @@ -78,6 +86,12 @@ class DevboxView(BaseModel): failure_reason: Optional[Literal["out_of_memory", "out_of_disk", "execution_failed"]] = None """The failure reason if the Devbox failed, if the Devbox has a 'failure' status.""" + gateway_specs: Optional[Dict[str, GatewaySpecs]] = None + """[Beta] Gateway specifications configured for this devbox. + + Map key is the environment variable prefix (e.g., 'GWS_ANTHROPIC'). + """ + initiator_id: Optional[str] = None """The ID of the initiator that created the Devbox.""" diff --git a/src/runloop_api_client/types/gateway_config_create_params.py b/src/runloop_api_client/types/gateway_config_create_params.py new file mode 100644 index 000000000..6a39055e4 --- /dev/null +++ b/src/runloop_api_client/types/gateway_config_create_params.py @@ -0,0 +1,41 @@ +# 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 Required, TypedDict + +__all__ = ["GatewayConfigCreateParams", "AuthMechanism"] + + +class GatewayConfigCreateParams(TypedDict, total=False): + auth_mechanism: Required[AuthMechanism] + """How credentials should be applied to proxied requests. + + Specify the type ('header', 'bearer') and optional key field. + """ + + endpoint: Required[str] + """The target endpoint URL (e.g., 'https://api.anthropic.com').""" + + name: Required[str] + """The human-readable name for the GatewayConfig. + + Must be unique within your account. + """ + + description: Optional[str] + """Optional description for this gateway configuration.""" + + +class AuthMechanism(TypedDict, total=False): + """How credentials should be applied to proxied requests. + + Specify the type ('header', 'bearer') and optional key field. + """ + + type: Required[str] + """The type of authentication mechanism: 'header', 'bearer'.""" + + key: Optional[str] + """For 'header' type: the header name (e.g., 'x-api-key').""" diff --git a/src/runloop_api_client/types/gateway_config_list_params.py b/src/runloop_api_client/types/gateway_config_list_params.py new file mode 100644 index 000000000..cc8706b95 --- /dev/null +++ b/src/runloop_api_client/types/gateway_config_list_params.py @@ -0,0 +1,21 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from typing_extensions import TypedDict + +__all__ = ["GatewayConfigListParams"] + + +class GatewayConfigListParams(TypedDict, total=False): + id: str + """Filter by ID.""" + + limit: int + """The limit of items to return. Default is 20. Max is 5000.""" + + name: str + """Filter by name (partial match supported).""" + + starting_after: str + """Load the next page of data starting after the item with the given ID.""" diff --git a/src/runloop_api_client/types/gateway_config_list_view.py b/src/runloop_api_client/types/gateway_config_list_view.py new file mode 100644 index 000000000..77fce4455 --- /dev/null +++ b/src/runloop_api_client/types/gateway_config_list_view.py @@ -0,0 +1,21 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from typing import List + +from .._models import BaseModel +from .gateway_config_view import GatewayConfigView + +__all__ = ["GatewayConfigListView"] + + +class GatewayConfigListView(BaseModel): + """A paginated list of GatewayConfigs.""" + + gateway_configs: List[GatewayConfigView] + """The list of GatewayConfigs.""" + + has_more: bool + """Whether there are more results available beyond this page.""" + + total_count: int + """Total count of GatewayConfigs that match the query.""" diff --git a/src/runloop_api_client/types/gateway_config_update_params.py b/src/runloop_api_client/types/gateway_config_update_params.py new file mode 100644 index 000000000..cdf385702 --- /dev/null +++ b/src/runloop_api_client/types/gateway_config_update_params.py @@ -0,0 +1,32 @@ +# 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 Required, TypedDict + +__all__ = ["GatewayConfigUpdateParams", "AuthMechanism"] + + +class GatewayConfigUpdateParams(TypedDict, total=False): + auth_mechanism: Optional[AuthMechanism] + """New authentication mechanism for applying credentials to proxied requests.""" + + description: Optional[str] + """New description for this gateway configuration.""" + + endpoint: Optional[str] + """New target endpoint URL (e.g., 'https://api.anthropic.com').""" + + name: Optional[str] + """New name for the GatewayConfig. Must be unique within your account.""" + + +class AuthMechanism(TypedDict, total=False): + """New authentication mechanism for applying credentials to proxied requests.""" + + type: Required[str] + """The type of authentication mechanism: 'header', 'bearer'.""" + + key: Optional[str] + """For 'header' type: the header name (e.g., 'x-api-key').""" diff --git a/src/runloop_api_client/types/gateway_config_view.py b/src/runloop_api_client/types/gateway_config_view.py new file mode 100644 index 000000000..a6e3b3ac4 --- /dev/null +++ b/src/runloop_api_client/types/gateway_config_view.py @@ -0,0 +1,47 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from typing import Optional + +from .._models import BaseModel + +__all__ = ["GatewayConfigView", "AuthMechanism"] + + +class AuthMechanism(BaseModel): + """How credentials should be applied to proxied requests.""" + + type: str + """The type of authentication mechanism: 'header', 'bearer'.""" + + key: Optional[str] = None + """For 'header' type: the header name (e.g., 'x-api-key').""" + + +class GatewayConfigView(BaseModel): + """ + A GatewayConfig defines a configuration for proxying API requests through the credential gateway. It specifies the target endpoint and how credentials should be applied. + """ + + id: str + """The unique identifier of the GatewayConfig.""" + + auth_mechanism: AuthMechanism + """How credentials should be applied to proxied requests.""" + + create_time_ms: int + """Creation time of the GatewayConfig (Unix timestamp in milliseconds).""" + + endpoint: str + """The target endpoint URL (e.g., 'https://api.anthropic.com').""" + + name: str + """The human-readable name of the GatewayConfig. + + Unique per account (or globally for system configs). + """ + + account_id: Optional[str] = None + """The account ID that owns this config.""" + + description: Optional[str] = None + """Optional description for this gateway configuration.""" diff --git a/tests/api_resources/test_devboxes.py b/tests/api_resources/test_devboxes.py index d9b75d1ac..36e4f2786 100644 --- a/tests/api_resources/test_devboxes.py +++ b/tests/api_resources/test_devboxes.py @@ -65,6 +65,12 @@ def test_method_create_with_all_params(self, client: Runloop) -> None: entrypoint="entrypoint", environment_variables={"foo": "string"}, file_mounts={"foo": "string"}, + gateways={ + "foo": { + "gateway": "gateway", + "secret": "secret", + } + }, launch_parameters={ "after_idle": { "idle_time_seconds": 0, @@ -1614,6 +1620,12 @@ async def test_method_create_with_all_params(self, async_client: AsyncRunloop) - entrypoint="entrypoint", environment_variables={"foo": "string"}, file_mounts={"foo": "string"}, + gateways={ + "foo": { + "gateway": "gateway", + "secret": "secret", + } + }, launch_parameters={ "after_idle": { "idle_time_seconds": 0, diff --git a/tests/api_resources/test_gateway_configs.py b/tests/api_resources/test_gateway_configs.py new file mode 100644 index 000000000..6265b9875 --- /dev/null +++ b/tests/api_resources/test_gateway_configs.py @@ -0,0 +1,453 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +import os +from typing import Any, cast + +import pytest + +from tests.utils import assert_matches_type +from runloop_api_client import Runloop, AsyncRunloop +from runloop_api_client.types import ( + GatewayConfigView, +) +from runloop_api_client.pagination import SyncGatewayConfigsCursorIDPage, AsyncGatewayConfigsCursorIDPage + +base_url = os.environ.get("TEST_API_BASE_URL", "http://127.0.0.1:4010") + + +class TestGatewayConfigs: + parametrize = pytest.mark.parametrize("client", [False, True], indirect=True, ids=["loose", "strict"]) + + @parametrize + def test_method_create(self, client: Runloop) -> None: + gateway_config = client.gateway_configs.create( + auth_mechanism={"type": "type"}, + endpoint="endpoint", + name="name", + ) + assert_matches_type(GatewayConfigView, gateway_config, path=["response"]) + + @parametrize + def test_method_create_with_all_params(self, client: Runloop) -> None: + gateway_config = client.gateway_configs.create( + auth_mechanism={ + "type": "type", + "key": "key", + }, + endpoint="endpoint", + name="name", + description="description", + ) + assert_matches_type(GatewayConfigView, gateway_config, path=["response"]) + + @parametrize + def test_raw_response_create(self, client: Runloop) -> None: + response = client.gateway_configs.with_raw_response.create( + auth_mechanism={"type": "type"}, + endpoint="endpoint", + name="name", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + gateway_config = response.parse() + assert_matches_type(GatewayConfigView, gateway_config, path=["response"]) + + @parametrize + def test_streaming_response_create(self, client: Runloop) -> None: + with client.gateway_configs.with_streaming_response.create( + auth_mechanism={"type": "type"}, + endpoint="endpoint", + name="name", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + gateway_config = response.parse() + assert_matches_type(GatewayConfigView, gateway_config, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @parametrize + def test_method_retrieve(self, client: Runloop) -> None: + gateway_config = client.gateway_configs.retrieve( + "id", + ) + assert_matches_type(GatewayConfigView, gateway_config, path=["response"]) + + @parametrize + def test_raw_response_retrieve(self, client: Runloop) -> None: + response = client.gateway_configs.with_raw_response.retrieve( + "id", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + gateway_config = response.parse() + assert_matches_type(GatewayConfigView, gateway_config, path=["response"]) + + @parametrize + def test_streaming_response_retrieve(self, client: Runloop) -> None: + with client.gateway_configs.with_streaming_response.retrieve( + "id", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + gateway_config = response.parse() + assert_matches_type(GatewayConfigView, gateway_config, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @parametrize + def test_path_params_retrieve(self, client: Runloop) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): + client.gateway_configs.with_raw_response.retrieve( + "", + ) + + @parametrize + def test_method_update(self, client: Runloop) -> None: + gateway_config = client.gateway_configs.update( + id="id", + ) + assert_matches_type(GatewayConfigView, gateway_config, path=["response"]) + + @parametrize + def test_method_update_with_all_params(self, client: Runloop) -> None: + gateway_config = client.gateway_configs.update( + id="id", + auth_mechanism={ + "type": "type", + "key": "key", + }, + description="description", + endpoint="endpoint", + name="name", + ) + assert_matches_type(GatewayConfigView, gateway_config, path=["response"]) + + @parametrize + def test_raw_response_update(self, client: Runloop) -> None: + response = client.gateway_configs.with_raw_response.update( + id="id", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + gateway_config = response.parse() + assert_matches_type(GatewayConfigView, gateway_config, path=["response"]) + + @parametrize + def test_streaming_response_update(self, client: Runloop) -> None: + with client.gateway_configs.with_streaming_response.update( + id="id", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + gateway_config = response.parse() + assert_matches_type(GatewayConfigView, gateway_config, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @parametrize + def test_path_params_update(self, client: Runloop) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): + client.gateway_configs.with_raw_response.update( + id="", + ) + + @parametrize + def test_method_list(self, client: Runloop) -> None: + gateway_config = client.gateway_configs.list() + assert_matches_type(SyncGatewayConfigsCursorIDPage[GatewayConfigView], gateway_config, path=["response"]) + + @parametrize + def test_method_list_with_all_params(self, client: Runloop) -> None: + gateway_config = client.gateway_configs.list( + id="id", + limit=0, + name="name", + starting_after="starting_after", + ) + assert_matches_type(SyncGatewayConfigsCursorIDPage[GatewayConfigView], gateway_config, path=["response"]) + + @parametrize + def test_raw_response_list(self, client: Runloop) -> None: + response = client.gateway_configs.with_raw_response.list() + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + gateway_config = response.parse() + assert_matches_type(SyncGatewayConfigsCursorIDPage[GatewayConfigView], gateway_config, path=["response"]) + + @parametrize + def test_streaming_response_list(self, client: Runloop) -> None: + with client.gateway_configs.with_streaming_response.list() as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + gateway_config = response.parse() + assert_matches_type(SyncGatewayConfigsCursorIDPage[GatewayConfigView], gateway_config, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @parametrize + def test_method_delete(self, client: Runloop) -> None: + gateway_config = client.gateway_configs.delete( + "id", + ) + assert_matches_type(GatewayConfigView, gateway_config, path=["response"]) + + @parametrize + def test_raw_response_delete(self, client: Runloop) -> None: + response = client.gateway_configs.with_raw_response.delete( + "id", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + gateway_config = response.parse() + assert_matches_type(GatewayConfigView, gateway_config, path=["response"]) + + @parametrize + def test_streaming_response_delete(self, client: Runloop) -> None: + with client.gateway_configs.with_streaming_response.delete( + "id", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + gateway_config = response.parse() + assert_matches_type(GatewayConfigView, gateway_config, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @parametrize + def test_path_params_delete(self, client: Runloop) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): + client.gateway_configs.with_raw_response.delete( + "", + ) + + +class TestAsyncGatewayConfigs: + parametrize = pytest.mark.parametrize( + "async_client", [False, True, {"http_client": "aiohttp"}], indirect=True, ids=["loose", "strict", "aiohttp"] + ) + + @parametrize + async def test_method_create(self, async_client: AsyncRunloop) -> None: + gateway_config = await async_client.gateway_configs.create( + auth_mechanism={"type": "type"}, + endpoint="endpoint", + name="name", + ) + assert_matches_type(GatewayConfigView, gateway_config, path=["response"]) + + @parametrize + async def test_method_create_with_all_params(self, async_client: AsyncRunloop) -> None: + gateway_config = await async_client.gateway_configs.create( + auth_mechanism={ + "type": "type", + "key": "key", + }, + endpoint="endpoint", + name="name", + description="description", + ) + assert_matches_type(GatewayConfigView, gateway_config, path=["response"]) + + @parametrize + async def test_raw_response_create(self, async_client: AsyncRunloop) -> None: + response = await async_client.gateway_configs.with_raw_response.create( + auth_mechanism={"type": "type"}, + endpoint="endpoint", + name="name", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + gateway_config = await response.parse() + assert_matches_type(GatewayConfigView, gateway_config, path=["response"]) + + @parametrize + async def test_streaming_response_create(self, async_client: AsyncRunloop) -> None: + async with async_client.gateway_configs.with_streaming_response.create( + auth_mechanism={"type": "type"}, + endpoint="endpoint", + name="name", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + gateway_config = await response.parse() + assert_matches_type(GatewayConfigView, gateway_config, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @parametrize + async def test_method_retrieve(self, async_client: AsyncRunloop) -> None: + gateway_config = await async_client.gateway_configs.retrieve( + "id", + ) + assert_matches_type(GatewayConfigView, gateway_config, path=["response"]) + + @parametrize + async def test_raw_response_retrieve(self, async_client: AsyncRunloop) -> None: + response = await async_client.gateway_configs.with_raw_response.retrieve( + "id", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + gateway_config = await response.parse() + assert_matches_type(GatewayConfigView, gateway_config, path=["response"]) + + @parametrize + async def test_streaming_response_retrieve(self, async_client: AsyncRunloop) -> None: + async with async_client.gateway_configs.with_streaming_response.retrieve( + "id", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + gateway_config = await response.parse() + assert_matches_type(GatewayConfigView, gateway_config, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @parametrize + async def test_path_params_retrieve(self, async_client: AsyncRunloop) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): + await async_client.gateway_configs.with_raw_response.retrieve( + "", + ) + + @parametrize + async def test_method_update(self, async_client: AsyncRunloop) -> None: + gateway_config = await async_client.gateway_configs.update( + id="id", + ) + assert_matches_type(GatewayConfigView, gateway_config, path=["response"]) + + @parametrize + async def test_method_update_with_all_params(self, async_client: AsyncRunloop) -> None: + gateway_config = await async_client.gateway_configs.update( + id="id", + auth_mechanism={ + "type": "type", + "key": "key", + }, + description="description", + endpoint="endpoint", + name="name", + ) + assert_matches_type(GatewayConfigView, gateway_config, path=["response"]) + + @parametrize + async def test_raw_response_update(self, async_client: AsyncRunloop) -> None: + response = await async_client.gateway_configs.with_raw_response.update( + id="id", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + gateway_config = await response.parse() + assert_matches_type(GatewayConfigView, gateway_config, path=["response"]) + + @parametrize + async def test_streaming_response_update(self, async_client: AsyncRunloop) -> None: + async with async_client.gateway_configs.with_streaming_response.update( + id="id", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + gateway_config = await response.parse() + assert_matches_type(GatewayConfigView, gateway_config, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @parametrize + async def test_path_params_update(self, async_client: AsyncRunloop) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): + await async_client.gateway_configs.with_raw_response.update( + id="", + ) + + @parametrize + async def test_method_list(self, async_client: AsyncRunloop) -> None: + gateway_config = await async_client.gateway_configs.list() + assert_matches_type(AsyncGatewayConfigsCursorIDPage[GatewayConfigView], gateway_config, path=["response"]) + + @parametrize + async def test_method_list_with_all_params(self, async_client: AsyncRunloop) -> None: + gateway_config = await async_client.gateway_configs.list( + id="id", + limit=0, + name="name", + starting_after="starting_after", + ) + assert_matches_type(AsyncGatewayConfigsCursorIDPage[GatewayConfigView], gateway_config, path=["response"]) + + @parametrize + async def test_raw_response_list(self, async_client: AsyncRunloop) -> None: + response = await async_client.gateway_configs.with_raw_response.list() + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + gateway_config = await response.parse() + assert_matches_type(AsyncGatewayConfigsCursorIDPage[GatewayConfigView], gateway_config, path=["response"]) + + @parametrize + async def test_streaming_response_list(self, async_client: AsyncRunloop) -> None: + async with async_client.gateway_configs.with_streaming_response.list() as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + gateway_config = await response.parse() + assert_matches_type(AsyncGatewayConfigsCursorIDPage[GatewayConfigView], gateway_config, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @parametrize + async def test_method_delete(self, async_client: AsyncRunloop) -> None: + gateway_config = await async_client.gateway_configs.delete( + "id", + ) + assert_matches_type(GatewayConfigView, gateway_config, path=["response"]) + + @parametrize + async def test_raw_response_delete(self, async_client: AsyncRunloop) -> None: + response = await async_client.gateway_configs.with_raw_response.delete( + "id", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + gateway_config = await response.parse() + assert_matches_type(GatewayConfigView, gateway_config, path=["response"]) + + @parametrize + async def test_streaming_response_delete(self, async_client: AsyncRunloop) -> None: + async with async_client.gateway_configs.with_streaming_response.delete( + "id", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + gateway_config = await response.parse() + assert_matches_type(GatewayConfigView, gateway_config, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @parametrize + async def test_path_params_delete(self, async_client: AsyncRunloop) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): + await async_client.gateway_configs.with_raw_response.delete( + "", + ) From 08e058660cf02e67701899e3b46173c5e9011b7d Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Fri, 30 Jan 2026 17:56:17 +0000 Subject: [PATCH 3/7] chore(documentation): made warning message language more accurate (#7215) --- .stats.yml | 4 ++-- .../types/shared/launch_parameters.py | 15 +++------------ .../types/shared_params/launch_parameters.py | 15 +++------------ 3 files changed, 8 insertions(+), 26 deletions(-) diff --git a/.stats.yml b/.stats.yml index cc6ae0f60..410475b64 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 111 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/runloop-ai%2Frunloop-294ebcf6886a5ddbebeaa70923b7674757459e73ef08cd2fbc63fb70e1932eac.yml -openapi_spec_hash: 3a2a14e7ddd646f53d9f21bef2e84ec5 +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/runloop-ai%2Frunloop-c89b52af46573ea81341b7115e59ee3f995ff9fe5b48a04318176d9b30e7eb79.yml +openapi_spec_hash: a8b42cc79a4fc993c8cc5cc13fc443a2 config_hash: 22f65246be4646c23dde9f69f51252e7 diff --git a/src/runloop_api_client/types/shared/launch_parameters.py b/src/runloop_api_client/types/shared/launch_parameters.py index 04d7f27a0..0264fa5c8 100644 --- a/src/runloop_api_client/types/shared/launch_parameters.py +++ b/src/runloop_api_client/types/shared/launch_parameters.py @@ -44,22 +44,13 @@ class LaunchParameters(BaseModel): """ custom_cpu_cores: Optional[int] = None - """custom resource size, number of cpu cores, must be multiple of 2. - - Min is 1, max is 16. - """ + """Custom CPU cores. Must be 0.5, 1, or a multiple of 2. Max is 16.""" custom_disk_size: Optional[int] = None - """custom disk size, number in GiB, must be a multiple of 2. - - Min is 2GiB, max is 64GiB. - """ + """Custom disk size in GiB. Must be a multiple of 2. Min is 2GiB, max is 64GiB.""" custom_gb_memory: Optional[int] = None - """custom memory size, number in GiB, must be a multiple of 2. - - Min is 2GiB, max is 64GiB. - """ + """Custom memory size in GiB. Must be 1 or a multiple of 2. Max is 64GiB.""" keep_alive_time_seconds: Optional[int] = None """Time in seconds after which Devbox will automatically shutdown. diff --git a/src/runloop_api_client/types/shared_params/launch_parameters.py b/src/runloop_api_client/types/shared_params/launch_parameters.py index e3f00d1d3..5c785b9f9 100644 --- a/src/runloop_api_client/types/shared_params/launch_parameters.py +++ b/src/runloop_api_client/types/shared_params/launch_parameters.py @@ -46,22 +46,13 @@ class LaunchParameters(TypedDict, total=False): """ custom_cpu_cores: Optional[int] - """custom resource size, number of cpu cores, must be multiple of 2. - - Min is 1, max is 16. - """ + """Custom CPU cores. Must be 0.5, 1, or a multiple of 2. Max is 16.""" custom_disk_size: Optional[int] - """custom disk size, number in GiB, must be a multiple of 2. - - Min is 2GiB, max is 64GiB. - """ + """Custom disk size in GiB. Must be a multiple of 2. Min is 2GiB, max is 64GiB.""" custom_gb_memory: Optional[int] - """custom memory size, number in GiB, must be a multiple of 2. - - Min is 2GiB, max is 64GiB. - """ + """Custom memory size in GiB. Must be 1 or a multiple of 2. Max is 64GiB.""" keep_alive_time_seconds: Optional[int] """Time in seconds after which Devbox will automatically shutdown. From 71620a114080506898864b6d73378d4ff30b1070 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Fri, 30 Jan 2026 19:38:53 +0000 Subject: [PATCH 4/7] feat(devbox): add new tunnel APIs and deprecate old tunnel API (#7227) --- .stats.yml | 4 +- .../resources/devboxes/devboxes.py | 52 +++++++--- .../types/devbox_create_params.py | 21 +++- src/runloop_api_client/types/devbox_view.py | 32 ++++++- tests/api_resources/test_devboxes.py | 96 +++++++++++-------- 5 files changed, 146 insertions(+), 59 deletions(-) diff --git a/.stats.yml b/.stats.yml index 410475b64..94d83f0c1 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 111 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/runloop-ai%2Frunloop-c89b52af46573ea81341b7115e59ee3f995ff9fe5b48a04318176d9b30e7eb79.yml -openapi_spec_hash: a8b42cc79a4fc993c8cc5cc13fc443a2 +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/runloop-ai%2Frunloop-debd1f9a9d599b009905cbaaea3095c025fe290493c8a836f54ece22265579c1.yml +openapi_spec_hash: ee42d2c73aaad86888360147d9ed0766 config_hash: 22f65246be4646c23dde9f69f51252e7 diff --git a/src/runloop_api_client/resources/devboxes/devboxes.py b/src/runloop_api_client/resources/devboxes/devboxes.py index 544dfd7d0..4a3e6c1ec 100644 --- a/src/runloop_api_client/resources/devboxes/devboxes.py +++ b/src/runloop_api_client/resources/devboxes/devboxes.py @@ -193,6 +193,7 @@ def create( repo_connection_id: Optional[str] | Omit = omit, secrets: Optional[Dict[str, str]] | Omit = omit, snapshot_id: Optional[str] | Omit = omit, + tunnel: Optional[devbox_create_params.Tunnel] | 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, @@ -252,6 +253,10 @@ def create( snapshot_id: Snapshot ID to use for the Devbox. Only one of (Snapshot ID, Blueprint ID, Blueprint name) should be specified. + tunnel: (Optional) Configuration for creating a V2 tunnel at Devbox launch time. When + specified, a tunnel will be automatically provisioned and the tunnel details + will be included in the Devbox response. + extra_headers: Send extra headers extra_query: Add additional query parameters to the request @@ -280,6 +285,7 @@ def create( "repo_connection_id": repo_connection_id, "secrets": secrets, "snapshot_id": snapshot_id, + "tunnel": tunnel, }, devbox_create_params.DevboxCreateParams, ), @@ -640,6 +646,7 @@ def create_ssh_key( cast_to=DevboxCreateSSHKeyResponse, ) + @typing_extensions.deprecated("deprecated") def create_tunnel( self, id: str, @@ -653,8 +660,11 @@ def create_tunnel( timeout: float | httpx.Timeout | None | NotGiven = not_given, idempotency_key: str | None = None, ) -> DevboxTunnelView: - """ - Create a live tunnel to an available port on the Devbox. + """[Deprecated] Use POST /v1/devboxes/{id}/enable_tunnel instead. + + This endpoint + creates a legacy tunnel. The new enable_tunnel endpoint provides improved tunnel + functionality with authentication options. Args: port: Devbox port that tunnel will expose. @@ -1739,6 +1749,7 @@ async def create( repo_connection_id: Optional[str] | Omit = omit, secrets: Optional[Dict[str, str]] | Omit = omit, snapshot_id: Optional[str] | Omit = omit, + tunnel: Optional[devbox_create_params.Tunnel] | 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, @@ -1798,6 +1809,10 @@ async def create( snapshot_id: Snapshot ID to use for the Devbox. Only one of (Snapshot ID, Blueprint ID, Blueprint name) should be specified. + tunnel: (Optional) Configuration for creating a V2 tunnel at Devbox launch time. When + specified, a tunnel will be automatically provisioned and the tunnel details + will be included in the Devbox response. + extra_headers: Send extra headers extra_query: Add additional query parameters to the request @@ -1826,6 +1841,7 @@ async def create( "repo_connection_id": repo_connection_id, "secrets": secrets, "snapshot_id": snapshot_id, + "tunnel": tunnel, }, devbox_create_params.DevboxCreateParams, ), @@ -2183,6 +2199,7 @@ async def create_ssh_key( cast_to=DevboxCreateSSHKeyResponse, ) + @typing_extensions.deprecated("deprecated") async def create_tunnel( self, id: str, @@ -2196,8 +2213,11 @@ async def create_tunnel( timeout: float | httpx.Timeout | None | NotGiven = not_given, idempotency_key: str | None = None, ) -> DevboxTunnelView: - """ - Create a live tunnel to an available port on the Devbox. + """[Deprecated] Use POST /v1/devboxes/{id}/enable_tunnel instead. + + This endpoint + creates a legacy tunnel. The new enable_tunnel endpoint provides improved tunnel + functionality with authentication options. Args: port: Devbox port that tunnel will expose. @@ -3247,8 +3267,10 @@ def __init__(self, devboxes: DevboxesResource) -> None: self.create_ssh_key = to_raw_response_wrapper( devboxes.create_ssh_key, ) - self.create_tunnel = to_raw_response_wrapper( - devboxes.create_tunnel, + self.create_tunnel = ( # pyright: ignore[reportDeprecated] + to_raw_response_wrapper( + devboxes.create_tunnel, # pyright: ignore[reportDeprecated], + ) ) self.delete_disk_snapshot = to_raw_response_wrapper( devboxes.delete_disk_snapshot, @@ -3345,8 +3367,10 @@ def __init__(self, devboxes: AsyncDevboxesResource) -> None: self.create_ssh_key = async_to_raw_response_wrapper( devboxes.create_ssh_key, ) - self.create_tunnel = async_to_raw_response_wrapper( - devboxes.create_tunnel, + self.create_tunnel = ( # pyright: ignore[reportDeprecated] + async_to_raw_response_wrapper( + devboxes.create_tunnel, # pyright: ignore[reportDeprecated], + ) ) self.delete_disk_snapshot = async_to_raw_response_wrapper( devboxes.delete_disk_snapshot, @@ -3443,8 +3467,10 @@ def __init__(self, devboxes: DevboxesResource) -> None: self.create_ssh_key = to_streamed_response_wrapper( devboxes.create_ssh_key, ) - self.create_tunnel = to_streamed_response_wrapper( - devboxes.create_tunnel, + self.create_tunnel = ( # pyright: ignore[reportDeprecated] + to_streamed_response_wrapper( + devboxes.create_tunnel, # pyright: ignore[reportDeprecated], + ) ) self.delete_disk_snapshot = to_streamed_response_wrapper( devboxes.delete_disk_snapshot, @@ -3541,8 +3567,10 @@ def __init__(self, devboxes: AsyncDevboxesResource) -> None: self.create_ssh_key = async_to_streamed_response_wrapper( devboxes.create_ssh_key, ) - self.create_tunnel = async_to_streamed_response_wrapper( - devboxes.create_tunnel, + self.create_tunnel = ( # pyright: ignore[reportDeprecated] + async_to_streamed_response_wrapper( + devboxes.create_tunnel, # pyright: ignore[reportDeprecated], + ) ) self.delete_disk_snapshot = async_to_streamed_response_wrapper( devboxes.delete_disk_snapshot, diff --git a/src/runloop_api_client/types/devbox_create_params.py b/src/runloop_api_client/types/devbox_create_params.py index b877861eb..b31efdfe7 100644 --- a/src/runloop_api_client/types/devbox_create_params.py +++ b/src/runloop_api_client/types/devbox_create_params.py @@ -3,13 +3,13 @@ from __future__ import annotations from typing import Dict, Iterable, Optional -from typing_extensions import Required, TypedDict +from typing_extensions import Literal, Required, TypedDict from .shared_params.mount import Mount from .shared_params.launch_parameters import LaunchParameters from .shared_params.code_mount_parameters import CodeMountParameters -__all__ = ["DevboxCreateParams", "Gateways"] +__all__ = ["DevboxCreateParams", "Gateways", "Tunnel"] # We split up the original DevboxCreateParams into two nested types to enable us to # omit blueprint_id, blueprint_name, and snapshot_id when we unpack the TypedDict @@ -94,6 +94,13 @@ class DevboxCreateParams(DevboxBaseCreateParams, total=False): Only one of (Snapshot ID, Blueprint ID, Blueprint name) should be specified. """ + tunnel: Optional[Tunnel] + """(Optional) Configuration for creating a V2 tunnel at Devbox launch time. + + When specified, a tunnel will be automatically provisioned and the tunnel + details will be included in the Devbox response. + """ + class Gateways(TypedDict, total=False): """ @@ -105,3 +112,13 @@ class Gateways(TypedDict, total=False): secret: Required[str] """The secret containing the credential. Can be a secret ID or name.""" + + +class Tunnel(TypedDict, total=False): + """(Optional) Configuration for creating a V2 tunnel at Devbox launch time. + + When specified, a tunnel will be automatically provisioned and the tunnel details will be included in the Devbox response. + """ + + 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 8db65da9b..50422db1f 100644 --- a/src/runloop_api_client/types/devbox_view.py +++ b/src/runloop_api_client/types/devbox_view.py @@ -6,7 +6,7 @@ from .._models import BaseModel from .shared.launch_parameters import LaunchParameters -__all__ = ["DevboxView", "StateTransition", "GatewaySpecs"] +__all__ = ["DevboxView", "StateTransition", "GatewaySpecs", "Tunnel"] class StateTransition(BaseModel): @@ -38,6 +38,30 @@ 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. @@ -112,3 +136,9 @@ class DevboxView(BaseModel): The Snapshot ID used in creation of the Devbox, if the devbox was created from a Snapshot. """ + + tunnel: Optional[Tunnel] = None + """ + V2 tunnel information if a tunnel was created at launch time or via the + createTunnel API. + """ diff --git a/tests/api_resources/test_devboxes.py b/tests/api_resources/test_devboxes.py index 36e4f2786..d96226bb9 100644 --- a/tests/api_resources/test_devboxes.py +++ b/tests/api_resources/test_devboxes.py @@ -103,6 +103,7 @@ def test_method_create_with_all_params(self, client: Runloop) -> None: repo_connection_id="repo_connection_id", secrets={"foo": "string"}, snapshot_id="snapshot_id", + tunnel={"auth_mode": "open"}, ) assert_matches_type(DevboxView, devbox, path=["response"]) @@ -285,18 +286,21 @@ def test_path_params_create_ssh_key(self, client: Runloop) -> None: @parametrize def test_method_create_tunnel(self, client: Runloop) -> None: - devbox = client.devboxes.create_tunnel( - id="id", - port=0, - ) + with pytest.warns(DeprecationWarning): + devbox = client.devboxes.create_tunnel( + id="id", + port=0, + ) + assert_matches_type(DevboxTunnelView, devbox, path=["response"]) @parametrize def test_raw_response_create_tunnel(self, client: Runloop) -> None: - response = client.devboxes.with_raw_response.create_tunnel( - id="id", - port=0, - ) + with pytest.warns(DeprecationWarning): + response = client.devboxes.with_raw_response.create_tunnel( + id="id", + port=0, + ) assert response.is_closed is True assert response.http_request.headers.get("X-Stainless-Lang") == "python" @@ -305,25 +309,27 @@ def test_raw_response_create_tunnel(self, client: Runloop) -> None: @parametrize def test_streaming_response_create_tunnel(self, client: Runloop) -> None: - with client.devboxes.with_streaming_response.create_tunnel( - id="id", - port=0, - ) as response: - assert not response.is_closed - assert response.http_request.headers.get("X-Stainless-Lang") == "python" + with pytest.warns(DeprecationWarning): + with client.devboxes.with_streaming_response.create_tunnel( + id="id", + port=0, + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" - devbox = response.parse() - assert_matches_type(DevboxTunnelView, devbox, path=["response"]) + devbox = response.parse() + assert_matches_type(DevboxTunnelView, devbox, path=["response"]) assert cast(Any, response.is_closed) is True @parametrize def test_path_params_create_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.create_tunnel( - id="", - port=0, - ) + with pytest.warns(DeprecationWarning): + with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): + client.devboxes.with_raw_response.create_tunnel( + id="", + port=0, + ) @parametrize def test_method_delete_disk_snapshot(self, client: Runloop) -> None: @@ -1658,6 +1664,7 @@ async def test_method_create_with_all_params(self, async_client: AsyncRunloop) - repo_connection_id="repo_connection_id", secrets={"foo": "string"}, snapshot_id="snapshot_id", + tunnel={"auth_mode": "open"}, ) assert_matches_type(DevboxView, devbox, path=["response"]) @@ -1840,18 +1847,21 @@ async def test_path_params_create_ssh_key(self, async_client: AsyncRunloop) -> N @parametrize async def test_method_create_tunnel(self, async_client: AsyncRunloop) -> None: - devbox = await async_client.devboxes.create_tunnel( - id="id", - port=0, - ) + with pytest.warns(DeprecationWarning): + devbox = await async_client.devboxes.create_tunnel( + id="id", + port=0, + ) + assert_matches_type(DevboxTunnelView, devbox, path=["response"]) @parametrize async def test_raw_response_create_tunnel(self, async_client: AsyncRunloop) -> None: - response = await async_client.devboxes.with_raw_response.create_tunnel( - id="id", - port=0, - ) + with pytest.warns(DeprecationWarning): + response = await async_client.devboxes.with_raw_response.create_tunnel( + id="id", + port=0, + ) assert response.is_closed is True assert response.http_request.headers.get("X-Stainless-Lang") == "python" @@ -1860,25 +1870,27 @@ async def test_raw_response_create_tunnel(self, async_client: AsyncRunloop) -> N @parametrize async def test_streaming_response_create_tunnel(self, async_client: AsyncRunloop) -> None: - async with async_client.devboxes.with_streaming_response.create_tunnel( - id="id", - port=0, - ) as response: - assert not response.is_closed - assert response.http_request.headers.get("X-Stainless-Lang") == "python" + with pytest.warns(DeprecationWarning): + async with async_client.devboxes.with_streaming_response.create_tunnel( + id="id", + port=0, + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" - devbox = await response.parse() - assert_matches_type(DevboxTunnelView, devbox, path=["response"]) + devbox = await response.parse() + assert_matches_type(DevboxTunnelView, devbox, path=["response"]) assert cast(Any, response.is_closed) is True @parametrize async def test_path_params_create_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.create_tunnel( - id="", - port=0, - ) + with pytest.warns(DeprecationWarning): + with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): + await async_client.devboxes.with_raw_response.create_tunnel( + id="", + port=0, + ) @parametrize async def test_method_delete_disk_snapshot(self, async_client: AsyncRunloop) -> None: From d48832d0aa076bd621decae3f5dadca7141c29d3 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Fri, 30 Jan 2026 20:26:45 +0000 Subject: [PATCH 5/7] chore(devbox): deprecate remove tunnel API (#7230) --- .stats.yml | 4 +- .../resources/devboxes/devboxes.py | 38 +++++--- tests/api_resources/test_devboxes.py | 94 ++++++++++--------- 3 files changed, 80 insertions(+), 56 deletions(-) diff --git a/.stats.yml b/.stats.yml index 94d83f0c1..e2f90d495 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 111 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/runloop-ai%2Frunloop-debd1f9a9d599b009905cbaaea3095c025fe290493c8a836f54ece22265579c1.yml -openapi_spec_hash: ee42d2c73aaad86888360147d9ed0766 +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/runloop-ai%2Frunloop-84e997ca5716b9378a58a1bdf3d6616cf3be80156a6aaed1bed469fe93ba2c95.yml +openapi_spec_hash: b44a4ba1c2c3cb775c14545f2bab05a8 config_hash: 22f65246be4646c23dde9f69f51252e7 diff --git a/src/runloop_api_client/resources/devboxes/devboxes.py b/src/runloop_api_client/resources/devboxes/devboxes.py index 4a3e6c1ec..5080c0845 100644 --- a/src/runloop_api_client/resources/devboxes/devboxes.py +++ b/src/runloop_api_client/resources/devboxes/devboxes.py @@ -1215,6 +1215,7 @@ def read_file_contents( cast_to=str, ) + @typing_extensions.deprecated("deprecated") def remove_tunnel( self, id: str, @@ -1228,8 +1229,10 @@ def remove_tunnel( timeout: float | httpx.Timeout | None | NotGiven = not_given, idempotency_key: str | None = None, ) -> object: - """ - Remove a previously opened tunnel on the Devbox. + """[Deprecated] Tunnels remain active until devbox is shutdown. + + This endpoint + removes a legacy tunnel. Args: port: Devbox port that tunnel will expose. @@ -2769,6 +2772,7 @@ async def read_file_contents( cast_to=str, ) + @typing_extensions.deprecated("deprecated") async def remove_tunnel( self, id: str, @@ -2782,8 +2786,10 @@ async def remove_tunnel( timeout: float | httpx.Timeout | None | NotGiven = not_given, idempotency_key: str | None = None, ) -> object: - """ - Remove a previously opened tunnel on the Devbox. + """[Deprecated] Tunnels remain active until devbox is shutdown. + + This endpoint + removes a legacy tunnel. Args: port: Devbox port that tunnel will expose. @@ -3299,8 +3305,10 @@ def __init__(self, devboxes: DevboxesResource) -> None: self.read_file_contents = to_raw_response_wrapper( devboxes.read_file_contents, ) - self.remove_tunnel = to_raw_response_wrapper( - devboxes.remove_tunnel, + self.remove_tunnel = ( # pyright: ignore[reportDeprecated] + to_raw_response_wrapper( + devboxes.remove_tunnel, # pyright: ignore[reportDeprecated], + ) ) self.resume = to_raw_response_wrapper( devboxes.resume, @@ -3399,8 +3407,10 @@ def __init__(self, devboxes: AsyncDevboxesResource) -> None: self.read_file_contents = async_to_raw_response_wrapper( devboxes.read_file_contents, ) - self.remove_tunnel = async_to_raw_response_wrapper( - devboxes.remove_tunnel, + self.remove_tunnel = ( # pyright: ignore[reportDeprecated] + async_to_raw_response_wrapper( + devboxes.remove_tunnel, # pyright: ignore[reportDeprecated], + ) ) self.resume = async_to_raw_response_wrapper( devboxes.resume, @@ -3499,8 +3509,10 @@ def __init__(self, devboxes: DevboxesResource) -> None: self.read_file_contents = to_streamed_response_wrapper( devboxes.read_file_contents, ) - self.remove_tunnel = to_streamed_response_wrapper( - devboxes.remove_tunnel, + self.remove_tunnel = ( # pyright: ignore[reportDeprecated] + to_streamed_response_wrapper( + devboxes.remove_tunnel, # pyright: ignore[reportDeprecated], + ) ) self.resume = to_streamed_response_wrapper( devboxes.resume, @@ -3599,8 +3611,10 @@ def __init__(self, devboxes: AsyncDevboxesResource) -> None: self.read_file_contents = async_to_streamed_response_wrapper( devboxes.read_file_contents, ) - self.remove_tunnel = async_to_streamed_response_wrapper( - devboxes.remove_tunnel, + self.remove_tunnel = ( # pyright: ignore[reportDeprecated] + async_to_streamed_response_wrapper( + devboxes.remove_tunnel, # pyright: ignore[reportDeprecated], + ) ) self.resume = async_to_streamed_response_wrapper( devboxes.resume, diff --git a/tests/api_resources/test_devboxes.py b/tests/api_resources/test_devboxes.py index d96226bb9..8af777181 100644 --- a/tests/api_resources/test_devboxes.py +++ b/tests/api_resources/test_devboxes.py @@ -715,18 +715,21 @@ def test_path_params_read_file_contents(self, client: Runloop) -> None: @parametrize def test_method_remove_tunnel(self, client: Runloop) -> None: - devbox = client.devboxes.remove_tunnel( - id="id", - port=0, - ) + with pytest.warns(DeprecationWarning): + devbox = client.devboxes.remove_tunnel( + id="id", + port=0, + ) + assert_matches_type(object, devbox, path=["response"]) @parametrize def test_raw_response_remove_tunnel(self, client: Runloop) -> None: - response = client.devboxes.with_raw_response.remove_tunnel( - id="id", - port=0, - ) + with pytest.warns(DeprecationWarning): + response = client.devboxes.with_raw_response.remove_tunnel( + id="id", + port=0, + ) assert response.is_closed is True assert response.http_request.headers.get("X-Stainless-Lang") == "python" @@ -735,25 +738,27 @@ def test_raw_response_remove_tunnel(self, client: Runloop) -> None: @parametrize def test_streaming_response_remove_tunnel(self, client: Runloop) -> None: - with client.devboxes.with_streaming_response.remove_tunnel( - id="id", - port=0, - ) as response: - assert not response.is_closed - assert response.http_request.headers.get("X-Stainless-Lang") == "python" + with pytest.warns(DeprecationWarning): + with client.devboxes.with_streaming_response.remove_tunnel( + id="id", + port=0, + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" - devbox = response.parse() - assert_matches_type(object, devbox, path=["response"]) + devbox = response.parse() + assert_matches_type(object, devbox, path=["response"]) assert cast(Any, response.is_closed) is True @parametrize def test_path_params_remove_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.remove_tunnel( - id="", - port=0, - ) + with pytest.warns(DeprecationWarning): + with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): + client.devboxes.with_raw_response.remove_tunnel( + id="", + port=0, + ) @parametrize def test_method_resume(self, client: Runloop) -> None: @@ -2276,18 +2281,21 @@ async def test_path_params_read_file_contents(self, async_client: AsyncRunloop) @parametrize async def test_method_remove_tunnel(self, async_client: AsyncRunloop) -> None: - devbox = await async_client.devboxes.remove_tunnel( - id="id", - port=0, - ) + with pytest.warns(DeprecationWarning): + devbox = await async_client.devboxes.remove_tunnel( + id="id", + port=0, + ) + assert_matches_type(object, devbox, path=["response"]) @parametrize async def test_raw_response_remove_tunnel(self, async_client: AsyncRunloop) -> None: - response = await async_client.devboxes.with_raw_response.remove_tunnel( - id="id", - port=0, - ) + with pytest.warns(DeprecationWarning): + response = await async_client.devboxes.with_raw_response.remove_tunnel( + id="id", + port=0, + ) assert response.is_closed is True assert response.http_request.headers.get("X-Stainless-Lang") == "python" @@ -2296,25 +2304,27 @@ async def test_raw_response_remove_tunnel(self, async_client: AsyncRunloop) -> N @parametrize async def test_streaming_response_remove_tunnel(self, async_client: AsyncRunloop) -> None: - async with async_client.devboxes.with_streaming_response.remove_tunnel( - id="id", - port=0, - ) as response: - assert not response.is_closed - assert response.http_request.headers.get("X-Stainless-Lang") == "python" + with pytest.warns(DeprecationWarning): + async with async_client.devboxes.with_streaming_response.remove_tunnel( + id="id", + port=0, + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" - devbox = await response.parse() - assert_matches_type(object, devbox, path=["response"]) + devbox = await response.parse() + assert_matches_type(object, devbox, path=["response"]) assert cast(Any, response.is_closed) is True @parametrize async def test_path_params_remove_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.remove_tunnel( - id="", - port=0, - ) + with pytest.warns(DeprecationWarning): + with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): + await async_client.devboxes.with_raw_response.remove_tunnel( + id="", + port=0, + ) @parametrize async def test_method_resume(self, async_client: AsyncRunloop) -> None: From 4deb4fa5c870b26691de56ee4ff0fabf292e1a63 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Fri, 30 Jan 2026 20:27:05 +0000 Subject: [PATCH 6/7] release: 1.4.0 --- .release-please-manifest.json | 2 +- CHANGELOG.md | 16 ++++++++++++++++ pyproject.toml | 2 +- src/runloop_api_client/_version.py | 2 +- 4 files changed, 19 insertions(+), 3 deletions(-) diff --git a/.release-please-manifest.json b/.release-please-manifest.json index c658eefef..3e9af1b3a 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,3 +1,3 @@ { - ".": "1.3.2" + ".": "1.4.0" } \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index 97b063ea7..6adca673a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,21 @@ # Changelog +## 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) + +### Features + +* **client:** add custom JSON encoder for extended type support ([840addd](https://github.com/runloopai/api-client-python/commit/840addd8884fd93ede0c9bbc2a16cf399d6372d0)) +* **devbox:** add gateway routes ([#7212](https://github.com/runloopai/api-client-python/issues/7212)) ([2527bb7](https://github.com/runloopai/api-client-python/commit/2527bb7714f9ce9114964286bfbb35582ceaa976)) +* **devbox:** add new tunnel APIs and deprecate old tunnel API ([#7227](https://github.com/runloopai/api-client-python/issues/7227)) ([71620a1](https://github.com/runloopai/api-client-python/commit/71620a114080506898864b6d73378d4ff30b1070)) + + +### Chores + +* **devbox:** deprecate remove tunnel API ([#7230](https://github.com/runloopai/api-client-python/issues/7230)) ([d48832d](https://github.com/runloopai/api-client-python/commit/d48832d0aa076bd621decae3f5dadca7141c29d3)) +* **documentation:** made warning message language more accurate ([#7215](https://github.com/runloopai/api-client-python/issues/7215)) ([08e0586](https://github.com/runloopai/api-client-python/commit/08e058660cf02e67701899e3b46173c5e9011b7d)) + ## 1.3.2 (2026-01-30) Full Changelog: [v1.3.1...v1.3.2](https://github.com/runloopai/api-client-python/compare/v1.3.1...v1.3.2) diff --git a/pyproject.toml b/pyproject.toml index f62c63eb8..2879cccda 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "runloop_api_client" -version = "1.3.2" +version = "1.4.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 521e1d656..0d6b7e275 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.3.2" # x-release-please-version +__version__ = "1.4.0" # x-release-please-version From a3dc231248f25f24057ec03afb4f9f6076abf4ac Mon Sep 17 00:00:00 2001 From: Albert Li Date: Fri, 30 Jan 2026 12:30:59 -0800 Subject: [PATCH 7/7] Deprecate APIs --- .../resources/devboxes/devboxes.py | 8 +++++++ src/runloop_api_client/sdk/async_devbox.py | 21 ++++++++++++------- src/runloop_api_client/sdk/devbox.py | 21 ++++++++++++------- uv.lock | 2 +- 4 files changed, 35 insertions(+), 17 deletions(-) diff --git a/src/runloop_api_client/resources/devboxes/devboxes.py b/src/runloop_api_client/resources/devboxes/devboxes.py index 5080c0845..ea563fa59 100644 --- a/src/runloop_api_client/resources/devboxes/devboxes.py +++ b/src/runloop_api_client/resources/devboxes/devboxes.py @@ -494,6 +494,7 @@ def create_and_await_running( entrypoint: Optional[str] | Omit = omit, environment_variables: Optional[Dict[str, str]] | Omit = omit, file_mounts: Optional[Dict[str, str]] | Omit = omit, + gateways: Optional[Dict[str, devbox_create_params.Gateways]] | Omit = omit, launch_parameters: Optional[LaunchParameters] | Omit = omit, metadata: Optional[Dict[str, str]] | Omit = omit, mounts: Optional[Iterable[Mount]] | Omit = omit, @@ -502,6 +503,7 @@ def create_and_await_running( repo_connection_id: Optional[str] | Omit = omit, secrets: Optional[Dict[str, str]] | Omit = omit, snapshot_id: Optional[str] | Omit = omit, + tunnel: Optional[devbox_create_params.Tunnel] | 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, @@ -533,6 +535,7 @@ def create_and_await_running( entrypoint=entrypoint, environment_variables=environment_variables, file_mounts=file_mounts, + gateways=gateways, launch_parameters=launch_parameters, metadata=metadata, mounts=mounts, @@ -540,6 +543,7 @@ def create_and_await_running( repo_connection_id=repo_connection_id, secrets=secrets, snapshot_id=snapshot_id, + tunnel=tunnel, extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, @@ -1900,6 +1904,7 @@ async def create_and_await_running( entrypoint: Optional[str] | Omit = omit, environment_variables: Optional[Dict[str, str]] | Omit = omit, file_mounts: Optional[Dict[str, str]] | Omit = omit, + gateways: Optional[Dict[str, devbox_create_params.Gateways]] | Omit = omit, launch_parameters: Optional[LaunchParameters] | Omit = omit, metadata: Optional[Dict[str, str]] | Omit = omit, mounts: Optional[Iterable[Mount]] | Omit = omit, @@ -1908,6 +1913,7 @@ async def create_and_await_running( repo_connection_id: Optional[str] | Omit = omit, secrets: Optional[Dict[str, str]] | Omit = omit, snapshot_id: Optional[str] | Omit = omit, + tunnel: Optional[devbox_create_params.Tunnel] | 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, @@ -1940,6 +1946,7 @@ async def create_and_await_running( entrypoint=entrypoint, environment_variables=environment_variables, file_mounts=file_mounts, + gateways=gateways, launch_parameters=launch_parameters, metadata=metadata, mounts=mounts, @@ -1947,6 +1954,7 @@ async def create_and_await_running( repo_connection_id=repo_connection_id, secrets=secrets, snapshot_id=snapshot_id, + tunnel=tunnel, extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, diff --git a/src/runloop_api_client/sdk/async_devbox.py b/src/runloop_api_client/sdk/async_devbox.py index 5f5acc01d..7e327826f 100644 --- a/src/runloop_api_client/sdk/async_devbox.py +++ b/src/runloop_api_client/sdk/async_devbox.py @@ -4,6 +4,7 @@ import asyncio import logging +import warnings from typing import TYPE_CHECKING, Any, Callable, Optional, Sequence, Awaitable, cast from typing_extensions import Unpack, override @@ -740,10 +741,12 @@ async def create_tunnel( >>> tunnel = await devbox.net.create_tunnel(port=8080) >>> print(f"Public URL: {tunnel.url}") """ - return await self._devbox._client.devboxes.create_tunnel( - self._devbox.id, - **params, - ) + with warnings.catch_warnings(): + warnings.simplefilter("ignore", DeprecationWarning) + return await self._devbox._client.devboxes.create_tunnel( # type: ignore[deprecated] + self._devbox.id, + **params, + ) async def remove_tunnel( self, @@ -758,7 +761,9 @@ async def remove_tunnel( Example: >>> await devbox.net.remove_tunnel(port=8080) """ - return await self._devbox._client.devboxes.remove_tunnel( - self._devbox.id, - **params, - ) + with warnings.catch_warnings(): + warnings.simplefilter("ignore", DeprecationWarning) + return await self._devbox._client.devboxes.remove_tunnel( # type: ignore[deprecated] + self._devbox.id, + **params, + ) diff --git a/src/runloop_api_client/sdk/devbox.py b/src/runloop_api_client/sdk/devbox.py index dee6261ab..0c11955a3 100644 --- a/src/runloop_api_client/sdk/devbox.py +++ b/src/runloop_api_client/sdk/devbox.py @@ -3,6 +3,7 @@ from __future__ import annotations import logging +import warnings import threading from typing import TYPE_CHECKING, Any, Callable, Optional, Sequence from typing_extensions import Unpack, override @@ -743,10 +744,12 @@ def create_tunnel( >>> tunnel = devbox.net.create_tunnel(port=8080) >>> print(f"Public URL: {tunnel.url}") """ - return self._devbox._client.devboxes.create_tunnel( - self._devbox.id, - **params, - ) + with warnings.catch_warnings(): + warnings.simplefilter("ignore", DeprecationWarning) + return self._devbox._client.devboxes.create_tunnel( # type: ignore[deprecated] + self._devbox.id, + **params, + ) def remove_tunnel( self, @@ -761,7 +764,9 @@ def remove_tunnel( Example: >>> devbox.net.remove_tunnel(port=8080) """ - return self._devbox._client.devboxes.remove_tunnel( - self._devbox.id, - **params, - ) + with warnings.catch_warnings(): + warnings.simplefilter("ignore", DeprecationWarning) + return self._devbox._client.devboxes.remove_tunnel( # type: ignore[deprecated] + self._devbox.id, + **params, + ) diff --git a/uv.lock b/uv.lock index 4aa6876fd..bbb1cac9e 100644 --- a/uv.lock +++ b/uv.lock @@ -2237,7 +2237,7 @@ wheels = [ [[package]] name = "runloop-api-client" -version = "1.3.0a0" +version = "1.4.0" source = { editable = "." } dependencies = [ { name = "anyio" },