diff --git a/.release-please-manifest.json b/.release-please-manifest.json index 1bc5713..6538ca9 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,3 +1,3 @@ { - ".": "0.7.1" + ".": "0.8.0" } \ No newline at end of file diff --git a/.stats.yml b/.stats.yml index aa1aff7..a0553b1 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ -configured_endpoints: 18 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel%2Fkernel-d173129101e26f450c200e84430d993479c034700cf826917425d513b88912e6.yml -openapi_spec_hash: 150b86da7588979d7619b1a894e4720c -config_hash: eaeed470b1070b34df69c49d68e67355 +configured_endpoints: 21 +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel%2Fkernel-a5b1d2c806c42c1534eefc8d34516f7f6e4ab68cb6a836534ee549bdbe4653f3.yml +openapi_spec_hash: 0be350cc8ddbd1fc7e058ce6c3a44ee8 +config_hash: 307153ecd5b85f77ce8e0d87f6e5dfab diff --git a/CHANGELOG.md b/CHANGELOG.md index 2bfb29a..3ee1358 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,27 @@ # Changelog +## 0.8.0 (2025-07-16) + +Full Changelog: [v0.7.1...v0.8.0](https://github.com/onkernel/kernel-python-sdk/compare/v0.7.1...v0.8.0) + +### Features + +* **api:** manual updates ([cd2b694](https://github.com/onkernel/kernel-python-sdk/commit/cd2b694c97b354c4eab38ed06eba04bf56218f97)) +* clean up environment call outs ([c31b1a2](https://github.com/onkernel/kernel-python-sdk/commit/c31b1a21a86381a5ee8162327e43934be4d661d2)) + + +### Bug Fixes + +* **client:** don't send Content-Type header on GET requests ([b44aeee](https://github.com/onkernel/kernel-python-sdk/commit/b44aeee6397b8a418c65a624c71f8a2c5272fdc2)) +* **parsing:** correctly handle nested discriminated unions ([7b25900](https://github.com/onkernel/kernel-python-sdk/commit/7b25900b5e12030c814231b1a212a95b090a977e)) + + +### Chores + +* **internal:** bump pinned h11 dep ([352aae2](https://github.com/onkernel/kernel-python-sdk/commit/352aae28c4cf9345c808910e13d7423613a6d80b)) +* **package:** mark python 3.13 as supported ([5ddf6d0](https://github.com/onkernel/kernel-python-sdk/commit/5ddf6d0b28a8108a6e0f7de628438c33857cc6dc)) +* **readme:** fix version rendering on pypi ([760753f](https://github.com/onkernel/kernel-python-sdk/commit/760753f31a974cac63ee5a8dc39462bbfb925249)) + ## 0.7.1 (2025-07-08) Full Changelog: [v0.6.4...v0.7.1](https://github.com/onkernel/kernel-python-sdk/compare/v0.6.4...v0.7.1) diff --git a/README.md b/README.md index 8ee8a72..5041da0 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,7 @@ # Kernel Python API library -[![PyPI version]()](https://pypi.org/project/kernel/) + +[![PyPI version](https://img.shields.io/pypi/v/kernel.svg?label=pypi%20(stable))](https://pypi.org/project/kernel/) The Kernel Python library provides convenient access to the Kernel REST API from any Python 3.8+ application. The library includes type definitions for all request params and response fields, @@ -92,7 +93,6 @@ pip install kernel[aiohttp] Then you can enable it by instantiating the client with `http_client=DefaultAioHttpClient()`: ```python -import os import asyncio from kernel import DefaultAioHttpClient from kernel import AsyncKernel @@ -100,7 +100,7 @@ from kernel import AsyncKernel async def main() -> None: async with AsyncKernel( - api_key=os.environ.get("KERNEL_API_KEY"), # This is the default and can be omitted + api_key="My API Key", http_client=DefaultAioHttpClient(), ) as client: deployment = await client.apps.deployments.create( diff --git a/api.md b/api.md index 8a0fbd3..58ceeac 100644 --- a/api.md +++ b/api.md @@ -87,9 +87,23 @@ from kernel.types import ( Methods: -- client.browsers.create(\*\*params) -> BrowserCreateResponse -- client.browsers.retrieve(id) -> BrowserRetrieveResponse -- client.browsers.list() -> BrowserListResponse -- client.browsers.delete(\*\*params) -> None -- client.browsers.delete_by_id(id) -> None -- client.browsers.retrieve_replay(id) -> BinaryAPIResponse +- client.browsers.create(\*\*params) -> BrowserCreateResponse +- client.browsers.retrieve(id) -> BrowserRetrieveResponse +- client.browsers.list() -> BrowserListResponse +- client.browsers.delete(\*\*params) -> None +- client.browsers.delete_by_id(id) -> None + +## Replays + +Types: + +```python +from kernel.types.browsers import ReplayListResponse, ReplayStartResponse +``` + +Methods: + +- client.browsers.replays.list(id) -> ReplayListResponse +- client.browsers.replays.download(replay_id, \*, id) -> BinaryAPIResponse +- client.browsers.replays.start(id, \*\*params) -> ReplayStartResponse +- client.browsers.replays.stop(replay_id, \*, id) -> None diff --git a/pyproject.toml b/pyproject.toml index a5baacd..0eb2b24 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "kernel" -version = "0.7.1" +version = "0.8.0" description = "The official Python library for the kernel API" dynamic = ["readme"] license = "Apache-2.0" @@ -24,6 +24,7 @@ classifiers = [ "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.13", "Operating System :: OS Independent", "Operating System :: POSIX", "Operating System :: MacOS", @@ -38,7 +39,7 @@ Homepage = "https://github.com/onkernel/kernel-python-sdk" Repository = "https://github.com/onkernel/kernel-python-sdk" [project.optional-dependencies] -aiohttp = ["aiohttp", "httpx_aiohttp>=0.1.6"] +aiohttp = ["aiohttp", "httpx_aiohttp>=0.1.8"] [tool.rye] managed = true diff --git a/requirements-dev.lock b/requirements-dev.lock index f4090db..55681a9 100644 --- a/requirements-dev.lock +++ b/requirements-dev.lock @@ -48,9 +48,9 @@ filelock==3.12.4 frozenlist==1.6.2 # via aiohttp # via aiosignal -h11==0.14.0 +h11==0.16.0 # via httpcore -httpcore==1.0.2 +httpcore==1.0.9 # via httpx httpx==0.28.1 # via httpx-aiohttp diff --git a/requirements.lock b/requirements.lock index c125a9e..61c4c7a 100644 --- a/requirements.lock +++ b/requirements.lock @@ -36,9 +36,9 @@ exceptiongroup==1.2.2 frozenlist==1.6.2 # via aiohttp # via aiosignal -h11==0.14.0 +h11==0.16.0 # via httpcore -httpcore==1.0.2 +httpcore==1.0.9 # via httpx httpx==0.28.1 # via httpx-aiohttp diff --git a/src/kernel/_base_client.py b/src/kernel/_base_client.py index c90f227..a654874 100644 --- a/src/kernel/_base_client.py +++ b/src/kernel/_base_client.py @@ -529,6 +529,15 @@ def _build_request( # work around https://github.com/encode/httpx/discussions/2880 kwargs["extensions"] = {"sni_hostname": prepared_url.host.replace("_", "-")} + is_body_allowed = options.method.lower() != "get" + + if is_body_allowed: + kwargs["json"] = json_data if is_given(json_data) else None + kwargs["files"] = files + else: + headers.pop("Content-Type", None) + kwargs.pop("data", None) + # TODO: report this error to httpx return self._client.build_request( # pyright: ignore[reportUnknownMemberType] headers=headers, @@ -540,8 +549,6 @@ def _build_request( # so that passing a `TypedDict` doesn't cause an error. # https://github.com/microsoft/pyright/issues/3526#event-6715453066 params=self.qs.stringify(cast(Mapping[str, Any], params)) if params else None, - json=json_data if is_given(json_data) else None, - files=files, **kwargs, ) diff --git a/src/kernel/_client.py b/src/kernel/_client.py index 63a7dc9..0051076 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 browsers, deployments, invocations +from .resources import deployments, invocations from ._streaming import Stream as Stream, AsyncStream as AsyncStream from ._exceptions import KernelError, APIStatusError from ._base_client import ( @@ -30,6 +30,7 @@ AsyncAPIClient, ) from .resources.apps import apps +from .resources.browsers import browsers __all__ = [ "ENVIRONMENTS", diff --git a/src/kernel/_models.py b/src/kernel/_models.py index 4f21498..528d568 100644 --- a/src/kernel/_models.py +++ b/src/kernel/_models.py @@ -2,9 +2,10 @@ import os import inspect -from typing import TYPE_CHECKING, Any, Type, Union, Generic, TypeVar, Callable, cast +from typing import TYPE_CHECKING, Any, Type, Union, Generic, TypeVar, Callable, Optional, cast from datetime import date, datetime from typing_extensions import ( + List, Unpack, Literal, ClassVar, @@ -366,7 +367,7 @@ def _construct_field(value: object, field: FieldInfo, key: str) -> object: if type_ is None: raise RuntimeError(f"Unexpected field type is None for {key}") - return construct_type(value=value, type_=type_) + return construct_type(value=value, type_=type_, metadata=getattr(field, "metadata", None)) def is_basemodel(type_: type) -> bool: @@ -420,7 +421,7 @@ def construct_type_unchecked(*, value: object, type_: type[_T]) -> _T: return cast(_T, construct_type(value=value, type_=type_)) -def construct_type(*, value: object, type_: object) -> object: +def construct_type(*, value: object, type_: object, metadata: Optional[List[Any]] = None) -> object: """Loose coercion to the expected type with construction of nested values. If the given value does not match the expected type then it is returned as-is. @@ -438,8 +439,10 @@ def construct_type(*, value: object, type_: object) -> object: type_ = type_.__value__ # type: ignore[unreachable] # unwrap `Annotated[T, ...]` -> `T` - if is_annotated_type(type_): - meta: tuple[Any, ...] = get_args(type_)[1:] + if metadata is not None: + meta: tuple[Any, ...] = tuple(metadata) + elif is_annotated_type(type_): + meta = get_args(type_)[1:] type_ = extract_type_arg(type_, 0) else: meta = tuple() diff --git a/src/kernel/_version.py b/src/kernel/_version.py index c3ceacf..73e122d 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.7.1" # x-release-please-version +__version__ = "0.8.0" # x-release-please-version diff --git a/src/kernel/resources/browsers/__init__.py b/src/kernel/resources/browsers/__init__.py new file mode 100644 index 0000000..236b5f7 --- /dev/null +++ b/src/kernel/resources/browsers/__init__.py @@ -0,0 +1,33 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from .replays import ( + ReplaysResource, + AsyncReplaysResource, + ReplaysResourceWithRawResponse, + AsyncReplaysResourceWithRawResponse, + ReplaysResourceWithStreamingResponse, + AsyncReplaysResourceWithStreamingResponse, +) +from .browsers import ( + BrowsersResource, + AsyncBrowsersResource, + BrowsersResourceWithRawResponse, + AsyncBrowsersResourceWithRawResponse, + BrowsersResourceWithStreamingResponse, + AsyncBrowsersResourceWithStreamingResponse, +) + +__all__ = [ + "ReplaysResource", + "AsyncReplaysResource", + "ReplaysResourceWithRawResponse", + "AsyncReplaysResourceWithRawResponse", + "ReplaysResourceWithStreamingResponse", + "AsyncReplaysResourceWithStreamingResponse", + "BrowsersResource", + "AsyncBrowsersResource", + "BrowsersResourceWithRawResponse", + "AsyncBrowsersResourceWithRawResponse", + "BrowsersResourceWithStreamingResponse", + "AsyncBrowsersResourceWithStreamingResponse", +] diff --git a/src/kernel/resources/browsers.py b/src/kernel/resources/browsers/browsers.py similarity index 80% rename from src/kernel/resources/browsers.py rename to src/kernel/resources/browsers/browsers.py index 01b529b..b44573e 100644 --- a/src/kernel/resources/browsers.py +++ b/src/kernel/resources/browsers/browsers.py @@ -4,35 +4,39 @@ import httpx -from ..types import browser_create_params, browser_delete_params -from .._types import NOT_GIVEN, Body, Query, Headers, NoneType, NotGiven -from .._utils import maybe_transform, async_maybe_transform -from .._compat import cached_property -from .._resource import SyncAPIResource, AsyncAPIResource -from .._response import ( - BinaryAPIResponse, - AsyncBinaryAPIResponse, - StreamedBinaryAPIResponse, - AsyncStreamedBinaryAPIResponse, +from ...types import browser_create_params, browser_delete_params +from .replays import ( + ReplaysResource, + AsyncReplaysResource, + ReplaysResourceWithRawResponse, + AsyncReplaysResourceWithRawResponse, + ReplaysResourceWithStreamingResponse, + AsyncReplaysResourceWithStreamingResponse, +) +from ..._types import NOT_GIVEN, Body, Query, Headers, NoneType, NotGiven +from ..._utils import maybe_transform, async_maybe_transform +from ..._compat import cached_property +from ..._resource import SyncAPIResource, AsyncAPIResource +from ..._response import ( to_raw_response_wrapper, to_streamed_response_wrapper, async_to_raw_response_wrapper, - to_custom_raw_response_wrapper, async_to_streamed_response_wrapper, - to_custom_streamed_response_wrapper, - async_to_custom_raw_response_wrapper, - async_to_custom_streamed_response_wrapper, ) -from .._base_client import make_request_options -from ..types.browser_list_response import BrowserListResponse -from ..types.browser_create_response import BrowserCreateResponse -from ..types.browser_persistence_param import BrowserPersistenceParam -from ..types.browser_retrieve_response import BrowserRetrieveResponse +from ..._base_client import make_request_options +from ...types.browser_list_response import BrowserListResponse +from ...types.browser_create_response import BrowserCreateResponse +from ...types.browser_persistence_param import BrowserPersistenceParam +from ...types.browser_retrieve_response import BrowserRetrieveResponse __all__ = ["BrowsersResource", "AsyncBrowsersResource"] class BrowsersResource(SyncAPIResource): + @cached_property + def replays(self) -> ReplaysResource: + return ReplaysResource(self._client) + @cached_property def with_raw_response(self) -> BrowsersResourceWithRawResponse: """ @@ -58,7 +62,6 @@ def create( headless: bool | NotGiven = NOT_GIVEN, invocation_id: str | NotGiven = NOT_GIVEN, persistence: BrowserPersistenceParam | NotGiven = NOT_GIVEN, - replay: bool | NotGiven = NOT_GIVEN, stealth: bool | NotGiven = NOT_GIVEN, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. # The extra values given here take precedence over values defined on the client or passed to this method. @@ -78,8 +81,6 @@ def create( persistence: Optional persistence configuration for the browser session. - replay: If true, enables replay recording of the browser session. Defaults to false. - stealth: If true, launches the browser in stealth mode to reduce detection by anti-bot mechanisms. @@ -98,7 +99,6 @@ def create( "headless": headless, "invocation_id": invocation_id, "persistence": persistence, - "replay": replay, "stealth": stealth, }, browser_create_params.BrowserCreateParams, @@ -233,42 +233,12 @@ def delete_by_id( cast_to=NoneType, ) - def retrieve_replay( - self, - id: str, - *, - # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. - # The extra values given here take precedence over values defined on the client or passed to this method. - extra_headers: Headers | None = None, - extra_query: Query | None = None, - extra_body: Body | None = None, - timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, - ) -> BinaryAPIResponse: - """ - Get browser session replay. - - Args: - extra_headers: Send extra headers - - extra_query: Add additional query parameters to the request - - extra_body: Add additional JSON properties to the request - - timeout: Override the client-level default timeout for this request, in seconds - """ - if not id: - raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") - extra_headers = {"Accept": "video/mp4", **(extra_headers or {})} - return self._get( - f"/browsers/{id}/replay", - options=make_request_options( - extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout - ), - cast_to=BinaryAPIResponse, - ) - class AsyncBrowsersResource(AsyncAPIResource): + @cached_property + def replays(self) -> AsyncReplaysResource: + return AsyncReplaysResource(self._client) + @cached_property def with_raw_response(self) -> AsyncBrowsersResourceWithRawResponse: """ @@ -294,7 +264,6 @@ async def create( headless: bool | NotGiven = NOT_GIVEN, invocation_id: str | NotGiven = NOT_GIVEN, persistence: BrowserPersistenceParam | NotGiven = NOT_GIVEN, - replay: bool | NotGiven = NOT_GIVEN, stealth: bool | NotGiven = NOT_GIVEN, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. # The extra values given here take precedence over values defined on the client or passed to this method. @@ -314,8 +283,6 @@ async def create( persistence: Optional persistence configuration for the browser session. - replay: If true, enables replay recording of the browser session. Defaults to false. - stealth: If true, launches the browser in stealth mode to reduce detection by anti-bot mechanisms. @@ -334,7 +301,6 @@ async def create( "headless": headless, "invocation_id": invocation_id, "persistence": persistence, - "replay": replay, "stealth": stealth, }, browser_create_params.BrowserCreateParams, @@ -471,40 +437,6 @@ async def delete_by_id( cast_to=NoneType, ) - async def retrieve_replay( - self, - id: str, - *, - # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. - # The extra values given here take precedence over values defined on the client or passed to this method. - extra_headers: Headers | None = None, - extra_query: Query | None = None, - extra_body: Body | None = None, - timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, - ) -> AsyncBinaryAPIResponse: - """ - Get browser session replay. - - Args: - extra_headers: Send extra headers - - extra_query: Add additional query parameters to the request - - extra_body: Add additional JSON properties to the request - - timeout: Override the client-level default timeout for this request, in seconds - """ - if not id: - raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") - extra_headers = {"Accept": "video/mp4", **(extra_headers or {})} - return await self._get( - f"/browsers/{id}/replay", - options=make_request_options( - extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout - ), - cast_to=AsyncBinaryAPIResponse, - ) - class BrowsersResourceWithRawResponse: def __init__(self, browsers: BrowsersResource) -> None: @@ -525,10 +457,10 @@ def __init__(self, browsers: BrowsersResource) -> None: self.delete_by_id = to_raw_response_wrapper( browsers.delete_by_id, ) - self.retrieve_replay = to_custom_raw_response_wrapper( - browsers.retrieve_replay, - BinaryAPIResponse, - ) + + @cached_property + def replays(self) -> ReplaysResourceWithRawResponse: + return ReplaysResourceWithRawResponse(self._browsers.replays) class AsyncBrowsersResourceWithRawResponse: @@ -550,10 +482,10 @@ def __init__(self, browsers: AsyncBrowsersResource) -> None: self.delete_by_id = async_to_raw_response_wrapper( browsers.delete_by_id, ) - self.retrieve_replay = async_to_custom_raw_response_wrapper( - browsers.retrieve_replay, - AsyncBinaryAPIResponse, - ) + + @cached_property + def replays(self) -> AsyncReplaysResourceWithRawResponse: + return AsyncReplaysResourceWithRawResponse(self._browsers.replays) class BrowsersResourceWithStreamingResponse: @@ -575,10 +507,10 @@ def __init__(self, browsers: BrowsersResource) -> None: self.delete_by_id = to_streamed_response_wrapper( browsers.delete_by_id, ) - self.retrieve_replay = to_custom_streamed_response_wrapper( - browsers.retrieve_replay, - StreamedBinaryAPIResponse, - ) + + @cached_property + def replays(self) -> ReplaysResourceWithStreamingResponse: + return ReplaysResourceWithStreamingResponse(self._browsers.replays) class AsyncBrowsersResourceWithStreamingResponse: @@ -600,7 +532,7 @@ def __init__(self, browsers: AsyncBrowsersResource) -> None: self.delete_by_id = async_to_streamed_response_wrapper( browsers.delete_by_id, ) - self.retrieve_replay = async_to_custom_streamed_response_wrapper( - browsers.retrieve_replay, - AsyncStreamedBinaryAPIResponse, - ) + + @cached_property + def replays(self) -> AsyncReplaysResourceWithStreamingResponse: + return AsyncReplaysResourceWithStreamingResponse(self._browsers.replays) diff --git a/src/kernel/resources/browsers/replays.py b/src/kernel/resources/browsers/replays.py new file mode 100644 index 0000000..b801e8f --- /dev/null +++ b/src/kernel/resources/browsers/replays.py @@ -0,0 +1,454 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +import httpx + +from ..._types import NOT_GIVEN, Body, Query, Headers, NoneType, NotGiven +from ..._utils import maybe_transform, 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.browsers import replay_start_params +from ...types.browsers.replay_list_response import ReplayListResponse +from ...types.browsers.replay_start_response import ReplayStartResponse + +__all__ = ["ReplaysResource", "AsyncReplaysResource"] + + +class ReplaysResource(SyncAPIResource): + @cached_property + def with_raw_response(self) -> ReplaysResourceWithRawResponse: + """ + 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 ReplaysResourceWithRawResponse(self) + + @cached_property + def with_streaming_response(self) -> ReplaysResourceWithStreamingResponse: + """ + 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 ReplaysResourceWithStreamingResponse(self) + + def list( + self, + id: str, + *, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> ReplayListResponse: + """ + List all replays for the specified browser session. + + Args: + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not id: + raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") + return self._get( + f"/browsers/{id}/replays", + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=ReplayListResponse, + ) + + def download( + self, + replay_id: str, + *, + id: str, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> BinaryAPIResponse: + """ + Download or stream the specified replay recording. + + Args: + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not id: + raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") + if not replay_id: + raise ValueError(f"Expected a non-empty value for `replay_id` but received {replay_id!r}") + extra_headers = {"Accept": "video/mp4", **(extra_headers or {})} + return self._get( + f"/browsers/{id}/replays/{replay_id}", + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=BinaryAPIResponse, + ) + + def start( + self, + id: str, + *, + framerate: int | NotGiven = NOT_GIVEN, + max_duration_in_seconds: int | NotGiven = NOT_GIVEN, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> ReplayStartResponse: + """ + Start recording the browser session and return a replay ID. + + Args: + framerate: Recording framerate in fps. + + max_duration_in_seconds: Maximum recording duration in seconds. + + 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}") + return self._post( + f"/browsers/{id}/replays", + body=maybe_transform( + { + "framerate": framerate, + "max_duration_in_seconds": max_duration_in_seconds, + }, + replay_start_params.ReplayStartParams, + ), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=ReplayStartResponse, + ) + + def stop( + self, + replay_id: str, + *, + id: str, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> None: + """ + Stop the specified replay recording and persist the video. + + Args: + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not id: + raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") + if not replay_id: + raise ValueError(f"Expected a non-empty value for `replay_id` but received {replay_id!r}") + extra_headers = {"Accept": "*/*", **(extra_headers or {})} + return self._post( + f"/browsers/{id}/replays/{replay_id}/stop", + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=NoneType, + ) + + +class AsyncReplaysResource(AsyncAPIResource): + @cached_property + def with_raw_response(self) -> AsyncReplaysResourceWithRawResponse: + """ + 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 AsyncReplaysResourceWithRawResponse(self) + + @cached_property + def with_streaming_response(self) -> AsyncReplaysResourceWithStreamingResponse: + """ + 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 AsyncReplaysResourceWithStreamingResponse(self) + + async def list( + self, + id: str, + *, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> ReplayListResponse: + """ + List all replays for the specified browser session. + + Args: + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not id: + raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") + return await self._get( + f"/browsers/{id}/replays", + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=ReplayListResponse, + ) + + async def download( + self, + replay_id: str, + *, + id: str, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> AsyncBinaryAPIResponse: + """ + Download or stream the specified replay recording. + + Args: + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not id: + raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") + if not replay_id: + raise ValueError(f"Expected a non-empty value for `replay_id` but received {replay_id!r}") + extra_headers = {"Accept": "video/mp4", **(extra_headers or {})} + return await self._get( + f"/browsers/{id}/replays/{replay_id}", + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=AsyncBinaryAPIResponse, + ) + + async def start( + self, + id: str, + *, + framerate: int | NotGiven = NOT_GIVEN, + max_duration_in_seconds: int | NotGiven = NOT_GIVEN, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> ReplayStartResponse: + """ + Start recording the browser session and return a replay ID. + + Args: + framerate: Recording framerate in fps. + + max_duration_in_seconds: Maximum recording duration in seconds. + + 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}") + return await self._post( + f"/browsers/{id}/replays", + body=await async_maybe_transform( + { + "framerate": framerate, + "max_duration_in_seconds": max_duration_in_seconds, + }, + replay_start_params.ReplayStartParams, + ), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=ReplayStartResponse, + ) + + async def stop( + self, + replay_id: str, + *, + id: str, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> None: + """ + Stop the specified replay recording and persist the video. + + Args: + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not id: + raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") + if not replay_id: + raise ValueError(f"Expected a non-empty value for `replay_id` but received {replay_id!r}") + extra_headers = {"Accept": "*/*", **(extra_headers or {})} + return await self._post( + f"/browsers/{id}/replays/{replay_id}/stop", + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=NoneType, + ) + + +class ReplaysResourceWithRawResponse: + def __init__(self, replays: ReplaysResource) -> None: + self._replays = replays + + self.list = to_raw_response_wrapper( + replays.list, + ) + self.download = to_custom_raw_response_wrapper( + replays.download, + BinaryAPIResponse, + ) + self.start = to_raw_response_wrapper( + replays.start, + ) + self.stop = to_raw_response_wrapper( + replays.stop, + ) + + +class AsyncReplaysResourceWithRawResponse: + def __init__(self, replays: AsyncReplaysResource) -> None: + self._replays = replays + + self.list = async_to_raw_response_wrapper( + replays.list, + ) + self.download = async_to_custom_raw_response_wrapper( + replays.download, + AsyncBinaryAPIResponse, + ) + self.start = async_to_raw_response_wrapper( + replays.start, + ) + self.stop = async_to_raw_response_wrapper( + replays.stop, + ) + + +class ReplaysResourceWithStreamingResponse: + def __init__(self, replays: ReplaysResource) -> None: + self._replays = replays + + self.list = to_streamed_response_wrapper( + replays.list, + ) + self.download = to_custom_streamed_response_wrapper( + replays.download, + StreamedBinaryAPIResponse, + ) + self.start = to_streamed_response_wrapper( + replays.start, + ) + self.stop = to_streamed_response_wrapper( + replays.stop, + ) + + +class AsyncReplaysResourceWithStreamingResponse: + def __init__(self, replays: AsyncReplaysResource) -> None: + self._replays = replays + + self.list = async_to_streamed_response_wrapper( + replays.list, + ) + self.download = async_to_custom_streamed_response_wrapper( + replays.download, + AsyncStreamedBinaryAPIResponse, + ) + self.start = async_to_streamed_response_wrapper( + replays.start, + ) + self.stop = async_to_streamed_response_wrapper( + replays.stop, + ) diff --git a/src/kernel/types/browser_create_params.py b/src/kernel/types/browser_create_params.py index 7019e53..746a92f 100644 --- a/src/kernel/types/browser_create_params.py +++ b/src/kernel/types/browser_create_params.py @@ -22,9 +22,6 @@ class BrowserCreateParams(TypedDict, total=False): persistence: BrowserPersistenceParam """Optional persistence configuration for the browser session.""" - replay: bool - """If true, enables replay recording of the browser session. Defaults to false.""" - stealth: bool """ If true, launches the browser in stealth mode to reduce detection by anti-bot diff --git a/src/kernel/types/browser_create_response.py b/src/kernel/types/browser_create_response.py index 4fc470b..afba2b3 100644 --- a/src/kernel/types/browser_create_response.py +++ b/src/kernel/types/browser_create_response.py @@ -23,6 +23,3 @@ class BrowserCreateResponse(BaseModel): persistence: Optional[BrowserPersistence] = None """Optional persistence configuration for the browser session.""" - - replay_view_url: Optional[str] = None - """Remote URL for viewing the browser session replay if enabled""" diff --git a/src/kernel/types/browser_list_response.py b/src/kernel/types/browser_list_response.py index 702c69b..43c8d92 100644 --- a/src/kernel/types/browser_list_response.py +++ b/src/kernel/types/browser_list_response.py @@ -25,8 +25,5 @@ class BrowserListResponseItem(BaseModel): persistence: Optional[BrowserPersistence] = None """Optional persistence configuration for the browser session.""" - replay_view_url: Optional[str] = None - """Remote URL for viewing the browser session replay if enabled""" - BrowserListResponse: TypeAlias = List[BrowserListResponseItem] diff --git a/src/kernel/types/browser_retrieve_response.py b/src/kernel/types/browser_retrieve_response.py index 8f44ddb..45cf74b 100644 --- a/src/kernel/types/browser_retrieve_response.py +++ b/src/kernel/types/browser_retrieve_response.py @@ -23,6 +23,3 @@ class BrowserRetrieveResponse(BaseModel): persistence: Optional[BrowserPersistence] = None """Optional persistence configuration for the browser session.""" - - replay_view_url: Optional[str] = None - """Remote URL for viewing the browser session replay if enabled""" diff --git a/src/kernel/types/browsers/__init__.py b/src/kernel/types/browsers/__init__.py new file mode 100644 index 0000000..61b18bc --- /dev/null +++ b/src/kernel/types/browsers/__init__.py @@ -0,0 +1,7 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from .replay_start_params import ReplayStartParams as ReplayStartParams +from .replay_list_response import ReplayListResponse as ReplayListResponse +from .replay_start_response import ReplayStartResponse as ReplayStartResponse diff --git a/src/kernel/types/browsers/replay_list_response.py b/src/kernel/types/browsers/replay_list_response.py new file mode 100644 index 0000000..f53dd4d --- /dev/null +++ b/src/kernel/types/browsers/replay_list_response.py @@ -0,0 +1,26 @@ +# 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__ = ["ReplayListResponse", "ReplayListResponseItem"] + + +class ReplayListResponseItem(BaseModel): + replay_id: str + """Unique identifier for the replay recording.""" + + finished_at: Optional[datetime] = None + """Timestamp when replay finished""" + + replay_view_url: Optional[str] = None + """URL for viewing the replay recording.""" + + started_at: Optional[datetime] = None + """Timestamp when replay started""" + + +ReplayListResponse: TypeAlias = List[ReplayListResponseItem] diff --git a/src/kernel/types/browsers/replay_start_params.py b/src/kernel/types/browsers/replay_start_params.py new file mode 100644 index 0000000..d668386 --- /dev/null +++ b/src/kernel/types/browsers/replay_start_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 TypedDict + +__all__ = ["ReplayStartParams"] + + +class ReplayStartParams(TypedDict, total=False): + framerate: int + """Recording framerate in fps.""" + + max_duration_in_seconds: int + """Maximum recording duration in seconds.""" diff --git a/src/kernel/types/browsers/replay_start_response.py b/src/kernel/types/browsers/replay_start_response.py new file mode 100644 index 0000000..dd837d5 --- /dev/null +++ b/src/kernel/types/browsers/replay_start_response.py @@ -0,0 +1,22 @@ +# 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__ = ["ReplayStartResponse"] + + +class ReplayStartResponse(BaseModel): + replay_id: str + """Unique identifier for the replay recording.""" + + finished_at: Optional[datetime] = None + """Timestamp when replay finished""" + + replay_view_url: Optional[str] = None + """URL for viewing the replay recording.""" + + started_at: Optional[datetime] = None + """Timestamp when replay started""" diff --git a/tests/api_resources/browsers/__init__.py b/tests/api_resources/browsers/__init__.py new file mode 100644 index 0000000..fd8019a --- /dev/null +++ b/tests/api_resources/browsers/__init__.py @@ -0,0 +1 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. diff --git a/tests/api_resources/browsers/test_replays.py b/tests/api_resources/browsers/test_replays.py new file mode 100644 index 0000000..930d008 --- /dev/null +++ b/tests/api_resources/browsers/test_replays.py @@ -0,0 +1,452 @@ +# 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._response import ( + BinaryAPIResponse, + AsyncBinaryAPIResponse, + StreamedBinaryAPIResponse, + AsyncStreamedBinaryAPIResponse, +) +from kernel.types.browsers import ReplayListResponse, ReplayStartResponse + +base_url = os.environ.get("TEST_API_BASE_URL", "http://127.0.0.1:4010") + + +class TestReplays: + parametrize = pytest.mark.parametrize("client", [False, True], indirect=True, ids=["loose", "strict"]) + + @pytest.mark.skip() + @parametrize + def test_method_list(self, client: Kernel) -> None: + replay = client.browsers.replays.list( + "id", + ) + assert_matches_type(ReplayListResponse, replay, path=["response"]) + + @pytest.mark.skip() + @parametrize + def test_raw_response_list(self, client: Kernel) -> None: + response = client.browsers.replays.with_raw_response.list( + "id", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + replay = response.parse() + assert_matches_type(ReplayListResponse, replay, path=["response"]) + + @pytest.mark.skip() + @parametrize + def test_streaming_response_list(self, client: Kernel) -> None: + with client.browsers.replays.with_streaming_response.list( + "id", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + replay = response.parse() + assert_matches_type(ReplayListResponse, replay, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @pytest.mark.skip() + @parametrize + def test_path_params_list(self, client: Kernel) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): + client.browsers.replays.with_raw_response.list( + "", + ) + + @pytest.mark.skip() + @parametrize + @pytest.mark.respx(base_url=base_url) + def test_method_download(self, client: Kernel, respx_mock: MockRouter) -> None: + respx_mock.get("/browsers/id/replays/replay_id").mock(return_value=httpx.Response(200, json={"foo": "bar"})) + replay = client.browsers.replays.download( + replay_id="replay_id", + id="id", + ) + assert replay.is_closed + assert replay.json() == {"foo": "bar"} + assert cast(Any, replay.is_closed) is True + assert isinstance(replay, BinaryAPIResponse) + + @pytest.mark.skip() + @parametrize + @pytest.mark.respx(base_url=base_url) + def test_raw_response_download(self, client: Kernel, respx_mock: MockRouter) -> None: + respx_mock.get("/browsers/id/replays/replay_id").mock(return_value=httpx.Response(200, json={"foo": "bar"})) + + replay = client.browsers.replays.with_raw_response.download( + replay_id="replay_id", + id="id", + ) + + assert replay.is_closed is True + assert replay.http_request.headers.get("X-Stainless-Lang") == "python" + assert replay.json() == {"foo": "bar"} + assert isinstance(replay, BinaryAPIResponse) + + @pytest.mark.skip() + @parametrize + @pytest.mark.respx(base_url=base_url) + def test_streaming_response_download(self, client: Kernel, respx_mock: MockRouter) -> None: + respx_mock.get("/browsers/id/replays/replay_id").mock(return_value=httpx.Response(200, json={"foo": "bar"})) + with client.browsers.replays.with_streaming_response.download( + replay_id="replay_id", + id="id", + ) as replay: + assert not replay.is_closed + assert replay.http_request.headers.get("X-Stainless-Lang") == "python" + + assert replay.json() == {"foo": "bar"} + assert cast(Any, replay.is_closed) is True + assert isinstance(replay, StreamedBinaryAPIResponse) + + assert cast(Any, replay.is_closed) is True + + @pytest.mark.skip() + @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` but received ''"): + client.browsers.replays.with_raw_response.download( + replay_id="replay_id", + id="", + ) + + with pytest.raises(ValueError, match=r"Expected a non-empty value for `replay_id` but received ''"): + client.browsers.replays.with_raw_response.download( + replay_id="", + id="id", + ) + + @pytest.mark.skip() + @parametrize + def test_method_start(self, client: Kernel) -> None: + replay = client.browsers.replays.start( + id="id", + ) + assert_matches_type(ReplayStartResponse, replay, path=["response"]) + + @pytest.mark.skip() + @parametrize + def test_method_start_with_all_params(self, client: Kernel) -> None: + replay = client.browsers.replays.start( + id="id", + framerate=1, + max_duration_in_seconds=1, + ) + assert_matches_type(ReplayStartResponse, replay, path=["response"]) + + @pytest.mark.skip() + @parametrize + def test_raw_response_start(self, client: Kernel) -> None: + response = client.browsers.replays.with_raw_response.start( + id="id", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + replay = response.parse() + assert_matches_type(ReplayStartResponse, replay, path=["response"]) + + @pytest.mark.skip() + @parametrize + def test_streaming_response_start(self, client: Kernel) -> None: + with client.browsers.replays.with_streaming_response.start( + id="id", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + replay = response.parse() + assert_matches_type(ReplayStartResponse, replay, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @pytest.mark.skip() + @parametrize + def test_path_params_start(self, client: Kernel) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): + client.browsers.replays.with_raw_response.start( + id="", + ) + + @pytest.mark.skip() + @parametrize + def test_method_stop(self, client: Kernel) -> None: + replay = client.browsers.replays.stop( + replay_id="replay_id", + id="id", + ) + assert replay is None + + @pytest.mark.skip() + @parametrize + def test_raw_response_stop(self, client: Kernel) -> None: + response = client.browsers.replays.with_raw_response.stop( + replay_id="replay_id", + id="id", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + replay = response.parse() + assert replay is None + + @pytest.mark.skip() + @parametrize + def test_streaming_response_stop(self, client: Kernel) -> None: + with client.browsers.replays.with_streaming_response.stop( + replay_id="replay_id", + id="id", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + replay = response.parse() + assert replay is None + + assert cast(Any, response.is_closed) is True + + @pytest.mark.skip() + @parametrize + def test_path_params_stop(self, client: Kernel) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): + client.browsers.replays.with_raw_response.stop( + replay_id="replay_id", + id="", + ) + + with pytest.raises(ValueError, match=r"Expected a non-empty value for `replay_id` but received ''"): + client.browsers.replays.with_raw_response.stop( + replay_id="", + id="id", + ) + + +class TestAsyncReplays: + parametrize = pytest.mark.parametrize( + "async_client", [False, True, {"http_client": "aiohttp"}], indirect=True, ids=["loose", "strict", "aiohttp"] + ) + + @pytest.mark.skip() + @parametrize + async def test_method_list(self, async_client: AsyncKernel) -> None: + replay = await async_client.browsers.replays.list( + "id", + ) + assert_matches_type(ReplayListResponse, replay, path=["response"]) + + @pytest.mark.skip() + @parametrize + async def test_raw_response_list(self, async_client: AsyncKernel) -> None: + response = await async_client.browsers.replays.with_raw_response.list( + "id", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + replay = await response.parse() + assert_matches_type(ReplayListResponse, replay, path=["response"]) + + @pytest.mark.skip() + @parametrize + async def test_streaming_response_list(self, async_client: AsyncKernel) -> None: + async with async_client.browsers.replays.with_streaming_response.list( + "id", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + replay = await response.parse() + assert_matches_type(ReplayListResponse, replay, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @pytest.mark.skip() + @parametrize + async def test_path_params_list(self, async_client: AsyncKernel) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): + await async_client.browsers.replays.with_raw_response.list( + "", + ) + + @pytest.mark.skip() + @parametrize + @pytest.mark.respx(base_url=base_url) + async def test_method_download(self, async_client: AsyncKernel, respx_mock: MockRouter) -> None: + respx_mock.get("/browsers/id/replays/replay_id").mock(return_value=httpx.Response(200, json={"foo": "bar"})) + replay = await async_client.browsers.replays.download( + replay_id="replay_id", + id="id", + ) + assert replay.is_closed + assert await replay.json() == {"foo": "bar"} + assert cast(Any, replay.is_closed) is True + assert isinstance(replay, AsyncBinaryAPIResponse) + + @pytest.mark.skip() + @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("/browsers/id/replays/replay_id").mock(return_value=httpx.Response(200, json={"foo": "bar"})) + + replay = await async_client.browsers.replays.with_raw_response.download( + replay_id="replay_id", + id="id", + ) + + assert replay.is_closed is True + assert replay.http_request.headers.get("X-Stainless-Lang") == "python" + assert await replay.json() == {"foo": "bar"} + assert isinstance(replay, AsyncBinaryAPIResponse) + + @pytest.mark.skip() + @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("/browsers/id/replays/replay_id").mock(return_value=httpx.Response(200, json={"foo": "bar"})) + async with async_client.browsers.replays.with_streaming_response.download( + replay_id="replay_id", + id="id", + ) as replay: + assert not replay.is_closed + assert replay.http_request.headers.get("X-Stainless-Lang") == "python" + + assert await replay.json() == {"foo": "bar"} + assert cast(Any, replay.is_closed) is True + assert isinstance(replay, AsyncStreamedBinaryAPIResponse) + + assert cast(Any, replay.is_closed) is True + + @pytest.mark.skip() + @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` but received ''"): + await async_client.browsers.replays.with_raw_response.download( + replay_id="replay_id", + id="", + ) + + with pytest.raises(ValueError, match=r"Expected a non-empty value for `replay_id` but received ''"): + await async_client.browsers.replays.with_raw_response.download( + replay_id="", + id="id", + ) + + @pytest.mark.skip() + @parametrize + async def test_method_start(self, async_client: AsyncKernel) -> None: + replay = await async_client.browsers.replays.start( + id="id", + ) + assert_matches_type(ReplayStartResponse, replay, path=["response"]) + + @pytest.mark.skip() + @parametrize + async def test_method_start_with_all_params(self, async_client: AsyncKernel) -> None: + replay = await async_client.browsers.replays.start( + id="id", + framerate=1, + max_duration_in_seconds=1, + ) + assert_matches_type(ReplayStartResponse, replay, path=["response"]) + + @pytest.mark.skip() + @parametrize + async def test_raw_response_start(self, async_client: AsyncKernel) -> None: + response = await async_client.browsers.replays.with_raw_response.start( + id="id", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + replay = await response.parse() + assert_matches_type(ReplayStartResponse, replay, path=["response"]) + + @pytest.mark.skip() + @parametrize + async def test_streaming_response_start(self, async_client: AsyncKernel) -> None: + async with async_client.browsers.replays.with_streaming_response.start( + id="id", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + replay = await response.parse() + assert_matches_type(ReplayStartResponse, replay, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @pytest.mark.skip() + @parametrize + async def test_path_params_start(self, async_client: AsyncKernel) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): + await async_client.browsers.replays.with_raw_response.start( + id="", + ) + + @pytest.mark.skip() + @parametrize + async def test_method_stop(self, async_client: AsyncKernel) -> None: + replay = await async_client.browsers.replays.stop( + replay_id="replay_id", + id="id", + ) + assert replay is None + + @pytest.mark.skip() + @parametrize + async def test_raw_response_stop(self, async_client: AsyncKernel) -> None: + response = await async_client.browsers.replays.with_raw_response.stop( + replay_id="replay_id", + id="id", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + replay = await response.parse() + assert replay is None + + @pytest.mark.skip() + @parametrize + async def test_streaming_response_stop(self, async_client: AsyncKernel) -> None: + async with async_client.browsers.replays.with_streaming_response.stop( + replay_id="replay_id", + id="id", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + replay = await response.parse() + assert replay is None + + assert cast(Any, response.is_closed) is True + + @pytest.mark.skip() + @parametrize + async def test_path_params_stop(self, async_client: AsyncKernel) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): + await async_client.browsers.replays.with_raw_response.stop( + replay_id="replay_id", + id="", + ) + + with pytest.raises(ValueError, match=r"Expected a non-empty value for `replay_id` but received ''"): + await async_client.browsers.replays.with_raw_response.stop( + replay_id="", + id="id", + ) diff --git a/tests/api_resources/test_browsers.py b/tests/api_resources/test_browsers.py index a9cb8a5..8f990be 100644 --- a/tests/api_resources/test_browsers.py +++ b/tests/api_resources/test_browsers.py @@ -5,9 +5,7 @@ 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 @@ -16,12 +14,6 @@ BrowserCreateResponse, BrowserRetrieveResponse, ) -from kernel._response import ( - BinaryAPIResponse, - AsyncBinaryAPIResponse, - StreamedBinaryAPIResponse, - AsyncStreamedBinaryAPIResponse, -) base_url = os.environ.get("TEST_API_BASE_URL", "http://127.0.0.1:4010") @@ -42,7 +34,6 @@ def test_method_create_with_all_params(self, client: Kernel) -> None: headless=False, invocation_id="rr33xuugxj9h0bkf1rdt2bet", persistence={"id": "my-awesome-browser-for-user-1234"}, - replay=True, stealth=True, ) assert_matches_type(BrowserCreateResponse, browser, path=["response"]) @@ -215,66 +206,6 @@ def test_path_params_delete_by_id(self, client: Kernel) -> None: "", ) - @pytest.mark.skip() - @parametrize - @pytest.mark.respx(base_url=base_url) - def test_method_retrieve_replay(self, client: Kernel, respx_mock: MockRouter) -> None: - respx_mock.get("/browsers/htzv5orfit78e1m2biiifpbv/replay").mock( - return_value=httpx.Response(200, json={"foo": "bar"}) - ) - browser = client.browsers.retrieve_replay( - "id", - ) - assert browser.is_closed - assert browser.json() == {"foo": "bar"} - assert cast(Any, browser.is_closed) is True - assert isinstance(browser, BinaryAPIResponse) - - @pytest.mark.skip() - @parametrize - @pytest.mark.respx(base_url=base_url) - def test_raw_response_retrieve_replay(self, client: Kernel, respx_mock: MockRouter) -> None: - respx_mock.get("/browsers/htzv5orfit78e1m2biiifpbv/replay").mock( - return_value=httpx.Response(200, json={"foo": "bar"}) - ) - - browser = client.browsers.with_raw_response.retrieve_replay( - "id", - ) - - assert browser.is_closed is True - assert browser.http_request.headers.get("X-Stainless-Lang") == "python" - assert browser.json() == {"foo": "bar"} - assert isinstance(browser, BinaryAPIResponse) - - @pytest.mark.skip() - @parametrize - @pytest.mark.respx(base_url=base_url) - def test_streaming_response_retrieve_replay(self, client: Kernel, respx_mock: MockRouter) -> None: - respx_mock.get("/browsers/htzv5orfit78e1m2biiifpbv/replay").mock( - return_value=httpx.Response(200, json={"foo": "bar"}) - ) - with client.browsers.with_streaming_response.retrieve_replay( - "id", - ) as browser: - assert not browser.is_closed - assert browser.http_request.headers.get("X-Stainless-Lang") == "python" - - assert browser.json() == {"foo": "bar"} - assert cast(Any, browser.is_closed) is True - assert isinstance(browser, StreamedBinaryAPIResponse) - - assert cast(Any, browser.is_closed) is True - - @pytest.mark.skip() - @parametrize - @pytest.mark.respx(base_url=base_url) - def test_path_params_retrieve_replay(self, client: Kernel) -> None: - with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): - client.browsers.with_raw_response.retrieve_replay( - "", - ) - class TestAsyncBrowsers: parametrize = pytest.mark.parametrize( @@ -294,7 +225,6 @@ async def test_method_create_with_all_params(self, async_client: AsyncKernel) -> headless=False, invocation_id="rr33xuugxj9h0bkf1rdt2bet", persistence={"id": "my-awesome-browser-for-user-1234"}, - replay=True, stealth=True, ) assert_matches_type(BrowserCreateResponse, browser, path=["response"]) @@ -466,63 +396,3 @@ async def test_path_params_delete_by_id(self, async_client: AsyncKernel) -> None await async_client.browsers.with_raw_response.delete_by_id( "", ) - - @pytest.mark.skip() - @parametrize - @pytest.mark.respx(base_url=base_url) - async def test_method_retrieve_replay(self, async_client: AsyncKernel, respx_mock: MockRouter) -> None: - respx_mock.get("/browsers/htzv5orfit78e1m2biiifpbv/replay").mock( - return_value=httpx.Response(200, json={"foo": "bar"}) - ) - browser = await async_client.browsers.retrieve_replay( - "id", - ) - assert browser.is_closed - assert await browser.json() == {"foo": "bar"} - assert cast(Any, browser.is_closed) is True - assert isinstance(browser, AsyncBinaryAPIResponse) - - @pytest.mark.skip() - @parametrize - @pytest.mark.respx(base_url=base_url) - async def test_raw_response_retrieve_replay(self, async_client: AsyncKernel, respx_mock: MockRouter) -> None: - respx_mock.get("/browsers/htzv5orfit78e1m2biiifpbv/replay").mock( - return_value=httpx.Response(200, json={"foo": "bar"}) - ) - - browser = await async_client.browsers.with_raw_response.retrieve_replay( - "id", - ) - - assert browser.is_closed is True - assert browser.http_request.headers.get("X-Stainless-Lang") == "python" - assert await browser.json() == {"foo": "bar"} - assert isinstance(browser, AsyncBinaryAPIResponse) - - @pytest.mark.skip() - @parametrize - @pytest.mark.respx(base_url=base_url) - async def test_streaming_response_retrieve_replay(self, async_client: AsyncKernel, respx_mock: MockRouter) -> None: - respx_mock.get("/browsers/htzv5orfit78e1m2biiifpbv/replay").mock( - return_value=httpx.Response(200, json={"foo": "bar"}) - ) - async with async_client.browsers.with_streaming_response.retrieve_replay( - "id", - ) as browser: - assert not browser.is_closed - assert browser.http_request.headers.get("X-Stainless-Lang") == "python" - - assert await browser.json() == {"foo": "bar"} - assert cast(Any, browser.is_closed) is True - assert isinstance(browser, AsyncStreamedBinaryAPIResponse) - - assert cast(Any, browser.is_closed) is True - - @pytest.mark.skip() - @parametrize - @pytest.mark.respx(base_url=base_url) - async def test_path_params_retrieve_replay(self, async_client: AsyncKernel) -> None: - with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): - await async_client.browsers.with_raw_response.retrieve_replay( - "", - ) diff --git a/tests/test_client.py b/tests/test_client.py index 05267fd..d5a1784 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -462,7 +462,7 @@ def test_request_extra_query(self) -> None: def test_multipart_repeating_array(self, client: Kernel) -> None: request = client._build_request( FinalRequestOptions.construct( - method="get", + method="post", url="/foo", headers={"Content-Type": "multipart/form-data; boundary=6b7ba517decee4a450543ea6ae821c82"}, json_data={"array": ["foo", "bar"]}, @@ -1273,7 +1273,7 @@ def test_request_extra_query(self) -> None: def test_multipart_repeating_array(self, async_client: AsyncKernel) -> None: request = async_client._build_request( FinalRequestOptions.construct( - method="get", + method="post", url="/foo", headers={"Content-Type": "multipart/form-data; boundary=6b7ba517decee4a450543ea6ae821c82"}, json_data={"array": ["foo", "bar"]}, diff --git a/tests/test_models.py b/tests/test_models.py index 4f41217..1e7293a 100644 --- a/tests/test_models.py +++ b/tests/test_models.py @@ -889,3 +889,48 @@ class ModelB(BaseModel): ) assert isinstance(m, ModelB) + + +def test_nested_discriminated_union() -> None: + class InnerType1(BaseModel): + type: Literal["type_1"] + + class InnerModel(BaseModel): + inner_value: str + + class InnerType2(BaseModel): + type: Literal["type_2"] + some_inner_model: InnerModel + + class Type1(BaseModel): + base_type: Literal["base_type_1"] + value: Annotated[ + Union[ + InnerType1, + InnerType2, + ], + PropertyInfo(discriminator="type"), + ] + + class Type2(BaseModel): + base_type: Literal["base_type_2"] + + T = Annotated[ + Union[ + Type1, + Type2, + ], + PropertyInfo(discriminator="base_type"), + ] + + model = construct_type( + type_=T, + value={ + "base_type": "base_type_1", + "value": { + "type": "type_2", + }, + }, + ) + assert isinstance(model, Type1) + assert isinstance(model.value, InnerType2)