From 89bac15e58e4892e653a9dafeb2d15c88d7fdbb9 Mon Sep 17 00:00:00 2001
From: "stainless-app[bot]"
<142633134+stainless-app[bot]@users.noreply.github.com>
Date: Tue, 7 Oct 2025 13:43:54 +0000
Subject: [PATCH 1/3] feat: WIP browser extensions
---
.stats.yml | 8 +-
api.md | 17 +
src/kernel/_client.py | 10 +-
src/kernel/resources/__init__.py | 14 +
src/kernel/resources/browsers/browsers.py | 118 +++-
src/kernel/resources/extensions.py | 539 ++++++++++++++++++
src/kernel/types/__init__.py | 7 +
src/kernel/types/browser_create_params.py | 20 +-
.../types/browser_upload_extensions_params.py | 26 +
...nsion_download_from_chrome_store_params.py | 15 +
src/kernel/types/extension_list_response.py | 32 ++
src/kernel/types/extension_upload_params.py | 17 +
src/kernel/types/extension_upload_response.py | 28 +
tests/api_resources/test_browsers.py | 144 +++++
tests/api_resources/test_extensions.py | 477 ++++++++++++++++
15 files changed, 1464 insertions(+), 8 deletions(-)
create mode 100644 src/kernel/resources/extensions.py
create mode 100644 src/kernel/types/browser_upload_extensions_params.py
create mode 100644 src/kernel/types/extension_download_from_chrome_store_params.py
create mode 100644 src/kernel/types/extension_list_response.py
create mode 100644 src/kernel/types/extension_upload_params.py
create mode 100644 src/kernel/types/extension_upload_response.py
create mode 100644 tests/api_resources/test_extensions.py
diff --git a/.stats.yml b/.stats.yml
index 0af2575f..b296ff8f 100644
--- a/.stats.yml
+++ b/.stats.yml
@@ -1,4 +1,4 @@
-configured_endpoints: 51
-openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel%2Fkernel-8a6175a75caa75c3de5400edf97a34e526ac3f62c63955375437461581deb0c2.yml
-openapi_spec_hash: 1a880e4ce337a0e44630e6d87ef5162a
-config_hash: 49c2ff978aaa5ccb4ce324a72f116010
+configured_endpoints: 57
+openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel%2Fkernel-936db268b3dcae5d64bd5d590506d8134304ffcbf67389eb9b1555b3febfd4cb.yml
+openapi_spec_hash: 145485087adf1b28c052bacb4df68462
+config_hash: 5236f9b34e39dc1930e36a88c714abd4
diff --git a/api.md b/api.md
index dc0a70f6..be7fff4e 100644
--- a/api.md
+++ b/api.md
@@ -82,6 +82,7 @@ Methods:
- client.browsers.list() -> BrowserListResponse
- client.browsers.delete(\*\*params) -> None
- client.browsers.delete_by_id(id) -> None
+- client.browsers.upload_extensions(id, \*\*params) -> None
## Replays
@@ -195,3 +196,19 @@ Methods:
- client.proxies.retrieve(id) -> ProxyRetrieveResponse
- client.proxies.list() -> ProxyListResponse
- client.proxies.delete(id) -> None
+
+# Extensions
+
+Types:
+
+```python
+from kernel.types import ExtensionListResponse, ExtensionUploadResponse
+```
+
+Methods:
+
+- client.extensions.list() -> ExtensionListResponse
+- client.extensions.delete(id_or_name) -> None
+- client.extensions.download(id_or_name) -> BinaryAPIResponse
+- client.extensions.download_from_chrome_store(\*\*params) -> BinaryAPIResponse
+- client.extensions.upload(\*\*params) -> ExtensionUploadResponse
diff --git a/src/kernel/_client.py b/src/kernel/_client.py
index 2821af63..ea9e51b9 100644
--- a/src/kernel/_client.py
+++ b/src/kernel/_client.py
@@ -21,7 +21,7 @@
)
from ._utils import is_given, get_async_library
from ._version import __version__
-from .resources import apps, proxies, profiles, deployments, invocations
+from .resources import apps, proxies, profiles, extensions, deployments, invocations
from ._streaming import Stream as Stream, AsyncStream as AsyncStream
from ._exceptions import KernelError, APIStatusError
from ._base_client import (
@@ -56,6 +56,7 @@ class Kernel(SyncAPIClient):
browsers: browsers.BrowsersResource
profiles: profiles.ProfilesResource
proxies: proxies.ProxiesResource
+ extensions: extensions.ExtensionsResource
with_raw_response: KernelWithRawResponse
with_streaming_response: KernelWithStreamedResponse
@@ -143,6 +144,7 @@ def __init__(
self.browsers = browsers.BrowsersResource(self)
self.profiles = profiles.ProfilesResource(self)
self.proxies = proxies.ProxiesResource(self)
+ self.extensions = extensions.ExtensionsResource(self)
self.with_raw_response = KernelWithRawResponse(self)
self.with_streaming_response = KernelWithStreamedResponse(self)
@@ -260,6 +262,7 @@ class AsyncKernel(AsyncAPIClient):
browsers: browsers.AsyncBrowsersResource
profiles: profiles.AsyncProfilesResource
proxies: proxies.AsyncProxiesResource
+ extensions: extensions.AsyncExtensionsResource
with_raw_response: AsyncKernelWithRawResponse
with_streaming_response: AsyncKernelWithStreamedResponse
@@ -347,6 +350,7 @@ def __init__(
self.browsers = browsers.AsyncBrowsersResource(self)
self.profiles = profiles.AsyncProfilesResource(self)
self.proxies = proxies.AsyncProxiesResource(self)
+ self.extensions = extensions.AsyncExtensionsResource(self)
self.with_raw_response = AsyncKernelWithRawResponse(self)
self.with_streaming_response = AsyncKernelWithStreamedResponse(self)
@@ -465,6 +469,7 @@ def __init__(self, client: Kernel) -> None:
self.browsers = browsers.BrowsersResourceWithRawResponse(client.browsers)
self.profiles = profiles.ProfilesResourceWithRawResponse(client.profiles)
self.proxies = proxies.ProxiesResourceWithRawResponse(client.proxies)
+ self.extensions = extensions.ExtensionsResourceWithRawResponse(client.extensions)
class AsyncKernelWithRawResponse:
@@ -475,6 +480,7 @@ def __init__(self, client: AsyncKernel) -> None:
self.browsers = browsers.AsyncBrowsersResourceWithRawResponse(client.browsers)
self.profiles = profiles.AsyncProfilesResourceWithRawResponse(client.profiles)
self.proxies = proxies.AsyncProxiesResourceWithRawResponse(client.proxies)
+ self.extensions = extensions.AsyncExtensionsResourceWithRawResponse(client.extensions)
class KernelWithStreamedResponse:
@@ -485,6 +491,7 @@ def __init__(self, client: Kernel) -> None:
self.browsers = browsers.BrowsersResourceWithStreamingResponse(client.browsers)
self.profiles = profiles.ProfilesResourceWithStreamingResponse(client.profiles)
self.proxies = proxies.ProxiesResourceWithStreamingResponse(client.proxies)
+ self.extensions = extensions.ExtensionsResourceWithStreamingResponse(client.extensions)
class AsyncKernelWithStreamedResponse:
@@ -495,6 +502,7 @@ def __init__(self, client: AsyncKernel) -> None:
self.browsers = browsers.AsyncBrowsersResourceWithStreamingResponse(client.browsers)
self.profiles = profiles.AsyncProfilesResourceWithStreamingResponse(client.profiles)
self.proxies = proxies.AsyncProxiesResourceWithStreamingResponse(client.proxies)
+ self.extensions = extensions.AsyncExtensionsResourceWithStreamingResponse(client.extensions)
Client = Kernel
diff --git a/src/kernel/resources/__init__.py b/src/kernel/resources/__init__.py
index 23b6b077..1b68d89f 100644
--- a/src/kernel/resources/__init__.py
+++ b/src/kernel/resources/__init__.py
@@ -32,6 +32,14 @@
ProfilesResourceWithStreamingResponse,
AsyncProfilesResourceWithStreamingResponse,
)
+from .extensions import (
+ ExtensionsResource,
+ AsyncExtensionsResource,
+ ExtensionsResourceWithRawResponse,
+ AsyncExtensionsResourceWithRawResponse,
+ ExtensionsResourceWithStreamingResponse,
+ AsyncExtensionsResourceWithStreamingResponse,
+)
from .deployments import (
DeploymentsResource,
AsyncDeploymentsResource,
@@ -86,4 +94,10 @@
"AsyncProxiesResourceWithRawResponse",
"ProxiesResourceWithStreamingResponse",
"AsyncProxiesResourceWithStreamingResponse",
+ "ExtensionsResource",
+ "AsyncExtensionsResource",
+ "ExtensionsResourceWithRawResponse",
+ "AsyncExtensionsResourceWithRawResponse",
+ "ExtensionsResourceWithStreamingResponse",
+ "AsyncExtensionsResourceWithStreamingResponse",
]
diff --git a/src/kernel/resources/browsers/browsers.py b/src/kernel/resources/browsers/browsers.py
index 145d0831..5e5530b1 100644
--- a/src/kernel/resources/browsers/browsers.py
+++ b/src/kernel/resources/browsers/browsers.py
@@ -2,6 +2,8 @@
from __future__ import annotations
+from typing import Mapping, Iterable, cast
+
import httpx
from .logs import (
@@ -20,7 +22,7 @@
FsResourceWithStreamingResponse,
AsyncFsResourceWithStreamingResponse,
)
-from ...types import browser_create_params, browser_delete_params
+from ...types import browser_create_params, browser_delete_params, browser_upload_extensions_params
from .process import (
ProcessResource,
AsyncProcessResource,
@@ -38,7 +40,7 @@
AsyncReplaysResourceWithStreamingResponse,
)
from ..._types import Body, Omit, Query, Headers, NoneType, NotGiven, omit, not_given
-from ..._utils import maybe_transform, async_maybe_transform
+from ..._utils import extract_files, maybe_transform, deepcopy_minimal, async_maybe_transform
from ..._compat import cached_property
from ..._resource import SyncAPIResource, AsyncAPIResource
from ..._response import (
@@ -95,6 +97,7 @@ def with_streaming_response(self) -> BrowsersResourceWithStreamingResponse:
def create(
self,
*,
+ extensions: Iterable[browser_create_params.Extension] | Omit = omit,
headless: bool | Omit = omit,
invocation_id: str | Omit = omit,
persistence: BrowserPersistenceParam | Omit = omit,
@@ -113,6 +116,8 @@ def create(
Create a new browser session from within an action.
Args:
+ extensions: List of browser extensions to load into the session. Provide each by id or name.
+
headless: If true, launches the browser using a headless image (no VNC/GUI). Defaults to
false.
@@ -149,6 +154,7 @@ def create(
"/browsers",
body=maybe_transform(
{
+ "extensions": extensions,
"headless": headless,
"invocation_id": invocation_id,
"persistence": persistence,
@@ -289,6 +295,52 @@ def delete_by_id(
cast_to=NoneType,
)
+ def upload_extensions(
+ self,
+ id: str,
+ *,
+ extensions: Iterable[browser_upload_extensions_params.Extension],
+ # 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,
+ ) -> None:
+ """
+ Loads one or more unpacked extensions and restarts Chromium on the browser
+ instance.
+
+ Args:
+ extensions: List of extensions to upload and activate
+
+ 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": "*/*", **(extra_headers or {})}
+ body = deepcopy_minimal({"extensions": extensions})
+ files = extract_files(cast(Mapping[str, object], body), paths=[["extensions", "", "zip_file"]])
+ # It should be noted that the actual Content-Type header that will be
+ # sent to the server will contain a `boundary` parameter, e.g.
+ # multipart/form-data; boundary=---abc--
+ extra_headers["Content-Type"] = "multipart/form-data"
+ return self._post(
+ f"/browsers/{id}/extensions",
+ body=maybe_transform(body, browser_upload_extensions_params.BrowserUploadExtensionsParams),
+ files=files,
+ options=make_request_options(
+ extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
+ ),
+ cast_to=NoneType,
+ )
+
class AsyncBrowsersResource(AsyncAPIResource):
@cached_property
@@ -329,6 +381,7 @@ def with_streaming_response(self) -> AsyncBrowsersResourceWithStreamingResponse:
async def create(
self,
*,
+ extensions: Iterable[browser_create_params.Extension] | Omit = omit,
headless: bool | Omit = omit,
invocation_id: str | Omit = omit,
persistence: BrowserPersistenceParam | Omit = omit,
@@ -347,6 +400,8 @@ async def create(
Create a new browser session from within an action.
Args:
+ extensions: List of browser extensions to load into the session. Provide each by id or name.
+
headless: If true, launches the browser using a headless image (no VNC/GUI). Defaults to
false.
@@ -383,6 +438,7 @@ async def create(
"/browsers",
body=await async_maybe_transform(
{
+ "extensions": extensions,
"headless": headless,
"invocation_id": invocation_id,
"persistence": persistence,
@@ -525,6 +581,52 @@ async def delete_by_id(
cast_to=NoneType,
)
+ async def upload_extensions(
+ self,
+ id: str,
+ *,
+ extensions: Iterable[browser_upload_extensions_params.Extension],
+ # 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,
+ ) -> None:
+ """
+ Loads one or more unpacked extensions and restarts Chromium on the browser
+ instance.
+
+ Args:
+ extensions: List of extensions to upload and activate
+
+ 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": "*/*", **(extra_headers or {})}
+ body = deepcopy_minimal({"extensions": extensions})
+ files = extract_files(cast(Mapping[str, object], body), paths=[["extensions", "", "zip_file"]])
+ # It should be noted that the actual Content-Type header that will be
+ # sent to the server will contain a `boundary` parameter, e.g.
+ # multipart/form-data; boundary=---abc--
+ extra_headers["Content-Type"] = "multipart/form-data"
+ return await self._post(
+ f"/browsers/{id}/extensions",
+ body=await async_maybe_transform(body, browser_upload_extensions_params.BrowserUploadExtensionsParams),
+ files=files,
+ options=make_request_options(
+ extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
+ ),
+ cast_to=NoneType,
+ )
+
class BrowsersResourceWithRawResponse:
def __init__(self, browsers: BrowsersResource) -> None:
@@ -545,6 +647,9 @@ def __init__(self, browsers: BrowsersResource) -> None:
self.delete_by_id = to_raw_response_wrapper(
browsers.delete_by_id,
)
+ self.upload_extensions = to_raw_response_wrapper(
+ browsers.upload_extensions,
+ )
@cached_property
def replays(self) -> ReplaysResourceWithRawResponse:
@@ -582,6 +687,9 @@ def __init__(self, browsers: AsyncBrowsersResource) -> None:
self.delete_by_id = async_to_raw_response_wrapper(
browsers.delete_by_id,
)
+ self.upload_extensions = async_to_raw_response_wrapper(
+ browsers.upload_extensions,
+ )
@cached_property
def replays(self) -> AsyncReplaysResourceWithRawResponse:
@@ -619,6 +727,9 @@ def __init__(self, browsers: BrowsersResource) -> None:
self.delete_by_id = to_streamed_response_wrapper(
browsers.delete_by_id,
)
+ self.upload_extensions = to_streamed_response_wrapper(
+ browsers.upload_extensions,
+ )
@cached_property
def replays(self) -> ReplaysResourceWithStreamingResponse:
@@ -656,6 +767,9 @@ def __init__(self, browsers: AsyncBrowsersResource) -> None:
self.delete_by_id = async_to_streamed_response_wrapper(
browsers.delete_by_id,
)
+ self.upload_extensions = async_to_streamed_response_wrapper(
+ browsers.upload_extensions,
+ )
@cached_property
def replays(self) -> AsyncReplaysResourceWithStreamingResponse:
diff --git a/src/kernel/resources/extensions.py b/src/kernel/resources/extensions.py
new file mode 100644
index 00000000..2f868716
--- /dev/null
+++ b/src/kernel/resources/extensions.py
@@ -0,0 +1,539 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from __future__ import annotations
+
+from typing import Mapping, cast
+from typing_extensions import Literal
+
+import httpx
+
+from ..types import extension_upload_params, extension_download_from_chrome_store_params
+from .._types import Body, Omit, Query, Headers, NoneType, NotGiven, FileTypes, omit, not_given
+from .._utils import extract_files, maybe_transform, deepcopy_minimal, async_maybe_transform
+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.extension_list_response import ExtensionListResponse
+from ..types.extension_upload_response import ExtensionUploadResponse
+
+__all__ = ["ExtensionsResource", "AsyncExtensionsResource"]
+
+
+class ExtensionsResource(SyncAPIResource):
+ @cached_property
+ def with_raw_response(self) -> ExtensionsResourceWithRawResponse:
+ """
+ This property can be used as a prefix for any HTTP method call to return
+ the raw response object instead of the parsed content.
+
+ For more information, see https://www.github.com/onkernel/kernel-python-sdk#accessing-raw-response-data-eg-headers
+ """
+ return ExtensionsResourceWithRawResponse(self)
+
+ @cached_property
+ def with_streaming_response(self) -> ExtensionsResourceWithStreamingResponse:
+ """
+ An alternative to `.with_raw_response` that doesn't eagerly read the response body.
+
+ For more information, see https://www.github.com/onkernel/kernel-python-sdk#with_streaming_response
+ """
+ return ExtensionsResourceWithStreamingResponse(self)
+
+ def list(
+ self,
+ *,
+ # 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,
+ ) -> ExtensionListResponse:
+ """List extensions owned by the caller's organization."""
+ return self._get(
+ "/extensions",
+ options=make_request_options(
+ extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
+ ),
+ cast_to=ExtensionListResponse,
+ )
+
+ def delete(
+ self,
+ id_or_name: 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,
+ ) -> None:
+ """
+ Delete an extension by its ID or by its name.
+
+ 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_or_name:
+ raise ValueError(f"Expected a non-empty value for `id_or_name` but received {id_or_name!r}")
+ extra_headers = {"Accept": "*/*", **(extra_headers or {})}
+ return self._delete(
+ f"/extensions/{id_or_name}",
+ options=make_request_options(
+ extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
+ ),
+ cast_to=NoneType,
+ )
+
+ def download(
+ self,
+ id_or_name: 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:
+ """
+ Download the extension as a ZIP archive by ID or name.
+
+ 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_or_name:
+ raise ValueError(f"Expected a non-empty value for `id_or_name` but received {id_or_name!r}")
+ extra_headers = {"Accept": "application/octet-stream", **(extra_headers or {})}
+ return self._get(
+ f"/extensions/{id_or_name}",
+ options=make_request_options(
+ extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
+ ),
+ cast_to=BinaryAPIResponse,
+ )
+
+ def download_from_chrome_store(
+ self,
+ *,
+ url: str,
+ os: Literal["win", "mac", "linux"] | Omit = omit,
+ # 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:
+ """
+ Returns a ZIP archive containing the unpacked extension fetched from the Chrome
+ Web Store.
+
+ Args:
+ url: Chrome Web Store URL for the extension.
+
+ os: Target operating system for the extension package. Defaults to linux.
+
+ 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
+ """
+ extra_headers = {"Accept": "application/octet-stream", **(extra_headers or {})}
+ return self._get(
+ "/extensions/from_chrome_store",
+ options=make_request_options(
+ extra_headers=extra_headers,
+ extra_query=extra_query,
+ extra_body=extra_body,
+ timeout=timeout,
+ query=maybe_transform(
+ {
+ "url": url,
+ "os": os,
+ },
+ extension_download_from_chrome_store_params.ExtensionDownloadFromChromeStoreParams,
+ ),
+ ),
+ cast_to=BinaryAPIResponse,
+ )
+
+ def upload(
+ self,
+ *,
+ file: FileTypes,
+ name: str | Omit = omit,
+ # 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,
+ ) -> ExtensionUploadResponse:
+ """Upload a zip file containing an unpacked browser extension.
+
+ Optionally provide a
+ unique name for later reference.
+
+ Args:
+ file: ZIP file containing the browser extension.
+
+ name: Optional unique name within the organization to reference this extension.
+
+ 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
+ """
+ body = deepcopy_minimal(
+ {
+ "file": file,
+ "name": name,
+ }
+ )
+ files = extract_files(cast(Mapping[str, object], body), paths=[["file"]])
+ # It should be noted that the actual Content-Type header that will be
+ # sent to the server will contain a `boundary` parameter, e.g.
+ # multipart/form-data; boundary=---abc--
+ extra_headers = {"Content-Type": "multipart/form-data", **(extra_headers or {})}
+ return self._post(
+ "/extensions",
+ body=maybe_transform(body, extension_upload_params.ExtensionUploadParams),
+ files=files,
+ options=make_request_options(
+ extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
+ ),
+ cast_to=ExtensionUploadResponse,
+ )
+
+
+class AsyncExtensionsResource(AsyncAPIResource):
+ @cached_property
+ def with_raw_response(self) -> AsyncExtensionsResourceWithRawResponse:
+ """
+ This property can be used as a prefix for any HTTP method call to return
+ the raw response object instead of the parsed content.
+
+ For more information, see https://www.github.com/onkernel/kernel-python-sdk#accessing-raw-response-data-eg-headers
+ """
+ return AsyncExtensionsResourceWithRawResponse(self)
+
+ @cached_property
+ def with_streaming_response(self) -> AsyncExtensionsResourceWithStreamingResponse:
+ """
+ An alternative to `.with_raw_response` that doesn't eagerly read the response body.
+
+ For more information, see https://www.github.com/onkernel/kernel-python-sdk#with_streaming_response
+ """
+ return AsyncExtensionsResourceWithStreamingResponse(self)
+
+ async def list(
+ self,
+ *,
+ # 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,
+ ) -> ExtensionListResponse:
+ """List extensions owned by the caller's organization."""
+ return await self._get(
+ "/extensions",
+ options=make_request_options(
+ extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
+ ),
+ cast_to=ExtensionListResponse,
+ )
+
+ async def delete(
+ self,
+ id_or_name: 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,
+ ) -> None:
+ """
+ Delete an extension by its ID or by its name.
+
+ 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_or_name:
+ raise ValueError(f"Expected a non-empty value for `id_or_name` but received {id_or_name!r}")
+ extra_headers = {"Accept": "*/*", **(extra_headers or {})}
+ return await self._delete(
+ f"/extensions/{id_or_name}",
+ options=make_request_options(
+ extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
+ ),
+ cast_to=NoneType,
+ )
+
+ async def download(
+ self,
+ id_or_name: 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:
+ """
+ Download the extension as a ZIP archive by ID or name.
+
+ 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_or_name:
+ raise ValueError(f"Expected a non-empty value for `id_or_name` but received {id_or_name!r}")
+ extra_headers = {"Accept": "application/octet-stream", **(extra_headers or {})}
+ return await self._get(
+ f"/extensions/{id_or_name}",
+ options=make_request_options(
+ extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
+ ),
+ cast_to=AsyncBinaryAPIResponse,
+ )
+
+ async def download_from_chrome_store(
+ self,
+ *,
+ url: str,
+ os: Literal["win", "mac", "linux"] | Omit = omit,
+ # 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:
+ """
+ Returns a ZIP archive containing the unpacked extension fetched from the Chrome
+ Web Store.
+
+ Args:
+ url: Chrome Web Store URL for the extension.
+
+ os: Target operating system for the extension package. Defaults to linux.
+
+ 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
+ """
+ extra_headers = {"Accept": "application/octet-stream", **(extra_headers or {})}
+ return await self._get(
+ "/extensions/from_chrome_store",
+ options=make_request_options(
+ extra_headers=extra_headers,
+ extra_query=extra_query,
+ extra_body=extra_body,
+ timeout=timeout,
+ query=await async_maybe_transform(
+ {
+ "url": url,
+ "os": os,
+ },
+ extension_download_from_chrome_store_params.ExtensionDownloadFromChromeStoreParams,
+ ),
+ ),
+ cast_to=AsyncBinaryAPIResponse,
+ )
+
+ async def upload(
+ self,
+ *,
+ file: FileTypes,
+ name: str | Omit = omit,
+ # 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,
+ ) -> ExtensionUploadResponse:
+ """Upload a zip file containing an unpacked browser extension.
+
+ Optionally provide a
+ unique name for later reference.
+
+ Args:
+ file: ZIP file containing the browser extension.
+
+ name: Optional unique name within the organization to reference this extension.
+
+ 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
+ """
+ body = deepcopy_minimal(
+ {
+ "file": file,
+ "name": name,
+ }
+ )
+ files = extract_files(cast(Mapping[str, object], body), paths=[["file"]])
+ # It should be noted that the actual Content-Type header that will be
+ # sent to the server will contain a `boundary` parameter, e.g.
+ # multipart/form-data; boundary=---abc--
+ extra_headers = {"Content-Type": "multipart/form-data", **(extra_headers or {})}
+ return await self._post(
+ "/extensions",
+ body=await async_maybe_transform(body, extension_upload_params.ExtensionUploadParams),
+ files=files,
+ options=make_request_options(
+ extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
+ ),
+ cast_to=ExtensionUploadResponse,
+ )
+
+
+class ExtensionsResourceWithRawResponse:
+ def __init__(self, extensions: ExtensionsResource) -> None:
+ self._extensions = extensions
+
+ self.list = to_raw_response_wrapper(
+ extensions.list,
+ )
+ self.delete = to_raw_response_wrapper(
+ extensions.delete,
+ )
+ self.download = to_custom_raw_response_wrapper(
+ extensions.download,
+ BinaryAPIResponse,
+ )
+ self.download_from_chrome_store = to_custom_raw_response_wrapper(
+ extensions.download_from_chrome_store,
+ BinaryAPIResponse,
+ )
+ self.upload = to_raw_response_wrapper(
+ extensions.upload,
+ )
+
+
+class AsyncExtensionsResourceWithRawResponse:
+ def __init__(self, extensions: AsyncExtensionsResource) -> None:
+ self._extensions = extensions
+
+ self.list = async_to_raw_response_wrapper(
+ extensions.list,
+ )
+ self.delete = async_to_raw_response_wrapper(
+ extensions.delete,
+ )
+ self.download = async_to_custom_raw_response_wrapper(
+ extensions.download,
+ AsyncBinaryAPIResponse,
+ )
+ self.download_from_chrome_store = async_to_custom_raw_response_wrapper(
+ extensions.download_from_chrome_store,
+ AsyncBinaryAPIResponse,
+ )
+ self.upload = async_to_raw_response_wrapper(
+ extensions.upload,
+ )
+
+
+class ExtensionsResourceWithStreamingResponse:
+ def __init__(self, extensions: ExtensionsResource) -> None:
+ self._extensions = extensions
+
+ self.list = to_streamed_response_wrapper(
+ extensions.list,
+ )
+ self.delete = to_streamed_response_wrapper(
+ extensions.delete,
+ )
+ self.download = to_custom_streamed_response_wrapper(
+ extensions.download,
+ StreamedBinaryAPIResponse,
+ )
+ self.download_from_chrome_store = to_custom_streamed_response_wrapper(
+ extensions.download_from_chrome_store,
+ StreamedBinaryAPIResponse,
+ )
+ self.upload = to_streamed_response_wrapper(
+ extensions.upload,
+ )
+
+
+class AsyncExtensionsResourceWithStreamingResponse:
+ def __init__(self, extensions: AsyncExtensionsResource) -> None:
+ self._extensions = extensions
+
+ self.list = async_to_streamed_response_wrapper(
+ extensions.list,
+ )
+ self.delete = async_to_streamed_response_wrapper(
+ extensions.delete,
+ )
+ self.download = async_to_custom_streamed_response_wrapper(
+ extensions.download,
+ AsyncStreamedBinaryAPIResponse,
+ )
+ self.download_from_chrome_store = async_to_custom_streamed_response_wrapper(
+ extensions.download_from_chrome_store,
+ AsyncStreamedBinaryAPIResponse,
+ )
+ self.upload = async_to_streamed_response_wrapper(
+ extensions.upload,
+ )
diff --git a/src/kernel/types/__init__.py b/src/kernel/types/__init__.py
index b14918e9..bc28375f 100644
--- a/src/kernel/types/__init__.py
+++ b/src/kernel/types/__init__.py
@@ -27,6 +27,8 @@
from .invocation_list_params import InvocationListParams as InvocationListParams
from .invocation_state_event import InvocationStateEvent as InvocationStateEvent
from .browser_create_response import BrowserCreateResponse as BrowserCreateResponse
+from .extension_list_response import ExtensionListResponse as ExtensionListResponse
+from .extension_upload_params import ExtensionUploadParams as ExtensionUploadParams
from .proxy_retrieve_response import ProxyRetrieveResponse as ProxyRetrieveResponse
from .deployment_create_params import DeploymentCreateParams as DeploymentCreateParams
from .deployment_follow_params import DeploymentFollowParams as DeploymentFollowParams
@@ -37,6 +39,7 @@
from .invocation_update_params import InvocationUpdateParams as InvocationUpdateParams
from .browser_persistence_param import BrowserPersistenceParam as BrowserPersistenceParam
from .browser_retrieve_response import BrowserRetrieveResponse as BrowserRetrieveResponse
+from .extension_upload_response import ExtensionUploadResponse as ExtensionUploadResponse
from .deployment_create_response import DeploymentCreateResponse as DeploymentCreateResponse
from .deployment_follow_response import DeploymentFollowResponse as DeploymentFollowResponse
from .invocation_create_response import InvocationCreateResponse as InvocationCreateResponse
@@ -44,3 +47,7 @@
from .invocation_update_response import InvocationUpdateResponse as InvocationUpdateResponse
from .deployment_retrieve_response import DeploymentRetrieveResponse as DeploymentRetrieveResponse
from .invocation_retrieve_response import InvocationRetrieveResponse as InvocationRetrieveResponse
+from .browser_upload_extensions_params import BrowserUploadExtensionsParams as BrowserUploadExtensionsParams
+from .extension_download_from_chrome_store_params import (
+ ExtensionDownloadFromChromeStoreParams as ExtensionDownloadFromChromeStoreParams,
+)
diff --git a/src/kernel/types/browser_create_params.py b/src/kernel/types/browser_create_params.py
index ed65be6f..4a1104c8 100644
--- a/src/kernel/types/browser_create_params.py
+++ b/src/kernel/types/browser_create_params.py
@@ -2,14 +2,21 @@
from __future__ import annotations
+from typing import Iterable
from typing_extensions import TypedDict
from .browser_persistence_param import BrowserPersistenceParam
-__all__ = ["BrowserCreateParams", "Profile"]
+__all__ = ["BrowserCreateParams", "Extension", "Profile"]
class BrowserCreateParams(TypedDict, total=False):
+ extensions: Iterable[Extension]
+ """List of browser extensions to load into the session.
+
+ Provide each by id or name.
+ """
+
headless: bool
"""If true, launches the browser using a headless image (no VNC/GUI).
@@ -52,6 +59,17 @@ class BrowserCreateParams(TypedDict, total=False):
"""
+class Extension(TypedDict, total=False):
+ id: str
+ """Extension ID to load for this browser session"""
+
+ name: str
+ """Extension name to load for this browser session (instead of id).
+
+ Must be 1-255 characters, using letters, numbers, dots, underscores, or hyphens.
+ """
+
+
class Profile(TypedDict, total=False):
id: str
"""Profile ID to load for this browser session"""
diff --git a/src/kernel/types/browser_upload_extensions_params.py b/src/kernel/types/browser_upload_extensions_params.py
new file mode 100644
index 00000000..ab0363cb
--- /dev/null
+++ b/src/kernel/types/browser_upload_extensions_params.py
@@ -0,0 +1,26 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from __future__ import annotations
+
+from typing import Iterable
+from typing_extensions import Required, TypedDict
+
+from .._types import FileTypes
+
+__all__ = ["BrowserUploadExtensionsParams", "Extension"]
+
+
+class BrowserUploadExtensionsParams(TypedDict, total=False):
+ extensions: Required[Iterable[Extension]]
+ """List of extensions to upload and activate"""
+
+
+class Extension(TypedDict, total=False):
+ name: Required[str]
+ """Folder name to place the extension under /home/kernel/extensions/"""
+
+ zip_file: Required[FileTypes]
+ """
+ Zip archive containing an unpacked Chromium extension (must include
+ manifest.json)
+ """
diff --git a/src/kernel/types/extension_download_from_chrome_store_params.py b/src/kernel/types/extension_download_from_chrome_store_params.py
new file mode 100644
index 00000000..e9ca538c
--- /dev/null
+++ b/src/kernel/types/extension_download_from_chrome_store_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 Literal, Required, TypedDict
+
+__all__ = ["ExtensionDownloadFromChromeStoreParams"]
+
+
+class ExtensionDownloadFromChromeStoreParams(TypedDict, total=False):
+ url: Required[str]
+ """Chrome Web Store URL for the extension."""
+
+ os: Literal["win", "mac", "linux"]
+ """Target operating system for the extension package. Defaults to linux."""
diff --git a/src/kernel/types/extension_list_response.py b/src/kernel/types/extension_list_response.py
new file mode 100644
index 00000000..c8c99e71
--- /dev/null
+++ b/src/kernel/types/extension_list_response.py
@@ -0,0 +1,32 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from typing import List, Optional
+from datetime import datetime
+from typing_extensions import TypeAlias
+
+from .._models import BaseModel
+
+__all__ = ["ExtensionListResponse", "ExtensionListResponseItem"]
+
+
+class ExtensionListResponseItem(BaseModel):
+ id: str
+ """Unique identifier for the extension"""
+
+ created_at: datetime
+ """Timestamp when the extension was created"""
+
+ size_bytes: int
+ """Size of the extension archive in bytes"""
+
+ last_used_at: Optional[datetime] = None
+ """Timestamp when the extension was last used"""
+
+ name: Optional[str] = None
+ """Optional, easier-to-reference name for the extension.
+
+ Must be unique within the organization.
+ """
+
+
+ExtensionListResponse: TypeAlias = List[ExtensionListResponseItem]
diff --git a/src/kernel/types/extension_upload_params.py b/src/kernel/types/extension_upload_params.py
new file mode 100644
index 00000000..d36dde31
--- /dev/null
+++ b/src/kernel/types/extension_upload_params.py
@@ -0,0 +1,17 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from __future__ import annotations
+
+from typing_extensions import Required, TypedDict
+
+from .._types import FileTypes
+
+__all__ = ["ExtensionUploadParams"]
+
+
+class ExtensionUploadParams(TypedDict, total=False):
+ file: Required[FileTypes]
+ """ZIP file containing the browser extension."""
+
+ name: str
+ """Optional unique name within the organization to reference this extension."""
diff --git a/src/kernel/types/extension_upload_response.py b/src/kernel/types/extension_upload_response.py
new file mode 100644
index 00000000..373e8861
--- /dev/null
+++ b/src/kernel/types/extension_upload_response.py
@@ -0,0 +1,28 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from typing import Optional
+from datetime import datetime
+
+from .._models import BaseModel
+
+__all__ = ["ExtensionUploadResponse"]
+
+
+class ExtensionUploadResponse(BaseModel):
+ id: str
+ """Unique identifier for the extension"""
+
+ created_at: datetime
+ """Timestamp when the extension was created"""
+
+ size_bytes: int
+ """Size of the extension archive in bytes"""
+
+ last_used_at: Optional[datetime] = None
+ """Timestamp when the extension was last used"""
+
+ name: Optional[str] = None
+ """Optional, easier-to-reference name for the extension.
+
+ Must be unique within the organization.
+ """
diff --git a/tests/api_resources/test_browsers.py b/tests/api_resources/test_browsers.py
index 349d74b4..c3e7a7fe 100644
--- a/tests/api_resources/test_browsers.py
+++ b/tests/api_resources/test_browsers.py
@@ -31,6 +31,12 @@ def test_method_create(self, client: Kernel) -> None:
@parametrize
def test_method_create_with_all_params(self, client: Kernel) -> None:
browser = client.browsers.create(
+ extensions=[
+ {
+ "id": "id",
+ "name": "name",
+ }
+ ],
headless=False,
invocation_id="rr33xuugxj9h0bkf1rdt2bet",
persistence={"id": "my-awesome-browser-for-user-1234"},
@@ -213,6 +219,72 @@ def test_path_params_delete_by_id(self, client: Kernel) -> None:
"",
)
+ @pytest.mark.skip(reason="Prism tests are disabled")
+ @parametrize
+ def test_method_upload_extensions(self, client: Kernel) -> None:
+ browser = client.browsers.upload_extensions(
+ id="id",
+ extensions=[
+ {
+ "name": "name",
+ "zip_file": b"raw file contents",
+ }
+ ],
+ )
+ assert browser is None
+
+ @pytest.mark.skip(reason="Prism tests are disabled")
+ @parametrize
+ def test_raw_response_upload_extensions(self, client: Kernel) -> None:
+ response = client.browsers.with_raw_response.upload_extensions(
+ id="id",
+ extensions=[
+ {
+ "name": "name",
+ "zip_file": b"raw file contents",
+ }
+ ],
+ )
+
+ assert response.is_closed is True
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+ browser = response.parse()
+ assert browser is None
+
+ @pytest.mark.skip(reason="Prism tests are disabled")
+ @parametrize
+ def test_streaming_response_upload_extensions(self, client: Kernel) -> None:
+ with client.browsers.with_streaming_response.upload_extensions(
+ id="id",
+ extensions=[
+ {
+ "name": "name",
+ "zip_file": b"raw file contents",
+ }
+ ],
+ ) as response:
+ assert not response.is_closed
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+
+ browser = response.parse()
+ assert browser is None
+
+ assert cast(Any, response.is_closed) is True
+
+ @pytest.mark.skip(reason="Prism tests are disabled")
+ @parametrize
+ def test_path_params_upload_extensions(self, client: Kernel) -> None:
+ with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"):
+ client.browsers.with_raw_response.upload_extensions(
+ id="",
+ extensions=[
+ {
+ "name": "name",
+ "zip_file": b"raw file contents",
+ }
+ ],
+ )
+
class TestAsyncBrowsers:
parametrize = pytest.mark.parametrize(
@@ -229,6 +301,12 @@ 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(
+ extensions=[
+ {
+ "id": "id",
+ "name": "name",
+ }
+ ],
headless=False,
invocation_id="rr33xuugxj9h0bkf1rdt2bet",
persistence={"id": "my-awesome-browser-for-user-1234"},
@@ -410,3 +488,69 @@ 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(reason="Prism tests are disabled")
+ @parametrize
+ async def test_method_upload_extensions(self, async_client: AsyncKernel) -> None:
+ browser = await async_client.browsers.upload_extensions(
+ id="id",
+ extensions=[
+ {
+ "name": "name",
+ "zip_file": b"raw file contents",
+ }
+ ],
+ )
+ assert browser is None
+
+ @pytest.mark.skip(reason="Prism tests are disabled")
+ @parametrize
+ async def test_raw_response_upload_extensions(self, async_client: AsyncKernel) -> None:
+ response = await async_client.browsers.with_raw_response.upload_extensions(
+ id="id",
+ extensions=[
+ {
+ "name": "name",
+ "zip_file": b"raw file contents",
+ }
+ ],
+ )
+
+ assert response.is_closed is True
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+ browser = await response.parse()
+ assert browser is None
+
+ @pytest.mark.skip(reason="Prism tests are disabled")
+ @parametrize
+ async def test_streaming_response_upload_extensions(self, async_client: AsyncKernel) -> None:
+ async with async_client.browsers.with_streaming_response.upload_extensions(
+ id="id",
+ extensions=[
+ {
+ "name": "name",
+ "zip_file": b"raw file contents",
+ }
+ ],
+ ) as response:
+ assert not response.is_closed
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+
+ browser = await response.parse()
+ assert browser is None
+
+ assert cast(Any, response.is_closed) is True
+
+ @pytest.mark.skip(reason="Prism tests are disabled")
+ @parametrize
+ async def test_path_params_upload_extensions(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.upload_extensions(
+ id="",
+ extensions=[
+ {
+ "name": "name",
+ "zip_file": b"raw file contents",
+ }
+ ],
+ )
diff --git a/tests/api_resources/test_extensions.py b/tests/api_resources/test_extensions.py
new file mode 100644
index 00000000..5d61f327
--- /dev/null
+++ b/tests/api_resources/test_extensions.py
@@ -0,0 +1,477 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from __future__ import annotations
+
+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
+from kernel.types import (
+ ExtensionListResponse,
+ ExtensionUploadResponse,
+)
+from kernel._response import (
+ BinaryAPIResponse,
+ AsyncBinaryAPIResponse,
+ StreamedBinaryAPIResponse,
+ AsyncStreamedBinaryAPIResponse,
+)
+
+base_url = os.environ.get("TEST_API_BASE_URL", "http://127.0.0.1:4010")
+
+
+class TestExtensions:
+ parametrize = pytest.mark.parametrize("client", [False, True], indirect=True, ids=["loose", "strict"])
+
+ @pytest.mark.skip(reason="Prism tests are disabled")
+ @parametrize
+ def test_method_list(self, client: Kernel) -> None:
+ extension = client.extensions.list()
+ assert_matches_type(ExtensionListResponse, extension, path=["response"])
+
+ @pytest.mark.skip(reason="Prism tests are disabled")
+ @parametrize
+ def test_raw_response_list(self, client: Kernel) -> None:
+ response = client.extensions.with_raw_response.list()
+
+ assert response.is_closed is True
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+ extension = response.parse()
+ assert_matches_type(ExtensionListResponse, extension, path=["response"])
+
+ @pytest.mark.skip(reason="Prism tests are disabled")
+ @parametrize
+ def test_streaming_response_list(self, client: Kernel) -> None:
+ with client.extensions.with_streaming_response.list() as response:
+ assert not response.is_closed
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+
+ extension = response.parse()
+ assert_matches_type(ExtensionListResponse, extension, path=["response"])
+
+ assert cast(Any, response.is_closed) is True
+
+ @pytest.mark.skip(reason="Prism tests are disabled")
+ @parametrize
+ def test_method_delete(self, client: Kernel) -> None:
+ extension = client.extensions.delete(
+ "id_or_name",
+ )
+ assert extension is None
+
+ @pytest.mark.skip(reason="Prism tests are disabled")
+ @parametrize
+ def test_raw_response_delete(self, client: Kernel) -> None:
+ response = client.extensions.with_raw_response.delete(
+ "id_or_name",
+ )
+
+ assert response.is_closed is True
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+ extension = response.parse()
+ assert extension is None
+
+ @pytest.mark.skip(reason="Prism tests are disabled")
+ @parametrize
+ def test_streaming_response_delete(self, client: Kernel) -> None:
+ with client.extensions.with_streaming_response.delete(
+ "id_or_name",
+ ) as response:
+ assert not response.is_closed
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+
+ extension = response.parse()
+ assert extension is None
+
+ assert cast(Any, response.is_closed) is True
+
+ @pytest.mark.skip(reason="Prism tests are disabled")
+ @parametrize
+ def test_path_params_delete(self, client: Kernel) -> None:
+ with pytest.raises(ValueError, match=r"Expected a non-empty value for `id_or_name` but received ''"):
+ client.extensions.with_raw_response.delete(
+ "",
+ )
+
+ @parametrize
+ @pytest.mark.respx(base_url=base_url)
+ def test_method_download(self, client: Kernel, respx_mock: MockRouter) -> None:
+ respx_mock.get("/extensions/id_or_name").mock(return_value=httpx.Response(200, json={"foo": "bar"}))
+ extension = client.extensions.download(
+ "id_or_name",
+ )
+ assert extension.is_closed
+ assert extension.json() == {"foo": "bar"}
+ assert cast(Any, extension.is_closed) is True
+ assert isinstance(extension, BinaryAPIResponse)
+
+ @parametrize
+ @pytest.mark.respx(base_url=base_url)
+ def test_raw_response_download(self, client: Kernel, respx_mock: MockRouter) -> None:
+ respx_mock.get("/extensions/id_or_name").mock(return_value=httpx.Response(200, json={"foo": "bar"}))
+
+ extension = client.extensions.with_raw_response.download(
+ "id_or_name",
+ )
+
+ assert extension.is_closed is True
+ assert extension.http_request.headers.get("X-Stainless-Lang") == "python"
+ assert extension.json() == {"foo": "bar"}
+ assert isinstance(extension, BinaryAPIResponse)
+
+ @parametrize
+ @pytest.mark.respx(base_url=base_url)
+ def test_streaming_response_download(self, client: Kernel, respx_mock: MockRouter) -> None:
+ respx_mock.get("/extensions/id_or_name").mock(return_value=httpx.Response(200, json={"foo": "bar"}))
+ with client.extensions.with_streaming_response.download(
+ "id_or_name",
+ ) as extension:
+ assert not extension.is_closed
+ assert extension.http_request.headers.get("X-Stainless-Lang") == "python"
+
+ assert extension.json() == {"foo": "bar"}
+ assert cast(Any, extension.is_closed) is True
+ assert isinstance(extension, StreamedBinaryAPIResponse)
+
+ assert cast(Any, extension.is_closed) is True
+
+ @parametrize
+ @pytest.mark.respx(base_url=base_url)
+ def test_path_params_download(self, client: Kernel) -> None:
+ with pytest.raises(ValueError, match=r"Expected a non-empty value for `id_or_name` but received ''"):
+ client.extensions.with_raw_response.download(
+ "",
+ )
+
+ @parametrize
+ @pytest.mark.respx(base_url=base_url)
+ def test_method_download_from_chrome_store(self, client: Kernel, respx_mock: MockRouter) -> None:
+ respx_mock.get("/extensions/from_chrome_store").mock(return_value=httpx.Response(200, json={"foo": "bar"}))
+ extension = client.extensions.download_from_chrome_store(
+ url="url",
+ )
+ assert extension.is_closed
+ assert extension.json() == {"foo": "bar"}
+ assert cast(Any, extension.is_closed) is True
+ assert isinstance(extension, BinaryAPIResponse)
+
+ @parametrize
+ @pytest.mark.respx(base_url=base_url)
+ def test_method_download_from_chrome_store_with_all_params(self, client: Kernel, respx_mock: MockRouter) -> None:
+ respx_mock.get("/extensions/from_chrome_store").mock(return_value=httpx.Response(200, json={"foo": "bar"}))
+ extension = client.extensions.download_from_chrome_store(
+ url="url",
+ os="win",
+ )
+ assert extension.is_closed
+ assert extension.json() == {"foo": "bar"}
+ assert cast(Any, extension.is_closed) is True
+ assert isinstance(extension, BinaryAPIResponse)
+
+ @parametrize
+ @pytest.mark.respx(base_url=base_url)
+ def test_raw_response_download_from_chrome_store(self, client: Kernel, respx_mock: MockRouter) -> None:
+ respx_mock.get("/extensions/from_chrome_store").mock(return_value=httpx.Response(200, json={"foo": "bar"}))
+
+ extension = client.extensions.with_raw_response.download_from_chrome_store(
+ url="url",
+ )
+
+ assert extension.is_closed is True
+ assert extension.http_request.headers.get("X-Stainless-Lang") == "python"
+ assert extension.json() == {"foo": "bar"}
+ assert isinstance(extension, BinaryAPIResponse)
+
+ @parametrize
+ @pytest.mark.respx(base_url=base_url)
+ def test_streaming_response_download_from_chrome_store(self, client: Kernel, respx_mock: MockRouter) -> None:
+ respx_mock.get("/extensions/from_chrome_store").mock(return_value=httpx.Response(200, json={"foo": "bar"}))
+ with client.extensions.with_streaming_response.download_from_chrome_store(
+ url="url",
+ ) as extension:
+ assert not extension.is_closed
+ assert extension.http_request.headers.get("X-Stainless-Lang") == "python"
+
+ assert extension.json() == {"foo": "bar"}
+ assert cast(Any, extension.is_closed) is True
+ assert isinstance(extension, StreamedBinaryAPIResponse)
+
+ assert cast(Any, extension.is_closed) is True
+
+ @pytest.mark.skip(reason="Prism tests are disabled")
+ @parametrize
+ def test_method_upload(self, client: Kernel) -> None:
+ extension = client.extensions.upload(
+ file=b"raw file contents",
+ )
+ assert_matches_type(ExtensionUploadResponse, extension, path=["response"])
+
+ @pytest.mark.skip(reason="Prism tests are disabled")
+ @parametrize
+ def test_method_upload_with_all_params(self, client: Kernel) -> None:
+ extension = client.extensions.upload(
+ file=b"raw file contents",
+ name="name",
+ )
+ assert_matches_type(ExtensionUploadResponse, extension, path=["response"])
+
+ @pytest.mark.skip(reason="Prism tests are disabled")
+ @parametrize
+ def test_raw_response_upload(self, client: Kernel) -> None:
+ response = client.extensions.with_raw_response.upload(
+ file=b"raw file contents",
+ )
+
+ assert response.is_closed is True
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+ extension = response.parse()
+ assert_matches_type(ExtensionUploadResponse, extension, path=["response"])
+
+ @pytest.mark.skip(reason="Prism tests are disabled")
+ @parametrize
+ def test_streaming_response_upload(self, client: Kernel) -> None:
+ with client.extensions.with_streaming_response.upload(
+ file=b"raw file contents",
+ ) as response:
+ assert not response.is_closed
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+
+ extension = response.parse()
+ assert_matches_type(ExtensionUploadResponse, extension, path=["response"])
+
+ assert cast(Any, response.is_closed) is True
+
+
+class TestAsyncExtensions:
+ parametrize = pytest.mark.parametrize(
+ "async_client", [False, True, {"http_client": "aiohttp"}], indirect=True, ids=["loose", "strict", "aiohttp"]
+ )
+
+ @pytest.mark.skip(reason="Prism tests are disabled")
+ @parametrize
+ async def test_method_list(self, async_client: AsyncKernel) -> None:
+ extension = await async_client.extensions.list()
+ assert_matches_type(ExtensionListResponse, extension, path=["response"])
+
+ @pytest.mark.skip(reason="Prism tests are disabled")
+ @parametrize
+ async def test_raw_response_list(self, async_client: AsyncKernel) -> None:
+ response = await async_client.extensions.with_raw_response.list()
+
+ assert response.is_closed is True
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+ extension = await response.parse()
+ assert_matches_type(ExtensionListResponse, extension, path=["response"])
+
+ @pytest.mark.skip(reason="Prism tests are disabled")
+ @parametrize
+ async def test_streaming_response_list(self, async_client: AsyncKernel) -> None:
+ async with async_client.extensions.with_streaming_response.list() as response:
+ assert not response.is_closed
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+
+ extension = await response.parse()
+ assert_matches_type(ExtensionListResponse, extension, path=["response"])
+
+ assert cast(Any, response.is_closed) is True
+
+ @pytest.mark.skip(reason="Prism tests are disabled")
+ @parametrize
+ async def test_method_delete(self, async_client: AsyncKernel) -> None:
+ extension = await async_client.extensions.delete(
+ "id_or_name",
+ )
+ assert extension is None
+
+ @pytest.mark.skip(reason="Prism tests are disabled")
+ @parametrize
+ async def test_raw_response_delete(self, async_client: AsyncKernel) -> None:
+ response = await async_client.extensions.with_raw_response.delete(
+ "id_or_name",
+ )
+
+ assert response.is_closed is True
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+ extension = await response.parse()
+ assert extension is None
+
+ @pytest.mark.skip(reason="Prism tests are disabled")
+ @parametrize
+ async def test_streaming_response_delete(self, async_client: AsyncKernel) -> None:
+ async with async_client.extensions.with_streaming_response.delete(
+ "id_or_name",
+ ) as response:
+ assert not response.is_closed
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+
+ extension = await response.parse()
+ assert extension is None
+
+ assert cast(Any, response.is_closed) is True
+
+ @pytest.mark.skip(reason="Prism tests are disabled")
+ @parametrize
+ async def test_path_params_delete(self, async_client: AsyncKernel) -> None:
+ with pytest.raises(ValueError, match=r"Expected a non-empty value for `id_or_name` but received ''"):
+ await async_client.extensions.with_raw_response.delete(
+ "",
+ )
+
+ @parametrize
+ @pytest.mark.respx(base_url=base_url)
+ async def test_method_download(self, async_client: AsyncKernel, respx_mock: MockRouter) -> None:
+ respx_mock.get("/extensions/id_or_name").mock(return_value=httpx.Response(200, json={"foo": "bar"}))
+ extension = await async_client.extensions.download(
+ "id_or_name",
+ )
+ assert extension.is_closed
+ assert await extension.json() == {"foo": "bar"}
+ assert cast(Any, extension.is_closed) is True
+ assert isinstance(extension, AsyncBinaryAPIResponse)
+
+ @parametrize
+ @pytest.mark.respx(base_url=base_url)
+ async def test_raw_response_download(self, async_client: AsyncKernel, respx_mock: MockRouter) -> None:
+ respx_mock.get("/extensions/id_or_name").mock(return_value=httpx.Response(200, json={"foo": "bar"}))
+
+ extension = await async_client.extensions.with_raw_response.download(
+ "id_or_name",
+ )
+
+ assert extension.is_closed is True
+ assert extension.http_request.headers.get("X-Stainless-Lang") == "python"
+ assert await extension.json() == {"foo": "bar"}
+ assert isinstance(extension, AsyncBinaryAPIResponse)
+
+ @parametrize
+ @pytest.mark.respx(base_url=base_url)
+ async def test_streaming_response_download(self, async_client: AsyncKernel, respx_mock: MockRouter) -> None:
+ respx_mock.get("/extensions/id_or_name").mock(return_value=httpx.Response(200, json={"foo": "bar"}))
+ async with async_client.extensions.with_streaming_response.download(
+ "id_or_name",
+ ) as extension:
+ assert not extension.is_closed
+ assert extension.http_request.headers.get("X-Stainless-Lang") == "python"
+
+ assert await extension.json() == {"foo": "bar"}
+ assert cast(Any, extension.is_closed) is True
+ assert isinstance(extension, AsyncStreamedBinaryAPIResponse)
+
+ assert cast(Any, extension.is_closed) is True
+
+ @parametrize
+ @pytest.mark.respx(base_url=base_url)
+ async def test_path_params_download(self, async_client: AsyncKernel) -> None:
+ with pytest.raises(ValueError, match=r"Expected a non-empty value for `id_or_name` but received ''"):
+ await async_client.extensions.with_raw_response.download(
+ "",
+ )
+
+ @parametrize
+ @pytest.mark.respx(base_url=base_url)
+ async def test_method_download_from_chrome_store(self, async_client: AsyncKernel, respx_mock: MockRouter) -> None:
+ respx_mock.get("/extensions/from_chrome_store").mock(return_value=httpx.Response(200, json={"foo": "bar"}))
+ extension = await async_client.extensions.download_from_chrome_store(
+ url="url",
+ )
+ assert extension.is_closed
+ assert await extension.json() == {"foo": "bar"}
+ assert cast(Any, extension.is_closed) is True
+ assert isinstance(extension, AsyncBinaryAPIResponse)
+
+ @parametrize
+ @pytest.mark.respx(base_url=base_url)
+ async def test_method_download_from_chrome_store_with_all_params(
+ self, async_client: AsyncKernel, respx_mock: MockRouter
+ ) -> None:
+ respx_mock.get("/extensions/from_chrome_store").mock(return_value=httpx.Response(200, json={"foo": "bar"}))
+ extension = await async_client.extensions.download_from_chrome_store(
+ url="url",
+ os="win",
+ )
+ assert extension.is_closed
+ assert await extension.json() == {"foo": "bar"}
+ assert cast(Any, extension.is_closed) is True
+ assert isinstance(extension, AsyncBinaryAPIResponse)
+
+ @parametrize
+ @pytest.mark.respx(base_url=base_url)
+ async def test_raw_response_download_from_chrome_store(
+ self, async_client: AsyncKernel, respx_mock: MockRouter
+ ) -> None:
+ respx_mock.get("/extensions/from_chrome_store").mock(return_value=httpx.Response(200, json={"foo": "bar"}))
+
+ extension = await async_client.extensions.with_raw_response.download_from_chrome_store(
+ url="url",
+ )
+
+ assert extension.is_closed is True
+ assert extension.http_request.headers.get("X-Stainless-Lang") == "python"
+ assert await extension.json() == {"foo": "bar"}
+ assert isinstance(extension, AsyncBinaryAPIResponse)
+
+ @parametrize
+ @pytest.mark.respx(base_url=base_url)
+ async def test_streaming_response_download_from_chrome_store(
+ self, async_client: AsyncKernel, respx_mock: MockRouter
+ ) -> None:
+ respx_mock.get("/extensions/from_chrome_store").mock(return_value=httpx.Response(200, json={"foo": "bar"}))
+ async with async_client.extensions.with_streaming_response.download_from_chrome_store(
+ url="url",
+ ) as extension:
+ assert not extension.is_closed
+ assert extension.http_request.headers.get("X-Stainless-Lang") == "python"
+
+ assert await extension.json() == {"foo": "bar"}
+ assert cast(Any, extension.is_closed) is True
+ assert isinstance(extension, AsyncStreamedBinaryAPIResponse)
+
+ assert cast(Any, extension.is_closed) is True
+
+ @pytest.mark.skip(reason="Prism tests are disabled")
+ @parametrize
+ async def test_method_upload(self, async_client: AsyncKernel) -> None:
+ extension = await async_client.extensions.upload(
+ file=b"raw file contents",
+ )
+ assert_matches_type(ExtensionUploadResponse, extension, path=["response"])
+
+ @pytest.mark.skip(reason="Prism tests are disabled")
+ @parametrize
+ async def test_method_upload_with_all_params(self, async_client: AsyncKernel) -> None:
+ extension = await async_client.extensions.upload(
+ file=b"raw file contents",
+ name="name",
+ )
+ assert_matches_type(ExtensionUploadResponse, extension, path=["response"])
+
+ @pytest.mark.skip(reason="Prism tests are disabled")
+ @parametrize
+ async def test_raw_response_upload(self, async_client: AsyncKernel) -> None:
+ response = await async_client.extensions.with_raw_response.upload(
+ file=b"raw file contents",
+ )
+
+ assert response.is_closed is True
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+ extension = await response.parse()
+ assert_matches_type(ExtensionUploadResponse, extension, path=["response"])
+
+ @pytest.mark.skip(reason="Prism tests are disabled")
+ @parametrize
+ async def test_streaming_response_upload(self, async_client: AsyncKernel) -> None:
+ async with async_client.extensions.with_streaming_response.upload(
+ file=b"raw file contents",
+ ) as response:
+ assert not response.is_closed
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+
+ extension = await response.parse()
+ assert_matches_type(ExtensionUploadResponse, extension, path=["response"])
+
+ assert cast(Any, response.is_closed) is True
From a33e861b32017103a21d2f453f540e2f43ab757b Mon Sep 17 00:00:00 2001
From: "stainless-app[bot]"
<142633134+stainless-app[bot]@users.noreply.github.com>
Date: Tue, 7 Oct 2025 13:51:54 +0000
Subject: [PATCH 2/3] codegen metadata
---
.stats.yml | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/.stats.yml b/.stats.yml
index b296ff8f..f577dd04 100644
--- a/.stats.yml
+++ b/.stats.yml
@@ -1,4 +1,4 @@
configured_endpoints: 57
openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel%2Fkernel-936db268b3dcae5d64bd5d590506d8134304ffcbf67389eb9b1555b3febfd4cb.yml
openapi_spec_hash: 145485087adf1b28c052bacb4df68462
-config_hash: 5236f9b34e39dc1930e36a88c714abd4
+config_hash: 15cd063f8e308686ac71bf9ee9634625
From 6173f471496a5916716dd118519df2df4f5449c2 Mon Sep 17 00:00:00 2001
From: "stainless-app[bot]"
<142633134+stainless-app[bot]@users.noreply.github.com>
Date: Tue, 7 Oct 2025 13:52:15 +0000
Subject: [PATCH 3/3] release: 0.14.0
---
.release-please-manifest.json | 2 +-
CHANGELOG.md | 8 ++++++++
pyproject.toml | 2 +-
src/kernel/_version.py | 2 +-
4 files changed, 11 insertions(+), 3 deletions(-)
diff --git a/.release-please-manifest.json b/.release-please-manifest.json
index d52d2b97..a26ebfc1 100644
--- a/.release-please-manifest.json
+++ b/.release-please-manifest.json
@@ -1,3 +1,3 @@
{
- ".": "0.13.0"
+ ".": "0.14.0"
}
\ No newline at end of file
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 7dd12e91..60ce7eee 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,13 @@
# Changelog
+## 0.14.0 (2025-10-07)
+
+Full Changelog: [v0.13.0...v0.14.0](https://github.com/onkernel/kernel-python-sdk/compare/v0.13.0...v0.14.0)
+
+### Features
+
+* WIP browser extensions ([89bac15](https://github.com/onkernel/kernel-python-sdk/commit/89bac15e58e4892e653a9dafeb2d15c88d7fdbb9))
+
## 0.13.0 (2025-10-03)
Full Changelog: [v0.12.0...v0.13.0](https://github.com/onkernel/kernel-python-sdk/compare/v0.12.0...v0.13.0)
diff --git a/pyproject.toml b/pyproject.toml
index a44ee3cb..3219e080 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -1,6 +1,6 @@
[project]
name = "kernel"
-version = "0.13.0"
+version = "0.14.0"
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 eed10067..0bebaf17 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.13.0" # x-release-please-version
+__version__ = "0.14.0" # x-release-please-version