diff --git a/.release-please-manifest.json b/.release-please-manifest.json
index 157f0355f..51acdaa4e 100644
--- a/.release-please-manifest.json
+++ b/.release-please-manifest.json
@@ -1,3 +1,3 @@
{
- ".": "0.36.0"
+ ".": "0.37.0"
}
\ No newline at end of file
diff --git a/.stats.yml b/.stats.yml
index 5a58d5b1e..cb2817919 100644
--- a/.stats.yml
+++ b/.stats.yml
@@ -1,4 +1,4 @@
-configured_endpoints: 85
-openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/runloop-ai%2Frunloop-3484df665f4c2b7cb17ad044b825223fc69ad67b05d967fbb6dfbb6a6ac9ccac.yml
-openapi_spec_hash: 58c0860078f5f26c8b517603956700b5
-config_hash: c03c6a4c057a38e2809a102c48fafe6c
+configured_endpoints: 86
+openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/runloop-ai%2Frunloop-141cac5a78c04271425090df61bb3f19446c1c065ae66a5b7a6329c53628aafa.yml
+openapi_spec_hash: 2380917eddc78d715847c7ef6bfa3b4a
+config_hash: 5b50498887b4fdca175b8377a9cb675f
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 8ab9dbf43..e80b2e50a 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,20 @@
# Changelog
+## 0.37.0 (2025-06-04)
+
+Full Changelog: [v0.36.0...v0.37.0](https://github.com/runloopai/api-client-python/compare/v0.36.0...v0.37.0)
+
+### Features
+
+* **api:** api update ([a167d0b](https://github.com/runloopai/api-client-python/commit/a167d0b2dbab59cb4ffbb029ea6dae22d0d7c2f4))
+* **client:** add follow_redirects request option ([c29d24c](https://github.com/runloopai/api-client-python/commit/c29d24c5483ee2ca7e4d53dfdf8d40278f04f1fe))
+
+
+### Chores
+
+* **docs:** remove reference to rye shell ([af51a95](https://github.com/runloopai/api-client-python/commit/af51a958afb08cc728959b154c52d0ca31f4206c))
+* **docs:** remove unnecessary param examples ([a29553d](https://github.com/runloopai/api-client-python/commit/a29553d048fa85207e28e5b3dbc4ad9ea326ad86))
+
## 0.36.0 (2025-06-02)
Full Changelog: [v0.35.0...v0.36.0](https://github.com/runloopai/api-client-python/compare/v0.35.0...v0.36.0)
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index fcf955358..67732aae5 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -17,8 +17,7 @@ $ rye sync --all-features
You can then run scripts using `rye run python script.py` or by activating the virtual environment:
```sh
-$ rye shell
-# or manually activate - https://docs.python.org/3/library/venv.html#how-venvs-work
+# Activate the virtual environment - https://docs.python.org/3/library/venv.html#how-venvs-work
$ source .venv/bin/activate
# now you can omit the `rye run` prefix
diff --git a/README.md b/README.md
index 05e74c625..b59a62b8d 100644
--- a/README.md
+++ b/README.md
@@ -146,23 +146,7 @@ from runloop_api_client import Runloop
client = Runloop()
devbox_view = client.devboxes.create(
- launch_parameters={
- "after_idle": {
- "idle_time_seconds": 0,
- "on_idle": "shutdown",
- },
- "architecture": "x86_64",
- "available_ports": [0],
- "custom_cpu_cores": 0,
- "custom_gb_memory": 0,
- "keep_alive_time_seconds": 0,
- "launch_commands": ["string"],
- "resource_size_request": "X_SMALL",
- "user_parameters": {
- "uid": 0,
- "username": "username",
- },
- },
+ launch_parameters={},
)
print(devbox_view.launch_parameters)
```
diff --git a/api.md b/api.md
index 367aa7f0d..7e511f221 100644
--- a/api.md
+++ b/api.md
@@ -35,6 +35,7 @@ Methods:
- client.benchmarks.runs.list(\*\*params) -> SyncBenchmarkRunsCursorIDPage[BenchmarkRunView]
- client.benchmarks.runs.cancel(id) -> BenchmarkRunView
- client.benchmarks.runs.complete(id) -> BenchmarkRunView
+- client.benchmarks.runs.list_scenario_runs(id, \*\*params) -> SyncBenchmarkRunsCursorIDPage[ScenarioRunView]
# Blueprints
diff --git a/pyproject.toml b/pyproject.toml
index 4bfa8e5a8..5ea158891 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -1,6 +1,6 @@
[project]
name = "runloop_api_client"
-version = "0.36.0"
+version = "0.37.0"
description = "The official Python library for the runloop API"
dynamic = ["readme"]
license = "MIT"
diff --git a/src/runloop_api_client/_base_client.py b/src/runloop_api_client/_base_client.py
index 1b3041060..bba4933f3 100644
--- a/src/runloop_api_client/_base_client.py
+++ b/src/runloop_api_client/_base_client.py
@@ -960,6 +960,9 @@ def request(
if self.custom_auth is not None:
kwargs["auth"] = self.custom_auth
+ if options.follow_redirects is not None:
+ kwargs["follow_redirects"] = options.follow_redirects
+
log.debug("Sending HTTP Request: %s %s", request.method, request.url)
response = None
@@ -1460,6 +1463,9 @@ async def request(
if self.custom_auth is not None:
kwargs["auth"] = self.custom_auth
+ if options.follow_redirects is not None:
+ kwargs["follow_redirects"] = options.follow_redirects
+
log.debug("Sending HTTP Request: %s %s", request.method, request.url)
response = None
diff --git a/src/runloop_api_client/_models.py b/src/runloop_api_client/_models.py
index 798956f17..4f2149805 100644
--- a/src/runloop_api_client/_models.py
+++ b/src/runloop_api_client/_models.py
@@ -737,6 +737,7 @@ class FinalRequestOptionsInput(TypedDict, total=False):
idempotency_key: str
json_data: Body
extra_json: AnyMapping
+ follow_redirects: bool
@final
@@ -750,6 +751,7 @@ class FinalRequestOptions(pydantic.BaseModel):
files: Union[HttpxRequestFiles, None] = None
idempotency_key: Union[str, None] = None
post_parser: Union[Callable[[Any], Any], NotGiven] = NotGiven()
+ follow_redirects: Union[bool, None] = None
# It should be noted that we cannot use `json` here as that would override
# a BaseModel method in an incompatible fashion.
diff --git a/src/runloop_api_client/_types.py b/src/runloop_api_client/_types.py
index 114eefd47..40ccb19ed 100644
--- a/src/runloop_api_client/_types.py
+++ b/src/runloop_api_client/_types.py
@@ -100,6 +100,7 @@ class RequestOptions(TypedDict, total=False):
params: Query
extra_json: AnyMapping
idempotency_key: str
+ follow_redirects: bool
# Sentinel class used until PEP 0661 is accepted
@@ -215,3 +216,4 @@ class _GenericAlias(Protocol):
class HttpxSendArgs(TypedDict, total=False):
auth: httpx.Auth
+ follow_redirects: bool
diff --git a/src/runloop_api_client/_version.py b/src/runloop_api_client/_version.py
index 12ac26a82..5eea31e80 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__ = "0.36.0" # x-release-please-version
+__version__ = "0.37.0" # x-release-please-version
diff --git a/src/runloop_api_client/resources/benchmarks/runs.py b/src/runloop_api_client/resources/benchmarks/runs.py
index ef6022d19..5735a5aa6 100644
--- a/src/runloop_api_client/resources/benchmarks/runs.py
+++ b/src/runloop_api_client/resources/benchmarks/runs.py
@@ -16,7 +16,8 @@
)
from ...pagination import SyncBenchmarkRunsCursorIDPage, AsyncBenchmarkRunsCursorIDPage
from ..._base_client import AsyncPaginator, make_request_options
-from ...types.benchmarks import run_list_params
+from ...types.benchmarks import run_list_params, run_list_scenario_runs_params
+from ...types.scenario_run_view import ScenarioRunView
from ...types.benchmark_run_view import BenchmarkRunView
__all__ = ["RunsResource", "AsyncRunsResource"]
@@ -206,6 +207,56 @@ def complete(
cast_to=BenchmarkRunView,
)
+ def list_scenario_runs(
+ self,
+ id: str,
+ *,
+ limit: int | NotGiven = NOT_GIVEN,
+ starting_after: str | NotGiven = NOT_GIVEN,
+ # 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,
+ ) -> SyncBenchmarkRunsCursorIDPage[ScenarioRunView]:
+ """
+ List started scenario runs for a benchmark run.
+
+ Args:
+ limit: The limit of items to return. Default is 20.
+
+ 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
+ """
+ if not id:
+ raise ValueError(f"Expected a non-empty value for `id` but received {id!r}")
+ return self._get_api_list(
+ f"/v1/benchmarks/runs/{id}/scenario_runs",
+ page=SyncBenchmarkRunsCursorIDPage[ScenarioRunView],
+ options=make_request_options(
+ extra_headers=extra_headers,
+ extra_query=extra_query,
+ extra_body=extra_body,
+ timeout=timeout,
+ query=maybe_transform(
+ {
+ "limit": limit,
+ "starting_after": starting_after,
+ },
+ run_list_scenario_runs_params.RunListScenarioRunsParams,
+ ),
+ ),
+ model=ScenarioRunView,
+ )
+
class AsyncRunsResource(AsyncAPIResource):
@cached_property
@@ -391,6 +442,56 @@ async def complete(
cast_to=BenchmarkRunView,
)
+ def list_scenario_runs(
+ self,
+ id: str,
+ *,
+ limit: int | NotGiven = NOT_GIVEN,
+ starting_after: str | NotGiven = NOT_GIVEN,
+ # 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[ScenarioRunView, AsyncBenchmarkRunsCursorIDPage[ScenarioRunView]]:
+ """
+ List started scenario runs for a benchmark run.
+
+ Args:
+ limit: The limit of items to return. Default is 20.
+
+ 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
+ """
+ if not id:
+ raise ValueError(f"Expected a non-empty value for `id` but received {id!r}")
+ return self._get_api_list(
+ f"/v1/benchmarks/runs/{id}/scenario_runs",
+ page=AsyncBenchmarkRunsCursorIDPage[ScenarioRunView],
+ options=make_request_options(
+ extra_headers=extra_headers,
+ extra_query=extra_query,
+ extra_body=extra_body,
+ timeout=timeout,
+ query=maybe_transform(
+ {
+ "limit": limit,
+ "starting_after": starting_after,
+ },
+ run_list_scenario_runs_params.RunListScenarioRunsParams,
+ ),
+ ),
+ model=ScenarioRunView,
+ )
+
class RunsResourceWithRawResponse:
def __init__(self, runs: RunsResource) -> None:
@@ -408,6 +509,9 @@ def __init__(self, runs: RunsResource) -> None:
self.complete = to_raw_response_wrapper(
runs.complete,
)
+ self.list_scenario_runs = to_raw_response_wrapper(
+ runs.list_scenario_runs,
+ )
class AsyncRunsResourceWithRawResponse:
@@ -426,6 +530,9 @@ def __init__(self, runs: AsyncRunsResource) -> None:
self.complete = async_to_raw_response_wrapper(
runs.complete,
)
+ self.list_scenario_runs = async_to_raw_response_wrapper(
+ runs.list_scenario_runs,
+ )
class RunsResourceWithStreamingResponse:
@@ -444,6 +551,9 @@ def __init__(self, runs: RunsResource) -> None:
self.complete = to_streamed_response_wrapper(
runs.complete,
)
+ self.list_scenario_runs = to_streamed_response_wrapper(
+ runs.list_scenario_runs,
+ )
class AsyncRunsResourceWithStreamingResponse:
@@ -462,3 +572,6 @@ def __init__(self, runs: AsyncRunsResource) -> None:
self.complete = async_to_streamed_response_wrapper(
runs.complete,
)
+ self.list_scenario_runs = async_to_streamed_response_wrapper(
+ runs.list_scenario_runs,
+ )
diff --git a/src/runloop_api_client/types/benchmarks/__init__.py b/src/runloop_api_client/types/benchmarks/__init__.py
index 4bc4e1112..2fb29daa0 100644
--- a/src/runloop_api_client/types/benchmarks/__init__.py
+++ b/src/runloop_api_client/types/benchmarks/__init__.py
@@ -3,3 +3,4 @@
from __future__ import annotations
from .run_list_params import RunListParams as RunListParams
+from .run_list_scenario_runs_params import RunListScenarioRunsParams as RunListScenarioRunsParams
diff --git a/src/runloop_api_client/types/benchmarks/run_list_scenario_runs_params.py b/src/runloop_api_client/types/benchmarks/run_list_scenario_runs_params.py
new file mode 100644
index 000000000..b001bd54e
--- /dev/null
+++ b/src/runloop_api_client/types/benchmarks/run_list_scenario_runs_params.py
@@ -0,0 +1,15 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from __future__ import annotations
+
+from typing_extensions import TypedDict
+
+__all__ = ["RunListScenarioRunsParams"]
+
+
+class RunListScenarioRunsParams(TypedDict, total=False):
+ limit: int
+ """The limit of items to return. Default is 20."""
+
+ starting_after: str
+ """Load the next page of data starting after the item with the given ID."""
diff --git a/tests/api_resources/benchmarks/test_runs.py b/tests/api_resources/benchmarks/test_runs.py
index d23aa33b7..a9a54ed76 100644
--- a/tests/api_resources/benchmarks/test_runs.py
+++ b/tests/api_resources/benchmarks/test_runs.py
@@ -9,7 +9,7 @@
from tests.utils import assert_matches_type
from runloop_api_client import Runloop, AsyncRunloop
-from runloop_api_client.types import BenchmarkRunView
+from runloop_api_client.types import ScenarioRunView, BenchmarkRunView
from runloop_api_client.pagination import SyncBenchmarkRunsCursorIDPage, AsyncBenchmarkRunsCursorIDPage
base_url = os.environ.get("TEST_API_BASE_URL", "http://127.0.0.1:4010")
@@ -166,6 +166,53 @@ def test_path_params_complete(self, client: Runloop) -> None:
"",
)
+ @parametrize
+ def test_method_list_scenario_runs(self, client: Runloop) -> None:
+ run = client.benchmarks.runs.list_scenario_runs(
+ id="id",
+ )
+ assert_matches_type(SyncBenchmarkRunsCursorIDPage[ScenarioRunView], run, path=["response"])
+
+ @parametrize
+ def test_method_list_scenario_runs_with_all_params(self, client: Runloop) -> None:
+ run = client.benchmarks.runs.list_scenario_runs(
+ id="id",
+ limit=0,
+ starting_after="starting_after",
+ )
+ assert_matches_type(SyncBenchmarkRunsCursorIDPage[ScenarioRunView], run, path=["response"])
+
+ @parametrize
+ def test_raw_response_list_scenario_runs(self, client: Runloop) -> None:
+ response = client.benchmarks.runs.with_raw_response.list_scenario_runs(
+ id="id",
+ )
+
+ assert response.is_closed is True
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+ run = response.parse()
+ assert_matches_type(SyncBenchmarkRunsCursorIDPage[ScenarioRunView], run, path=["response"])
+
+ @parametrize
+ def test_streaming_response_list_scenario_runs(self, client: Runloop) -> None:
+ with client.benchmarks.runs.with_streaming_response.list_scenario_runs(
+ id="id",
+ ) as response:
+ assert not response.is_closed
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+
+ run = response.parse()
+ assert_matches_type(SyncBenchmarkRunsCursorIDPage[ScenarioRunView], run, path=["response"])
+
+ assert cast(Any, response.is_closed) is True
+
+ @parametrize
+ def test_path_params_list_scenario_runs(self, client: Runloop) -> None:
+ with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"):
+ client.benchmarks.runs.with_raw_response.list_scenario_runs(
+ id="",
+ )
+
class TestAsyncRuns:
parametrize = pytest.mark.parametrize("async_client", [False, True], indirect=True, ids=["loose", "strict"])
@@ -317,3 +364,50 @@ async def test_path_params_complete(self, async_client: AsyncRunloop) -> None:
await async_client.benchmarks.runs.with_raw_response.complete(
"",
)
+
+ @parametrize
+ async def test_method_list_scenario_runs(self, async_client: AsyncRunloop) -> None:
+ run = await async_client.benchmarks.runs.list_scenario_runs(
+ id="id",
+ )
+ assert_matches_type(AsyncBenchmarkRunsCursorIDPage[ScenarioRunView], run, path=["response"])
+
+ @parametrize
+ async def test_method_list_scenario_runs_with_all_params(self, async_client: AsyncRunloop) -> None:
+ run = await async_client.benchmarks.runs.list_scenario_runs(
+ id="id",
+ limit=0,
+ starting_after="starting_after",
+ )
+ assert_matches_type(AsyncBenchmarkRunsCursorIDPage[ScenarioRunView], run, path=["response"])
+
+ @parametrize
+ async def test_raw_response_list_scenario_runs(self, async_client: AsyncRunloop) -> None:
+ response = await async_client.benchmarks.runs.with_raw_response.list_scenario_runs(
+ id="id",
+ )
+
+ assert response.is_closed is True
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+ run = await response.parse()
+ assert_matches_type(AsyncBenchmarkRunsCursorIDPage[ScenarioRunView], run, path=["response"])
+
+ @parametrize
+ async def test_streaming_response_list_scenario_runs(self, async_client: AsyncRunloop) -> None:
+ async with async_client.benchmarks.runs.with_streaming_response.list_scenario_runs(
+ id="id",
+ ) as response:
+ assert not response.is_closed
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+
+ run = await response.parse()
+ assert_matches_type(AsyncBenchmarkRunsCursorIDPage[ScenarioRunView], run, path=["response"])
+
+ assert cast(Any, response.is_closed) is True
+
+ @parametrize
+ async def test_path_params_list_scenario_runs(self, async_client: AsyncRunloop) -> None:
+ with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"):
+ await async_client.benchmarks.runs.with_raw_response.list_scenario_runs(
+ id="",
+ )
diff --git a/tests/test_client.py b/tests/test_client.py
index 2453b22ac..e3faa0506 100644
--- a/tests/test_client.py
+++ b/tests/test_client.py
@@ -873,6 +873,33 @@ def retry_handler(_request: httpx.Request) -> httpx.Response:
assert response.http_request.headers.get("x-stainless-retry-count") == "42"
+ @pytest.mark.respx(base_url=base_url)
+ def test_follow_redirects(self, respx_mock: MockRouter) -> None:
+ # Test that the default follow_redirects=True allows following redirects
+ respx_mock.post("/redirect").mock(
+ return_value=httpx.Response(302, headers={"Location": f"{base_url}/redirected"})
+ )
+ respx_mock.get("/redirected").mock(return_value=httpx.Response(200, json={"status": "ok"}))
+
+ response = self.client.post("/redirect", body={"key": "value"}, cast_to=httpx.Response)
+ assert response.status_code == 200
+ assert response.json() == {"status": "ok"}
+
+ @pytest.mark.respx(base_url=base_url)
+ def test_follow_redirects_disabled(self, respx_mock: MockRouter) -> None:
+ # Test that follow_redirects=False prevents following redirects
+ respx_mock.post("/redirect").mock(
+ return_value=httpx.Response(302, headers={"Location": f"{base_url}/redirected"})
+ )
+
+ with pytest.raises(APIStatusError) as exc_info:
+ self.client.post(
+ "/redirect", body={"key": "value"}, options={"follow_redirects": False}, cast_to=httpx.Response
+ )
+
+ assert exc_info.value.response.status_code == 302
+ assert exc_info.value.response.headers["Location"] == f"{base_url}/redirected"
+
class TestAsyncRunloop:
client = AsyncRunloop(base_url=base_url, bearer_token=bearer_token, _strict_response_validation=True)
@@ -1741,3 +1768,30 @@ async def test_main() -> None:
raise AssertionError("calling get_platform using asyncify resulted in a hung process")
time.sleep(0.1)
+
+ @pytest.mark.respx(base_url=base_url)
+ async def test_follow_redirects(self, respx_mock: MockRouter) -> None:
+ # Test that the default follow_redirects=True allows following redirects
+ respx_mock.post("/redirect").mock(
+ return_value=httpx.Response(302, headers={"Location": f"{base_url}/redirected"})
+ )
+ respx_mock.get("/redirected").mock(return_value=httpx.Response(200, json={"status": "ok"}))
+
+ response = await self.client.post("/redirect", body={"key": "value"}, cast_to=httpx.Response)
+ assert response.status_code == 200
+ assert response.json() == {"status": "ok"}
+
+ @pytest.mark.respx(base_url=base_url)
+ async def test_follow_redirects_disabled(self, respx_mock: MockRouter) -> None:
+ # Test that follow_redirects=False prevents following redirects
+ respx_mock.post("/redirect").mock(
+ return_value=httpx.Response(302, headers={"Location": f"{base_url}/redirected"})
+ )
+
+ with pytest.raises(APIStatusError) as exc_info:
+ await self.client.post(
+ "/redirect", body={"key": "value"}, options={"follow_redirects": False}, cast_to=httpx.Response
+ )
+
+ assert exc_info.value.response.status_code == 302
+ assert exc_info.value.response.headers["Location"] == f"{base_url}/redirected"