diff --git a/.release-please-manifest.json b/.release-please-manifest.json index a9d0cc14..56ee145c 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,3 +1,3 @@ { - ".": "0.69.0" + ".": "0.70.0" } \ No newline at end of file diff --git a/.stats.yml b/.stats.yml index 4424fe73..2018924b 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ -configured_endpoints: 120 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel/kernel-cde481b2f320ce48f83db84ae96226b0e7568146c9387c4fefebf286ecb0dd0a.yml -openapi_spec_hash: 6bd86d767290fcd7e2a6aae26dff5417 -config_hash: 03c7e57f268c750e2415831662e95969 +configured_endpoints: 122 +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel/kernel-d9b82fc5346c9be1bf9c2b18792fdcdec6a80a86153ca765c9e93e597b46fa24.yml +openapi_spec_hash: 9cbaab975acfa421b795d11aa635c57e +config_hash: 99b2b2a25e8067ad9c9214e38e01d64c diff --git a/CHANGELOG.md b/CHANGELOG.md index a818acee..95989bf0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,18 @@ # Changelog +## 0.70.0 (2026-06-23) + +Full Changelog: [v0.69.0...v0.70.0](https://github.com/kernel/kernel-python-sdk/compare/v0.69.0...v0.70.0) + +### Features + +* Add GET /browsers/{id}/telemetry/events (read from S2) ([9a84bff](https://github.com/kernel/kernel-python-sdk/commit/9a84bff15de4d6ed1930f0d9dc01ace64548866c)) +* Align browser-pool timeout/viewport/fill-rate contract with implementation; reject save_changes on update ([5fdc97b](https://github.com/kernel/kernel-python-sdk/commit/5fdc97b27bdd92cb6f91f1162c1a5827e4a6a172)) +* api: support per-acquire start_url override on browser pool acquire ([31e7cef](https://github.com/kernel/kernel-python-sdk/commit/31e7cefe14fa6d8a98652f387563b395d7af7b09)) +* **api:** add GET /extensions/{id_or_name}/metadata ([a88cbff](https://github.com/kernel/kernel-python-sdk/commit/a88cbff17b47ecc14a8ba74170dcf467bff55816)) +* **api:** resolve GET /org/projects/{id} by ID or name ([b5ff4c6](https://github.com/kernel/kernel-python-sdk/commit/b5ff4c68b5915e77c63c9f9719870d1e7c38e8f2)) +* Forward replay param through telemetry stream passthrough ([e330f60](https://github.com/kernel/kernel-python-sdk/commit/e330f6024b254fe4275f222ee834026528675324)) + ## 0.69.0 (2026-06-18) Full Changelog: [v0.68.0...v0.69.0](https://github.com/kernel/kernel-python-sdk/compare/v0.68.0...v0.69.0) diff --git a/api.md b/api.md index 8ba0dd44..88588c40 100644 --- a/api.md +++ b/api.md @@ -146,13 +146,15 @@ from kernel.types.browsers import ( BrowserTelemetryCategoryConfig, BrowserTelemetryConfig, BrowserTelemetryEvent, + TelemetryEventsResponse, TelemetryStreamResponse, ) ``` Methods: -- client.browsers.telemetry.stream(id) -> TelemetryStreamResponse +- client.browsers.telemetry.events(id, \*\*params) -> SyncOffsetPagination[TelemetryEventsResponse] +- client.browsers.telemetry.stream(id, \*\*params) -> TelemetryStreamResponse ## Replays @@ -342,7 +344,7 @@ Methods: Types: ```python -from kernel.types import ExtensionListResponse, ExtensionUploadResponse +from kernel.types import ExtensionListResponse, ExtensionGetResponse, ExtensionUploadResponse ``` Methods: @@ -351,6 +353,7 @@ Methods: - client.extensions.delete(id_or_name) -> None - client.extensions.download(id_or_name) -> BinaryAPIResponse - client.extensions.download_from_chrome_store(\*\*params) -> BinaryAPIResponse +- client.extensions.get(id_or_name) -> ExtensionGetResponse - client.extensions.upload(\*\*params) -> ExtensionUploadResponse # BrowserPools diff --git a/pyproject.toml b/pyproject.toml index 2f216eb5..84fce8f7 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "kernel" -version = "0.69.0" +version = "0.70.0" description = "The official Python library for the kernel API" dynamic = ["readme"] license = "Apache-2.0" diff --git a/src/kernel/_version.py b/src/kernel/_version.py index 1d6a78bf..6d6da8f0 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.69.0" # x-release-please-version +__version__ = "0.70.0" # x-release-please-version diff --git a/src/kernel/resources/browser_pools.py b/src/kernel/resources/browser_pools.py index 7c3abda9..019489fa 100644 --- a/src/kernel/resources/browser_pools.py +++ b/src/kernel/resources/browser_pools.py @@ -96,7 +96,9 @@ def create( extensions: List of browser extensions to load into the session. Provide each by id or name. - fill_rate_per_minute: Percentage of the pool to fill per minute. Defaults to 10%. + fill_rate_per_minute: Percentage of the pool to fill per minute. Defaults to 10. The cap is 25 for + most organizations but can be raised per-organization, so only the lower bound + is enforced here. headless: If true, launches the browser using a headless image. Defaults to false. @@ -122,7 +124,7 @@ def create( mechanisms. timeout_seconds: Default idle timeout in seconds for browsers acquired from this pool before they - are destroyed. Defaults to 600 seconds if not specified + are destroyed. Defaults to 600 seconds. Minimum 10, maximum 259200 (72 hours). viewport: Initial browser window size in pixels with optional refresh rate. If omitted, image defaults apply (1920x1080@25). For GPU images, the default is @@ -243,7 +245,9 @@ def update( extensions: List of browser extensions to load into the session. Provide each by id or name. - fill_rate_per_minute: Percentage of the pool to fill per minute. Defaults to 10%. + fill_rate_per_minute: Percentage of the pool to fill per minute. Defaults to 10. The cap is 25 for + most organizations but can be raised per-organization, so only the lower bound + is enforced here. headless: If true, launches the browser using a headless image. Defaults to false. @@ -273,7 +277,7 @@ def update( mechanisms. timeout_seconds: Default idle timeout in seconds for browsers acquired from this pool before they - are destroyed. Defaults to 600 seconds if not specified + are destroyed. Defaults to 600 seconds. Minimum 10, maximum 259200 (72 hours). viewport: Initial browser window size in pixels with optional refresh rate. If omitted, image defaults apply (1920x1080@25). For GPU images, the default is @@ -423,6 +427,7 @@ def acquire( *, acquire_timeout_seconds: int | Omit = omit, name: str | Omit = omit, + start_url: str | Omit = omit, tags: TagsParam | 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. @@ -448,6 +453,10 @@ def acquire( project. Applies to this lease only and is cleared when the browser is released back to the pool. + start_url: Optional URL to navigate the acquired browser to. Overrides the pool's start_url + for this acquire only. Best-effort: failures to navigate do not fail the + acquire. + tags: Optional user-defined key-value tags for the acquired browser session, used to find and group sessions later. Applies to this lease only and are cleared when the browser is released back to the pool. Up to 50 pairs. @@ -468,6 +477,7 @@ def acquire( { "acquire_timeout_seconds": acquire_timeout_seconds, "name": name, + "start_url": start_url, "tags": tags, }, browser_pool_acquire_params.BrowserPoolAcquireParams, @@ -621,7 +631,9 @@ async def create( extensions: List of browser extensions to load into the session. Provide each by id or name. - fill_rate_per_minute: Percentage of the pool to fill per minute. Defaults to 10%. + fill_rate_per_minute: Percentage of the pool to fill per minute. Defaults to 10. The cap is 25 for + most organizations but can be raised per-organization, so only the lower bound + is enforced here. headless: If true, launches the browser using a headless image. Defaults to false. @@ -647,7 +659,7 @@ async def create( mechanisms. timeout_seconds: Default idle timeout in seconds for browsers acquired from this pool before they - are destroyed. Defaults to 600 seconds if not specified + are destroyed. Defaults to 600 seconds. Minimum 10, maximum 259200 (72 hours). viewport: Initial browser window size in pixels with optional refresh rate. If omitted, image defaults apply (1920x1080@25). For GPU images, the default is @@ -768,7 +780,9 @@ async def update( extensions: List of browser extensions to load into the session. Provide each by id or name. - fill_rate_per_minute: Percentage of the pool to fill per minute. Defaults to 10%. + fill_rate_per_minute: Percentage of the pool to fill per minute. Defaults to 10. The cap is 25 for + most organizations but can be raised per-organization, so only the lower bound + is enforced here. headless: If true, launches the browser using a headless image. Defaults to false. @@ -798,7 +812,7 @@ async def update( mechanisms. timeout_seconds: Default idle timeout in seconds for browsers acquired from this pool before they - are destroyed. Defaults to 600 seconds if not specified + are destroyed. Defaults to 600 seconds. Minimum 10, maximum 259200 (72 hours). viewport: Initial browser window size in pixels with optional refresh rate. If omitted, image defaults apply (1920x1080@25). For GPU images, the default is @@ -948,6 +962,7 @@ async def acquire( *, acquire_timeout_seconds: int | Omit = omit, name: str | Omit = omit, + start_url: str | Omit = omit, tags: TagsParam | 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. @@ -973,6 +988,10 @@ async def acquire( project. Applies to this lease only and is cleared when the browser is released back to the pool. + start_url: Optional URL to navigate the acquired browser to. Overrides the pool's start_url + for this acquire only. Best-effort: failures to navigate do not fail the + acquire. + tags: Optional user-defined key-value tags for the acquired browser session, used to find and group sessions later. Applies to this lease only and are cleared when the browser is released back to the pool. Up to 50 pairs. @@ -993,6 +1012,7 @@ async def acquire( { "acquire_timeout_seconds": acquire_timeout_seconds, "name": name, + "start_url": start_url, "tags": tags, }, browser_pool_acquire_params.BrowserPoolAcquireParams, diff --git a/src/kernel/resources/browsers/telemetry.py b/src/kernel/resources/browsers/telemetry.py index 008b1d29..aa3e9ed2 100644 --- a/src/kernel/resources/browsers/telemetry.py +++ b/src/kernel/resources/browsers/telemetry.py @@ -2,10 +2,13 @@ from __future__ import annotations +from typing import List +from typing_extensions import Literal + import httpx from ..._types import Body, Omit, Query, Headers, NotGiven, omit, not_given -from ..._utils import path_template, strip_not_given +from ..._utils import path_template, maybe_transform, strip_not_given, async_maybe_transform from ..._compat import cached_property from ..._resource import SyncAPIResource, AsyncAPIResource from ..._response import ( @@ -15,7 +18,10 @@ async_to_streamed_response_wrapper, ) from ..._streaming import Stream, AsyncStream -from ..._base_client import make_request_options +from ...pagination import SyncOffsetPagination, AsyncOffsetPagination +from ..._base_client import AsyncPaginator, make_request_options +from ...types.browsers import telemetry_events_params, telemetry_stream_params +from ...types.browsers.telemetry_events_response import TelemetryEventsResponse from ...types.browsers.telemetry_stream_response import TelemetryStreamResponse __all__ = ["TelemetryResource", "AsyncTelemetryResource"] @@ -43,10 +49,96 @@ def with_streaming_response(self) -> TelemetryResourceWithStreamingResponse: """ return TelemetryResourceWithStreamingResponse(self) + def events( + self, + id: str, + *, + category: List[ + Literal[ + "console", + "network", + "page", + "interaction", + "control", + "connection", + "system", + "screenshot", + "captcha", + "monitor", + ] + ] + | Omit = omit, + limit: int | Omit = omit, + offset: int | Omit = omit, + since: str | Omit = omit, + until: 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, + ) -> SyncOffsetPagination[TelemetryEventsResponse]: + """ + Reads a page of telemetry events for the browser session in ascending sequence + order. To page through results, pass the X-Next-Offset value from the previous + response as offset and repeat while X-Has-More is true. Returns an empty list + when telemetry data is unavailable. + + Args: + category: Restrict results to these event categories. Repeat the parameter for multiple + values. + + limit: Maximum number of events per page. Defaults to 20. + + offset: Opaque pagination cursor: pass the X-Next-Offset value from the previous + response to fetch the next page. When set, paging continues from this cursor and + since is ignored, while until still bounds the page. It is not an event's seq + field, so do not derive it from the response body. + + since: Start of the window: an RFC-3339 timestamp, or a duration like 5m meaning that + long ago. Defaults to 5m. Ignored when offset is set. + + until: End of the window (exclusive): an RFC-3339 timestamp, or a duration like 5m + meaning that long ago. + + 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_api_list( + path_template("/browsers/{id}/telemetry/events", id=id), + page=SyncOffsetPagination[TelemetryEventsResponse], + options=make_request_options( + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + query=maybe_transform( + { + "category": category, + "limit": limit, + "offset": offset, + "since": since, + "until": until, + }, + telemetry_events_params.TelemetryEventsParams, + ), + ), + model=TelemetryEventsResponse, + ) + def stream( self, id: str, *, + replay: str | Omit = omit, last_event_id: 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. @@ -64,9 +156,14 @@ def stream( set; all frames carry JSON in the data: field. A keepalive comment frame is sent every 15 seconds when no events arrive. Returns 404 if the browser session does not exist. If telemetry was not enabled on the session, the stream opens but no - events are delivered. + events are delivered. Fresh connections only see new events; pass replay=all to + start from the oldest retained event instead. Args: + replay: Pass `all` to start from the oldest retained event instead of only new events; + any other value is treated as from-now. The buffer is bounded, so the first + event id may be greater than 1 if older events were evicted. + extra_headers: Send extra headers extra_query: Add additional query parameters to the request @@ -82,7 +179,11 @@ def stream( return self._get( path_template("/browsers/{id}/telemetry/stream", id=id), options=make_request_options( - extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + query=maybe_transform({"replay": replay}, telemetry_stream_params.TelemetryStreamParams), ), cast_to=TelemetryStreamResponse, stream=True, @@ -112,10 +213,96 @@ def with_streaming_response(self) -> AsyncTelemetryResourceWithStreamingResponse """ return AsyncTelemetryResourceWithStreamingResponse(self) + def events( + self, + id: str, + *, + category: List[ + Literal[ + "console", + "network", + "page", + "interaction", + "control", + "connection", + "system", + "screenshot", + "captcha", + "monitor", + ] + ] + | Omit = omit, + limit: int | Omit = omit, + offset: int | Omit = omit, + since: str | Omit = omit, + until: 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, + ) -> AsyncPaginator[TelemetryEventsResponse, AsyncOffsetPagination[TelemetryEventsResponse]]: + """ + Reads a page of telemetry events for the browser session in ascending sequence + order. To page through results, pass the X-Next-Offset value from the previous + response as offset and repeat while X-Has-More is true. Returns an empty list + when telemetry data is unavailable. + + Args: + category: Restrict results to these event categories. Repeat the parameter for multiple + values. + + limit: Maximum number of events per page. Defaults to 20. + + offset: Opaque pagination cursor: pass the X-Next-Offset value from the previous + response to fetch the next page. When set, paging continues from this cursor and + since is ignored, while until still bounds the page. It is not an event's seq + field, so do not derive it from the response body. + + since: Start of the window: an RFC-3339 timestamp, or a duration like 5m meaning that + long ago. Defaults to 5m. Ignored when offset is set. + + until: End of the window (exclusive): an RFC-3339 timestamp, or a duration like 5m + meaning that long ago. + + 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_api_list( + path_template("/browsers/{id}/telemetry/events", id=id), + page=AsyncOffsetPagination[TelemetryEventsResponse], + options=make_request_options( + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + query=maybe_transform( + { + "category": category, + "limit": limit, + "offset": offset, + "since": since, + "until": until, + }, + telemetry_events_params.TelemetryEventsParams, + ), + ), + model=TelemetryEventsResponse, + ) + async def stream( self, id: str, *, + replay: str | Omit = omit, last_event_id: 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. @@ -133,9 +320,14 @@ async def stream( set; all frames carry JSON in the data: field. A keepalive comment frame is sent every 15 seconds when no events arrive. Returns 404 if the browser session does not exist. If telemetry was not enabled on the session, the stream opens but no - events are delivered. + events are delivered. Fresh connections only see new events; pass replay=all to + start from the oldest retained event instead. Args: + replay: Pass `all` to start from the oldest retained event instead of only new events; + any other value is treated as from-now. The buffer is bounded, so the first + event id may be greater than 1 if older events were evicted. + extra_headers: Send extra headers extra_query: Add additional query parameters to the request @@ -151,7 +343,11 @@ async def stream( return await self._get( path_template("/browsers/{id}/telemetry/stream", id=id), options=make_request_options( - extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + query=await async_maybe_transform({"replay": replay}, telemetry_stream_params.TelemetryStreamParams), ), cast_to=TelemetryStreamResponse, stream=True, @@ -163,6 +359,9 @@ class TelemetryResourceWithRawResponse: def __init__(self, telemetry: TelemetryResource) -> None: self._telemetry = telemetry + self.events = to_raw_response_wrapper( + telemetry.events, + ) self.stream = to_raw_response_wrapper( telemetry.stream, ) @@ -172,6 +371,9 @@ class AsyncTelemetryResourceWithRawResponse: def __init__(self, telemetry: AsyncTelemetryResource) -> None: self._telemetry = telemetry + self.events = async_to_raw_response_wrapper( + telemetry.events, + ) self.stream = async_to_raw_response_wrapper( telemetry.stream, ) @@ -181,6 +383,9 @@ class TelemetryResourceWithStreamingResponse: def __init__(self, telemetry: TelemetryResource) -> None: self._telemetry = telemetry + self.events = to_streamed_response_wrapper( + telemetry.events, + ) self.stream = to_streamed_response_wrapper( telemetry.stream, ) @@ -190,6 +395,9 @@ class AsyncTelemetryResourceWithStreamingResponse: def __init__(self, telemetry: AsyncTelemetryResource) -> None: self._telemetry = telemetry + self.events = async_to_streamed_response_wrapper( + telemetry.events, + ) self.stream = async_to_streamed_response_wrapper( telemetry.stream, ) diff --git a/src/kernel/resources/extensions.py b/src/kernel/resources/extensions.py index f358d341..5c023531 100644 --- a/src/kernel/resources/extensions.py +++ b/src/kernel/resources/extensions.py @@ -29,6 +29,7 @@ ) from ..pagination import SyncOffsetPagination, AsyncOffsetPagination from .._base_client import AsyncPaginator, make_request_options +from ..types.extension_get_response import ExtensionGetResponse from ..types.extension_list_response import ExtensionListResponse from ..types.extension_upload_response import ExtensionUploadResponse @@ -224,6 +225,40 @@ def download_from_chrome_store( cast_to=BinaryAPIResponse, ) + def get( + 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, + ) -> ExtensionGetResponse: + """ + Get an extension's metadata (name, size, timestamps) by ID or name, without + downloading the archive. + + 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}") + return self._get( + path_template("/extensions/{id_or_name}/metadata", id_or_name=id_or_name), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=ExtensionGetResponse, + ) + def upload( self, *, @@ -466,6 +501,40 @@ async def download_from_chrome_store( cast_to=AsyncBinaryAPIResponse, ) + async def get( + 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, + ) -> ExtensionGetResponse: + """ + Get an extension's metadata (name, size, timestamps) by ID or name, without + downloading the archive. + + 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}") + return await self._get( + path_template("/extensions/{id_or_name}/metadata", id_or_name=id_or_name), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=ExtensionGetResponse, + ) + async def upload( self, *, @@ -537,6 +606,9 @@ def __init__(self, extensions: ExtensionsResource) -> None: extensions.download_from_chrome_store, BinaryAPIResponse, ) + self.get = to_raw_response_wrapper( + extensions.get, + ) self.upload = to_raw_response_wrapper( extensions.upload, ) @@ -560,6 +632,9 @@ def __init__(self, extensions: AsyncExtensionsResource) -> None: extensions.download_from_chrome_store, AsyncBinaryAPIResponse, ) + self.get = async_to_raw_response_wrapper( + extensions.get, + ) self.upload = async_to_raw_response_wrapper( extensions.upload, ) @@ -583,6 +658,9 @@ def __init__(self, extensions: ExtensionsResource) -> None: extensions.download_from_chrome_store, StreamedBinaryAPIResponse, ) + self.get = to_streamed_response_wrapper( + extensions.get, + ) self.upload = to_streamed_response_wrapper( extensions.upload, ) @@ -606,6 +684,9 @@ def __init__(self, extensions: AsyncExtensionsResource) -> None: extensions.download_from_chrome_store, AsyncStreamedBinaryAPIResponse, ) + self.get = async_to_streamed_response_wrapper( + extensions.get, + ) self.upload = async_to_streamed_response_wrapper( extensions.upload, ) diff --git a/src/kernel/resources/projects/projects.py b/src/kernel/resources/projects/projects.py index ea977689..786a6c1f 100644 --- a/src/kernel/resources/projects/projects.py +++ b/src/kernel/resources/projects/projects.py @@ -104,8 +104,9 @@ def retrieve( extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = not_given, ) -> Project: - """ - Get a project by ID. + """Get a project by its ID or by its name. + + Names are unique within an organization. Args: extra_headers: Send extra headers @@ -331,8 +332,9 @@ async def retrieve( extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = not_given, ) -> Project: - """ - Get a project by ID. + """Get a project by its ID or by its name. + + Names are unique within an organization. Args: extra_headers: Send extra headers diff --git a/src/kernel/types/__init__.py b/src/kernel/types/__init__.py index e38ba407..660875d7 100644 --- a/src/kernel/types/__init__.py +++ b/src/kernel/types/__init__.py @@ -53,6 +53,7 @@ from .credential_list_params import CredentialListParams as CredentialListParams from .deployment_list_params import DeploymentListParams as DeploymentListParams from .deployment_state_event import DeploymentStateEvent as DeploymentStateEvent +from .extension_get_response import ExtensionGetResponse as ExtensionGetResponse from .invocation_list_params import InvocationListParams as InvocationListParams from .invocation_state_event import InvocationStateEvent as InvocationStateEvent from .api_key_retrieve_params import APIKeyRetrieveParams as APIKeyRetrieveParams @@ -110,9 +111,11 @@ browsers.browser_call_stack.BrowserCallStack.update_forward_refs() # type: ignore browsers.browser_console_error_event.BrowserConsoleErrorEvent.update_forward_refs() # type: ignore browsers.browser_console_log_event.BrowserConsoleLogEvent.update_forward_refs() # type: ignore + browsers.telemetry_events_response.TelemetryEventsResponse.update_forward_refs() # type: ignore browsers.telemetry_stream_response.TelemetryStreamResponse.update_forward_refs() # type: ignore else: browsers.browser_call_stack.BrowserCallStack.model_rebuild(_parent_namespace_depth=0) browsers.browser_console_error_event.BrowserConsoleErrorEvent.model_rebuild(_parent_namespace_depth=0) browsers.browser_console_log_event.BrowserConsoleLogEvent.model_rebuild(_parent_namespace_depth=0) + browsers.telemetry_events_response.TelemetryEventsResponse.model_rebuild(_parent_namespace_depth=0) browsers.telemetry_stream_response.TelemetryStreamResponse.model_rebuild(_parent_namespace_depth=0) diff --git a/src/kernel/types/browser_pool.py b/src/kernel/types/browser_pool.py index 8ad50acf..877fbcde 100644 --- a/src/kernel/types/browser_pool.py +++ b/src/kernel/types/browser_pool.py @@ -36,7 +36,11 @@ class BrowserPoolConfig(BaseModel): """ fill_rate_per_minute: Optional[int] = None - """Percentage of the pool to fill per minute. Defaults to 10%.""" + """Percentage of the pool to fill per minute. + + Defaults to 10. The cap is 25 for most organizations but can be raised + per-organization, so only the lower bound is enforced here. + """ headless: Optional[bool] = None """If true, launches the browser using a headless image. Defaults to false.""" @@ -81,7 +85,7 @@ class BrowserPoolConfig(BaseModel): timeout_seconds: Optional[int] = None """ Default idle timeout in seconds for browsers acquired from this pool before they - are destroyed. Defaults to 600 seconds if not specified + are destroyed. Defaults to 600 seconds. Minimum 10, maximum 259200 (72 hours). """ viewport: Optional[BrowserViewport] = None diff --git a/src/kernel/types/browser_pool_acquire_params.py b/src/kernel/types/browser_pool_acquire_params.py index 022d69bd..80b9be26 100644 --- a/src/kernel/types/browser_pool_acquire_params.py +++ b/src/kernel/types/browser_pool_acquire_params.py @@ -25,6 +25,13 @@ class BrowserPoolAcquireParams(TypedDict, total=False): back to the pool. """ + start_url: str + """Optional URL to navigate the acquired browser to. + + Overrides the pool's start_url for this acquire only. Best-effort: failures to + navigate do not fail the acquire. + """ + tags: TagsParam """ Optional user-defined key-value tags for the acquired browser session, used to diff --git a/src/kernel/types/browser_pool_create_params.py b/src/kernel/types/browser_pool_create_params.py index 33d04ce4..d404d4e8 100644 --- a/src/kernel/types/browser_pool_create_params.py +++ b/src/kernel/types/browser_pool_create_params.py @@ -35,7 +35,11 @@ class BrowserPoolCreateParams(TypedDict, total=False): """ fill_rate_per_minute: int - """Percentage of the pool to fill per minute. Defaults to 10%.""" + """Percentage of the pool to fill per minute. + + Defaults to 10. The cap is 25 for most organizations but can be raised + per-organization, so only the lower bound is enforced here. + """ headless: bool """If true, launches the browser using a headless image. Defaults to false.""" @@ -80,7 +84,7 @@ class BrowserPoolCreateParams(TypedDict, total=False): timeout_seconds: int """ Default idle timeout in seconds for browsers acquired from this pool before they - are destroyed. Defaults to 600 seconds if not specified + are destroyed. Defaults to 600 seconds. Minimum 10, maximum 259200 (72 hours). """ viewport: BrowserViewport diff --git a/src/kernel/types/browser_pool_update_params.py b/src/kernel/types/browser_pool_update_params.py index e80b5aa0..79e8172b 100644 --- a/src/kernel/types/browser_pool_update_params.py +++ b/src/kernel/types/browser_pool_update_params.py @@ -34,7 +34,11 @@ class BrowserPoolUpdateParams(TypedDict, total=False): """ fill_rate_per_minute: int - """Percentage of the pool to fill per minute. Defaults to 10%.""" + """Percentage of the pool to fill per minute. + + Defaults to 10. The cap is 25 for most organizations but can be raised + per-organization, so only the lower bound is enforced here. + """ headless: bool """If true, launches the browser using a headless image. Defaults to false.""" @@ -86,7 +90,7 @@ class BrowserPoolUpdateParams(TypedDict, total=False): timeout_seconds: int """ Default idle timeout in seconds for browsers acquired from this pool before they - are destroyed. Defaults to 600 seconds if not specified + are destroyed. Defaults to 600 seconds. Minimum 10, maximum 259200 (72 hours). """ viewport: BrowserViewport diff --git a/src/kernel/types/browsers/__init__.py b/src/kernel/types/browsers/__init__.py index 5f14ecd1..990e8e19 100644 --- a/src/kernel/types/browsers/__init__.py +++ b/src/kernel/types/browsers/__init__.py @@ -37,6 +37,8 @@ from .browser_telemetry_event import BrowserTelemetryEvent as BrowserTelemetryEvent from .process_resize_response import ProcessResizeResponse as ProcessResizeResponse from .process_status_response import ProcessStatusResponse as ProcessStatusResponse +from .telemetry_events_params import TelemetryEventsParams as TelemetryEventsParams +from .telemetry_stream_params import TelemetryStreamParams as TelemetryStreamParams from .browser_telemetry_config import BrowserTelemetryConfig as BrowserTelemetryConfig from .browser_cdp_connect_event import BrowserCdpConnectEvent as BrowserCdpConnectEvent from .browser_console_log_event import BrowserConsoleLogEvent as BrowserConsoleLogEvent @@ -46,6 +48,7 @@ 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 .telemetry_events_response import TelemetryEventsResponse as TelemetryEventsResponse from .telemetry_stream_response import TelemetryStreamResponse as TelemetryStreamResponse from .browser_network_idle_event import BrowserNetworkIdleEvent as BrowserNetworkIdleEvent from .computer_drag_mouse_params import ComputerDragMouseParams as ComputerDragMouseParams diff --git a/src/kernel/types/browsers/telemetry_events_params.py b/src/kernel/types/browsers/telemetry_events_params.py new file mode 100644 index 00000000..223bb2fc --- /dev/null +++ b/src/kernel/types/browsers/telemetry_events_params.py @@ -0,0 +1,52 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from typing import List +from typing_extensions import Literal, TypedDict + +__all__ = ["TelemetryEventsParams"] + + +class TelemetryEventsParams(TypedDict, total=False): + category: List[ + Literal[ + "console", + "network", + "page", + "interaction", + "control", + "connection", + "system", + "screenshot", + "captcha", + "monitor", + ] + ] + """Restrict results to these event categories. + + Repeat the parameter for multiple values. + """ + + limit: int + """Maximum number of events per page. Defaults to 20.""" + + offset: int + """ + Opaque pagination cursor: pass the X-Next-Offset value from the previous + response to fetch the next page. When set, paging continues from this cursor and + since is ignored, while until still bounds the page. It is not an event's seq + field, so do not derive it from the response body. + """ + + since: str + """ + Start of the window: an RFC-3339 timestamp, or a duration like 5m meaning that + long ago. Defaults to 5m. Ignored when offset is set. + """ + + until: str + """ + End of the window (exclusive): an RFC-3339 timestamp, or a duration like 5m + meaning that long ago. + """ diff --git a/src/kernel/types/browsers/telemetry_events_response.py b/src/kernel/types/browsers/telemetry_events_response.py new file mode 100644 index 00000000..3002f470 --- /dev/null +++ b/src/kernel/types/browsers/telemetry_events_response.py @@ -0,0 +1,36 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from ..._models import BaseModel + +__all__ = ["TelemetryEventsResponse"] + + +class TelemetryEventsResponse(BaseModel): + """Envelope wrapping a browser telemetry event with its monotonic sequence number. + + Each SSE data: frame carries one envelope as JSON. The seq value is also emitted as the SSE id: field so clients can pass it as Last-Event-ID on reconnect. + """ + + event: "BrowserTelemetryEvent" + """Union type representing any browser telemetry event. + + Discriminated on `type`. Each event's `category` determines when it is captured. + The CDP collector-health events (monitor_disconnected, monitor_reconnected, + monitor_reconnect_failed, monitor_init_failed) use the `monitor` category, which + is not user-configurable: it flows automatically whenever any CDP category + (console, network, page, interaction) is captured, and is silent otherwise. + monitor_screenshot uses the opt-in `screenshot` category. All other event types + are controlled by their per-category enable/disable flags. + """ + + seq: int + """Process-monotonic sequence number assigned by the browser VM. + + Pass as Last-Event-ID on reconnect to resume without gaps. Gaps in received seq + values indicate dropped events. + """ + + +from .browser_telemetry_event import BrowserTelemetryEvent diff --git a/src/kernel/types/browsers/telemetry_stream_params.py b/src/kernel/types/browsers/telemetry_stream_params.py new file mode 100644 index 00000000..d9703bc7 --- /dev/null +++ b/src/kernel/types/browsers/telemetry_stream_params.py @@ -0,0 +1,20 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from typing_extensions import Annotated, TypedDict + +from ..._utils import PropertyInfo + +__all__ = ["TelemetryStreamParams"] + + +class TelemetryStreamParams(TypedDict, total=False): + replay: str + """ + Pass `all` to start from the oldest retained event instead of only new events; + any other value is treated as from-now. The buffer is bounded, so the first + event id may be greater than 1 if older events were evicted. + """ + + last_event_id: Annotated[str, PropertyInfo(alias="Last-Event-ID")] diff --git a/src/kernel/types/extension_get_response.py b/src/kernel/types/extension_get_response.py new file mode 100644 index 00000000..0143e623 --- /dev/null +++ b/src/kernel/types/extension_get_response.py @@ -0,0 +1,30 @@ +# 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__ = ["ExtensionGetResponse"] + + +class ExtensionGetResponse(BaseModel): + """A browser extension uploaded to Kernel.""" + + 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 project. + """ diff --git a/src/kernel/types/shared/browser_viewport.py b/src/kernel/types/shared/browser_viewport.py index c53505be..7314603e 100644 --- a/src/kernel/types/shared/browser_viewport.py +++ b/src/kernel/types/shared/browser_viewport.py @@ -23,13 +23,14 @@ class BrowserViewport(BaseModel): """ height: int - """Browser window height in pixels.""" + """Browser window height in pixels. Any positive integer is accepted.""" width: int - """Browser window width in pixels.""" + """Browser window width in pixels. Any positive integer is accepted.""" refresh_rate: Optional[int] = None """Display refresh rate in Hz. - If omitted, automatically determined from width and height. + Any positive integer is accepted; if omitted, automatically determined from + width and height. """ diff --git a/src/kernel/types/shared_params/browser_viewport.py b/src/kernel/types/shared_params/browser_viewport.py index 2290f930..1250eada 100644 --- a/src/kernel/types/shared_params/browser_viewport.py +++ b/src/kernel/types/shared_params/browser_viewport.py @@ -23,13 +23,14 @@ class BrowserViewport(TypedDict, total=False): """ height: Required[int] - """Browser window height in pixels.""" + """Browser window height in pixels. Any positive integer is accepted.""" width: Required[int] - """Browser window width in pixels.""" + """Browser window width in pixels. Any positive integer is accepted.""" refresh_rate: int """Display refresh rate in Hz. - If omitted, automatically determined from width and height. + Any positive integer is accepted; if omitted, automatically determined from + width and height. """ diff --git a/tests/api_resources/browsers/test_telemetry.py b/tests/api_resources/browsers/test_telemetry.py index 0fcd2715..7ad9ca02 100644 --- a/tests/api_resources/browsers/test_telemetry.py +++ b/tests/api_resources/browsers/test_telemetry.py @@ -8,6 +8,9 @@ import pytest from kernel import Kernel, AsyncKernel +from tests.utils import assert_matches_type +from kernel.pagination import SyncOffsetPagination, AsyncOffsetPagination +from kernel.types.browsers import TelemetryEventsResponse base_url = os.environ.get("TEST_API_BASE_URL", "http://127.0.0.1:4010") @@ -15,6 +18,61 @@ class TestTelemetry: parametrize = pytest.mark.parametrize("client", [False, True], indirect=True, ids=["loose", "strict"]) + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_method_events(self, client: Kernel) -> None: + telemetry = client.browsers.telemetry.events( + id="id", + ) + assert_matches_type(SyncOffsetPagination[TelemetryEventsResponse], telemetry, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_method_events_with_all_params(self, client: Kernel) -> None: + telemetry = client.browsers.telemetry.events( + id="id", + category=["console"], + limit=1, + offset=0, + since="since", + until="until", + ) + assert_matches_type(SyncOffsetPagination[TelemetryEventsResponse], telemetry, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_raw_response_events(self, client: Kernel) -> None: + response = client.browsers.telemetry.with_raw_response.events( + id="id", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + telemetry = response.parse() + assert_matches_type(SyncOffsetPagination[TelemetryEventsResponse], telemetry, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_streaming_response_events(self, client: Kernel) -> None: + with client.browsers.telemetry.with_streaming_response.events( + id="id", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + telemetry = response.parse() + assert_matches_type(SyncOffsetPagination[TelemetryEventsResponse], telemetry, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_path_params_events(self, client: Kernel) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): + client.browsers.telemetry.with_raw_response.events( + id="", + ) + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_method_stream(self, client: Kernel) -> None: @@ -28,6 +86,7 @@ def test_method_stream(self, client: Kernel) -> None: def test_method_stream_with_all_params(self, client: Kernel) -> None: telemetry_stream = client.browsers.telemetry.stream( id="id", + replay="replay", last_event_id="Last-Event-ID", ) telemetry_stream.response.close() @@ -71,6 +130,61 @@ class TestAsyncTelemetry: "async_client", [False, True, {"http_client": "aiohttp"}], indirect=True, ids=["loose", "strict", "aiohttp"] ) + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_method_events(self, async_client: AsyncKernel) -> None: + telemetry = await async_client.browsers.telemetry.events( + id="id", + ) + assert_matches_type(AsyncOffsetPagination[TelemetryEventsResponse], telemetry, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_method_events_with_all_params(self, async_client: AsyncKernel) -> None: + telemetry = await async_client.browsers.telemetry.events( + id="id", + category=["console"], + limit=1, + offset=0, + since="since", + until="until", + ) + assert_matches_type(AsyncOffsetPagination[TelemetryEventsResponse], telemetry, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_raw_response_events(self, async_client: AsyncKernel) -> None: + response = await async_client.browsers.telemetry.with_raw_response.events( + id="id", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + telemetry = await response.parse() + assert_matches_type(AsyncOffsetPagination[TelemetryEventsResponse], telemetry, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_streaming_response_events(self, async_client: AsyncKernel) -> None: + async with async_client.browsers.telemetry.with_streaming_response.events( + id="id", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + telemetry = await response.parse() + assert_matches_type(AsyncOffsetPagination[TelemetryEventsResponse], telemetry, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_path_params_events(self, async_client: AsyncKernel) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): + await async_client.browsers.telemetry.with_raw_response.events( + id="", + ) + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_method_stream(self, async_client: AsyncKernel) -> None: @@ -84,6 +198,7 @@ async def test_method_stream(self, async_client: AsyncKernel) -> None: async def test_method_stream_with_all_params(self, async_client: AsyncKernel) -> None: telemetry_stream = await async_client.browsers.telemetry.stream( id="id", + replay="replay", last_event_id="Last-Event-ID", ) await telemetry_stream.response.aclose() diff --git a/tests/api_resources/test_browser_pools.py b/tests/api_resources/test_browser_pools.py index c6910837..ba603e7c 100644 --- a/tests/api_resources/test_browser_pools.py +++ b/tests/api_resources/test_browser_pools.py @@ -53,7 +53,7 @@ def test_method_create_with_all_params(self, client: Kernel) -> None: proxy_id="proxy_id", start_url="https://example.com", stealth=True, - timeout_seconds=60, + timeout_seconds=10, viewport={ "height": 800, "width": 1280, @@ -164,7 +164,7 @@ def test_method_update_with_all_params(self, client: Kernel) -> None: size=10, start_url="https://example.com", stealth=True, - timeout_seconds=60, + timeout_seconds=10, viewport={ "height": 800, "width": 1280, @@ -311,6 +311,7 @@ def test_method_acquire_with_all_params(self, client: Kernel) -> None: id_or_name="id_or_name", acquire_timeout_seconds=0, name="checkout-flow-1", + start_url="https://example.com", tags={ "team": "backend", "env": "staging", @@ -488,7 +489,7 @@ async def test_method_create_with_all_params(self, async_client: AsyncKernel) -> proxy_id="proxy_id", start_url="https://example.com", stealth=True, - timeout_seconds=60, + timeout_seconds=10, viewport={ "height": 800, "width": 1280, @@ -599,7 +600,7 @@ async def test_method_update_with_all_params(self, async_client: AsyncKernel) -> size=10, start_url="https://example.com", stealth=True, - timeout_seconds=60, + timeout_seconds=10, viewport={ "height": 800, "width": 1280, @@ -746,6 +747,7 @@ async def test_method_acquire_with_all_params(self, async_client: AsyncKernel) - id_or_name="id_or_name", acquire_timeout_seconds=0, name="checkout-flow-1", + start_url="https://example.com", tags={ "team": "backend", "env": "staging", diff --git a/tests/api_resources/test_extensions.py b/tests/api_resources/test_extensions.py index fe8a4cb6..cda54a2d 100644 --- a/tests/api_resources/test_extensions.py +++ b/tests/api_resources/test_extensions.py @@ -12,6 +12,7 @@ from kernel import Kernel, AsyncKernel from tests.utils import assert_matches_type from kernel.types import ( + ExtensionGetResponse, ExtensionListResponse, ExtensionUploadResponse, ) @@ -214,6 +215,48 @@ def test_streaming_response_download_from_chrome_store(self, client: Kernel, res assert cast(Any, extension.is_closed) is True + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_method_get(self, client: Kernel) -> None: + extension = client.extensions.get( + "id_or_name", + ) + assert_matches_type(ExtensionGetResponse, extension, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_raw_response_get(self, client: Kernel) -> None: + response = client.extensions.with_raw_response.get( + "id_or_name", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + extension = response.parse() + assert_matches_type(ExtensionGetResponse, extension, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_streaming_response_get(self, client: Kernel) -> None: + with client.extensions.with_streaming_response.get( + "id_or_name", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + extension = response.parse() + assert_matches_type(ExtensionGetResponse, extension, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_path_params_get(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.get( + "", + ) + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_method_upload(self, client: Kernel) -> None: @@ -454,6 +497,48 @@ async def test_streaming_response_download_from_chrome_store( assert cast(Any, extension.is_closed) is True + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_method_get(self, async_client: AsyncKernel) -> None: + extension = await async_client.extensions.get( + "id_or_name", + ) + assert_matches_type(ExtensionGetResponse, extension, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_raw_response_get(self, async_client: AsyncKernel) -> None: + response = await async_client.extensions.with_raw_response.get( + "id_or_name", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + extension = await response.parse() + assert_matches_type(ExtensionGetResponse, extension, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_streaming_response_get(self, async_client: AsyncKernel) -> None: + async with async_client.extensions.with_streaming_response.get( + "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_matches_type(ExtensionGetResponse, extension, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_path_params_get(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.get( + "", + ) + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_method_upload(self, async_client: AsyncKernel) -> None: