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