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/.stats.yml b/.stats.yml index 0af2575f..f577dd04 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: 15cd063f8e308686ac71bf9ee9634625 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/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/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/_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/_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 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