Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .release-please-manifest.json
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
{
".": "0.59.0"
".": "0.60.0"
}
4 changes: 2 additions & 2 deletions .stats.yml
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
configured_endpoints: 117
openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel/kernel-1acd8f0b76ab00e36b53cc3ca90b72b2199f3388b3e307890adb464b87f9a2d8.yml
openapi_spec_hash: 82003125c1c2c5d82d19270bafb4a6ca
openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel/kernel-3a1db7f11a92b28681929255ada59d2317ee4db98b5ff5aa6ce142a0664058b3.yml
openapi_spec_hash: b3064eaa589ae2a84993686ad1a3ee43
config_hash: ede72e4ae65cc5a6d6927938b3455c46
16 changes: 16 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,21 @@
# Changelog

## 0.60.0 (2026-06-03)

Full Changelog: [v0.59.0...v0.60.0](https://github.com/kernel/kernel-python-sdk/compare/v0.59.0...v0.60.0)

### Features

* Add API-backed API key management endpoints ([55bd31e](https://github.com/kernel/kernel-python-sdk/commit/55bd31edfed74f3bc4b08f83e07448e2e53378b0))
* **examples:** add browser-telemetry example ([4c29993](https://github.com/kernel/kernel-python-sdk/commit/4c29993cdda50fcde19ee7bf696a268cd8b0eb0b))
* Fix browser pool update schema ([903fe13](https://github.com/kernel/kernel-python-sdk/commit/903fe13b84242acea9c27bc451c16fb5cd58e40c))
* route browser telemetry directly to the VM by default ([cb50725](https://github.com/kernel/kernel-python-sdk/commit/cb5072516416627ffde7198900484c46dda5b9dc))


### Bug Fixes

* **streaming:** don't dispatch empty SSE keepalive comment frames ([a0ee2b2](https://github.com/kernel/kernel-python-sdk/commit/a0ee2b2653c581839be11bb29be5b68166e80658))

## 0.59.0 (2026-06-03)

Full Changelog: [v0.58.0...v0.59.0](https://github.com/kernel/kernel-python-sdk/compare/v0.58.0...v0.59.0)
Expand Down
32 changes: 32 additions & 0 deletions examples/browser_telemetry.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
"""Example: stream live browser telemetry events from a session."""

from kernel import Kernel


def main() -> None:
client = Kernel()

# Enable telemetry capture when creating the browser.
browser = client.browsers.create(telemetry={"enabled": True})

try:
# Telemetry is a default direct-to-VM routing subresource, so the stream
# connects straight to the browser VM automatically.
stream = client.browsers.telemetry.stream(browser.session_id)

# Make a few browser activity calls to generate events. The "api" telemetry
# category emits an event per VM API call, so events arrive within ~1s.
for _ in range(3):
client.browsers.curl(browser.session_id, url="https://example.com", method="GET")

# Print a few events, then stop so we don't wait on the 15s keepalive.
for count, message in enumerate(stream, start=1):
print(message.seq, message.event.type)
if count >= 3:
break
finally:
client.browsers.delete_by_id(browser.session_id)


if __name__ == "__main__":
main()
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[project]
name = "kernel"
version = "0.59.0"
version = "0.60.0"
description = "The official Python library for the kernel API"
dynamic = ["readme"]
license = "Apache-2.0"
Expand Down
7 changes: 6 additions & 1 deletion src/kernel/_streaming.py
Original file line number Diff line number Diff line change
Expand Up @@ -251,7 +251,12 @@ def decode(self, line: str) -> ServerSentEvent | None:
# See: https://html.spec.whatwg.org/multipage/server-sent-events.html#event-stream-interpretation # noqa: E501

if not line:
if not self._event and not self._data and not self._last_event_id and self._retry is None:
# Whether to dispatch depends only on what was set in the *current* block. last_event_id
# is sticky across events (per the SSE spec, it is intentionally not reset below), so it
# must not be part of this check -- otherwise, once any event carries an id, every
# subsequent comment-only block (e.g. a ``:\n\n`` keepalive) would dispatch an empty
# event, which then fails to JSON-decode in the typed Stream wrapper.
if not self._event and not self._data and self._retry is None:
return None

sse = ServerSentEvent(
Expand Down
2 changes: 1 addition & 1 deletion src/kernel/_version.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.

__title__ = "kernel"
__version__ = "0.59.0" # x-release-please-version
__version__ = "0.60.0" # x-release-please-version
2 changes: 1 addition & 1 deletion src/kernel/lib/browser_routing/routing.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ class BrowserRoutingConfig:
def browser_routing_config_from_env() -> BrowserRoutingConfig:
raw = os.environ.get("KERNEL_BROWSER_ROUTING_SUBRESOURCES")
if raw is None:
return BrowserRoutingConfig(subresources=("curl",))
return BrowserRoutingConfig(subresources=("curl", "telemetry"))
if raw.strip() == "":
return BrowserRoutingConfig()

Expand Down
27 changes: 27 additions & 0 deletions src/kernel/resources/api_keys.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
from __future__ import annotations

from typing import Optional
from typing_extensions import Literal

import httpx

Expand Down Expand Up @@ -171,6 +172,9 @@ def list(
*,
limit: int | Omit = omit,
offset: int | Omit = omit,
query: str | Omit = omit,
sort_by: Literal["created_at", "name", "expires_at"] | Omit = omit,
sort_direction: Literal["asc", "desc"] | 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,
Expand All @@ -187,6 +191,13 @@ def list(

offset: Number of results to skip

query: Case-insensitive substring match against API key name, creator, and project. API
key identifiers and masked keys match by exact value or prefix.

sort_by: Field to sort API keys by.

sort_direction: Sort direction for API keys.

extra_headers: Send extra headers

extra_query: Add additional query parameters to the request
Expand All @@ -207,6 +218,9 @@ def list(
{
"limit": limit,
"offset": offset,
"query": query,
"sort_by": sort_by,
"sort_direction": sort_direction,
},
api_key_list_params.APIKeyListParams,
),
Expand Down Expand Up @@ -395,6 +409,9 @@ def list(
*,
limit: int | Omit = omit,
offset: int | Omit = omit,
query: str | Omit = omit,
sort_by: Literal["created_at", "name", "expires_at"] | Omit = omit,
sort_direction: Literal["asc", "desc"] | 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,
Expand All @@ -411,6 +428,13 @@ def list(

offset: Number of results to skip

query: Case-insensitive substring match against API key name, creator, and project. API
key identifiers and masked keys match by exact value or prefix.

sort_by: Field to sort API keys by.

sort_direction: Sort direction for API keys.

extra_headers: Send extra headers

extra_query: Add additional query parameters to the request
Expand All @@ -431,6 +455,9 @@ def list(
{
"limit": limit,
"offset": offset,
"query": query,
"sort_by": sort_by,
"sort_direction": sort_direction,
},
api_key_list_params.APIKeyListParams,
),
Expand Down
24 changes: 12 additions & 12 deletions src/kernel/resources/browser_pools.py
Original file line number Diff line number Diff line change
Expand Up @@ -206,7 +206,6 @@ def update(
self,
id_or_name: str,
*,
size: int,
chrome_policy: Dict[str, object] | Omit = omit,
discard_all_idle: bool | Omit = omit,
extensions: Iterable[BrowserExtension] | Omit = omit,
Expand All @@ -216,6 +215,7 @@ def update(
name: str | Omit = omit,
profile: BrowserProfile | Omit = omit,
proxy_id: str | Omit = omit,
size: int | Omit = omit,
start_url: str | Omit = omit,
stealth: bool | Omit = omit,
timeout_seconds: int | Omit = omit,
Expand All @@ -231,10 +231,6 @@ def update(
Updates the configuration used to create browsers in the pool.

Args:
size: Number of browsers to maintain in the pool. The maximum size is determined by
your organization's pooled sessions limit (the sum of all pool sizes cannot
exceed your limit).

chrome_policy: Custom Chrome enterprise policy overrides applied to all browsers in this pool.
Keys are Chrome enterprise policy names; values must match their expected types.
Blocked: kernel-managed policies (extensions, proxy, CDP/automation). See
Expand All @@ -261,6 +257,10 @@ def update(
proxy_id: Optional proxy to associate to the browser session. Must reference a proxy
belonging to the caller's org.

size: Number of browsers to maintain in the pool. The maximum size is determined by
your organization's pooled sessions limit (the sum of all pool sizes cannot
exceed your limit).

start_url: Optional URL to navigate to when a new browser is warmed into the pool.
Best-effort: failures to navigate do not fail pool fill. Only applied to
newly-warmed browsers; browsers reused via release/acquire keep whatever URL the
Expand Down Expand Up @@ -300,7 +300,6 @@ def update(
path_template("/browser_pools/{id_or_name}", id_or_name=id_or_name),
body=maybe_transform(
{
"size": size,
"chrome_policy": chrome_policy,
"discard_all_idle": discard_all_idle,
"extensions": extensions,
Expand All @@ -310,6 +309,7 @@ def update(
"name": name,
"profile": profile,
"proxy_id": proxy_id,
"size": size,
"start_url": start_url,
"stealth": stealth,
"timeout_seconds": timeout_seconds,
Expand Down Expand Up @@ -684,7 +684,6 @@ async def update(
self,
id_or_name: str,
*,
size: int,
chrome_policy: Dict[str, object] | Omit = omit,
discard_all_idle: bool | Omit = omit,
extensions: Iterable[BrowserExtension] | Omit = omit,
Expand All @@ -694,6 +693,7 @@ async def update(
name: str | Omit = omit,
profile: BrowserProfile | Omit = omit,
proxy_id: str | Omit = omit,
size: int | Omit = omit,
start_url: str | Omit = omit,
stealth: bool | Omit = omit,
timeout_seconds: int | Omit = omit,
Expand All @@ -709,10 +709,6 @@ async def update(
Updates the configuration used to create browsers in the pool.

Args:
size: Number of browsers to maintain in the pool. The maximum size is determined by
your organization's pooled sessions limit (the sum of all pool sizes cannot
exceed your limit).

chrome_policy: Custom Chrome enterprise policy overrides applied to all browsers in this pool.
Keys are Chrome enterprise policy names; values must match their expected types.
Blocked: kernel-managed policies (extensions, proxy, CDP/automation). See
Expand All @@ -739,6 +735,10 @@ async def update(
proxy_id: Optional proxy to associate to the browser session. Must reference a proxy
belonging to the caller's org.

size: Number of browsers to maintain in the pool. The maximum size is determined by
your organization's pooled sessions limit (the sum of all pool sizes cannot
exceed your limit).

start_url: Optional URL to navigate to when a new browser is warmed into the pool.
Best-effort: failures to navigate do not fail pool fill. Only applied to
newly-warmed browsers; browsers reused via release/acquire keep whatever URL the
Expand Down Expand Up @@ -778,7 +778,6 @@ async def update(
path_template("/browser_pools/{id_or_name}", id_or_name=id_or_name),
body=await async_maybe_transform(
{
"size": size,
"chrome_policy": chrome_policy,
"discard_all_idle": discard_all_idle,
"extensions": extensions,
Expand All @@ -788,6 +787,7 @@ async def update(
"name": name,
"profile": profile,
"proxy_id": proxy_id,
"size": size,
"start_url": start_url,
"stealth": stealth,
"timeout_seconds": timeout_seconds,
Expand Down
14 changes: 13 additions & 1 deletion src/kernel/types/api_key_list_params.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

from __future__ import annotations

from typing_extensions import TypedDict
from typing_extensions import Literal, TypedDict

__all__ = ["APIKeyListParams"]

Expand All @@ -13,3 +13,15 @@ class APIKeyListParams(TypedDict, total=False):

offset: int
"""Number of results to skip"""

query: str
"""Case-insensitive substring match against API key name, creator, and project.

API key identifiers and masked keys match by exact value or prefix.
"""

sort_by: Literal["created_at", "name", "expires_at"]
"""Field to sort API keys by."""

sort_direction: Literal["asc", "desc"]
"""Sort direction for API keys."""
16 changes: 8 additions & 8 deletions src/kernel/types/browser_pool_update_params.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
from __future__ import annotations

from typing import Dict, Iterable
from typing_extensions import Required, TypedDict
from typing_extensions import TypedDict

from .shared_params.browser_profile import BrowserProfile
from .shared_params.browser_viewport import BrowserViewport
Expand All @@ -13,13 +13,6 @@


class BrowserPoolUpdateParams(TypedDict, total=False):
size: Required[int]
"""Number of browsers to maintain in the pool.

The maximum size is determined by your organization's pooled sessions limit (the
sum of all pool sizes cannot exceed your limit).
"""

chrome_policy: Dict[str, object]
"""Custom Chrome enterprise policy overrides applied to all browsers in this pool.

Expand Down Expand Up @@ -68,6 +61,13 @@ class BrowserPoolUpdateParams(TypedDict, total=False):
Must reference a proxy belonging to the caller's org.
"""

size: int
"""Number of browsers to maintain in the pool.

The maximum size is determined by your organization's pooled sessions limit (the
sum of all pool sizes cannot exceed your limit).
"""

start_url: str
"""Optional URL to navigate to when a new browser is warmed into the pool.

Expand Down
6 changes: 6 additions & 0 deletions tests/api_resources/test_api_keys.py
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,9 @@ def test_method_list_with_all_params(self, client: Kernel) -> None:
api_key = client.api_keys.list(
limit=100,
offset=0,
query="query",
sort_by="created_at",
sort_direction="asc",
)
assert_matches_type(SyncOffsetPagination[APIKey], api_key, path=["response"])

Expand Down Expand Up @@ -379,6 +382,9 @@ async def test_method_list_with_all_params(self, async_client: AsyncKernel) -> N
api_key = await async_client.api_keys.list(
limit=100,
offset=0,
query="query",
sort_by="created_at",
sort_direction="asc",
)
assert_matches_type(AsyncOffsetPagination[APIKey], api_key, path=["response"])

Expand Down
Loading
Loading