From 3c2e206de7095e050e83f43ab52e755709bceeac Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Thu, 19 Mar 2026 21:48:31 +0000 Subject: [PATCH 1/2] chore: remove dead port configuration code, mark deprecated / ignored in the API (#8195) --- .stats.yml | 4 ++-- src/runloop_api_client/types/shared/launch_parameters.py | 7 ------- .../types/shared_params/launch_parameters.py | 9 +-------- tests/api_resources/test_benchmarks.py | 2 -- tests/api_resources/test_blueprints.py | 6 ------ tests/api_resources/test_devboxes.py | 2 -- tests/api_resources/test_scenarios.py | 6 ------ 7 files changed, 3 insertions(+), 33 deletions(-) diff --git a/.stats.yml b/.stats.yml index ab8cc33c0..57fda9e71 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 117 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/runloop-ai%2Frunloop-0568973e19e8af9fa953b2ded109ab2b69e76e90e2b74f33617dbf7092e26274.yml -openapi_spec_hash: 10ba804ce69510d7985e05c77d0ffcf6 +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/runloop-ai%2Frunloop-b56067b06cdf0eb4150f81d8f53e46d9c79b8cbecba6f8c0ee82f798d8cd2447.yml +openapi_spec_hash: 4e0020f255cd31baa79227a5888e1eac config_hash: de99cfce88e2d1f02246dc6c2f43bc6c diff --git a/src/runloop_api_client/types/shared/launch_parameters.py b/src/runloop_api_client/types/shared/launch_parameters.py index 0264fa5c8..c96996c72 100644 --- a/src/runloop_api_client/types/shared/launch_parameters.py +++ b/src/runloop_api_client/types/shared/launch_parameters.py @@ -36,13 +36,6 @@ class LaunchParameters(BaseModel): architecture: Optional[Literal["x86_64", "arm64"]] = None """The target architecture for the Devbox. If unset, defaults to x86_64.""" - available_ports: Optional[List[int]] = None - """A list of ports to make available on the Devbox. - - Only ports made available will be surfaced to create tunnels via the - 'createTunnel' API. - """ - custom_cpu_cores: Optional[int] = None """Custom CPU cores. Must be 0.5, 1, or a multiple of 2. Max is 16.""" 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 5c785b9f9..dc0a54f9b 100644 --- a/src/runloop_api_client/types/shared_params/launch_parameters.py +++ b/src/runloop_api_client/types/shared_params/launch_parameters.py @@ -2,7 +2,7 @@ from __future__ import annotations -from typing import Iterable, Optional +from typing import Optional from typing_extensions import Literal, Required, TypedDict from ..._types import SequenceNotStr @@ -38,13 +38,6 @@ class LaunchParameters(TypedDict, total=False): architecture: Optional[Literal["x86_64", "arm64"]] """The target architecture for the Devbox. If unset, defaults to x86_64.""" - available_ports: Optional[Iterable[int]] - """A list of ports to make available on the Devbox. - - Only ports made available will be surfaced to create tunnels via the - 'createTunnel' API. - """ - custom_cpu_cores: Optional[int] """Custom CPU cores. Must be 0.5, 1, or a multiple of 2. Max is 16.""" diff --git a/tests/api_resources/test_benchmarks.py b/tests/api_resources/test_benchmarks.py index 803c75c60..f25be7420 100644 --- a/tests/api_resources/test_benchmarks.py +++ b/tests/api_resources/test_benchmarks.py @@ -291,7 +291,6 @@ def test_method_start_run_with_all_params(self, client: Runloop) -> None: "on_idle": "shutdown", }, "architecture": "x86_64", - "available_ports": [0], "custom_cpu_cores": 0, "custom_disk_size": 0, "custom_gb_memory": 0, @@ -664,7 +663,6 @@ async def test_method_start_run_with_all_params(self, async_client: AsyncRunloop "on_idle": "shutdown", }, "architecture": "x86_64", - "available_ports": [0], "custom_cpu_cores": 0, "custom_disk_size": 0, "custom_gb_memory": 0, diff --git a/tests/api_resources/test_blueprints.py b/tests/api_resources/test_blueprints.py index d325ea2db..460fb9768 100644 --- a/tests/api_resources/test_blueprints.py +++ b/tests/api_resources/test_blueprints.py @@ -56,7 +56,6 @@ def test_method_create_with_all_params(self, client: Runloop) -> None: "on_idle": "shutdown", }, "architecture": "x86_64", - "available_ports": [0], "custom_cpu_cores": 0, "custom_disk_size": 0, "custom_gb_memory": 0, @@ -270,7 +269,6 @@ def test_method_create_from_inspection_with_all_params(self, client: Runloop) -> "on_idle": "shutdown", }, "architecture": "x86_64", - "available_ports": [0], "custom_cpu_cores": 0, "custom_disk_size": 0, "custom_gb_memory": 0, @@ -424,7 +422,6 @@ def test_method_preview_with_all_params(self, client: Runloop) -> None: "on_idle": "shutdown", }, "architecture": "x86_64", - "available_ports": [0], "custom_cpu_cores": 0, "custom_disk_size": 0, "custom_gb_memory": 0, @@ -522,7 +519,6 @@ async def test_method_create_with_all_params(self, async_client: AsyncRunloop) - "on_idle": "shutdown", }, "architecture": "x86_64", - "available_ports": [0], "custom_cpu_cores": 0, "custom_disk_size": 0, "custom_gb_memory": 0, @@ -736,7 +732,6 @@ async def test_method_create_from_inspection_with_all_params(self, async_client: "on_idle": "shutdown", }, "architecture": "x86_64", - "available_ports": [0], "custom_cpu_cores": 0, "custom_disk_size": 0, "custom_gb_memory": 0, @@ -890,7 +885,6 @@ async def test_method_preview_with_all_params(self, async_client: AsyncRunloop) "on_idle": "shutdown", }, "architecture": "x86_64", - "available_ports": [0], "custom_cpu_cores": 0, "custom_disk_size": 0, "custom_gb_memory": 0, diff --git a/tests/api_resources/test_devboxes.py b/tests/api_resources/test_devboxes.py index b7eaeb2f0..209900587 100644 --- a/tests/api_resources/test_devboxes.py +++ b/tests/api_resources/test_devboxes.py @@ -78,7 +78,6 @@ def test_method_create_with_all_params(self, client: Runloop) -> None: "on_idle": "shutdown", }, "architecture": "x86_64", - "available_ports": [0], "custom_cpu_cores": 0, "custom_disk_size": 0, "custom_gb_memory": 0, @@ -1699,7 +1698,6 @@ async def test_method_create_with_all_params(self, async_client: AsyncRunloop) - "on_idle": "shutdown", }, "architecture": "x86_64", - "available_ports": [0], "custom_cpu_cores": 0, "custom_disk_size": 0, "custom_gb_memory": 0, diff --git a/tests/api_resources/test_scenarios.py b/tests/api_resources/test_scenarios.py index 736a8395c..e212f6e81 100644 --- a/tests/api_resources/test_scenarios.py +++ b/tests/api_resources/test_scenarios.py @@ -72,7 +72,6 @@ def test_method_create_with_all_params(self, client: Runloop) -> None: "on_idle": "shutdown", }, "architecture": "x86_64", - "available_ports": [0], "custom_cpu_cores": 0, "custom_disk_size": 0, "custom_gb_memory": 0, @@ -207,7 +206,6 @@ def test_method_update_with_all_params(self, client: Runloop) -> None: "on_idle": "shutdown", }, "architecture": "x86_64", - "available_ports": [0], "custom_cpu_cores": 0, "custom_disk_size": 0, "custom_gb_memory": 0, @@ -413,7 +411,6 @@ def test_method_start_run_with_all_params(self, client: Runloop) -> None: "on_idle": "shutdown", }, "architecture": "x86_64", - "available_ports": [0], "custom_cpu_cores": 0, "custom_disk_size": 0, "custom_gb_memory": 0, @@ -521,7 +518,6 @@ async def test_method_create_with_all_params(self, async_client: AsyncRunloop) - "on_idle": "shutdown", }, "architecture": "x86_64", - "available_ports": [0], "custom_cpu_cores": 0, "custom_disk_size": 0, "custom_gb_memory": 0, @@ -656,7 +652,6 @@ async def test_method_update_with_all_params(self, async_client: AsyncRunloop) - "on_idle": "shutdown", }, "architecture": "x86_64", - "available_ports": [0], "custom_cpu_cores": 0, "custom_disk_size": 0, "custom_gb_memory": 0, @@ -862,7 +857,6 @@ async def test_method_start_run_with_all_params(self, async_client: AsyncRunloop "on_idle": "shutdown", }, "architecture": "x86_64", - "available_ports": [0], "custom_cpu_cores": 0, "custom_disk_size": 0, "custom_gb_memory": 0, From 14ec42baa45a770f4defe7b06d4d0387edabf4bd Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Fri, 20 Mar 2026 02:13:19 +0000 Subject: [PATCH 2/2] fix: sanitize endpoint path params --- src/runloop_api_client/_utils/_path.py | 127 ++++++++++++++++++ src/runloop_api_client/resources/agents.py | 6 +- .../resources/benchmark_jobs.py | 6 +- .../resources/benchmark_runs.py | 18 +-- .../resources/benchmarks.py | 18 +-- .../resources/blueprints.py | 14 +- .../resources/devboxes/browsers.py | 6 +- .../resources/devboxes/computers.py | 18 +-- .../resources/devboxes/devboxes.py | 94 +++++++------ .../resources/devboxes/disk_snapshots.py | 14 +- .../resources/devboxes/logs.py | 6 +- .../resources/gateway_configs.py | 14 +- .../resources/mcp_configs.py | 14 +- .../resources/network_policies.py | 14 +- src/runloop_api_client/resources/objects.py | 18 +-- .../resources/repositories.py | 26 ++-- .../resources/scenarios/runs.py | 22 +-- .../resources/scenarios/scenarios.py | 14 +- .../resources/scenarios/scorers.py | 10 +- src/runloop_api_client/resources/secrets.py | 10 +- tests/test_utils/test_path.py | 89 ++++++++++++ 21 files changed, 391 insertions(+), 167 deletions(-) create mode 100644 src/runloop_api_client/_utils/_path.py create mode 100644 tests/test_utils/test_path.py diff --git a/src/runloop_api_client/_utils/_path.py b/src/runloop_api_client/_utils/_path.py new file mode 100644 index 000000000..4d6e1e4cb --- /dev/null +++ b/src/runloop_api_client/_utils/_path.py @@ -0,0 +1,127 @@ +from __future__ import annotations + +import re +from typing import ( + Any, + Mapping, + Callable, +) +from urllib.parse import quote + +# Matches '.' or '..' where each dot is either literal or percent-encoded (%2e / %2E). +_DOT_SEGMENT_RE = re.compile(r"^(?:\.|%2[eE]){1,2}$") + +_PLACEHOLDER_RE = re.compile(r"\{(\w+)\}") + + +def _quote_path_segment_part(value: str) -> str: + """Percent-encode `value` for use in a URI path segment. + + Considers characters not in `pchar` set from RFC 3986 §3.3 to be unsafe. + https://datatracker.ietf.org/doc/html/rfc3986#section-3.3 + """ + # quote() already treats unreserved characters (letters, digits, and -._~) + # as safe, so we only need to add sub-delims, ':', and '@'. + # Notably, unlike the default `safe` for quote(), / is unsafe and must be quoted. + return quote(value, safe="!$&'()*+,;=:@") + + +def _quote_query_part(value: str) -> str: + """Percent-encode `value` for use in a URI query string. + + Considers &, = and characters not in `query` set from RFC 3986 §3.4 to be unsafe. + https://datatracker.ietf.org/doc/html/rfc3986#section-3.4 + """ + return quote(value, safe="!$'()*+,;:@/?") + + +def _quote_fragment_part(value: str) -> str: + """Percent-encode `value` for use in a URI fragment. + + Considers characters not in `fragment` set from RFC 3986 §3.5 to be unsafe. + https://datatracker.ietf.org/doc/html/rfc3986#section-3.5 + """ + return quote(value, safe="!$&'()*+,;=:@/?") + + +def _interpolate( + template: str, + values: Mapping[str, Any], + quoter: Callable[[str], str], +) -> str: + """Replace {name} placeholders in `template`, quoting each value with `quoter`. + + Placeholder names are looked up in `values`. + + Raises: + KeyError: If a placeholder is not found in `values`. + """ + # re.split with a capturing group returns alternating + # [text, name, text, name, ..., text] elements. + parts = _PLACEHOLDER_RE.split(template) + + for i in range(1, len(parts), 2): + name = parts[i] + if name not in values: + raise KeyError(f"a value for placeholder {{{name}}} was not provided") + val = values[name] + if val is None: + parts[i] = "null" + elif isinstance(val, bool): + parts[i] = "true" if val else "false" + else: + parts[i] = quoter(str(values[name])) + + return "".join(parts) + + +def path_template(template: str, /, **kwargs: Any) -> str: + """Interpolate {name} placeholders in `template` from keyword arguments. + + Args: + template: The template string containing {name} placeholders. + **kwargs: Keyword arguments to interpolate into the template. + + Returns: + The template with placeholders interpolated and percent-encoded. + + Safe characters for percent-encoding are dependent on the URI component. + Placeholders in path and fragment portions are percent-encoded where the `segment` + and `fragment` sets from RFC 3986 respectively are considered safe. + Placeholders in the query portion are percent-encoded where the `query` set from + RFC 3986 §3.3 is considered safe except for = and & characters. + + Raises: + KeyError: If a placeholder is not found in `kwargs`. + ValueError: If resulting path contains /./ or /../ segments (including percent-encoded dot-segments). + """ + # Split the template into path, query, and fragment portions. + fragment_template: str | None = None + query_template: str | None = None + + rest = template + if "#" in rest: + rest, fragment_template = rest.split("#", 1) + if "?" in rest: + rest, query_template = rest.split("?", 1) + path_template = rest + + # Interpolate each portion with the appropriate quoting rules. + path_result = _interpolate(path_template, kwargs, _quote_path_segment_part) + + # Reject dot-segments (. and ..) in the final assembled path. The check + # runs after interpolation so that adjacent placeholders or a mix of static + # text and placeholders that together form a dot-segment are caught. + # Also reject percent-encoded dot-segments to protect against incorrectly + # implemented normalization in servers/proxies. + for segment in path_result.split("/"): + if _DOT_SEGMENT_RE.match(segment): + raise ValueError(f"Constructed path {path_result!r} contains dot-segment {segment!r} which is not allowed") + + result = path_result + if query_template is not None: + result += "?" + _interpolate(query_template, kwargs, _quote_query_part) + if fragment_template is not None: + result += "#" + _interpolate(fragment_template, kwargs, _quote_fragment_part) + + return result diff --git a/src/runloop_api_client/resources/agents.py b/src/runloop_api_client/resources/agents.py index 9ac9f8c02..8c4d108ea 100644 --- a/src/runloop_api_client/resources/agents.py +++ b/src/runloop_api_client/resources/agents.py @@ -8,7 +8,7 @@ from ..types import agent_list_params, agent_create_params from .._types import Body, Omit, Query, Headers, NotGiven, omit, not_given -from .._utils import maybe_transform, async_maybe_transform +from .._utils import path_template, maybe_transform, async_maybe_transform from .._compat import cached_property from .._resource import SyncAPIResource, AsyncAPIResource from .._response import ( @@ -127,7 +127,7 @@ def retrieve( if not id: raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") return self._get( - f"/v1/agents/{id}", + path_template("/v1/agents/{id}", id=id), options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), @@ -300,7 +300,7 @@ async def retrieve( if not id: raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") return await self._get( - f"/v1/agents/{id}", + path_template("/v1/agents/{id}", id=id), options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), diff --git a/src/runloop_api_client/resources/benchmark_jobs.py b/src/runloop_api_client/resources/benchmark_jobs.py index f6172d118..2841df6f9 100644 --- a/src/runloop_api_client/resources/benchmark_jobs.py +++ b/src/runloop_api_client/resources/benchmark_jobs.py @@ -8,7 +8,7 @@ from ..types import benchmark_job_list_params, benchmark_job_create_params from .._types import Body, Omit, Query, Headers, NotGiven, omit, not_given -from .._utils import maybe_transform, async_maybe_transform +from .._utils import path_template, maybe_transform, async_maybe_transform from .._compat import cached_property from .._resource import SyncAPIResource, AsyncAPIResource from .._response import ( @@ -121,7 +121,7 @@ def retrieve( if not id: raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") return self._get( - f"/v1/benchmark_jobs/{id}", + path_template("/v1/benchmark_jobs/{id}", id=id), options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), @@ -276,7 +276,7 @@ async def retrieve( if not id: raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") return await self._get( - f"/v1/benchmark_jobs/{id}", + path_template("/v1/benchmark_jobs/{id}", id=id), options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), diff --git a/src/runloop_api_client/resources/benchmark_runs.py b/src/runloop_api_client/resources/benchmark_runs.py index 964497cde..b889244a7 100644 --- a/src/runloop_api_client/resources/benchmark_runs.py +++ b/src/runloop_api_client/resources/benchmark_runs.py @@ -8,7 +8,7 @@ from ..types import benchmark_run_list_params, benchmark_run_list_scenario_runs_params from .._types import Body, Omit, Query, Headers, NotGiven, omit, not_given -from .._utils import maybe_transform +from .._utils import path_template, maybe_transform from .._compat import cached_property from .._resource import SyncAPIResource, AsyncAPIResource from .._response import ( @@ -71,7 +71,7 @@ def retrieve( if not id: raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") return self._get( - f"/v1/benchmark_runs/{id}", + path_template("/v1/benchmark_runs/{id}", id=id), options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), @@ -165,7 +165,7 @@ def cancel( if not id: raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") return self._post( - f"/v1/benchmark_runs/{id}/cancel", + path_template("/v1/benchmark_runs/{id}/cancel", id=id), options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, @@ -205,7 +205,7 @@ def complete( if not id: raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") return self._post( - f"/v1/benchmark_runs/{id}/complete", + path_template("/v1/benchmark_runs/{id}/complete", id=id), options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, @@ -251,7 +251,7 @@ def list_scenario_runs( if not id: raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") return self._get_api_list( - f"/v1/benchmark_runs/{id}/scenario_runs", + path_template("/v1/benchmark_runs/{id}/scenario_runs", id=id), page=SyncBenchmarkRunsCursorIDPage[ScenarioRunView], options=make_request_options( extra_headers=extra_headers, @@ -317,7 +317,7 @@ async def retrieve( if not id: raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") return await self._get( - f"/v1/benchmark_runs/{id}", + path_template("/v1/benchmark_runs/{id}", id=id), options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), @@ -411,7 +411,7 @@ async def cancel( if not id: raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") return await self._post( - f"/v1/benchmark_runs/{id}/cancel", + path_template("/v1/benchmark_runs/{id}/cancel", id=id), options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, @@ -451,7 +451,7 @@ async def complete( if not id: raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") return await self._post( - f"/v1/benchmark_runs/{id}/complete", + path_template("/v1/benchmark_runs/{id}/complete", id=id), options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, @@ -497,7 +497,7 @@ def list_scenario_runs( if not id: raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") return self._get_api_list( - f"/v1/benchmark_runs/{id}/scenario_runs", + path_template("/v1/benchmark_runs/{id}/scenario_runs", id=id), page=AsyncBenchmarkRunsCursorIDPage[ScenarioRunView], options=make_request_options( extra_headers=extra_headers, diff --git a/src/runloop_api_client/resources/benchmarks.py b/src/runloop_api_client/resources/benchmarks.py index d23992bd2..fecf3e422 100644 --- a/src/runloop_api_client/resources/benchmarks.py +++ b/src/runloop_api_client/resources/benchmarks.py @@ -16,7 +16,7 @@ benchmark_update_scenarios_params, ) from .._types import Body, Omit, Query, Headers, NotGiven, SequenceNotStr, omit, not_given -from .._utils import maybe_transform, async_maybe_transform +from .._utils import path_template, maybe_transform, async_maybe_transform from .._compat import cached_property from .._resource import SyncAPIResource, AsyncAPIResource from .._response import ( @@ -154,7 +154,7 @@ def retrieve( if not id: raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") return self._get( - f"/v1/benchmarks/{id}", + path_template("/v1/benchmarks/{id}", id=id), options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), @@ -218,7 +218,7 @@ def update( if not id: raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") return self._post( - f"/v1/benchmarks/{id}", + path_template("/v1/benchmarks/{id}", id=id), body=maybe_transform( { "attribution": attribution, @@ -324,7 +324,7 @@ def definitions( if not id: raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") return self._get( - f"/v1/benchmarks/{id}/definitions", + path_template("/v1/benchmarks/{id}/definitions", id=id), options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, @@ -481,7 +481,7 @@ def update_scenarios( if not id: raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") return self._post( - f"/v1/benchmarks/{id}/scenarios", + path_template("/v1/benchmarks/{id}/scenarios", id=id), body=maybe_transform( { "scenarios_to_add": scenarios_to_add, @@ -619,7 +619,7 @@ async def retrieve( if not id: raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") return await self._get( - f"/v1/benchmarks/{id}", + path_template("/v1/benchmarks/{id}", id=id), options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), @@ -683,7 +683,7 @@ async def update( if not id: raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") return await self._post( - f"/v1/benchmarks/{id}", + path_template("/v1/benchmarks/{id}", id=id), body=await async_maybe_transform( { "attribution": attribution, @@ -789,7 +789,7 @@ async def definitions( if not id: raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") return await self._get( - f"/v1/benchmarks/{id}/definitions", + path_template("/v1/benchmarks/{id}/definitions", id=id), options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, @@ -946,7 +946,7 @@ async def update_scenarios( if not id: raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") return await self._post( - f"/v1/benchmarks/{id}/scenarios", + path_template("/v1/benchmarks/{id}/scenarios", id=id), body=await async_maybe_transform( { "scenarios_to_add": scenarios_to_add, diff --git a/src/runloop_api_client/resources/blueprints.py b/src/runloop_api_client/resources/blueprints.py index 39eae98aa..6c6082054 100644 --- a/src/runloop_api_client/resources/blueprints.py +++ b/src/runloop_api_client/resources/blueprints.py @@ -14,7 +14,7 @@ blueprint_create_from_inspection_params, ) from .._types import NOT_GIVEN, Body, Omit, Query, Headers, NotGiven, SequenceNotStr, omit, not_given -from .._utils import is_given, maybe_transform, async_maybe_transform +from .._utils import is_given, path_template, maybe_transform, async_maybe_transform from .._compat import cached_property from .._resource import SyncAPIResource, AsyncAPIResource from .._response import ( @@ -268,7 +268,7 @@ def retrieve( if not id: raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") return self._get( - f"/v1/blueprints/{id}", + path_template("/v1/blueprints/{id}", id=id), options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), @@ -480,7 +480,7 @@ def delete( if not id: raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") return self._post( - f"/v1/blueprints/{id}/delete", + path_template("/v1/blueprints/{id}/delete", id=id), options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, @@ -652,7 +652,7 @@ def logs( if not id: raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") return self._get( - f"/v1/blueprints/{id}/logs", + path_template("/v1/blueprints/{id}/logs", id=id), options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), @@ -938,7 +938,7 @@ async def retrieve( if not id: raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") return await self._get( - f"/v1/blueprints/{id}", + path_template("/v1/blueprints/{id}", id=id), options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), @@ -1150,7 +1150,7 @@ async def delete( if not id: raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") return await self._post( - f"/v1/blueprints/{id}/delete", + path_template("/v1/blueprints/{id}/delete", id=id), options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, @@ -1322,7 +1322,7 @@ async def logs( if not id: raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") return await self._get( - f"/v1/blueprints/{id}/logs", + path_template("/v1/blueprints/{id}/logs", id=id), options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), diff --git a/src/runloop_api_client/resources/devboxes/browsers.py b/src/runloop_api_client/resources/devboxes/browsers.py index 517857f56..d2977464b 100644 --- a/src/runloop_api_client/resources/devboxes/browsers.py +++ b/src/runloop_api_client/resources/devboxes/browsers.py @@ -7,7 +7,7 @@ import httpx from ..._types import Body, Omit, Query, Headers, NotGiven, omit, not_given -from ..._utils import maybe_transform, async_maybe_transform +from ..._utils import path_template, maybe_transform, async_maybe_transform from ..._compat import cached_property from ..._resource import SyncAPIResource, AsyncAPIResource from ..._response import ( @@ -113,7 +113,7 @@ def retrieve( if not id: raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") return self._get( - f"/v1/devboxes/browsers/{id}", + path_template("/v1/devboxes/browsers/{id}", id=id), options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), @@ -211,7 +211,7 @@ async def retrieve( if not id: raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") return await self._get( - f"/v1/devboxes/browsers/{id}", + path_template("/v1/devboxes/browsers/{id}", id=id), options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), diff --git a/src/runloop_api_client/resources/devboxes/computers.py b/src/runloop_api_client/resources/devboxes/computers.py index be051db2e..4eddfccc8 100644 --- a/src/runloop_api_client/resources/devboxes/computers.py +++ b/src/runloop_api_client/resources/devboxes/computers.py @@ -8,7 +8,7 @@ import httpx from ..._types import Body, Omit, Query, Headers, NotGiven, omit, not_given -from ..._utils import maybe_transform, async_maybe_transform +from ..._utils import path_template, maybe_transform, async_maybe_transform from ..._compat import cached_property from ..._resource import SyncAPIResource, AsyncAPIResource from ..._response import ( @@ -133,7 +133,7 @@ def retrieve( if not id: raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") return self._get( - f"/v1/devboxes/computers/{id}", + path_template("/v1/devboxes/computers/{id}", id=id), options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), @@ -176,7 +176,7 @@ def keyboard_interaction( if not id: raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") return self._post( - f"/v1/devboxes/computers/{id}/keyboard_interaction", + path_template("/v1/devboxes/computers/{id}/keyboard_interaction", id=id), body=maybe_transform( { "action": action, @@ -232,7 +232,7 @@ def mouse_interaction( if not id: raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") return self._post( - f"/v1/devboxes/computers/{id}/mouse_interaction", + path_template("/v1/devboxes/computers/{id}/mouse_interaction", id=id), body=maybe_transform( { "action": action, @@ -283,7 +283,7 @@ def screen_interaction( if not id: raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") return self._post( - f"/v1/devboxes/computers/{id}/screen_interaction", + path_template("/v1/devboxes/computers/{id}/screen_interaction", id=id), body=maybe_transform( {"action": action}, computer_screen_interaction_params.ComputerScreenInteractionParams ), @@ -399,7 +399,7 @@ async def retrieve( if not id: raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") return await self._get( - f"/v1/devboxes/computers/{id}", + path_template("/v1/devboxes/computers/{id}", id=id), options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), @@ -442,7 +442,7 @@ async def keyboard_interaction( if not id: raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") return await self._post( - f"/v1/devboxes/computers/{id}/keyboard_interaction", + path_template("/v1/devboxes/computers/{id}/keyboard_interaction", id=id), body=await async_maybe_transform( { "action": action, @@ -498,7 +498,7 @@ async def mouse_interaction( if not id: raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") return await self._post( - f"/v1/devboxes/computers/{id}/mouse_interaction", + path_template("/v1/devboxes/computers/{id}/mouse_interaction", id=id), body=await async_maybe_transform( { "action": action, @@ -549,7 +549,7 @@ async def screen_interaction( if not id: raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") return await self._post( - f"/v1/devboxes/computers/{id}/screen_interaction", + path_template("/v1/devboxes/computers/{id}/screen_interaction", id=id), body=await async_maybe_transform( {"action": action}, computer_screen_interaction_params.ComputerScreenInteractionParams ), diff --git a/src/runloop_api_client/resources/devboxes/devboxes.py b/src/runloop_api_client/resources/devboxes/devboxes.py index 406af8626..6e9f52d92 100644 --- a/src/runloop_api_client/resources/devboxes/devboxes.py +++ b/src/runloop_api_client/resources/devboxes/devboxes.py @@ -39,7 +39,7 @@ devbox_write_file_contents_params, ) from ..._types import Body, Omit, Query, Headers, NotGiven, FileTypes, omit, not_given -from ..._utils import is_given, extract_files, maybe_transform, deepcopy_minimal, async_maybe_transform +from ..._utils import is_given, extract_files, path_template, maybe_transform, deepcopy_minimal, async_maybe_transform from .browsers import ( BrowsersResource, AsyncBrowsersResource, @@ -335,7 +335,7 @@ def retrieve( if not id: raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") return self._get( - f"/v1/devboxes/{id}", + path_template("/v1/devboxes/{id}", id=id), options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), @@ -378,7 +378,7 @@ def update( if not id: raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") return self._post( - f"/v1/devboxes/{id}", + path_template("/v1/devboxes/{id}", id=id), body=maybe_transform( { "metadata": metadata, @@ -651,7 +651,7 @@ def create_ssh_key( if not id: raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") return self._post( - f"/v1/devboxes/{id}/create_ssh_key", + path_template("/v1/devboxes/{id}/create_ssh_key", id=id), options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, @@ -691,7 +691,7 @@ def delete_disk_snapshot( if not id: raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") return self._post( - f"/v1/devboxes/disk_snapshots/{id}/delete", + path_template("/v1/devboxes/disk_snapshots/{id}/delete", id=id), options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, @@ -739,7 +739,7 @@ def download_file( timeout = 600 extra_headers = {"Accept": "application/octet-stream", **(extra_headers or {})} return self._post( - f"/v1/devboxes/{id}/download_file", + path_template("/v1/devboxes/{id}/download_file", id=id), body=maybe_transform({"path": path}, devbox_download_file_params.DevboxDownloadFileParams), options=make_request_options( extra_headers=extra_headers, @@ -792,7 +792,7 @@ def enable_tunnel( if not id: raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") return self._post( - f"/v1/devboxes/{id}/enable_tunnel", + path_template("/v1/devboxes/{id}/enable_tunnel", id=id), body=maybe_transform( { "auth_mode": auth_mode, @@ -869,7 +869,7 @@ def execute( if not is_given(timeout) and self._client.timeout == DEFAULT_TIMEOUT: timeout = 600 return self._post( - f"/v1/devboxes/{id}/execute", + path_template("/v1/devboxes/{id}/execute", id=id), body=maybe_transform( { "command": command, @@ -997,7 +997,7 @@ def execute_async( if not id: raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") return self._post( - f"/v1/devboxes/{id}/execute_async", + path_template("/v1/devboxes/{id}/execute_async", id=id), body=maybe_transform( { "command": command, @@ -1069,7 +1069,7 @@ def execute_sync( if not is_given(timeout) and self._client.timeout == DEFAULT_TIMEOUT: timeout = 600 return self._post( - f"/v1/devboxes/{id}/execute_sync", + path_template("/v1/devboxes/{id}/execute_sync", id=id), body=maybe_transform( { "command": command, @@ -1118,7 +1118,7 @@ def keep_alive( if not id: raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") return self._post( - f"/v1/devboxes/{id}/keep_alive", + path_template("/v1/devboxes/{id}/keep_alive", id=id), options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, @@ -1233,7 +1233,7 @@ def read_file_contents( timeout = 600 extra_headers = {"Accept": "text/plain", **(extra_headers or {})} return self._post( - f"/v1/devboxes/{id}/read_file_contents", + path_template("/v1/devboxes/{id}/read_file_contents", id=id), body=maybe_transform( {"file_path": file_path}, devbox_read_file_contents_params.DevboxReadFileContentsParams ), @@ -1284,7 +1284,7 @@ def remove_tunnel( if not id: raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") return self._post( - f"/v1/devboxes/{id}/remove_tunnel", + path_template("/v1/devboxes/{id}/remove_tunnel", id=id), body=maybe_transform({"port": port}, devbox_remove_tunnel_params.DevboxRemoveTunnelParams), options=make_request_options( extra_headers=extra_headers, @@ -1328,7 +1328,7 @@ def resume( if not id: raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") return self._post( - f"/v1/devboxes/{id}/resume", + path_template("/v1/devboxes/{id}/resume", id=id), options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, @@ -1369,7 +1369,7 @@ def retrieve_resource_usage( if not id: raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") return self._get( - f"/v1/devboxes/{id}/usage", + path_template("/v1/devboxes/{id}/usage", id=id), options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), @@ -1413,7 +1413,7 @@ def shutdown( if not id: raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") return self._post( - f"/v1/devboxes/{id}/shutdown", + path_template("/v1/devboxes/{id}/shutdown", id=id), options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, @@ -1466,7 +1466,7 @@ def snapshot_disk( if not is_given(timeout) and self._client.timeout == DEFAULT_TIMEOUT: timeout = 600 return self._post( - f"/v1/devboxes/{id}/snapshot_disk", + path_template("/v1/devboxes/{id}/snapshot_disk", id=id), body=maybe_transform( { "commit_message": commit_message, @@ -1525,7 +1525,7 @@ def snapshot_disk_async( if not id: raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") return self._post( - f"/v1/devboxes/{id}/snapshot_disk_async", + path_template("/v1/devboxes/{id}/snapshot_disk_async", id=id), body=maybe_transform( { "commit_message": commit_message, @@ -1575,7 +1575,7 @@ def suspend( if not id: raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") return self._post( - f"/v1/devboxes/{id}/suspend", + path_template("/v1/devboxes/{id}/suspend", id=id), options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, @@ -1636,7 +1636,7 @@ def upload_file( # multipart/form-data; boundary=---abc-- extra_headers = {"Content-Type": "multipart/form-data", **(extra_headers or {})} return self._post( - f"/v1/devboxes/{id}/upload_file", + path_template("/v1/devboxes/{id}/upload_file", id=id), body=maybe_transform(body, devbox_upload_file_params.DevboxUploadFileParams), files=files, options=make_request_options( @@ -1694,7 +1694,11 @@ def wait_for_command( if not execution_id: raise ValueError(f"Expected a non-empty value for `execution_id` but received {execution_id!r}") return self._post( - f"/v1/devboxes/{devbox_id}/executions/{execution_id}/wait_for_status", + path_template( + "/v1/devboxes/{devbox_id}/executions/{execution_id}/wait_for_status", + devbox_id=devbox_id, + execution_id=execution_id, + ), body=maybe_transform( { "statuses": statuses, @@ -1753,7 +1757,7 @@ def write_file_contents( if not is_given(timeout) and self._client.timeout == DEFAULT_TIMEOUT: timeout = 600 return self._post( - f"/v1/devboxes/{id}/write_file_contents", + path_template("/v1/devboxes/{id}/write_file_contents", id=id), body=maybe_transform( { "contents": contents, @@ -1969,7 +1973,7 @@ async def retrieve( if not id: raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") return await self._get( - f"/v1/devboxes/{id}", + path_template("/v1/devboxes/{id}", id=id), options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), @@ -2181,7 +2185,7 @@ async def update( if not id: raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") return await self._post( - f"/v1/devboxes/{id}", + path_template("/v1/devboxes/{id}", id=id), body=await async_maybe_transform( { "metadata": metadata, @@ -2282,7 +2286,7 @@ async def create_ssh_key( if not id: raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") return await self._post( - f"/v1/devboxes/{id}/create_ssh_key", + path_template("/v1/devboxes/{id}/create_ssh_key", id=id), options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, @@ -2322,7 +2326,7 @@ async def delete_disk_snapshot( if not id: raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") return await self._post( - f"/v1/devboxes/disk_snapshots/{id}/delete", + path_template("/v1/devboxes/disk_snapshots/{id}/delete", id=id), options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, @@ -2370,7 +2374,7 @@ async def download_file( timeout = 600 extra_headers = {"Accept": "application/octet-stream", **(extra_headers or {})} return await self._post( - f"/v1/devboxes/{id}/download_file", + path_template("/v1/devboxes/{id}/download_file", id=id), body=await async_maybe_transform({"path": path}, devbox_download_file_params.DevboxDownloadFileParams), options=make_request_options( extra_headers=extra_headers, @@ -2423,7 +2427,7 @@ async def enable_tunnel( if not id: raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") return await self._post( - f"/v1/devboxes/{id}/enable_tunnel", + path_template("/v1/devboxes/{id}/enable_tunnel", id=id), body=await async_maybe_transform( { "auth_mode": auth_mode, @@ -2500,7 +2504,7 @@ async def execute( if not is_given(timeout) and self._client.timeout == DEFAULT_TIMEOUT: timeout = 600 return await self._post( - f"/v1/devboxes/{id}/execute", + path_template("/v1/devboxes/{id}/execute", id=id), body=await async_maybe_transform( { "command": command, @@ -2627,7 +2631,7 @@ async def execute_async( if not id: raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") return await self._post( - f"/v1/devboxes/{id}/execute_async", + path_template("/v1/devboxes/{id}/execute_async", id=id), body=await async_maybe_transform( { "command": command, @@ -2699,7 +2703,7 @@ async def execute_sync( if not is_given(timeout) and self._client.timeout == DEFAULT_TIMEOUT: timeout = 600 return await self._post( - f"/v1/devboxes/{id}/execute_sync", + path_template("/v1/devboxes/{id}/execute_sync", id=id), body=await async_maybe_transform( { "command": command, @@ -2748,7 +2752,7 @@ async def keep_alive( if not id: raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") return await self._post( - f"/v1/devboxes/{id}/keep_alive", + path_template("/v1/devboxes/{id}/keep_alive", id=id), options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, @@ -2863,7 +2867,7 @@ async def read_file_contents( timeout = 600 extra_headers = {"Accept": "text/plain", **(extra_headers or {})} return await self._post( - f"/v1/devboxes/{id}/read_file_contents", + path_template("/v1/devboxes/{id}/read_file_contents", id=id), body=await async_maybe_transform( {"file_path": file_path}, devbox_read_file_contents_params.DevboxReadFileContentsParams ), @@ -2914,7 +2918,7 @@ async def remove_tunnel( if not id: raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") return await self._post( - f"/v1/devboxes/{id}/remove_tunnel", + path_template("/v1/devboxes/{id}/remove_tunnel", id=id), body=await async_maybe_transform({"port": port}, devbox_remove_tunnel_params.DevboxRemoveTunnelParams), options=make_request_options( extra_headers=extra_headers, @@ -2958,7 +2962,7 @@ async def resume( if not id: raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") return await self._post( - f"/v1/devboxes/{id}/resume", + path_template("/v1/devboxes/{id}/resume", id=id), options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, @@ -2999,7 +3003,7 @@ async def retrieve_resource_usage( if not id: raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") return await self._get( - f"/v1/devboxes/{id}/usage", + path_template("/v1/devboxes/{id}/usage", id=id), options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), @@ -3043,7 +3047,7 @@ async def shutdown( if not id: raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") return await self._post( - f"/v1/devboxes/{id}/shutdown", + path_template("/v1/devboxes/{id}/shutdown", id=id), options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, @@ -3096,7 +3100,7 @@ async def snapshot_disk( if not is_given(timeout) and self._client.timeout == DEFAULT_TIMEOUT: timeout = 600 return await self._post( - f"/v1/devboxes/{id}/snapshot_disk", + path_template("/v1/devboxes/{id}/snapshot_disk", id=id), body=await async_maybe_transform( { "commit_message": commit_message, @@ -3155,7 +3159,7 @@ async def snapshot_disk_async( if not id: raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") return await self._post( - f"/v1/devboxes/{id}/snapshot_disk_async", + path_template("/v1/devboxes/{id}/snapshot_disk_async", id=id), body=await async_maybe_transform( { "commit_message": commit_message, @@ -3205,7 +3209,7 @@ async def suspend( if not id: raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") return await self._post( - f"/v1/devboxes/{id}/suspend", + path_template("/v1/devboxes/{id}/suspend", id=id), options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, @@ -3266,7 +3270,7 @@ async def upload_file( # multipart/form-data; boundary=---abc-- extra_headers = {"Content-Type": "multipart/form-data", **(extra_headers or {})} return await self._post( - f"/v1/devboxes/{id}/upload_file", + path_template("/v1/devboxes/{id}/upload_file", id=id), body=await async_maybe_transform(body, devbox_upload_file_params.DevboxUploadFileParams), files=files, options=make_request_options( @@ -3324,7 +3328,11 @@ async def wait_for_command( if not execution_id: raise ValueError(f"Expected a non-empty value for `execution_id` but received {execution_id!r}") return await self._post( - f"/v1/devboxes/{devbox_id}/executions/{execution_id}/wait_for_status", + path_template( + "/v1/devboxes/{devbox_id}/executions/{execution_id}/wait_for_status", + devbox_id=devbox_id, + execution_id=execution_id, + ), body=await async_maybe_transform( { "statuses": statuses, @@ -3385,7 +3393,7 @@ async def write_file_contents( if not is_given(timeout) and self._client.timeout == DEFAULT_TIMEOUT: timeout = 600 return await self._post( - f"/v1/devboxes/{id}/write_file_contents", + path_template("/v1/devboxes/{id}/write_file_contents", id=id), body=await async_maybe_transform( { "contents": contents, diff --git a/src/runloop_api_client/resources/devboxes/disk_snapshots.py b/src/runloop_api_client/resources/devboxes/disk_snapshots.py index b896adbb6..d6d43c26f 100644 --- a/src/runloop_api_client/resources/devboxes/disk_snapshots.py +++ b/src/runloop_api_client/resources/devboxes/disk_snapshots.py @@ -7,7 +7,7 @@ import httpx from ..._types import Body, Omit, Query, Headers, NotGiven, omit, not_given -from ..._utils import maybe_transform, async_maybe_transform +from ..._utils import path_template, maybe_transform, async_maybe_transform from ..._compat import cached_property from ..._resource import SyncAPIResource, AsyncAPIResource from ..._response import ( @@ -88,7 +88,7 @@ def update( if not id: raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") return self._post( - f"/v1/devboxes/disk_snapshots/{id}", + path_template("/v1/devboxes/disk_snapshots/{id}", id=id), body=maybe_transform( { "commit_message": commit_message, @@ -201,7 +201,7 @@ def delete( if not id: raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") return self._post( - f"/v1/devboxes/disk_snapshots/{id}/delete", + path_template("/v1/devboxes/disk_snapshots/{id}/delete", id=id), options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, @@ -239,7 +239,7 @@ def query_status( if not id: raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") return self._get( - f"/v1/devboxes/disk_snapshots/{id}/status", + path_template("/v1/devboxes/disk_snapshots/{id}/status", id=id), options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), @@ -339,7 +339,7 @@ async def update( if not id: raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") return await self._post( - f"/v1/devboxes/disk_snapshots/{id}", + path_template("/v1/devboxes/disk_snapshots/{id}", id=id), body=await async_maybe_transform( { "commit_message": commit_message, @@ -452,7 +452,7 @@ async def delete( if not id: raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") return await self._post( - f"/v1/devboxes/disk_snapshots/{id}/delete", + path_template("/v1/devboxes/disk_snapshots/{id}/delete", id=id), options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, @@ -490,7 +490,7 @@ async def query_status( if not id: raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") return await self._get( - f"/v1/devboxes/disk_snapshots/{id}/status", + path_template("/v1/devboxes/disk_snapshots/{id}/status", id=id), options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), diff --git a/src/runloop_api_client/resources/devboxes/logs.py b/src/runloop_api_client/resources/devboxes/logs.py index 19d2c06e1..1e7383914 100644 --- a/src/runloop_api_client/resources/devboxes/logs.py +++ b/src/runloop_api_client/resources/devboxes/logs.py @@ -5,7 +5,7 @@ import httpx from ..._types import Body, Omit, Query, Headers, NotGiven, omit, not_given -from ..._utils import maybe_transform, async_maybe_transform +from ..._utils import path_template, maybe_transform, async_maybe_transform from ..._compat import cached_property from ..._resource import SyncAPIResource, AsyncAPIResource from ..._response import ( @@ -73,7 +73,7 @@ def list( if not id: raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") return self._get( - f"/v1/devboxes/{id}/logs", + path_template("/v1/devboxes/{id}/logs", id=id), options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, @@ -143,7 +143,7 @@ async def list( if not id: raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") return await self._get( - f"/v1/devboxes/{id}/logs", + path_template("/v1/devboxes/{id}/logs", id=id), options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, diff --git a/src/runloop_api_client/resources/gateway_configs.py b/src/runloop_api_client/resources/gateway_configs.py index 86ec22ce6..a521b871e 100644 --- a/src/runloop_api_client/resources/gateway_configs.py +++ b/src/runloop_api_client/resources/gateway_configs.py @@ -8,7 +8,7 @@ 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 .._utils import path_template, maybe_transform, async_maybe_transform from .._compat import cached_property from .._resource import SyncAPIResource, AsyncAPIResource from .._response import ( @@ -132,7 +132,7 @@ def retrieve( 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}", + path_template("/v1/gateway-configs/{id}", id=id), options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), @@ -181,7 +181,7 @@ def update( 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}", + path_template("/v1/gateway-configs/{id}", id=id), body=maybe_transform( { "auth_mechanism": auth_mechanism, @@ -287,7 +287,7 @@ def delete( 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", + path_template("/v1/gateway-configs/{id}/delete", id=id), options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, @@ -407,7 +407,7 @@ async def retrieve( 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}", + path_template("/v1/gateway-configs/{id}", id=id), options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), @@ -456,7 +456,7 @@ async def update( 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}", + path_template("/v1/gateway-configs/{id}", id=id), body=await async_maybe_transform( { "auth_mechanism": auth_mechanism, @@ -562,7 +562,7 @@ async def delete( 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", + path_template("/v1/gateway-configs/{id}/delete", id=id), options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, diff --git a/src/runloop_api_client/resources/mcp_configs.py b/src/runloop_api_client/resources/mcp_configs.py index 24b05cc2d..9c1cabc0c 100644 --- a/src/runloop_api_client/resources/mcp_configs.py +++ b/src/runloop_api_client/resources/mcp_configs.py @@ -8,7 +8,7 @@ from ..types import mcp_config_list_params, mcp_config_create_params, mcp_config_update_params from .._types import Body, Omit, Query, Headers, NotGiven, SequenceNotStr, omit, not_given -from .._utils import maybe_transform, async_maybe_transform +from .._utils import path_template, maybe_transform, async_maybe_transform from .._compat import cached_property from .._resource import SyncAPIResource, AsyncAPIResource from .._response import ( @@ -134,7 +134,7 @@ def retrieve( if not id: raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") return self._get( - f"/v1/mcp-configs/{id}", + path_template("/v1/mcp-configs/{id}", id=id), options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), @@ -184,7 +184,7 @@ def update( if not id: raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") return self._post( - f"/v1/mcp-configs/{id}", + path_template("/v1/mcp-configs/{id}", id=id), body=maybe_transform( { "allowed_tools": allowed_tools, @@ -289,7 +289,7 @@ def delete( if not id: raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") return self._post( - f"/v1/mcp-configs/{id}/delete", + path_template("/v1/mcp-configs/{id}/delete", id=id), options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, @@ -411,7 +411,7 @@ async def retrieve( if not id: raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") return await self._get( - f"/v1/mcp-configs/{id}", + path_template("/v1/mcp-configs/{id}", id=id), options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), @@ -461,7 +461,7 @@ async def update( if not id: raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") return await self._post( - f"/v1/mcp-configs/{id}", + path_template("/v1/mcp-configs/{id}", id=id), body=await async_maybe_transform( { "allowed_tools": allowed_tools, @@ -566,7 +566,7 @@ async def delete( if not id: raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") return await self._post( - f"/v1/mcp-configs/{id}/delete", + path_template("/v1/mcp-configs/{id}/delete", id=id), options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, diff --git a/src/runloop_api_client/resources/network_policies.py b/src/runloop_api_client/resources/network_policies.py index f50d4fe6d..d472b54a8 100644 --- a/src/runloop_api_client/resources/network_policies.py +++ b/src/runloop_api_client/resources/network_policies.py @@ -8,7 +8,7 @@ from ..types import network_policy_list_params, network_policy_create_params, network_policy_update_params from .._types import Body, Omit, Query, Headers, NotGiven, SequenceNotStr, omit, not_given -from .._utils import maybe_transform, async_maybe_transform +from .._utils import path_template, maybe_transform, async_maybe_transform from .._compat import cached_property from .._resource import SyncAPIResource, AsyncAPIResource from .._response import ( @@ -149,7 +149,7 @@ def retrieve( if not id: raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") return self._get( - f"/v1/network-policies/{id}", + path_template("/v1/network-policies/{id}", id=id), options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), @@ -208,7 +208,7 @@ def update( if not id: raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") return self._post( - f"/v1/network-policies/{id}", + path_template("/v1/network-policies/{id}", id=id), body=maybe_transform( { "allow_agent_gateway": allow_agent_gateway, @@ -316,7 +316,7 @@ def delete( if not id: raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") return self._post( - f"/v1/network-policies/{id}/delete", + path_template("/v1/network-policies/{id}/delete", id=id), options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, @@ -453,7 +453,7 @@ async def retrieve( if not id: raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") return await self._get( - f"/v1/network-policies/{id}", + path_template("/v1/network-policies/{id}", id=id), options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), @@ -512,7 +512,7 @@ async def update( if not id: raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") return await self._post( - f"/v1/network-policies/{id}", + path_template("/v1/network-policies/{id}", id=id), body=await async_maybe_transform( { "allow_agent_gateway": allow_agent_gateway, @@ -620,7 +620,7 @@ async def delete( if not id: raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") return await self._post( - f"/v1/network-policies/{id}/delete", + path_template("/v1/network-policies/{id}/delete", id=id), options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, diff --git a/src/runloop_api_client/resources/objects.py b/src/runloop_api_client/resources/objects.py index 409d5f6f3..78ab27aa3 100644 --- a/src/runloop_api_client/resources/objects.py +++ b/src/runloop_api_client/resources/objects.py @@ -9,7 +9,7 @@ from ..types import object_list_params, object_create_params, object_download_params, object_list_public_params from .._types import Body, Omit, Query, Headers, NotGiven, omit, not_given -from .._utils import maybe_transform, async_maybe_transform +from .._utils import path_template, maybe_transform, async_maybe_transform from .._compat import cached_property from .._resource import SyncAPIResource, AsyncAPIResource from .._response import ( @@ -133,7 +133,7 @@ def retrieve( if not id: raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") return self._get( - f"/v1/objects/{id}", + path_template("/v1/objects/{id}", id=id), options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), @@ -234,7 +234,7 @@ def delete( if not id: raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") return self._post( - f"/v1/objects/{id}/delete", + path_template("/v1/objects/{id}/delete", id=id), options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, @@ -275,7 +275,7 @@ def complete( if not id: raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") return self._post( - f"/v1/objects/{id}/complete", + path_template("/v1/objects/{id}/complete", id=id), options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, @@ -317,7 +317,7 @@ def download( if not id: raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") return self._get( - f"/v1/objects/{id}/download", + path_template("/v1/objects/{id}/download", id=id), options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, @@ -501,7 +501,7 @@ async def retrieve( if not id: raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") return await self._get( - f"/v1/objects/{id}", + path_template("/v1/objects/{id}", id=id), options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), @@ -602,7 +602,7 @@ async def delete( if not id: raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") return await self._post( - f"/v1/objects/{id}/delete", + path_template("/v1/objects/{id}/delete", id=id), options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, @@ -643,7 +643,7 @@ async def complete( if not id: raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") return await self._post( - f"/v1/objects/{id}/complete", + path_template("/v1/objects/{id}/complete", id=id), options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, @@ -685,7 +685,7 @@ async def download( if not id: raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") return await self._get( - f"/v1/objects/{id}/download", + path_template("/v1/objects/{id}/download", id=id), options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, diff --git a/src/runloop_api_client/resources/repositories.py b/src/runloop_api_client/resources/repositories.py index a22075540..58421b8d2 100644 --- a/src/runloop_api_client/resources/repositories.py +++ b/src/runloop_api_client/resources/repositories.py @@ -14,7 +14,7 @@ repository_refresh_params, ) from .._types import Body, Omit, Query, Headers, NotGiven, omit, not_given -from .._utils import maybe_transform, async_maybe_transform +from .._utils import path_template, maybe_transform, async_maybe_transform from .._compat import cached_property from .._resource import SyncAPIResource, AsyncAPIResource from .._response import ( @@ -138,7 +138,7 @@ def retrieve( if not id: raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") return self._get( - f"/v1/repositories/{id}", + path_template("/v1/repositories/{id}", id=id), options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), @@ -230,7 +230,7 @@ def delete( if not id: raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") return self._post( - f"/v1/repositories/{id}/delete", + path_template("/v1/repositories/{id}/delete", id=id), options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, @@ -274,7 +274,7 @@ def inspect( if not id: raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") return self._post( - f"/v1/repositories/{id}/inspect", + path_template("/v1/repositories/{id}/inspect", id=id), body=maybe_transform( {"github_auth_token": github_auth_token}, repository_inspect_params.RepositoryInspectParams ), @@ -315,7 +315,7 @@ def list_inspections( if not id: raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") return self._get( - f"/v1/repositories/{id}/inspections", + path_template("/v1/repositories/{id}/inspections", id=id), options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), @@ -359,7 +359,7 @@ def refresh( if not id: raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") return self._post( - f"/v1/repositories/{id}/refresh", + path_template("/v1/repositories/{id}/refresh", id=id), body=maybe_transform( { "blueprint_id": blueprint_id, @@ -403,7 +403,7 @@ def retrieve_inspection( if not id: raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") return self._get( - f"/v1/repositories/inspections/{id}", + path_template("/v1/repositories/inspections/{id}", id=id), options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), @@ -517,7 +517,7 @@ async def retrieve( if not id: raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") return await self._get( - f"/v1/repositories/{id}", + path_template("/v1/repositories/{id}", id=id), options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), @@ -609,7 +609,7 @@ async def delete( if not id: raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") return await self._post( - f"/v1/repositories/{id}/delete", + path_template("/v1/repositories/{id}/delete", id=id), options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, @@ -653,7 +653,7 @@ async def inspect( if not id: raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") return await self._post( - f"/v1/repositories/{id}/inspect", + path_template("/v1/repositories/{id}/inspect", id=id), body=await async_maybe_transform( {"github_auth_token": github_auth_token}, repository_inspect_params.RepositoryInspectParams ), @@ -694,7 +694,7 @@ async def list_inspections( if not id: raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") return await self._get( - f"/v1/repositories/{id}/inspections", + path_template("/v1/repositories/{id}/inspections", id=id), options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), @@ -738,7 +738,7 @@ async def refresh( if not id: raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") return await self._post( - f"/v1/repositories/{id}/refresh", + path_template("/v1/repositories/{id}/refresh", id=id), body=await async_maybe_transform( { "blueprint_id": blueprint_id, @@ -782,7 +782,7 @@ async def retrieve_inspection( if not id: raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") return await self._get( - f"/v1/repositories/inspections/{id}", + path_template("/v1/repositories/inspections/{id}", id=id), options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), diff --git a/src/runloop_api_client/resources/scenarios/runs.py b/src/runloop_api_client/resources/scenarios/runs.py index 3ea9a960f..f655fe565 100644 --- a/src/runloop_api_client/resources/scenarios/runs.py +++ b/src/runloop_api_client/resources/scenarios/runs.py @@ -5,7 +5,7 @@ import httpx from ..._types import NOT_GIVEN, Body, Omit, Query, Headers, NotGiven, omit, not_given -from ..._utils import maybe_transform +from ..._utils import path_template, maybe_transform from ..._compat import cached_property from ..._resource import SyncAPIResource, AsyncAPIResource from ..._response import ( @@ -79,7 +79,7 @@ def retrieve( if not id: raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") return self._get( - f"/v1/scenarios/runs/{id}", + path_template("/v1/scenarios/runs/{id}", id=id), options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), @@ -180,7 +180,7 @@ def cancel( if not id: raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") return self._post( - f"/v1/scenarios/runs/{id}/cancel", + path_template("/v1/scenarios/runs/{id}/cancel", id=id), options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, @@ -222,7 +222,7 @@ def complete( if not id: raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") return self._post( - f"/v1/scenarios/runs/{id}/complete", + path_template("/v1/scenarios/runs/{id}/complete", id=id), options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, @@ -264,7 +264,7 @@ def download_logs( raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") extra_headers = {"Accept": "application/zip", **(extra_headers or {})} return self._post( - f"/v1/scenarios/runs/{id}/download_logs", + path_template("/v1/scenarios/runs/{id}/download_logs", id=id), options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, @@ -304,7 +304,7 @@ def score( if not id: raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") return self._post( - f"/v1/scenarios/runs/{id}/score", + path_template("/v1/scenarios/runs/{id}/score", id=id), options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, @@ -499,7 +499,7 @@ async def retrieve( if not id: raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") return await self._get( - f"/v1/scenarios/runs/{id}", + path_template("/v1/scenarios/runs/{id}", id=id), options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), @@ -600,7 +600,7 @@ async def cancel( if not id: raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") return await self._post( - f"/v1/scenarios/runs/{id}/cancel", + path_template("/v1/scenarios/runs/{id}/cancel", id=id), options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, @@ -642,7 +642,7 @@ async def complete( if not id: raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") return await self._post( - f"/v1/scenarios/runs/{id}/complete", + path_template("/v1/scenarios/runs/{id}/complete", id=id), options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, @@ -684,7 +684,7 @@ async def download_logs( raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") extra_headers = {"Accept": "application/zip", **(extra_headers or {})} return await self._post( - f"/v1/scenarios/runs/{id}/download_logs", + path_template("/v1/scenarios/runs/{id}/download_logs", id=id), options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, @@ -724,7 +724,7 @@ async def score( if not id: raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") return await self._post( - f"/v1/scenarios/runs/{id}/score", + path_template("/v1/scenarios/runs/{id}/score", id=id), options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, diff --git a/src/runloop_api_client/resources/scenarios/scenarios.py b/src/runloop_api_client/resources/scenarios/scenarios.py index 5c31e4282..65f282ed7 100644 --- a/src/runloop_api_client/resources/scenarios/scenarios.py +++ b/src/runloop_api_client/resources/scenarios/scenarios.py @@ -31,7 +31,7 @@ AsyncScorersResourceWithStreamingResponse, ) from ..._types import Body, Omit, Query, Headers, NotGiven, SequenceNotStr, omit, not_given -from ..._utils import maybe_transform, async_maybe_transform +from ..._utils import path_template, maybe_transform, async_maybe_transform from ..._compat import cached_property from ..._resource import SyncAPIResource, AsyncAPIResource from ..._response import ( @@ -197,7 +197,7 @@ def retrieve( if not id: raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") return self._get( - f"/v1/scenarios/{id}", + path_template("/v1/scenarios/{id}", id=id), options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), @@ -268,7 +268,7 @@ def update( if not id: raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") return self._post( - f"/v1/scenarios/{id}", + path_template("/v1/scenarios/{id}", id=id), body=maybe_transform( { "environment_parameters": environment_parameters, @@ -384,7 +384,7 @@ def archive( if not id: raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") return self._post( - f"/v1/scenarios/{id}/archive", + path_template("/v1/scenarios/{id}/archive", id=id), options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, @@ -709,7 +709,7 @@ async def retrieve( if not id: raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") return await self._get( - f"/v1/scenarios/{id}", + path_template("/v1/scenarios/{id}", id=id), options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), @@ -780,7 +780,7 @@ async def update( if not id: raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") return await self._post( - f"/v1/scenarios/{id}", + path_template("/v1/scenarios/{id}", id=id), body=await async_maybe_transform( { "environment_parameters": environment_parameters, @@ -896,7 +896,7 @@ async def archive( if not id: raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") return await self._post( - f"/v1/scenarios/{id}/archive", + path_template("/v1/scenarios/{id}/archive", id=id), options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, diff --git a/src/runloop_api_client/resources/scenarios/scorers.py b/src/runloop_api_client/resources/scenarios/scorers.py index cdb011dc7..118439745 100644 --- a/src/runloop_api_client/resources/scenarios/scorers.py +++ b/src/runloop_api_client/resources/scenarios/scorers.py @@ -5,7 +5,7 @@ import httpx from ..._types import Body, Omit, Query, Headers, NotGiven, omit, not_given -from ..._utils import maybe_transform, async_maybe_transform +from ..._utils import path_template, maybe_transform, async_maybe_transform from ..._compat import cached_property from ..._resource import SyncAPIResource, AsyncAPIResource from ..._response import ( @@ -122,7 +122,7 @@ def retrieve( if not id: raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") return self._get( - f"/v1/scenarios/scorers/{id}", + path_template("/v1/scenarios/scorers/{id}", id=id), options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), @@ -165,7 +165,7 @@ def update( if not id: raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") return self._post( - f"/v1/scenarios/scorers/{id}", + path_template("/v1/scenarios/scorers/{id}", id=id), body=maybe_transform( { "bash_script": bash_script, @@ -328,7 +328,7 @@ async def retrieve( if not id: raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") return await self._get( - f"/v1/scenarios/scorers/{id}", + path_template("/v1/scenarios/scorers/{id}", id=id), options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), @@ -371,7 +371,7 @@ async def update( if not id: raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") return await self._post( - f"/v1/scenarios/scorers/{id}", + path_template("/v1/scenarios/scorers/{id}", id=id), body=await async_maybe_transform( { "bash_script": bash_script, diff --git a/src/runloop_api_client/resources/secrets.py b/src/runloop_api_client/resources/secrets.py index fa7d45471..38a9d8fc0 100644 --- a/src/runloop_api_client/resources/secrets.py +++ b/src/runloop_api_client/resources/secrets.py @@ -6,7 +6,7 @@ from ..types import secret_list_params, secret_create_params, secret_update_params from .._types import Body, Omit, Query, Headers, NotGiven, omit, not_given -from .._utils import maybe_transform, async_maybe_transform +from .._utils import path_template, maybe_transform, async_maybe_transform from .._compat import cached_property from .._resource import SyncAPIResource, AsyncAPIResource from .._response import ( @@ -164,7 +164,7 @@ def update( if not name: raise ValueError(f"Expected a non-empty value for `name` but received {name!r}") return self._post( - f"/v1/secrets/{name}", + path_template("/v1/secrets/{name}", name=name), body=maybe_transform({"value": value}, secret_update_params.SecretUpdateParams), options=make_request_options( extra_headers=extra_headers, @@ -246,7 +246,7 @@ def delete( if not name: raise ValueError(f"Expected a non-empty value for `name` but received {name!r}") return self._post( - f"/v1/secrets/{name}/delete", + path_template("/v1/secrets/{name}/delete", name=name), options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, @@ -400,7 +400,7 @@ async def update( if not name: raise ValueError(f"Expected a non-empty value for `name` but received {name!r}") return await self._post( - f"/v1/secrets/{name}", + path_template("/v1/secrets/{name}", name=name), body=await async_maybe_transform({"value": value}, secret_update_params.SecretUpdateParams), options=make_request_options( extra_headers=extra_headers, @@ -482,7 +482,7 @@ async def delete( if not name: raise ValueError(f"Expected a non-empty value for `name` but received {name!r}") return await self._post( - f"/v1/secrets/{name}/delete", + path_template("/v1/secrets/{name}/delete", name=name), options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, diff --git a/tests/test_utils/test_path.py b/tests/test_utils/test_path.py new file mode 100644 index 000000000..f69d655a1 --- /dev/null +++ b/tests/test_utils/test_path.py @@ -0,0 +1,89 @@ +from __future__ import annotations + +from typing import Any + +import pytest + +from runloop_api_client._utils._path import path_template + + +@pytest.mark.parametrize( + "template, kwargs, expected", + [ + ("/v1/{id}", dict(id="abc"), "/v1/abc"), + ("/v1/{a}/{b}", dict(a="x", b="y"), "/v1/x/y"), + ("/v1/{a}{b}/path/{c}?val={d}#{e}", dict(a="x", b="y", c="z", d="u", e="v"), "/v1/xy/path/z?val=u#v"), + ("/{w}/{w}", dict(w="echo"), "/echo/echo"), + ("/v1/static", {}, "/v1/static"), + ("", {}, ""), + ("/v1/?q={n}&count=10", dict(n=42), "/v1/?q=42&count=10"), + ("/v1/{v}", dict(v=None), "/v1/null"), + ("/v1/{v}", dict(v=True), "/v1/true"), + ("/v1/{v}", dict(v=False), "/v1/false"), + ("/v1/{v}", dict(v=".hidden"), "/v1/.hidden"), # dot prefix ok + ("/v1/{v}", dict(v="file.txt"), "/v1/file.txt"), # dot in middle ok + ("/v1/{v}", dict(v="..."), "/v1/..."), # triple dot ok + ("/v1/{a}{b}", dict(a=".", b="txt"), "/v1/.txt"), # dot var combining with adjacent to be ok + ("/items?q={v}#{f}", dict(v=".", f=".."), "/items?q=.#.."), # dots in query/fragment are fine + ( + "/v1/{a}?query={b}", + dict(a="../../other/endpoint", b="a&bad=true"), + "/v1/..%2F..%2Fother%2Fendpoint?query=a%26bad%3Dtrue", + ), + ("/v1/{val}", dict(val="a/b/c"), "/v1/a%2Fb%2Fc"), + ("/v1/{val}", dict(val="a/b/c?query=value"), "/v1/a%2Fb%2Fc%3Fquery=value"), + ("/v1/{val}", dict(val="a/b/c?query=value&bad=true"), "/v1/a%2Fb%2Fc%3Fquery=value&bad=true"), + ("/v1/{val}", dict(val="%20"), "/v1/%2520"), # escapes escape sequences in input + # Query: slash and ? are safe, # is not + ("/items?q={v}", dict(v="a/b"), "/items?q=a/b"), + ("/items?q={v}", dict(v="a?b"), "/items?q=a?b"), + ("/items?q={v}", dict(v="a#b"), "/items?q=a%23b"), + ("/items?q={v}", dict(v="a b"), "/items?q=a%20b"), + # Fragment: slash and ? are safe + ("/docs#{v}", dict(v="a/b"), "/docs#a/b"), + ("/docs#{v}", dict(v="a?b"), "/docs#a?b"), + # Path: slash, ? and # are all encoded + ("/v1/{v}", dict(v="a/b"), "/v1/a%2Fb"), + ("/v1/{v}", dict(v="a?b"), "/v1/a%3Fb"), + ("/v1/{v}", dict(v="a#b"), "/v1/a%23b"), + # same var encoded differently by component + ( + "/v1/{v}?q={v}#{v}", + dict(v="a/b?c#d"), + "/v1/a%2Fb%3Fc%23d?q=a/b?c%23d#a/b?c%23d", + ), + ("/v1/{val}", dict(val="x?admin=true"), "/v1/x%3Fadmin=true"), # query injection + ("/v1/{val}", dict(val="x#admin"), "/v1/x%23admin"), # fragment injection + ], +) +def test_interpolation(template: str, kwargs: dict[str, Any], expected: str) -> None: + assert path_template(template, **kwargs) == expected + + +def test_missing_kwarg_raises_key_error() -> None: + with pytest.raises(KeyError, match="org_id"): + path_template("/v1/{org_id}") + + +@pytest.mark.parametrize( + "template, kwargs", + [ + ("{a}/path", dict(a=".")), + ("{a}/path", dict(a="..")), + ("/v1/{a}", dict(a=".")), + ("/v1/{a}", dict(a="..")), + ("/v1/{a}/path", dict(a=".")), + ("/v1/{a}/path", dict(a="..")), + ("/v1/{a}{b}", dict(a=".", b=".")), # adjacent vars → ".." + ("/v1/{a}.", dict(a=".")), # var + static → ".." + ("/v1/{a}{b}", dict(a="", b=".")), # empty + dot → "." + ("/v1/%2e/{x}", dict(x="ok")), # encoded dot in static text + ("/v1/%2e./{x}", dict(x="ok")), # mixed encoded ".." in static + ("/v1/.%2E/{x}", dict(x="ok")), # mixed encoded ".." in static + ("/v1/{v}?q=1", dict(v="..")), + ("/v1/{v}#frag", dict(v="..")), + ], +) +def test_dot_segment_rejected(template: str, kwargs: dict[str, Any]) -> None: + with pytest.raises(ValueError, match="dot-segment"): + path_template(template, **kwargs)