From e9a45fd655812a9bf2c3edec3cccdbde3ab89f73 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Sat, 28 Jun 2025 08:45:32 +0000 Subject: [PATCH 1/8] chore(ci): only run for pushes and fork pull requests --- .github/workflows/ci.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c3f5bc4..596c371 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 @@ -42,6 +43,7 @@ jobs: contents: read id-token: write runs-on: depot-ubuntu-24.04 + if: github.event_name == 'push' || github.event.pull_request.head.repo.fork steps: - uses: actions/checkout@v4 @@ -62,6 +64,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 From 116779521b08014f5be7588f1e0a7975c13e8e05 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Mon, 30 Jun 2025 02:29:51 +0000 Subject: [PATCH 2/8] fix(ci): correct conditional --- .github/workflows/ci.yml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 596c371..d53ac62 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -36,14 +36,13 @@ jobs: run: ./scripts/lint upload: - if: github.repository == 'stainless-sdks/kernel-python' + if: github.repository == 'stainless-sdks/kernel-python' && (github.event_name == 'push' || github.event.pull_request.head.repo.fork) timeout-minutes: 10 name: upload permissions: contents: read id-token: write runs-on: depot-ubuntu-24.04 - if: github.event_name == 'push' || github.event.pull_request.head.repo.fork steps: - uses: actions/checkout@v4 From dabede0456032d69d0c4b05c740d04002fc900a9 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Wed, 2 Jul 2025 05:04:51 +0000 Subject: [PATCH 3/8] chore(ci): change upload type --- .github/workflows/ci.yml | 18 ++++++++++++++++-- scripts/utils/upload-artifact.sh | 12 +++++++----- 2 files changed, 23 insertions(+), 7 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d53ac62..1692944 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -35,10 +35,10 @@ jobs: - name: Run lints run: ./scripts/lint - upload: + 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 @@ -46,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 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 From d3d4f049fc455e0d563593cb42b26e10e05b4d58 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Wed, 2 Jul 2025 19:04:18 +0000 Subject: [PATCH 4/8] codegen metadata --- .stats.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.stats.yml b/.stats.yml index 9625f4e..d45a618 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 +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel%2Fkernel-0ac9428eb663361184124cdd6a6e80ae8dc72c927626c949f22aacc4f40095de.yml +openapi_spec_hash: 27707667d706ac33f2d9ccb23c0f15c3 config_hash: 00ec9df250b9dc077f8d3b93a442d252 From de0b235998be2299459b54df15e83dd9dc8c0b7f Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Wed, 2 Jul 2025 21:49:30 +0000 Subject: [PATCH 5/8] feat(api): headless browsers --- .stats.yml | 4 ++-- src/kernel/resources/browsers.py | 10 ++++++++++ src/kernel/types/browser_create_params.py | 6 ++++++ src/kernel/types/browser_create_response.py | 9 ++++++--- src/kernel/types/browser_list_response.py | 9 ++++++--- src/kernel/types/browser_retrieve_response.py | 9 ++++++--- tests/api_resources/test_browsers.py | 2 ++ 7 files changed, 38 insertions(+), 11 deletions(-) diff --git a/.stats.yml b/.stats.yml index d45a618..401ed65 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-0ac9428eb663361184124cdd6a6e80ae8dc72c927626c949f22aacc4f40095de.yml -openapi_spec_hash: 27707667d706ac33f2d9ccb23c0f15c3 +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel%2Fkernel-3ec96d0022acb32aa2676c2e7ae20152b899a776ccd499380c334c955b9ba071.yml +openapi_spec_hash: b64c095d82185c1cd0355abea88b606f config_hash: 00ec9df250b9dc077f8d3b93a442d252 diff --git a/src/kernel/resources/browsers.py b/src/kernel/resources/browsers.py index 0cbe820..56a95f0 100644 --- a/src/kernel/resources/browsers.py +++ b/src/kernel/resources/browsers.py @@ -47,6 +47,7 @@ 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, stealth: bool | NotGiven = NOT_GIVEN, @@ -61,6 +62,9 @@ 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. @@ -80,6 +84,7 @@ def create( "/browsers", body=maybe_transform( { + "headless": headless, "invocation_id": invocation_id, "persistence": persistence, "stealth": stealth, @@ -240,6 +245,7 @@ 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, stealth: bool | NotGiven = NOT_GIVEN, @@ -254,6 +260,9 @@ 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. @@ -273,6 +282,7 @@ async def create( "/browsers", body=await async_maybe_transform( { + "headless": headless, "invocation_id": invocation_id, "persistence": persistence, "stealth": stealth, diff --git a/src/kernel/types/browser_create_params.py b/src/kernel/types/browser_create_params.py index 2153abf..746a92f 100644 --- a/src/kernel/types/browser_create_params.py +++ b/src/kernel/types/browser_create_params.py @@ -10,6 +10,12 @@ 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""" diff --git a/src/kernel/types/browser_create_response.py b/src/kernel/types/browser_create_response.py index f44f336..afba2b3 100644 --- a/src/kernel/types/browser_create_response.py +++ b/src/kernel/types/browser_create_response.py @@ -9,14 +9,17 @@ 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.""" diff --git a/src/kernel/types/browser_list_response.py b/src/kernel/types/browser_list_response.py index d3e90d5..43c8d92 100644 --- a/src/kernel/types/browser_list_response.py +++ b/src/kernel/types/browser_list_response.py @@ -10,15 +10,18 @@ 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.""" diff --git a/src/kernel/types/browser_retrieve_response.py b/src/kernel/types/browser_retrieve_response.py index 8676b53..45cf74b 100644 --- a/src/kernel/types/browser_retrieve_response.py +++ b/src/kernel/types/browser_retrieve_response.py @@ -9,14 +9,17 @@ 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.""" diff --git a/tests/api_resources/test_browsers.py b/tests/api_resources/test_browsers.py index f4111fa..8f990be 100644 --- a/tests/api_resources/test_browsers.py +++ b/tests/api_resources/test_browsers.py @@ -31,6 +31,7 @@ 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"}, stealth=True, @@ -221,6 +222,7 @@ 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"}, stealth=True, From 2c50b08edb7f73a7c20a459f2ffeb52f56583e5f Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Tue, 8 Jul 2025 02:13:13 +0000 Subject: [PATCH 6/8] chore(internal): codegen related update --- requirements-dev.lock | 2 +- requirements.lock | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) 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 From 7d0a2bd8dd25bac6d688e2b5f5076c916d80f800 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Tue, 8 Jul 2025 18:15:22 +0000 Subject: [PATCH 7/8] feat(api): manual updates --- .stats.yml | 8 +- api.md | 1 + src/kernel/resources/browsers.py | 100 ++++++++++++++ src/kernel/types/browser_create_params.py | 3 + src/kernel/types/browser_create_response.py | 3 + src/kernel/types/browser_list_response.py | 3 + src/kernel/types/browser_retrieve_response.py | 3 + tests/api_resources/test_browsers.py | 130 ++++++++++++++++++ 8 files changed, 247 insertions(+), 4 deletions(-) diff --git a/.stats.yml b/.stats.yml index 401ed65..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-3ec96d0022acb32aa2676c2e7ae20152b899a776ccd499380c334c955b9ba071.yml -openapi_spec_hash: b64c095d82185c1cd0355abea88b606f -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/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/src/kernel/resources/browsers.py b/src/kernel/resources/browsers.py index 56a95f0..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 @@ -50,6 +58,7 @@ def create( 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. @@ -69,6 +78,8 @@ def create( 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. @@ -87,6 +98,7 @@ def create( "headless": headless, "invocation_id": invocation_id, "persistence": persistence, + "replay": replay, "stealth": stealth, }, browser_create_params.BrowserCreateParams, @@ -221,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 @@ -248,6 +294,7 @@ async def create( 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. @@ -267,6 +314,8 @@ async def create( 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. @@ -285,6 +334,7 @@ async def create( "headless": headless, "invocation_id": invocation_id, "persistence": persistence, + "replay": replay, "stealth": stealth, }, browser_create_params.BrowserCreateParams, @@ -421,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: @@ -441,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: @@ -462,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: @@ -483,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: @@ -504,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 746a92f..7019e53 100644 --- a/src/kernel/types/browser_create_params.py +++ b/src/kernel/types/browser_create_params.py @@ -22,6 +22,9 @@ class BrowserCreateParams(TypedDict, total=False): 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 afba2b3..4fc470b 100644 --- a/src/kernel/types/browser_create_response.py +++ b/src/kernel/types/browser_create_response.py @@ -23,3 +23,6 @@ class BrowserCreateResponse(BaseModel): 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 43c8d92..702c69b 100644 --- a/src/kernel/types/browser_list_response.py +++ b/src/kernel/types/browser_list_response.py @@ -25,5 +25,8 @@ class BrowserListResponseItem(BaseModel): 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 45cf74b..8f44ddb 100644 --- a/src/kernel/types/browser_retrieve_response.py +++ b/src/kernel/types/browser_retrieve_response.py @@ -23,3 +23,6 @@ class BrowserRetrieveResponse(BaseModel): 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 8f990be..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") @@ -34,6 +42,7 @@ def test_method_create_with_all_params(self, client: Kernel) -> None: headless=False, invocation_id="rr33xuugxj9h0bkf1rdt2bet", persistence={"id": "my-awesome-browser-for-user-1234"}, + replay=True, stealth=True, ) assert_matches_type(BrowserCreateResponse, browser, path=["response"]) @@ -206,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( @@ -225,6 +294,7 @@ async def test_method_create_with_all_params(self, async_client: AsyncKernel) -> headless=False, invocation_id="rr33xuugxj9h0bkf1rdt2bet", persistence={"id": "my-awesome-browser-for-user-1234"}, + replay=True, stealth=True, ) assert_matches_type(BrowserCreateResponse, browser, path=["response"]) @@ -396,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( + "", + ) From 7288c7491d2739b93b96dc783eae578bc4ef4415 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Tue, 8 Jul 2025 18:16:28 +0000 Subject: [PATCH 8/8] release: 0.7.1 --- .release-please-manifest.json | 2 +- CHANGELOG.md | 21 +++++++++++++++++++++ pyproject.toml | 2 +- src/kernel/_version.py | 2 +- 4 files changed, 24 insertions(+), 3 deletions(-) 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/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/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/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