diff --git a/.release-please-manifest.json b/.release-please-manifest.json index 8f3e0a49..b4e9013b 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,3 +1,3 @@ { - ".": "0.15.0" + ".": "0.16.0" } \ No newline at end of file diff --git a/.stats.yml b/.stats.yml index b4dc6064..8cd6a7c5 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ -configured_endpoints: 64 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel%2Fkernel-e21f0324774a1762bc2bba0da3a8a6b0d0e74720d7a1c83dec813f9e027fcf58.yml -openapi_spec_hash: f1b636abfd6cb8e7c2ba7ffb8e53b9ba -config_hash: 09a2df23048cb16689c9a390d9e5bc47 +configured_endpoints: 65 +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel%2Fkernel-ecf484375ede1edd7754779ad8beeebd4ba9118152fe6cd65772dc7245a19dee.yml +openapi_spec_hash: b1f3f05005f75cbf5b82299459e2aa9b +config_hash: 3ded7a0ed77b1bfd68eabc6763521fe8 diff --git a/CHANGELOG.md b/CHANGELOG.md index e9fc2228..73ea0c7a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,18 @@ # Changelog +## 0.16.0 (2025-10-27) + +Full Changelog: [v0.15.0...v0.16.0](https://github.com/onkernel/kernel-python-sdk/compare/v0.15.0...v0.16.0) + +### Features + +* ad hoc playwright code exec AP| ([4204ad5](https://github.com/onkernel/kernel-python-sdk/commit/4204ad587838ec90c3b54ca2eaf7d768da570a29)) + + +### Chores + +* bump `httpx-aiohttp` version to 0.1.9 ([1130759](https://github.com/onkernel/kernel-python-sdk/commit/1130759ebc7eb511d8788332b59e84d0819f9715)) + ## 0.15.0 (2025-10-17) Full Changelog: [v0.14.2...v0.15.0](https://github.com/onkernel/kernel-python-sdk/compare/v0.14.2...v0.15.0) diff --git a/api.md b/api.md index 858dbfd8..164a8c47 100644 --- a/api.md +++ b/api.md @@ -178,6 +178,18 @@ Methods: - client.browsers.computer.scroll(id, \*\*params) -> None - client.browsers.computer.type_text(id, \*\*params) -> None +## Playwright + +Types: + +```python +from kernel.types.browsers import PlaywrightExecuteResponse +``` + +Methods: + +- client.browsers.playwright.execute(id, \*\*params) -> PlaywrightExecuteResponse + # Profiles Types: diff --git a/pyproject.toml b/pyproject.toml index 0f8cb140..18317f85 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "kernel" -version = "0.15.0" +version = "0.16.0" description = "The official Python library for the kernel API" dynamic = ["readme"] license = "Apache-2.0" @@ -39,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.8"] +aiohttp = ["aiohttp", "httpx_aiohttp>=0.1.9"] [tool.rye] managed = true diff --git a/requirements-dev.lock b/requirements-dev.lock index 249acb1e..fc032738 100644 --- a/requirements-dev.lock +++ b/requirements-dev.lock @@ -56,7 +56,7 @@ httpx==0.28.1 # via httpx-aiohttp # via kernel # via respx -httpx-aiohttp==0.1.8 +httpx-aiohttp==0.1.9 # via kernel idna==3.4 # via anyio diff --git a/requirements.lock b/requirements.lock index 49cc9054..ed64e3db 100644 --- a/requirements.lock +++ b/requirements.lock @@ -43,7 +43,7 @@ httpcore==1.0.9 httpx==0.28.1 # via httpx-aiohttp # via kernel -httpx-aiohttp==0.1.8 +httpx-aiohttp==0.1.9 # via kernel idna==3.4 # via anyio diff --git a/src/kernel/_version.py b/src/kernel/_version.py index e65ca7f2..211e253d 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.15.0" # x-release-please-version +__version__ = "0.16.0" # x-release-please-version diff --git a/src/kernel/resources/browsers/__init__.py b/src/kernel/resources/browsers/__init__.py index abcc8f78..a1acee20 100644 --- a/src/kernel/resources/browsers/__init__.py +++ b/src/kernel/resources/browsers/__init__.py @@ -48,6 +48,14 @@ ComputerResourceWithStreamingResponse, AsyncComputerResourceWithStreamingResponse, ) +from .playwright import ( + PlaywrightResource, + AsyncPlaywrightResource, + PlaywrightResourceWithRawResponse, + AsyncPlaywrightResourceWithRawResponse, + PlaywrightResourceWithStreamingResponse, + AsyncPlaywrightResourceWithStreamingResponse, +) __all__ = [ "ReplaysResource", @@ -80,6 +88,12 @@ "AsyncComputerResourceWithRawResponse", "ComputerResourceWithStreamingResponse", "AsyncComputerResourceWithStreamingResponse", + "PlaywrightResource", + "AsyncPlaywrightResource", + "PlaywrightResourceWithRawResponse", + "AsyncPlaywrightResourceWithRawResponse", + "PlaywrightResourceWithStreamingResponse", + "AsyncPlaywrightResourceWithStreamingResponse", "BrowsersResource", "AsyncBrowsersResource", "BrowsersResourceWithRawResponse", diff --git a/src/kernel/resources/browsers/browsers.py b/src/kernel/resources/browsers/browsers.py index c65a738a..6a0129c0 100644 --- a/src/kernel/resources/browsers/browsers.py +++ b/src/kernel/resources/browsers/browsers.py @@ -50,6 +50,14 @@ AsyncComputerResourceWithStreamingResponse, ) from ..._compat import cached_property +from .playwright import ( + PlaywrightResource, + AsyncPlaywrightResource, + PlaywrightResourceWithRawResponse, + AsyncPlaywrightResourceWithRawResponse, + PlaywrightResourceWithStreamingResponse, + AsyncPlaywrightResourceWithStreamingResponse, +) from ..._resource import SyncAPIResource, AsyncAPIResource from ..._response import ( to_raw_response_wrapper, @@ -87,6 +95,10 @@ def logs(self) -> LogsResource: def computer(self) -> ComputerResource: return ComputerResource(self._client) + @cached_property + def playwright(self) -> PlaywrightResource: + return PlaywrightResource(self._client) + @cached_property def with_raw_response(self) -> BrowsersResourceWithRawResponse: """ @@ -391,6 +403,10 @@ def logs(self) -> AsyncLogsResource: def computer(self) -> AsyncComputerResource: return AsyncComputerResource(self._client) + @cached_property + def playwright(self) -> AsyncPlaywrightResource: + return AsyncPlaywrightResource(self._client) + @cached_property def with_raw_response(self) -> AsyncBrowsersResourceWithRawResponse: """ @@ -719,6 +735,10 @@ def logs(self) -> LogsResourceWithRawResponse: def computer(self) -> ComputerResourceWithRawResponse: return ComputerResourceWithRawResponse(self._browsers.computer) + @cached_property + def playwright(self) -> PlaywrightResourceWithRawResponse: + return PlaywrightResourceWithRawResponse(self._browsers.playwright) + class AsyncBrowsersResourceWithRawResponse: def __init__(self, browsers: AsyncBrowsersResource) -> None: @@ -763,6 +783,10 @@ def logs(self) -> AsyncLogsResourceWithRawResponse: def computer(self) -> AsyncComputerResourceWithRawResponse: return AsyncComputerResourceWithRawResponse(self._browsers.computer) + @cached_property + def playwright(self) -> AsyncPlaywrightResourceWithRawResponse: + return AsyncPlaywrightResourceWithRawResponse(self._browsers.playwright) + class BrowsersResourceWithStreamingResponse: def __init__(self, browsers: BrowsersResource) -> None: @@ -807,6 +831,10 @@ def logs(self) -> LogsResourceWithStreamingResponse: def computer(self) -> ComputerResourceWithStreamingResponse: return ComputerResourceWithStreamingResponse(self._browsers.computer) + @cached_property + def playwright(self) -> PlaywrightResourceWithStreamingResponse: + return PlaywrightResourceWithStreamingResponse(self._browsers.playwright) + class AsyncBrowsersResourceWithStreamingResponse: def __init__(self, browsers: AsyncBrowsersResource) -> None: @@ -850,3 +878,7 @@ def logs(self) -> AsyncLogsResourceWithStreamingResponse: @cached_property def computer(self) -> AsyncComputerResourceWithStreamingResponse: return AsyncComputerResourceWithStreamingResponse(self._browsers.computer) + + @cached_property + def playwright(self) -> AsyncPlaywrightResourceWithStreamingResponse: + return AsyncPlaywrightResourceWithStreamingResponse(self._browsers.playwright) diff --git a/src/kernel/resources/browsers/playwright.py b/src/kernel/resources/browsers/playwright.py new file mode 100644 index 00000000..c168a4a7 --- /dev/null +++ b/src/kernel/resources/browsers/playwright.py @@ -0,0 +1,205 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +import httpx + +from ..._types import Body, Omit, Query, Headers, NotGiven, omit, not_given +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, + async_to_streamed_response_wrapper, +) +from ..._base_client import make_request_options +from ...types.browsers import playwright_execute_params +from ...types.browsers.playwright_execute_response import PlaywrightExecuteResponse + +__all__ = ["PlaywrightResource", "AsyncPlaywrightResource"] + + +class PlaywrightResource(SyncAPIResource): + @cached_property + def with_raw_response(self) -> PlaywrightResourceWithRawResponse: + """ + 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 PlaywrightResourceWithRawResponse(self) + + @cached_property + def with_streaming_response(self) -> PlaywrightResourceWithStreamingResponse: + """ + 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 PlaywrightResourceWithStreamingResponse(self) + + def execute( + self, + id: str, + *, + code: str, + timeout_sec: int | 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, + ) -> PlaywrightExecuteResponse: + """ + Execute arbitrary Playwright code in a fresh execution context against the + browser. The code runs in the same VM as the browser, minimizing latency and + maximizing throughput. It has access to 'page', 'context', and 'browser' + variables. It can `return` a value, and this value is returned in the response. + + Args: + code: TypeScript/JavaScript code to execute. The code has access to 'page', 'context', + and 'browser' variables. It runs within a function, so you can use a return + statement at the end to return a value. This value is returned as the `result` + property in the response. Example: "await page.goto('https://example.com'); + return await page.title();" + + timeout_sec: Maximum execution time in seconds. Default is 60. + + 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}/playwright/execute", + body=maybe_transform( + { + "code": code, + "timeout_sec": timeout_sec, + }, + playwright_execute_params.PlaywrightExecuteParams, + ), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=PlaywrightExecuteResponse, + ) + + +class AsyncPlaywrightResource(AsyncAPIResource): + @cached_property + def with_raw_response(self) -> AsyncPlaywrightResourceWithRawResponse: + """ + 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 AsyncPlaywrightResourceWithRawResponse(self) + + @cached_property + def with_streaming_response(self) -> AsyncPlaywrightResourceWithStreamingResponse: + """ + 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 AsyncPlaywrightResourceWithStreamingResponse(self) + + async def execute( + self, + id: str, + *, + code: str, + timeout_sec: int | 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, + ) -> PlaywrightExecuteResponse: + """ + Execute arbitrary Playwright code in a fresh execution context against the + browser. The code runs in the same VM as the browser, minimizing latency and + maximizing throughput. It has access to 'page', 'context', and 'browser' + variables. It can `return` a value, and this value is returned in the response. + + Args: + code: TypeScript/JavaScript code to execute. The code has access to 'page', 'context', + and 'browser' variables. It runs within a function, so you can use a return + statement at the end to return a value. This value is returned as the `result` + property in the response. Example: "await page.goto('https://example.com'); + return await page.title();" + + timeout_sec: Maximum execution time in seconds. Default is 60. + + 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}/playwright/execute", + body=await async_maybe_transform( + { + "code": code, + "timeout_sec": timeout_sec, + }, + playwright_execute_params.PlaywrightExecuteParams, + ), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=PlaywrightExecuteResponse, + ) + + +class PlaywrightResourceWithRawResponse: + def __init__(self, playwright: PlaywrightResource) -> None: + self._playwright = playwright + + self.execute = to_raw_response_wrapper( + playwright.execute, + ) + + +class AsyncPlaywrightResourceWithRawResponse: + def __init__(self, playwright: AsyncPlaywrightResource) -> None: + self._playwright = playwright + + self.execute = async_to_raw_response_wrapper( + playwright.execute, + ) + + +class PlaywrightResourceWithStreamingResponse: + def __init__(self, playwright: PlaywrightResource) -> None: + self._playwright = playwright + + self.execute = to_streamed_response_wrapper( + playwright.execute, + ) + + +class AsyncPlaywrightResourceWithStreamingResponse: + def __init__(self, playwright: AsyncPlaywrightResource) -> None: + self._playwright = playwright + + self.execute = async_to_streamed_response_wrapper( + playwright.execute, + ) diff --git a/src/kernel/types/browsers/__init__.py b/src/kernel/types/browsers/__init__.py index 9b0ed53a..f8a263c1 100644 --- a/src/kernel/types/browsers/__init__.py +++ b/src/kernel/types/browsers/__init__.py @@ -31,9 +31,11 @@ from .f_create_directory_params import FCreateDirectoryParams as FCreateDirectoryParams from .f_delete_directory_params import FDeleteDirectoryParams as FDeleteDirectoryParams from .f_download_dir_zip_params import FDownloadDirZipParams as FDownloadDirZipParams +from .playwright_execute_params import PlaywrightExecuteParams as PlaywrightExecuteParams from .computer_drag_mouse_params import ComputerDragMouseParams as ComputerDragMouseParams from .computer_move_mouse_params import ComputerMoveMouseParams as ComputerMoveMouseParams from .computer_click_mouse_params import ComputerClickMouseParams as ComputerClickMouseParams +from .playwright_execute_response import PlaywrightExecuteResponse as PlaywrightExecuteResponse from .f_set_file_permissions_params import FSetFilePermissionsParams as FSetFilePermissionsParams from .process_stdout_stream_response import ProcessStdoutStreamResponse as ProcessStdoutStreamResponse from .computer_capture_screenshot_params import ComputerCaptureScreenshotParams as ComputerCaptureScreenshotParams diff --git a/src/kernel/types/browsers/playwright_execute_params.py b/src/kernel/types/browsers/playwright_execute_params.py new file mode 100644 index 00000000..948a74c1 --- /dev/null +++ b/src/kernel/types/browsers/playwright_execute_params.py @@ -0,0 +1,21 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from typing_extensions import Required, TypedDict + +__all__ = ["PlaywrightExecuteParams"] + + +class PlaywrightExecuteParams(TypedDict, total=False): + code: Required[str] + """TypeScript/JavaScript code to execute. + + The code has access to 'page', 'context', and 'browser' variables. It runs + within a function, so you can use a return statement at the end to return a + value. This value is returned as the `result` property in the response. Example: + "await page.goto('https://example.com'); return await page.title();" + """ + + timeout_sec: int + """Maximum execution time in seconds. Default is 60.""" diff --git a/src/kernel/types/browsers/playwright_execute_response.py b/src/kernel/types/browsers/playwright_execute_response.py new file mode 100644 index 00000000..a805ba85 --- /dev/null +++ b/src/kernel/types/browsers/playwright_execute_response.py @@ -0,0 +1,24 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from typing import Optional + +from ..._models import BaseModel + +__all__ = ["PlaywrightExecuteResponse"] + + +class PlaywrightExecuteResponse(BaseModel): + success: bool + """Whether the code executed successfully""" + + error: Optional[str] = None + """Error message if execution failed""" + + result: Optional[object] = None + """The value returned by the code (if any)""" + + stderr: Optional[str] = None + """Standard error from the execution""" + + stdout: Optional[str] = None + """Standard output from the execution""" diff --git a/tests/api_resources/browsers/test_playwright.py b/tests/api_resources/browsers/test_playwright.py new file mode 100644 index 00000000..cb79410c --- /dev/null +++ b/tests/api_resources/browsers/test_playwright.py @@ -0,0 +1,136 @@ +# 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 pytest + +from kernel import Kernel, AsyncKernel +from tests.utils import assert_matches_type +from kernel.types.browsers import PlaywrightExecuteResponse + +base_url = os.environ.get("TEST_API_BASE_URL", "http://127.0.0.1:4010") + + +class TestPlaywright: + parametrize = pytest.mark.parametrize("client", [False, True], indirect=True, ids=["loose", "strict"]) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + def test_method_execute(self, client: Kernel) -> None: + playwright = client.browsers.playwright.execute( + id="id", + code="code", + ) + assert_matches_type(PlaywrightExecuteResponse, playwright, path=["response"]) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + def test_method_execute_with_all_params(self, client: Kernel) -> None: + playwright = client.browsers.playwright.execute( + id="id", + code="code", + timeout_sec=1, + ) + assert_matches_type(PlaywrightExecuteResponse, playwright, path=["response"]) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + def test_raw_response_execute(self, client: Kernel) -> None: + response = client.browsers.playwright.with_raw_response.execute( + id="id", + code="code", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + playwright = response.parse() + assert_matches_type(PlaywrightExecuteResponse, playwright, path=["response"]) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + def test_streaming_response_execute(self, client: Kernel) -> None: + with client.browsers.playwright.with_streaming_response.execute( + id="id", + code="code", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + playwright = response.parse() + assert_matches_type(PlaywrightExecuteResponse, playwright, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + def test_path_params_execute(self, client: Kernel) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): + client.browsers.playwright.with_raw_response.execute( + id="", + code="code", + ) + + +class TestAsyncPlaywright: + 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_execute(self, async_client: AsyncKernel) -> None: + playwright = await async_client.browsers.playwright.execute( + id="id", + code="code", + ) + assert_matches_type(PlaywrightExecuteResponse, playwright, path=["response"]) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + async def test_method_execute_with_all_params(self, async_client: AsyncKernel) -> None: + playwright = await async_client.browsers.playwright.execute( + id="id", + code="code", + timeout_sec=1, + ) + assert_matches_type(PlaywrightExecuteResponse, playwright, path=["response"]) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + async def test_raw_response_execute(self, async_client: AsyncKernel) -> None: + response = await async_client.browsers.playwright.with_raw_response.execute( + id="id", + code="code", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + playwright = await response.parse() + assert_matches_type(PlaywrightExecuteResponse, playwright, path=["response"]) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + async def test_streaming_response_execute(self, async_client: AsyncKernel) -> None: + async with async_client.browsers.playwright.with_streaming_response.execute( + id="id", + code="code", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + playwright = await response.parse() + assert_matches_type(PlaywrightExecuteResponse, playwright, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + async def test_path_params_execute(self, async_client: AsyncKernel) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): + await async_client.browsers.playwright.with_raw_response.execute( + id="", + code="code", + )