diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c3f5bc4..1692944 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -17,6 +17,7 @@ jobs: timeout-minutes: 10 name: lint runs-on: ${{ github.repository == 'stainless-sdks/kernel-python' && 'depot-ubuntu-24.04' || 'ubuntu-latest' }} + if: github.event_name == 'push' || github.event.pull_request.head.repo.fork steps: - uses: actions/checkout@v4 @@ -34,10 +35,10 @@ jobs: - name: Run lints run: ./scripts/lint - upload: - if: github.repository == 'stainless-sdks/kernel-python' + build: + if: github.repository == 'stainless-sdks/kernel-python' && (github.event_name == 'push' || github.event.pull_request.head.repo.fork) timeout-minutes: 10 - name: upload + name: build permissions: contents: read id-token: write @@ -45,6 +46,20 @@ jobs: steps: - uses: actions/checkout@v4 + - name: Install Rye + run: | + curl -sSf https://rye.astral.sh/get | bash + echo "$HOME/.rye/shims" >> $GITHUB_PATH + env: + RYE_VERSION: '0.44.0' + RYE_INSTALL_OPTION: '--yes' + + - name: Install dependencies + run: rye sync --all-features + + - name: Run build + run: rye build + - name: Get GitHub OIDC Token id: github-oidc uses: actions/github-script@v6 @@ -62,6 +77,7 @@ jobs: timeout-minutes: 10 name: test runs-on: ${{ github.repository == 'stainless-sdks/kernel-python' && 'depot-ubuntu-24.04' || 'ubuntu-latest' }} + if: github.event_name == 'push' || github.event.pull_request.head.repo.fork steps: - uses: actions/checkout@v4 diff --git a/.release-please-manifest.json b/.release-please-manifest.json index 12aa896..1bc5713 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,3 +1,3 @@ { - ".": "0.6.4" + ".": "0.7.1" } \ No newline at end of file diff --git a/.stats.yml b/.stats.yml index 9625f4e..aa1aff7 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ -configured_endpoints: 17 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel%2Fkernel-2eeb61205775c5997abf8154cd6f6fe81a1e83870eff10050b17ed415aa7860b.yml -openapi_spec_hash: 63405add4a3f53718f8183cbb8c1a22f -config_hash: 00ec9df250b9dc077f8d3b93a442d252 +configured_endpoints: 18 +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel%2Fkernel-d173129101e26f450c200e84430d993479c034700cf826917425d513b88912e6.yml +openapi_spec_hash: 150b86da7588979d7619b1a894e4720c +config_hash: eaeed470b1070b34df69c49d68e67355 diff --git a/CHANGELOG.md b/CHANGELOG.md index 5dab9a3..2bfb29a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,26 @@ # Changelog +## 0.7.1 (2025-07-08) + +Full Changelog: [v0.6.4...v0.7.1](https://github.com/onkernel/kernel-python-sdk/compare/v0.6.4...v0.7.1) + +### Features + +* **api:** headless browsers ([de0b235](https://github.com/onkernel/kernel-python-sdk/commit/de0b235998be2299459b54df15e83dd9dc8c0b7f)) +* **api:** manual updates ([7d0a2bd](https://github.com/onkernel/kernel-python-sdk/commit/7d0a2bd8dd25bac6d688e2b5f5076c916d80f800)) + + +### Bug Fixes + +* **ci:** correct conditional ([1167795](https://github.com/onkernel/kernel-python-sdk/commit/116779521b08014f5be7588f1e0a7975c13e8e05)) + + +### Chores + +* **ci:** change upload type ([dabede0](https://github.com/onkernel/kernel-python-sdk/commit/dabede0456032d69d0c4b05c740d04002fc900a9)) +* **ci:** only run for pushes and fork pull requests ([e9a45fd](https://github.com/onkernel/kernel-python-sdk/commit/e9a45fd655812a9bf2c3edec3cccdbde3ab89f73)) +* **internal:** codegen related update ([2c50b08](https://github.com/onkernel/kernel-python-sdk/commit/2c50b08edb7f73a7c20a459f2ffeb52f56583e5f)) + ## 0.6.4 (2025-06-27) Full Changelog: [v0.6.3...v0.6.4](https://github.com/onkernel/kernel-python-sdk/compare/v0.6.3...v0.6.4) diff --git a/api.md b/api.md index 49538b6..8a0fbd3 100644 --- a/api.md +++ b/api.md @@ -92,3 +92,4 @@ Methods: - client.browsers.list() -> BrowserListResponse - client.browsers.delete(\*\*params) -> None - client.browsers.delete_by_id(id) -> None +- client.browsers.retrieve_replay(id) -> BinaryAPIResponse diff --git a/pyproject.toml b/pyproject.toml index 0c6e6ab..a5baacd 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "kernel" -version = "0.6.4" +version = "0.7.1" description = "The official Python library for the kernel API" dynamic = ["readme"] license = "Apache-2.0" diff --git a/requirements-dev.lock b/requirements-dev.lock index 27de013..f4090db 100644 --- a/requirements-dev.lock +++ b/requirements-dev.lock @@ -56,7 +56,7 @@ httpx==0.28.1 # via httpx-aiohttp # via kernel # via respx -httpx-aiohttp==0.1.6 +httpx-aiohttp==0.1.8 # via kernel idna==3.4 # via anyio diff --git a/requirements.lock b/requirements.lock index 4006aa2..c125a9e 100644 --- a/requirements.lock +++ b/requirements.lock @@ -43,7 +43,7 @@ httpcore==1.0.2 httpx==0.28.1 # via httpx-aiohttp # via kernel -httpx-aiohttp==0.1.6 +httpx-aiohttp==0.1.8 # via kernel idna==3.4 # via anyio diff --git a/scripts/utils/upload-artifact.sh b/scripts/utils/upload-artifact.sh index 7b344b4..14b2cc8 100755 --- a/scripts/utils/upload-artifact.sh +++ b/scripts/utils/upload-artifact.sh @@ -1,7 +1,9 @@ #!/usr/bin/env bash set -exuo pipefail -RESPONSE=$(curl -X POST "$URL" \ +FILENAME=$(basename dist/*.whl) + +RESPONSE=$(curl -X POST "$URL?filename=$FILENAME" \ -H "Authorization: Bearer $AUTH" \ -H "Content-Type: application/json") @@ -12,13 +14,13 @@ if [[ "$SIGNED_URL" == "null" ]]; then exit 1 fi -UPLOAD_RESPONSE=$(tar -cz . | curl -v -X PUT \ - -H "Content-Type: application/gzip" \ - --data-binary @- "$SIGNED_URL" 2>&1) +UPLOAD_RESPONSE=$(curl -v -X PUT \ + -H "Content-Type: binary/octet-stream" \ + --data-binary "@dist/$FILENAME" "$SIGNED_URL" 2>&1) if echo "$UPLOAD_RESPONSE" | grep -q "HTTP/[0-9.]* 200"; then echo -e "\033[32mUploaded build to Stainless storage.\033[0m" - echo -e "\033[32mInstallation: pip install 'https://pkg.stainless.com/s/kernel-python/$SHA'\033[0m" + echo -e "\033[32mInstallation: pip install 'https://pkg.stainless.com/s/kernel-python/$SHA/$FILENAME'\033[0m" else echo -e "\033[31mFailed to upload artifact.\033[0m" exit 1 diff --git a/src/kernel/_version.py b/src/kernel/_version.py index e0e69ec..c3ceacf 100644 --- a/src/kernel/_version.py +++ b/src/kernel/_version.py @@ -1,4 +1,4 @@ # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. __title__ = "kernel" -__version__ = "0.6.4" # x-release-please-version +__version__ = "0.7.1" # x-release-please-version diff --git a/src/kernel/resources/browsers.py b/src/kernel/resources/browsers.py index 0cbe820..01b529b 100644 --- a/src/kernel/resources/browsers.py +++ b/src/kernel/resources/browsers.py @@ -10,10 +10,18 @@ from .._compat import cached_property from .._resource import SyncAPIResource, AsyncAPIResource from .._response import ( + BinaryAPIResponse, + AsyncBinaryAPIResponse, + StreamedBinaryAPIResponse, + AsyncStreamedBinaryAPIResponse, to_raw_response_wrapper, to_streamed_response_wrapper, async_to_raw_response_wrapper, + to_custom_raw_response_wrapper, async_to_streamed_response_wrapper, + to_custom_streamed_response_wrapper, + async_to_custom_raw_response_wrapper, + async_to_custom_streamed_response_wrapper, ) from .._base_client import make_request_options from ..types.browser_list_response import BrowserListResponse @@ -47,8 +55,10 @@ def with_streaming_response(self) -> BrowsersResourceWithStreamingResponse: def create( self, *, + headless: bool | NotGiven = NOT_GIVEN, invocation_id: str | NotGiven = NOT_GIVEN, persistence: BrowserPersistenceParam | NotGiven = NOT_GIVEN, + replay: bool | NotGiven = NOT_GIVEN, stealth: bool | 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. @@ -61,10 +71,15 @@ def create( Create a new browser session from within an action. Args: + headless: If true, launches the browser using a headless image (no VNC/GUI). Defaults to + false. + invocation_id: action invocation ID persistence: Optional persistence configuration for the browser session. + replay: If true, enables replay recording of the browser session. Defaults to false. + stealth: If true, launches the browser in stealth mode to reduce detection by anti-bot mechanisms. @@ -80,8 +95,10 @@ def create( "/browsers", body=maybe_transform( { + "headless": headless, "invocation_id": invocation_id, "persistence": persistence, + "replay": replay, "stealth": stealth, }, browser_create_params.BrowserCreateParams, @@ -216,6 +233,40 @@ def delete_by_id( cast_to=NoneType, ) + def retrieve_replay( + self, + id: str, + *, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> BinaryAPIResponse: + """ + Get browser session replay. + + Args: + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not id: + raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") + extra_headers = {"Accept": "video/mp4", **(extra_headers or {})} + return self._get( + f"/browsers/{id}/replay", + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=BinaryAPIResponse, + ) + class AsyncBrowsersResource(AsyncAPIResource): @cached_property @@ -240,8 +291,10 @@ def with_streaming_response(self) -> AsyncBrowsersResourceWithStreamingResponse: async def create( self, *, + headless: bool | NotGiven = NOT_GIVEN, invocation_id: str | NotGiven = NOT_GIVEN, persistence: BrowserPersistenceParam | NotGiven = NOT_GIVEN, + replay: bool | NotGiven = NOT_GIVEN, stealth: bool | 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. @@ -254,10 +307,15 @@ async def create( Create a new browser session from within an action. Args: + headless: If true, launches the browser using a headless image (no VNC/GUI). Defaults to + false. + invocation_id: action invocation ID persistence: Optional persistence configuration for the browser session. + replay: If true, enables replay recording of the browser session. Defaults to false. + stealth: If true, launches the browser in stealth mode to reduce detection by anti-bot mechanisms. @@ -273,8 +331,10 @@ async def create( "/browsers", body=await async_maybe_transform( { + "headless": headless, "invocation_id": invocation_id, "persistence": persistence, + "replay": replay, "stealth": stealth, }, browser_create_params.BrowserCreateParams, @@ -411,6 +471,40 @@ async def delete_by_id( cast_to=NoneType, ) + async def retrieve_replay( + self, + id: str, + *, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> AsyncBinaryAPIResponse: + """ + Get browser session replay. + + Args: + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not id: + raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") + extra_headers = {"Accept": "video/mp4", **(extra_headers or {})} + return await self._get( + f"/browsers/{id}/replay", + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=AsyncBinaryAPIResponse, + ) + class BrowsersResourceWithRawResponse: def __init__(self, browsers: BrowsersResource) -> None: @@ -431,6 +525,10 @@ def __init__(self, browsers: BrowsersResource) -> None: self.delete_by_id = to_raw_response_wrapper( browsers.delete_by_id, ) + self.retrieve_replay = to_custom_raw_response_wrapper( + browsers.retrieve_replay, + BinaryAPIResponse, + ) class AsyncBrowsersResourceWithRawResponse: @@ -452,6 +550,10 @@ def __init__(self, browsers: AsyncBrowsersResource) -> None: self.delete_by_id = async_to_raw_response_wrapper( browsers.delete_by_id, ) + self.retrieve_replay = async_to_custom_raw_response_wrapper( + browsers.retrieve_replay, + AsyncBinaryAPIResponse, + ) class BrowsersResourceWithStreamingResponse: @@ -473,6 +575,10 @@ def __init__(self, browsers: BrowsersResource) -> None: self.delete_by_id = to_streamed_response_wrapper( browsers.delete_by_id, ) + self.retrieve_replay = to_custom_streamed_response_wrapper( + browsers.retrieve_replay, + StreamedBinaryAPIResponse, + ) class AsyncBrowsersResourceWithStreamingResponse: @@ -494,3 +600,7 @@ def __init__(self, browsers: AsyncBrowsersResource) -> None: self.delete_by_id = async_to_streamed_response_wrapper( browsers.delete_by_id, ) + self.retrieve_replay = async_to_custom_streamed_response_wrapper( + browsers.retrieve_replay, + AsyncStreamedBinaryAPIResponse, + ) diff --git a/src/kernel/types/browser_create_params.py b/src/kernel/types/browser_create_params.py index 2153abf..7019e53 100644 --- a/src/kernel/types/browser_create_params.py +++ b/src/kernel/types/browser_create_params.py @@ -10,12 +10,21 @@ class BrowserCreateParams(TypedDict, total=False): + headless: bool + """If true, launches the browser using a headless image (no VNC/GUI). + + Defaults to false. + """ + invocation_id: str """action invocation ID""" persistence: BrowserPersistenceParam """Optional persistence configuration for the browser session.""" + replay: bool + """If true, enables replay recording of the browser session. Defaults to false.""" + stealth: bool """ If true, launches the browser in stealth mode to reduce detection by anti-bot diff --git a/src/kernel/types/browser_create_response.py b/src/kernel/types/browser_create_response.py index f44f336..4fc470b 100644 --- a/src/kernel/types/browser_create_response.py +++ b/src/kernel/types/browser_create_response.py @@ -9,14 +9,20 @@ class BrowserCreateResponse(BaseModel): - browser_live_view_url: str - """Remote URL for live viewing the browser session""" - cdp_ws_url: str """Websocket URL for Chrome DevTools Protocol connections to the browser session""" session_id: str """Unique identifier for the browser session""" + browser_live_view_url: Optional[str] = None + """Remote URL for live viewing the browser session. + + Only available for non-headless browsers. + """ + persistence: Optional[BrowserPersistence] = None """Optional persistence configuration for the browser session.""" + + replay_view_url: Optional[str] = None + """Remote URL for viewing the browser session replay if enabled""" diff --git a/src/kernel/types/browser_list_response.py b/src/kernel/types/browser_list_response.py index d3e90d5..702c69b 100644 --- a/src/kernel/types/browser_list_response.py +++ b/src/kernel/types/browser_list_response.py @@ -10,17 +10,23 @@ class BrowserListResponseItem(BaseModel): - browser_live_view_url: str - """Remote URL for live viewing the browser session""" - cdp_ws_url: str """Websocket URL for Chrome DevTools Protocol connections to the browser session""" session_id: str """Unique identifier for the browser session""" + browser_live_view_url: Optional[str] = None + """Remote URL for live viewing the browser session. + + Only available for non-headless browsers. + """ + persistence: Optional[BrowserPersistence] = None """Optional persistence configuration for the browser session.""" + replay_view_url: Optional[str] = None + """Remote URL for viewing the browser session replay if enabled""" + BrowserListResponse: TypeAlias = List[BrowserListResponseItem] diff --git a/src/kernel/types/browser_retrieve_response.py b/src/kernel/types/browser_retrieve_response.py index 8676b53..8f44ddb 100644 --- a/src/kernel/types/browser_retrieve_response.py +++ b/src/kernel/types/browser_retrieve_response.py @@ -9,14 +9,20 @@ class BrowserRetrieveResponse(BaseModel): - browser_live_view_url: str - """Remote URL for live viewing the browser session""" - cdp_ws_url: str """Websocket URL for Chrome DevTools Protocol connections to the browser session""" session_id: str """Unique identifier for the browser session""" + browser_live_view_url: Optional[str] = None + """Remote URL for live viewing the browser session. + + Only available for non-headless browsers. + """ + persistence: Optional[BrowserPersistence] = None """Optional persistence configuration for the browser session.""" + + replay_view_url: Optional[str] = None + """Remote URL for viewing the browser session replay if enabled""" diff --git a/tests/api_resources/test_browsers.py b/tests/api_resources/test_browsers.py index f4111fa..a9cb8a5 100644 --- a/tests/api_resources/test_browsers.py +++ b/tests/api_resources/test_browsers.py @@ -5,7 +5,9 @@ import os from typing import Any, cast +import httpx import pytest +from respx import MockRouter from kernel import Kernel, AsyncKernel from tests.utils import assert_matches_type @@ -14,6 +16,12 @@ BrowserCreateResponse, BrowserRetrieveResponse, ) +from kernel._response import ( + BinaryAPIResponse, + AsyncBinaryAPIResponse, + StreamedBinaryAPIResponse, + AsyncStreamedBinaryAPIResponse, +) base_url = os.environ.get("TEST_API_BASE_URL", "http://127.0.0.1:4010") @@ -31,8 +39,10 @@ def test_method_create(self, client: Kernel) -> None: @parametrize def test_method_create_with_all_params(self, client: Kernel) -> None: browser = client.browsers.create( + headless=False, invocation_id="rr33xuugxj9h0bkf1rdt2bet", persistence={"id": "my-awesome-browser-for-user-1234"}, + replay=True, stealth=True, ) assert_matches_type(BrowserCreateResponse, browser, path=["response"]) @@ -205,6 +215,66 @@ def test_path_params_delete_by_id(self, client: Kernel) -> None: "", ) + @pytest.mark.skip() + @parametrize + @pytest.mark.respx(base_url=base_url) + def test_method_retrieve_replay(self, client: Kernel, respx_mock: MockRouter) -> None: + respx_mock.get("/browsers/htzv5orfit78e1m2biiifpbv/replay").mock( + return_value=httpx.Response(200, json={"foo": "bar"}) + ) + browser = client.browsers.retrieve_replay( + "id", + ) + assert browser.is_closed + assert browser.json() == {"foo": "bar"} + assert cast(Any, browser.is_closed) is True + assert isinstance(browser, BinaryAPIResponse) + + @pytest.mark.skip() + @parametrize + @pytest.mark.respx(base_url=base_url) + def test_raw_response_retrieve_replay(self, client: Kernel, respx_mock: MockRouter) -> None: + respx_mock.get("/browsers/htzv5orfit78e1m2biiifpbv/replay").mock( + return_value=httpx.Response(200, json={"foo": "bar"}) + ) + + browser = client.browsers.with_raw_response.retrieve_replay( + "id", + ) + + assert browser.is_closed is True + assert browser.http_request.headers.get("X-Stainless-Lang") == "python" + assert browser.json() == {"foo": "bar"} + assert isinstance(browser, BinaryAPIResponse) + + @pytest.mark.skip() + @parametrize + @pytest.mark.respx(base_url=base_url) + def test_streaming_response_retrieve_replay(self, client: Kernel, respx_mock: MockRouter) -> None: + respx_mock.get("/browsers/htzv5orfit78e1m2biiifpbv/replay").mock( + return_value=httpx.Response(200, json={"foo": "bar"}) + ) + with client.browsers.with_streaming_response.retrieve_replay( + "id", + ) as browser: + assert not browser.is_closed + assert browser.http_request.headers.get("X-Stainless-Lang") == "python" + + assert browser.json() == {"foo": "bar"} + assert cast(Any, browser.is_closed) is True + assert isinstance(browser, StreamedBinaryAPIResponse) + + assert cast(Any, browser.is_closed) is True + + @pytest.mark.skip() + @parametrize + @pytest.mark.respx(base_url=base_url) + def test_path_params_retrieve_replay(self, client: Kernel) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): + client.browsers.with_raw_response.retrieve_replay( + "", + ) + class TestAsyncBrowsers: parametrize = pytest.mark.parametrize( @@ -221,8 +291,10 @@ async def test_method_create(self, async_client: AsyncKernel) -> None: @parametrize async def test_method_create_with_all_params(self, async_client: AsyncKernel) -> None: browser = await async_client.browsers.create( + headless=False, invocation_id="rr33xuugxj9h0bkf1rdt2bet", persistence={"id": "my-awesome-browser-for-user-1234"}, + replay=True, stealth=True, ) assert_matches_type(BrowserCreateResponse, browser, path=["response"]) @@ -394,3 +466,63 @@ async def test_path_params_delete_by_id(self, async_client: AsyncKernel) -> None await async_client.browsers.with_raw_response.delete_by_id( "", ) + + @pytest.mark.skip() + @parametrize + @pytest.mark.respx(base_url=base_url) + async def test_method_retrieve_replay(self, async_client: AsyncKernel, respx_mock: MockRouter) -> None: + respx_mock.get("/browsers/htzv5orfit78e1m2biiifpbv/replay").mock( + return_value=httpx.Response(200, json={"foo": "bar"}) + ) + browser = await async_client.browsers.retrieve_replay( + "id", + ) + assert browser.is_closed + assert await browser.json() == {"foo": "bar"} + assert cast(Any, browser.is_closed) is True + assert isinstance(browser, AsyncBinaryAPIResponse) + + @pytest.mark.skip() + @parametrize + @pytest.mark.respx(base_url=base_url) + async def test_raw_response_retrieve_replay(self, async_client: AsyncKernel, respx_mock: MockRouter) -> None: + respx_mock.get("/browsers/htzv5orfit78e1m2biiifpbv/replay").mock( + return_value=httpx.Response(200, json={"foo": "bar"}) + ) + + browser = await async_client.browsers.with_raw_response.retrieve_replay( + "id", + ) + + assert browser.is_closed is True + assert browser.http_request.headers.get("X-Stainless-Lang") == "python" + assert await browser.json() == {"foo": "bar"} + assert isinstance(browser, AsyncBinaryAPIResponse) + + @pytest.mark.skip() + @parametrize + @pytest.mark.respx(base_url=base_url) + async def test_streaming_response_retrieve_replay(self, async_client: AsyncKernel, respx_mock: MockRouter) -> None: + respx_mock.get("/browsers/htzv5orfit78e1m2biiifpbv/replay").mock( + return_value=httpx.Response(200, json={"foo": "bar"}) + ) + async with async_client.browsers.with_streaming_response.retrieve_replay( + "id", + ) as browser: + assert not browser.is_closed + assert browser.http_request.headers.get("X-Stainless-Lang") == "python" + + assert await browser.json() == {"foo": "bar"} + assert cast(Any, browser.is_closed) is True + assert isinstance(browser, AsyncStreamedBinaryAPIResponse) + + assert cast(Any, browser.is_closed) is True + + @pytest.mark.skip() + @parametrize + @pytest.mark.respx(base_url=base_url) + async def test_path_params_retrieve_replay(self, async_client: AsyncKernel) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): + await async_client.browsers.with_raw_response.retrieve_replay( + "", + )