diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 5f78a6a..c7bc99a 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -1,12 +1,14 @@
name: CI
on:
push:
- branches-ignore:
- - 'generated'
- - 'codegen/**'
- - 'integrated/**'
- - 'stl-preview-head/**'
- - 'stl-preview-base/**'
+ branches:
+ - '**'
+ - '!integrated/**'
+ - '!stl-preview-head/**'
+ - '!stl-preview-base/**'
+ - '!generated'
+ - '!codegen/**'
+ - 'codegen/stl/**'
pull_request:
branches-ignore:
- 'stl-preview-head/**'
diff --git a/.release-please-manifest.json b/.release-please-manifest.json
index fe87cd9..cc51f6f 100644
--- a/.release-please-manifest.json
+++ b/.release-please-manifest.json
@@ -1,3 +1,3 @@
{
- ".": "0.43.0"
+ ".": "0.44.0"
}
\ No newline at end of file
diff --git a/.stats.yml b/.stats.yml
index ae22a71..be60802 100644
--- a/.stats.yml
+++ b/.stats.yml
@@ -1,4 +1,4 @@
-configured_endpoints: 103
-openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel%2Fkernel-bda5e58fa0bbd08761f27a1e0edbc602c44141ac9483bf6c96d52b7f4d10d9a7.yml
-openapi_spec_hash: 10833b36358e8cda023e5bb0abeab0ba
-config_hash: cff4d43372b6fa66b64e2d4150f6aa76
+configured_endpoints: 104
+openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel%2Fkernel-bb2ac8e0d3a1c08e8afcbcbad7cb733d0f84bd22a8d233c1ec3100a01ee078ae.yml
+openapi_spec_hash: a83f7d1c422c85d6dc6158af7afe1d09
+config_hash: 16e4457a0bb26e98a335a1c2a572290a
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 3cf8970..a11503f 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,30 @@
# Changelog
+## 0.44.0 (2026-03-20)
+
+Full Changelog: [v0.43.0...v0.44.0](https://github.com/kernel/kernel-python-sdk/compare/v0.43.0...v0.44.0)
+
+### Features
+
+* Add GPU viewport presets and GPU encoder defaults ([0735b45](https://github.com/kernel/kernel-python-sdk/commit/0735b45fc92950cef3afea92a879712ea0ebdf0f))
+* Adds description to OAS spec for docs about delta_x, delta_y ([9841aac](https://github.com/kernel/kernel-python-sdk/commit/9841aac21588beb0c6a22baf5c5b0bf8e8cdd024))
+* Drop headless GPU support and disable pooling ([cda8f94](https://github.com/kernel/kernel-python-sdk/commit/cda8f94f6cebabc3b3b6f95aff765816255a9270))
+* Enhance managed authentication with CUA support and new features ([ece76c2](https://github.com/kernel/kernel-python-sdk/commit/ece76c2f7d2a20af7c00988d161c4e275623aaac))
+* expose smooth drag mouse movement via public API ([c6f6862](https://github.com/kernel/kernel-python-sdk/commit/c6f6862d03620bb218a99c85431e384c5c8e5e4c))
+* Rename hardware acceleration UI/docs wording to GPU acceleration ([9ee4d0c](https://github.com/kernel/kernel-python-sdk/commit/9ee4d0c080da7f649a3bde63640593f33d5d0f6b))
+
+
+### Bug Fixes
+
+* **deps:** bump minimum typing-extensions version ([fd55947](https://github.com/kernel/kernel-python-sdk/commit/fd55947f1776b43cca804622d0c771ebe99ead60))
+* **pydantic:** do not pass `by_alias` unless set ([a815a82](https://github.com/kernel/kernel-python-sdk/commit/a815a8237cce2098b2f5d6a8ad5def400d418fbb))
+* sanitize endpoint path params ([9b55d2b](https://github.com/kernel/kernel-python-sdk/commit/9b55d2be9472f779fe20d7d863dacae1030d2b49))
+
+
+### Chores
+
+* **internal:** tweak CI branches ([8781c7d](https://github.com/kernel/kernel-python-sdk/commit/8781c7d9aa8759e487e5d86cbb82bfb7eb3d314e))
+
## 0.43.0 (2026-03-10)
Full Changelog: [v0.42.1...v0.43.0](https://github.com/kernel/kernel-python-sdk/compare/v0.42.1...v0.43.0)
diff --git a/api.md b/api.md
index cce7302..696c481 100644
--- a/api.md
+++ b/api.md
@@ -245,6 +245,7 @@ from kernel.types.auth import (
LoginResponse,
ManagedAuth,
ManagedAuthCreateRequest,
+ ManagedAuthUpdateRequest,
SubmitFieldsRequest,
SubmitFieldsResponse,
ConnectionFollowResponse,
@@ -255,6 +256,7 @@ Methods:
- client.auth.connections.create(\*\*params) -> ManagedAuth
- client.auth.connections.retrieve(id) -> ManagedAuth
+- client.auth.connections.update(id, \*\*params) -> ManagedAuth
- client.auth.connections.list(\*\*params) -> SyncOffsetPagination[ManagedAuth]
- client.auth.connections.delete(id) -> None
- client.auth.connections.follow(id) -> ConnectionFollowResponse
diff --git a/pyproject.toml b/pyproject.toml
index 021d31e..62ca833 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -1,6 +1,6 @@
[project]
name = "kernel"
-version = "0.43.0"
+version = "0.44.0"
description = "The official Python library for the kernel API"
dynamic = ["readme"]
license = "Apache-2.0"
@@ -11,7 +11,7 @@ authors = [
dependencies = [
"httpx>=0.23.0, <1",
"pydantic>=1.9.0, <3",
- "typing-extensions>=4.10, <5",
+ "typing-extensions>=4.14, <5",
"anyio>=3.5.0, <5",
"distro>=1.7.0, <2",
"sniffio",
diff --git a/src/kernel/_compat.py b/src/kernel/_compat.py
index 786ff42..e6690a4 100644
--- a/src/kernel/_compat.py
+++ b/src/kernel/_compat.py
@@ -2,7 +2,7 @@
from typing import TYPE_CHECKING, Any, Union, Generic, TypeVar, Callable, cast, overload
from datetime import date, datetime
-from typing_extensions import Self, Literal
+from typing_extensions import Self, Literal, TypedDict
import pydantic
from pydantic.fields import FieldInfo
@@ -131,6 +131,10 @@ def model_json(model: pydantic.BaseModel, *, indent: int | None = None) -> str:
return model.model_dump_json(indent=indent)
+class _ModelDumpKwargs(TypedDict, total=False):
+ by_alias: bool
+
+
def model_dump(
model: pydantic.BaseModel,
*,
@@ -142,6 +146,9 @@ def model_dump(
by_alias: bool | None = None,
) -> dict[str, Any]:
if (not PYDANTIC_V1) or hasattr(model, "model_dump"):
+ kwargs: _ModelDumpKwargs = {}
+ if by_alias is not None:
+ kwargs["by_alias"] = by_alias
return model.model_dump(
mode=mode,
exclude=exclude,
@@ -149,7 +156,7 @@ def model_dump(
exclude_defaults=exclude_defaults,
# warnings are not supported in Pydantic v1
warnings=True if PYDANTIC_V1 else warnings,
- by_alias=by_alias,
+ **kwargs,
)
return cast(
"dict[str, Any]",
diff --git a/src/kernel/_utils/__init__.py b/src/kernel/_utils/__init__.py
index dc64e29..10cb66d 100644
--- a/src/kernel/_utils/__init__.py
+++ b/src/kernel/_utils/__init__.py
@@ -1,3 +1,4 @@
+from ._path import path_template as path_template
from ._sync import asyncify as asyncify
from ._proxy import LazyProxy as LazyProxy
from ._utils import (
diff --git a/src/kernel/_utils/_path.py b/src/kernel/_utils/_path.py
new file mode 100644
index 0000000..4d6e1e4
--- /dev/null
+++ b/src/kernel/_utils/_path.py
@@ -0,0 +1,127 @@
+from __future__ import annotations
+
+import re
+from typing import (
+ Any,
+ Mapping,
+ Callable,
+)
+from urllib.parse import quote
+
+# Matches '.' or '..' where each dot is either literal or percent-encoded (%2e / %2E).
+_DOT_SEGMENT_RE = re.compile(r"^(?:\.|%2[eE]){1,2}$")
+
+_PLACEHOLDER_RE = re.compile(r"\{(\w+)\}")
+
+
+def _quote_path_segment_part(value: str) -> str:
+ """Percent-encode `value` for use in a URI path segment.
+
+ Considers characters not in `pchar` set from RFC 3986 §3.3 to be unsafe.
+ https://datatracker.ietf.org/doc/html/rfc3986#section-3.3
+ """
+ # quote() already treats unreserved characters (letters, digits, and -._~)
+ # as safe, so we only need to add sub-delims, ':', and '@'.
+ # Notably, unlike the default `safe` for quote(), / is unsafe and must be quoted.
+ return quote(value, safe="!$&'()*+,;=:@")
+
+
+def _quote_query_part(value: str) -> str:
+ """Percent-encode `value` for use in a URI query string.
+
+ Considers &, = and characters not in `query` set from RFC 3986 §3.4 to be unsafe.
+ https://datatracker.ietf.org/doc/html/rfc3986#section-3.4
+ """
+ return quote(value, safe="!$'()*+,;:@/?")
+
+
+def _quote_fragment_part(value: str) -> str:
+ """Percent-encode `value` for use in a URI fragment.
+
+ Considers characters not in `fragment` set from RFC 3986 §3.5 to be unsafe.
+ https://datatracker.ietf.org/doc/html/rfc3986#section-3.5
+ """
+ return quote(value, safe="!$&'()*+,;=:@/?")
+
+
+def _interpolate(
+ template: str,
+ values: Mapping[str, Any],
+ quoter: Callable[[str], str],
+) -> str:
+ """Replace {name} placeholders in `template`, quoting each value with `quoter`.
+
+ Placeholder names are looked up in `values`.
+
+ Raises:
+ KeyError: If a placeholder is not found in `values`.
+ """
+ # re.split with a capturing group returns alternating
+ # [text, name, text, name, ..., text] elements.
+ parts = _PLACEHOLDER_RE.split(template)
+
+ for i in range(1, len(parts), 2):
+ name = parts[i]
+ if name not in values:
+ raise KeyError(f"a value for placeholder {{{name}}} was not provided")
+ val = values[name]
+ if val is None:
+ parts[i] = "null"
+ elif isinstance(val, bool):
+ parts[i] = "true" if val else "false"
+ else:
+ parts[i] = quoter(str(values[name]))
+
+ return "".join(parts)
+
+
+def path_template(template: str, /, **kwargs: Any) -> str:
+ """Interpolate {name} placeholders in `template` from keyword arguments.
+
+ Args:
+ template: The template string containing {name} placeholders.
+ **kwargs: Keyword arguments to interpolate into the template.
+
+ Returns:
+ The template with placeholders interpolated and percent-encoded.
+
+ Safe characters for percent-encoding are dependent on the URI component.
+ Placeholders in path and fragment portions are percent-encoded where the `segment`
+ and `fragment` sets from RFC 3986 respectively are considered safe.
+ Placeholders in the query portion are percent-encoded where the `query` set from
+ RFC 3986 §3.3 is considered safe except for = and & characters.
+
+ Raises:
+ KeyError: If a placeholder is not found in `kwargs`.
+ ValueError: If resulting path contains /./ or /../ segments (including percent-encoded dot-segments).
+ """
+ # Split the template into path, query, and fragment portions.
+ fragment_template: str | None = None
+ query_template: str | None = None
+
+ rest = template
+ if "#" in rest:
+ rest, fragment_template = rest.split("#", 1)
+ if "?" in rest:
+ rest, query_template = rest.split("?", 1)
+ path_template = rest
+
+ # Interpolate each portion with the appropriate quoting rules.
+ path_result = _interpolate(path_template, kwargs, _quote_path_segment_part)
+
+ # Reject dot-segments (. and ..) in the final assembled path. The check
+ # runs after interpolation so that adjacent placeholders or a mix of static
+ # text and placeholders that together form a dot-segment are caught.
+ # Also reject percent-encoded dot-segments to protect against incorrectly
+ # implemented normalization in servers/proxies.
+ for segment in path_result.split("/"):
+ if _DOT_SEGMENT_RE.match(segment):
+ raise ValueError(f"Constructed path {path_result!r} contains dot-segment {segment!r} which is not allowed")
+
+ result = path_result
+ if query_template is not None:
+ result += "?" + _interpolate(query_template, kwargs, _quote_query_part)
+ if fragment_template is not None:
+ result += "#" + _interpolate(fragment_template, kwargs, _quote_fragment_part)
+
+ return result
diff --git a/src/kernel/_version.py b/src/kernel/_version.py
index 068f52c..2c4ce41 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.43.0" # x-release-please-version
+__version__ = "0.44.0" # x-release-please-version
diff --git a/src/kernel/resources/auth/connections.py b/src/kernel/resources/auth/connections.py
index fed6679..c610da4 100644
--- a/src/kernel/resources/auth/connections.py
+++ b/src/kernel/resources/auth/connections.py
@@ -7,7 +7,7 @@
import httpx
from ..._types import Body, Omit, Query, Headers, NoneType, NotGiven, SequenceNotStr, omit, not_given
-from ..._utils import maybe_transform, async_maybe_transform
+from ..._utils import path_template, maybe_transform, async_maybe_transform
from ..._compat import cached_property
from ..._resource import SyncAPIResource, AsyncAPIResource
from ..._response import (
@@ -23,6 +23,7 @@
connection_login_params,
connection_create_params,
connection_submit_params,
+ connection_update_params,
)
from ..._base_client import AsyncPaginator, make_request_options
from ...types.auth.managed_auth import ManagedAuth
@@ -179,7 +180,77 @@ def retrieve(
if not id:
raise ValueError(f"Expected a non-empty value for `id` but received {id!r}")
return self._get(
- f"/auth/connections/{id}",
+ path_template("/auth/connections/{id}", id=id),
+ options=make_request_options(
+ extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
+ ),
+ cast_to=ManagedAuth,
+ )
+
+ def update(
+ self,
+ id: str,
+ *,
+ allowed_domains: SequenceNotStr[str] | Omit = omit,
+ credential: connection_update_params.Credential | Omit = omit,
+ health_check_interval: int | Omit = omit,
+ login_url: str | Omit = omit,
+ proxy: connection_update_params.Proxy | Omit = omit,
+ save_credentials: bool | 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,
+ ) -> ManagedAuth:
+ """Update an auth connection's configuration.
+
+ Only the fields provided will be
+ updated.
+
+ Args:
+ allowed_domains: Additional domains valid for this auth flow (replaces existing list)
+
+ credential:
+ Reference to credentials for the auth connection. Use one of:
+
+ - { name } for Kernel credentials
+ - { provider, path } for external provider item
+ - { provider, auto: true } for external provider domain lookup
+
+ health_check_interval: Interval in seconds between automatic health checks
+
+ login_url: Login page URL. Set to empty string to clear.
+
+ proxy: Proxy selection. Provide either id or name. The proxy must belong to the
+ caller's org.
+
+ save_credentials: Whether to save credentials after every successful login
+
+ 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._patch(
+ path_template("/auth/connections/{id}", id=id),
+ body=maybe_transform(
+ {
+ "allowed_domains": allowed_domains,
+ "credential": credential,
+ "health_check_interval": health_check_interval,
+ "login_url": login_url,
+ "proxy": proxy,
+ "save_credentials": save_credentials,
+ },
+ connection_update_params.ConnectionUpdateParams,
+ ),
options=make_request_options(
extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
),
@@ -273,7 +344,7 @@ def delete(
raise ValueError(f"Expected a non-empty value for `id` but received {id!r}")
extra_headers = {"Accept": "*/*", **(extra_headers or {})}
return self._delete(
- f"/auth/connections/{id}",
+ path_template("/auth/connections/{id}", id=id),
options=make_request_options(
extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
),
@@ -309,7 +380,7 @@ def follow(
raise ValueError(f"Expected a non-empty value for `id` but received {id!r}")
extra_headers = {"Accept": "text/event-stream", **(extra_headers or {})}
return self._get(
- f"/auth/connections/{id}/events",
+ path_template("/auth/connections/{id}/events", id=id),
options=make_request_options(
extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
),
@@ -353,7 +424,7 @@ def login(
if not id:
raise ValueError(f"Expected a non-empty value for `id` but received {id!r}")
return self._post(
- f"/auth/connections/{id}/login",
+ path_template("/auth/connections/{id}/login", id=id),
body=maybe_transform({"proxy": proxy}, connection_login_params.ConnectionLoginParams),
options=make_request_options(
extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
@@ -367,7 +438,9 @@ def submit(
*,
fields: Dict[str, str] | Omit = omit,
mfa_option_id: str | Omit = omit,
+ sign_in_option_id: str | Omit = omit,
sso_button_selector: str | Omit = omit,
+ sso_provider: 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,
@@ -383,9 +456,15 @@ def submit(
Args:
fields: Map of field name to value
- mfa_option_id: Optional MFA option ID if user selected an MFA method
+ mfa_option_id: The MFA method type to select (when mfa_options were returned)
+
+ sign_in_option_id: The sign-in option ID to select (when sign_in_options were returned)
- sso_button_selector: Optional XPath selector if user chose to click an SSO button instead
+ sso_button_selector: XPath selector for the SSO button to click (ODA). Use sso_provider instead for
+ CUA.
+
+ sso_provider: SSO provider to click, matching the provider field from pending_sso_buttons
+ (e.g., "google", "github"). Cannot be used with sso_button_selector.
extra_headers: Send extra headers
@@ -398,12 +477,14 @@ def submit(
if not id:
raise ValueError(f"Expected a non-empty value for `id` but received {id!r}")
return self._post(
- f"/auth/connections/{id}/submit",
+ path_template("/auth/connections/{id}/submit", id=id),
body=maybe_transform(
{
"fields": fields,
"mfa_option_id": mfa_option_id,
+ "sign_in_option_id": sign_in_option_id,
"sso_button_selector": sso_button_selector,
+ "sso_provider": sso_provider,
},
connection_submit_params.ConnectionSubmitParams,
),
@@ -560,7 +641,77 @@ async def retrieve(
if not id:
raise ValueError(f"Expected a non-empty value for `id` but received {id!r}")
return await self._get(
- f"/auth/connections/{id}",
+ path_template("/auth/connections/{id}", id=id),
+ options=make_request_options(
+ extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
+ ),
+ cast_to=ManagedAuth,
+ )
+
+ async def update(
+ self,
+ id: str,
+ *,
+ allowed_domains: SequenceNotStr[str] | Omit = omit,
+ credential: connection_update_params.Credential | Omit = omit,
+ health_check_interval: int | Omit = omit,
+ login_url: str | Omit = omit,
+ proxy: connection_update_params.Proxy | Omit = omit,
+ save_credentials: bool | 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,
+ ) -> ManagedAuth:
+ """Update an auth connection's configuration.
+
+ Only the fields provided will be
+ updated.
+
+ Args:
+ allowed_domains: Additional domains valid for this auth flow (replaces existing list)
+
+ credential:
+ Reference to credentials for the auth connection. Use one of:
+
+ - { name } for Kernel credentials
+ - { provider, path } for external provider item
+ - { provider, auto: true } for external provider domain lookup
+
+ health_check_interval: Interval in seconds between automatic health checks
+
+ login_url: Login page URL. Set to empty string to clear.
+
+ proxy: Proxy selection. Provide either id or name. The proxy must belong to the
+ caller's org.
+
+ save_credentials: Whether to save credentials after every successful login
+
+ 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._patch(
+ path_template("/auth/connections/{id}", id=id),
+ body=await async_maybe_transform(
+ {
+ "allowed_domains": allowed_domains,
+ "credential": credential,
+ "health_check_interval": health_check_interval,
+ "login_url": login_url,
+ "proxy": proxy,
+ "save_credentials": save_credentials,
+ },
+ connection_update_params.ConnectionUpdateParams,
+ ),
options=make_request_options(
extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
),
@@ -654,7 +805,7 @@ async def delete(
raise ValueError(f"Expected a non-empty value for `id` but received {id!r}")
extra_headers = {"Accept": "*/*", **(extra_headers or {})}
return await self._delete(
- f"/auth/connections/{id}",
+ path_template("/auth/connections/{id}", id=id),
options=make_request_options(
extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
),
@@ -690,7 +841,7 @@ async def follow(
raise ValueError(f"Expected a non-empty value for `id` but received {id!r}")
extra_headers = {"Accept": "text/event-stream", **(extra_headers or {})}
return await self._get(
- f"/auth/connections/{id}/events",
+ path_template("/auth/connections/{id}/events", id=id),
options=make_request_options(
extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
),
@@ -734,7 +885,7 @@ async def login(
if not id:
raise ValueError(f"Expected a non-empty value for `id` but received {id!r}")
return await self._post(
- f"/auth/connections/{id}/login",
+ path_template("/auth/connections/{id}/login", id=id),
body=await async_maybe_transform({"proxy": proxy}, connection_login_params.ConnectionLoginParams),
options=make_request_options(
extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
@@ -748,7 +899,9 @@ async def submit(
*,
fields: Dict[str, str] | Omit = omit,
mfa_option_id: str | Omit = omit,
+ sign_in_option_id: str | Omit = omit,
sso_button_selector: str | Omit = omit,
+ sso_provider: 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,
@@ -764,9 +917,15 @@ async def submit(
Args:
fields: Map of field name to value
- mfa_option_id: Optional MFA option ID if user selected an MFA method
+ mfa_option_id: The MFA method type to select (when mfa_options were returned)
+
+ sign_in_option_id: The sign-in option ID to select (when sign_in_options were returned)
- sso_button_selector: Optional XPath selector if user chose to click an SSO button instead
+ sso_button_selector: XPath selector for the SSO button to click (ODA). Use sso_provider instead for
+ CUA.
+
+ sso_provider: SSO provider to click, matching the provider field from pending_sso_buttons
+ (e.g., "google", "github"). Cannot be used with sso_button_selector.
extra_headers: Send extra headers
@@ -779,12 +938,14 @@ async def submit(
if not id:
raise ValueError(f"Expected a non-empty value for `id` but received {id!r}")
return await self._post(
- f"/auth/connections/{id}/submit",
+ path_template("/auth/connections/{id}/submit", id=id),
body=await async_maybe_transform(
{
"fields": fields,
"mfa_option_id": mfa_option_id,
+ "sign_in_option_id": sign_in_option_id,
"sso_button_selector": sso_button_selector,
+ "sso_provider": sso_provider,
},
connection_submit_params.ConnectionSubmitParams,
),
@@ -805,6 +966,9 @@ def __init__(self, connections: ConnectionsResource) -> None:
self.retrieve = to_raw_response_wrapper(
connections.retrieve,
)
+ self.update = to_raw_response_wrapper(
+ connections.update,
+ )
self.list = to_raw_response_wrapper(
connections.list,
)
@@ -832,6 +996,9 @@ def __init__(self, connections: AsyncConnectionsResource) -> None:
self.retrieve = async_to_raw_response_wrapper(
connections.retrieve,
)
+ self.update = async_to_raw_response_wrapper(
+ connections.update,
+ )
self.list = async_to_raw_response_wrapper(
connections.list,
)
@@ -859,6 +1026,9 @@ def __init__(self, connections: ConnectionsResource) -> None:
self.retrieve = to_streamed_response_wrapper(
connections.retrieve,
)
+ self.update = to_streamed_response_wrapper(
+ connections.update,
+ )
self.list = to_streamed_response_wrapper(
connections.list,
)
@@ -886,6 +1056,9 @@ def __init__(self, connections: AsyncConnectionsResource) -> None:
self.retrieve = async_to_streamed_response_wrapper(
connections.retrieve,
)
+ self.update = async_to_streamed_response_wrapper(
+ connections.update,
+ )
self.list = async_to_streamed_response_wrapper(
connections.list,
)
diff --git a/src/kernel/resources/browser_pools.py b/src/kernel/resources/browser_pools.py
index a5b6e59..c0ce659 100644
--- a/src/kernel/resources/browser_pools.py
+++ b/src/kernel/resources/browser_pools.py
@@ -14,7 +14,7 @@
browser_pool_release_params,
)
from .._types import Body, Omit, Query, Headers, NoneType, NotGiven, omit, not_given
-from .._utils import maybe_transform, async_maybe_transform
+from .._utils import path_template, maybe_transform, async_maybe_transform
from .._compat import cached_property
from .._resource import SyncAPIResource, AsyncAPIResource
from .._response import (
@@ -110,9 +110,13 @@ def create(
are destroyed. Defaults to 600 seconds if not specified
viewport: Initial browser window size in pixels with optional refresh rate. If omitted,
- image defaults apply (1920x1080@25). Arbitrary viewport dimensions are accepted,
- but the following configurations are known-good and fully tested: 2560x1440@10,
- 1920x1080@25, 1920x1200@25, 1440x900@25, 1280x800@60, 1024x768@60, 1200x800@60.
+ image defaults apply (1920x1080@25). For GPU images, the default is
+ 1920x1080@60. Arbitrary viewport dimensions and refresh rates are accepted.
+ Known-good presets include: 2560x1440@10, 1920x1080@25, 1920x1200@25,
+ 1440x900@25, 1280x800@60, 1024x768@60, 1200x800@60. For GPU images, recommended
+ presets use one of these resolutions with refresh rates 60, 30, 25, or 10:
+ 800x600, 960x720, 1024x576, 1024x768, 1152x648, 1200x800, 1280x720, 1368x768,
+ 1440x900, 1600x900, 1920x1080, 1920x1200, 390x844, 360x250, 768x1024, 800x1600.
Viewports outside this list may exhibit unstable live view or recording
behavior. If refresh_rate is not provided, it will be automatically determined
based on the resolution (higher resolutions use lower refresh rates to keep
@@ -176,7 +180,7 @@ def retrieve(
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(
- f"/browser_pools/{id_or_name}",
+ path_template("/browser_pools/{id_or_name}", id_or_name=id_or_name),
options=make_request_options(
extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
),
@@ -242,9 +246,13 @@ def update(
are destroyed. Defaults to 600 seconds if not specified
viewport: Initial browser window size in pixels with optional refresh rate. If omitted,
- image defaults apply (1920x1080@25). Arbitrary viewport dimensions are accepted,
- but the following configurations are known-good and fully tested: 2560x1440@10,
- 1920x1080@25, 1920x1200@25, 1440x900@25, 1280x800@60, 1024x768@60, 1200x800@60.
+ image defaults apply (1920x1080@25). For GPU images, the default is
+ 1920x1080@60. Arbitrary viewport dimensions and refresh rates are accepted.
+ Known-good presets include: 2560x1440@10, 1920x1080@25, 1920x1200@25,
+ 1440x900@25, 1280x800@60, 1024x768@60, 1200x800@60. For GPU images, recommended
+ presets use one of these resolutions with refresh rates 60, 30, 25, or 10:
+ 800x600, 960x720, 1024x576, 1024x768, 1152x648, 1200x800, 1280x720, 1368x768,
+ 1440x900, 1600x900, 1920x1080, 1920x1200, 390x844, 360x250, 768x1024, 800x1600.
Viewports outside this list may exhibit unstable live view or recording
behavior. If refresh_rate is not provided, it will be automatically determined
based on the resolution (higher resolutions use lower refresh rates to keep
@@ -261,7 +269,7 @@ def update(
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._patch(
- f"/browser_pools/{id_or_name}",
+ path_template("/browser_pools/{id_or_name}", id_or_name=id_or_name),
body=maybe_transform(
{
"size": size,
@@ -337,7 +345,7 @@ def delete(
raise ValueError(f"Expected a non-empty value for `id_or_name` but received {id_or_name!r}")
extra_headers = {"Accept": "*/*", **(extra_headers or {})}
return self._delete(
- f"/browser_pools/{id_or_name}",
+ path_template("/browser_pools/{id_or_name}", id_or_name=id_or_name),
body=maybe_transform({"force": force}, browser_pool_delete_params.BrowserPoolDeleteParams),
options=make_request_options(
extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
@@ -380,7 +388,7 @@ def acquire(
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._post(
- f"/browser_pools/{id_or_name}/acquire",
+ path_template("/browser_pools/{id_or_name}/acquire", id_or_name=id_or_name),
body=maybe_transform(
{"acquire_timeout_seconds": acquire_timeout_seconds},
browser_pool_acquire_params.BrowserPoolAcquireParams,
@@ -418,7 +426,7 @@ def flush(
raise ValueError(f"Expected a non-empty value for `id_or_name` but received {id_or_name!r}")
extra_headers = {"Accept": "*/*", **(extra_headers or {})}
return self._post(
- f"/browser_pools/{id_or_name}/flush",
+ path_template("/browser_pools/{id_or_name}/flush", id_or_name=id_or_name),
options=make_request_options(
extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
),
@@ -459,7 +467,7 @@ def release(
raise ValueError(f"Expected a non-empty value for `id_or_name` but received {id_or_name!r}")
extra_headers = {"Accept": "*/*", **(extra_headers or {})}
return self._post(
- f"/browser_pools/{id_or_name}/release",
+ path_template("/browser_pools/{id_or_name}/release", id_or_name=id_or_name),
body=maybe_transform(
{
"session_id": session_id,
@@ -550,9 +558,13 @@ async def create(
are destroyed. Defaults to 600 seconds if not specified
viewport: Initial browser window size in pixels with optional refresh rate. If omitted,
- image defaults apply (1920x1080@25). Arbitrary viewport dimensions are accepted,
- but the following configurations are known-good and fully tested: 2560x1440@10,
- 1920x1080@25, 1920x1200@25, 1440x900@25, 1280x800@60, 1024x768@60, 1200x800@60.
+ image defaults apply (1920x1080@25). For GPU images, the default is
+ 1920x1080@60. Arbitrary viewport dimensions and refresh rates are accepted.
+ Known-good presets include: 2560x1440@10, 1920x1080@25, 1920x1200@25,
+ 1440x900@25, 1280x800@60, 1024x768@60, 1200x800@60. For GPU images, recommended
+ presets use one of these resolutions with refresh rates 60, 30, 25, or 10:
+ 800x600, 960x720, 1024x576, 1024x768, 1152x648, 1200x800, 1280x720, 1368x768,
+ 1440x900, 1600x900, 1920x1080, 1920x1200, 390x844, 360x250, 768x1024, 800x1600.
Viewports outside this list may exhibit unstable live view or recording
behavior. If refresh_rate is not provided, it will be automatically determined
based on the resolution (higher resolutions use lower refresh rates to keep
@@ -616,7 +628,7 @@ async def retrieve(
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(
- f"/browser_pools/{id_or_name}",
+ path_template("/browser_pools/{id_or_name}", id_or_name=id_or_name),
options=make_request_options(
extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
),
@@ -682,9 +694,13 @@ async def update(
are destroyed. Defaults to 600 seconds if not specified
viewport: Initial browser window size in pixels with optional refresh rate. If omitted,
- image defaults apply (1920x1080@25). Arbitrary viewport dimensions are accepted,
- but the following configurations are known-good and fully tested: 2560x1440@10,
- 1920x1080@25, 1920x1200@25, 1440x900@25, 1280x800@60, 1024x768@60, 1200x800@60.
+ image defaults apply (1920x1080@25). For GPU images, the default is
+ 1920x1080@60. Arbitrary viewport dimensions and refresh rates are accepted.
+ Known-good presets include: 2560x1440@10, 1920x1080@25, 1920x1200@25,
+ 1440x900@25, 1280x800@60, 1024x768@60, 1200x800@60. For GPU images, recommended
+ presets use one of these resolutions with refresh rates 60, 30, 25, or 10:
+ 800x600, 960x720, 1024x576, 1024x768, 1152x648, 1200x800, 1280x720, 1368x768,
+ 1440x900, 1600x900, 1920x1080, 1920x1200, 390x844, 360x250, 768x1024, 800x1600.
Viewports outside this list may exhibit unstable live view or recording
behavior. If refresh_rate is not provided, it will be automatically determined
based on the resolution (higher resolutions use lower refresh rates to keep
@@ -701,7 +717,7 @@ async def update(
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._patch(
- f"/browser_pools/{id_or_name}",
+ path_template("/browser_pools/{id_or_name}", id_or_name=id_or_name),
body=await async_maybe_transform(
{
"size": size,
@@ -777,7 +793,7 @@ async def delete(
raise ValueError(f"Expected a non-empty value for `id_or_name` but received {id_or_name!r}")
extra_headers = {"Accept": "*/*", **(extra_headers or {})}
return await self._delete(
- f"/browser_pools/{id_or_name}",
+ path_template("/browser_pools/{id_or_name}", id_or_name=id_or_name),
body=await async_maybe_transform({"force": force}, browser_pool_delete_params.BrowserPoolDeleteParams),
options=make_request_options(
extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
@@ -820,7 +836,7 @@ async def acquire(
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._post(
- f"/browser_pools/{id_or_name}/acquire",
+ path_template("/browser_pools/{id_or_name}/acquire", id_or_name=id_or_name),
body=await async_maybe_transform(
{"acquire_timeout_seconds": acquire_timeout_seconds},
browser_pool_acquire_params.BrowserPoolAcquireParams,
@@ -858,7 +874,7 @@ async def flush(
raise ValueError(f"Expected a non-empty value for `id_or_name` but received {id_or_name!r}")
extra_headers = {"Accept": "*/*", **(extra_headers or {})}
return await self._post(
- f"/browser_pools/{id_or_name}/flush",
+ path_template("/browser_pools/{id_or_name}/flush", id_or_name=id_or_name),
options=make_request_options(
extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
),
@@ -899,7 +915,7 @@ async def release(
raise ValueError(f"Expected a non-empty value for `id_or_name` but received {id_or_name!r}")
extra_headers = {"Accept": "*/*", **(extra_headers or {})}
return await self._post(
- f"/browser_pools/{id_or_name}/release",
+ path_template("/browser_pools/{id_or_name}/release", id_or_name=id_or_name),
body=await async_maybe_transform(
{
"session_id": session_id,
diff --git a/src/kernel/resources/browsers/browsers.py b/src/kernel/resources/browsers/browsers.py
index 235da23..c28a16c 100644
--- a/src/kernel/resources/browsers/browsers.py
+++ b/src/kernel/resources/browsers/browsers.py
@@ -49,7 +49,7 @@
AsyncReplaysResourceWithStreamingResponse,
)
from ..._types import Body, Omit, Query, Headers, NoneType, NotGiven, omit, not_given
-from ..._utils import extract_files, maybe_transform, deepcopy_minimal, async_maybe_transform
+from ..._utils import extract_files, path_template, maybe_transform, deepcopy_minimal, async_maybe_transform
from .computer import (
ComputerResource,
AsyncComputerResource,
@@ -166,8 +166,8 @@ def create(
Args:
extensions: List of browser extensions to load into the session. Provide each by id or name.
- gpu: If true, launches a hardware-accelerated browser with GPU rendering. Requires
- Start-Up or Enterprise plan.
+ gpu: If true, enables GPU acceleration for the browser session. Requires Start-Up or
+ Enterprise plan and headless=false.
headless: If true, launches the browser using a headless image (no VNC/GUI). Defaults to
false.
@@ -196,9 +196,13 @@ def create(
see is +/- 5 seconds around the specified value.
viewport: Initial browser window size in pixels with optional refresh rate. If omitted,
- image defaults apply (1920x1080@25). Arbitrary viewport dimensions are accepted,
- but the following configurations are known-good and fully tested: 2560x1440@10,
- 1920x1080@25, 1920x1200@25, 1440x900@25, 1280x800@60, 1024x768@60, 1200x800@60.
+ image defaults apply (1920x1080@25). For GPU images, the default is
+ 1920x1080@60. Arbitrary viewport dimensions and refresh rates are accepted.
+ Known-good presets include: 2560x1440@10, 1920x1080@25, 1920x1200@25,
+ 1440x900@25, 1280x800@60, 1024x768@60, 1200x800@60. For GPU images, recommended
+ presets use one of these resolutions with refresh rates 60, 30, 25, or 10:
+ 800x600, 960x720, 1024x576, 1024x768, 1152x648, 1200x800, 1280x720, 1368x768,
+ 1440x900, 1600x900, 1920x1080, 1920x1200, 390x844, 360x250, 768x1024, 800x1600.
Viewports outside this list may exhibit unstable live view or recording
behavior. If refresh_rate is not provided, it will be automatically determined
based on the resolution (higher resolutions use lower refresh rates to keep
@@ -265,7 +269,7 @@ def retrieve(
if not id:
raise ValueError(f"Expected a non-empty value for `id` but received {id!r}")
return self._get(
- f"/browsers/{id}",
+ path_template("/browsers/{id}", id=id),
options=make_request_options(
extra_headers=extra_headers,
extra_query=extra_query,
@@ -315,7 +319,7 @@ def update(
if not id:
raise ValueError(f"Expected a non-empty value for `id` but received {id!r}")
return self._patch(
- f"/browsers/{id}",
+ path_template("/browsers/{id}", id=id),
body=maybe_transform(
{
"profile": profile,
@@ -461,7 +465,7 @@ def delete_by_id(
raise ValueError(f"Expected a non-empty value for `id` but received {id!r}")
extra_headers = {"Accept": "*/*", **(extra_headers or {})}
return self._delete(
- f"/browsers/{id}",
+ path_template("/browsers/{id}", id=id),
options=make_request_options(
extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
),
@@ -505,7 +509,7 @@ def load_extensions(
# multipart/form-data; boundary=---abc--
extra_headers["Content-Type"] = "multipart/form-data"
return self._post(
- f"/browsers/{id}/extensions",
+ path_template("/browsers/{id}/extensions", id=id),
body=maybe_transform(body, browser_load_extensions_params.BrowserLoadExtensionsParams),
files=files,
options=make_request_options(
@@ -593,8 +597,8 @@ async def create(
Args:
extensions: List of browser extensions to load into the session. Provide each by id or name.
- gpu: If true, launches a hardware-accelerated browser with GPU rendering. Requires
- Start-Up or Enterprise plan.
+ gpu: If true, enables GPU acceleration for the browser session. Requires Start-Up or
+ Enterprise plan and headless=false.
headless: If true, launches the browser using a headless image (no VNC/GUI). Defaults to
false.
@@ -623,9 +627,13 @@ async def create(
see is +/- 5 seconds around the specified value.
viewport: Initial browser window size in pixels with optional refresh rate. If omitted,
- image defaults apply (1920x1080@25). Arbitrary viewport dimensions are accepted,
- but the following configurations are known-good and fully tested: 2560x1440@10,
- 1920x1080@25, 1920x1200@25, 1440x900@25, 1280x800@60, 1024x768@60, 1200x800@60.
+ image defaults apply (1920x1080@25). For GPU images, the default is
+ 1920x1080@60. Arbitrary viewport dimensions and refresh rates are accepted.
+ Known-good presets include: 2560x1440@10, 1920x1080@25, 1920x1200@25,
+ 1440x900@25, 1280x800@60, 1024x768@60, 1200x800@60. For GPU images, recommended
+ presets use one of these resolutions with refresh rates 60, 30, 25, or 10:
+ 800x600, 960x720, 1024x576, 1024x768, 1152x648, 1200x800, 1280x720, 1368x768,
+ 1440x900, 1600x900, 1920x1080, 1920x1200, 390x844, 360x250, 768x1024, 800x1600.
Viewports outside this list may exhibit unstable live view or recording
behavior. If refresh_rate is not provided, it will be automatically determined
based on the resolution (higher resolutions use lower refresh rates to keep
@@ -692,7 +700,7 @@ async def retrieve(
if not id:
raise ValueError(f"Expected a non-empty value for `id` but received {id!r}")
return await self._get(
- f"/browsers/{id}",
+ path_template("/browsers/{id}", id=id),
options=make_request_options(
extra_headers=extra_headers,
extra_query=extra_query,
@@ -742,7 +750,7 @@ async def update(
if not id:
raise ValueError(f"Expected a non-empty value for `id` but received {id!r}")
return await self._patch(
- f"/browsers/{id}",
+ path_template("/browsers/{id}", id=id),
body=await async_maybe_transform(
{
"profile": profile,
@@ -890,7 +898,7 @@ async def delete_by_id(
raise ValueError(f"Expected a non-empty value for `id` but received {id!r}")
extra_headers = {"Accept": "*/*", **(extra_headers or {})}
return await self._delete(
- f"/browsers/{id}",
+ path_template("/browsers/{id}", id=id),
options=make_request_options(
extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
),
@@ -934,7 +942,7 @@ async def load_extensions(
# multipart/form-data; boundary=---abc--
extra_headers["Content-Type"] = "multipart/form-data"
return await self._post(
- f"/browsers/{id}/extensions",
+ path_template("/browsers/{id}/extensions", id=id),
body=await async_maybe_transform(body, browser_load_extensions_params.BrowserLoadExtensionsParams),
files=files,
options=make_request_options(
diff --git a/src/kernel/resources/browsers/computer.py b/src/kernel/resources/browsers/computer.py
index 1357c1e..54b638e 100644
--- a/src/kernel/resources/browsers/computer.py
+++ b/src/kernel/resources/browsers/computer.py
@@ -8,7 +8,7 @@
import httpx
from ..._types import Body, Omit, Query, Headers, NoneType, NotGiven, SequenceNotStr, omit, not_given
-from ..._utils import maybe_transform, async_maybe_transform
+from ..._utils import path_template, maybe_transform, async_maybe_transform
from ..._compat import cached_property
from ..._resource import SyncAPIResource, AsyncAPIResource
from ..._response import (
@@ -97,7 +97,7 @@ def batch(
raise ValueError(f"Expected a non-empty value for `id` but received {id!r}")
extra_headers = {"Accept": "*/*", **(extra_headers or {})}
return self._post(
- f"/browsers/{id}/computer/batch",
+ path_template("/browsers/{id}/computer/batch", id=id),
body=maybe_transform({"actions": actions}, computer_batch_params.ComputerBatchParams),
options=make_request_options(
extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
@@ -133,7 +133,7 @@ def capture_screenshot(
raise ValueError(f"Expected a non-empty value for `id` but received {id!r}")
extra_headers = {"Accept": "image/png", **(extra_headers or {})}
return self._post(
- f"/browsers/{id}/computer/screenshot",
+ path_template("/browsers/{id}/computer/screenshot", id=id),
body=maybe_transform(
{"region": region}, computer_capture_screenshot_params.ComputerCaptureScreenshotParams
),
@@ -188,7 +188,7 @@ def click_mouse(
raise ValueError(f"Expected a non-empty value for `id` but received {id!r}")
extra_headers = {"Accept": "*/*", **(extra_headers or {})}
return self._post(
- f"/browsers/{id}/computer/click_mouse",
+ path_template("/browsers/{id}/computer/click_mouse", id=id),
body=maybe_transform(
{
"x": x,
@@ -213,7 +213,9 @@ def drag_mouse(
path: Iterable[Iterable[int]],
button: Literal["left", "middle", "right"] | Omit = omit,
delay: int | Omit = omit,
+ duration_ms: int | Omit = omit,
hold_keys: SequenceNotStr[str] | Omit = omit,
+ smooth: bool | Omit = omit,
step_delay_ms: int | Omit = omit,
steps_per_segment: int | Omit = omit,
# Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
@@ -234,8 +236,14 @@ def drag_mouse(
delay: Delay in milliseconds between button down and starting to move along the path.
+ duration_ms: Target total duration in milliseconds for the entire drag movement when
+ smooth=true. Omit for automatic timing based on total path length.
+
hold_keys: Modifier keys to hold during the drag
+ smooth: Use human-like Bezier curves between path waypoints instead of linear
+ interpolation. When true, steps_per_segment and step_delay_ms are ignored.
+
step_delay_ms: Delay in milliseconds between relative steps while dragging (not the initial
delay).
@@ -253,13 +261,15 @@ def drag_mouse(
raise ValueError(f"Expected a non-empty value for `id` but received {id!r}")
extra_headers = {"Accept": "*/*", **(extra_headers or {})}
return self._post(
- f"/browsers/{id}/computer/drag_mouse",
+ path_template("/browsers/{id}/computer/drag_mouse", id=id),
body=maybe_transform(
{
"path": path,
"button": button,
"delay": delay,
+ "duration_ms": duration_ms,
"hold_keys": hold_keys,
+ "smooth": smooth,
"step_delay_ms": step_delay_ms,
"steps_per_segment": steps_per_segment,
},
@@ -297,7 +307,7 @@ def get_mouse_position(
if not id:
raise ValueError(f"Expected a non-empty value for `id` but received {id!r}")
return self._post(
- f"/browsers/{id}/computer/get_mouse_position",
+ path_template("/browsers/{id}/computer/get_mouse_position", id=id),
options=make_request_options(
extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
),
@@ -347,7 +357,7 @@ def move_mouse(
raise ValueError(f"Expected a non-empty value for `id` but received {id!r}")
extra_headers = {"Accept": "*/*", **(extra_headers or {})}
return self._post(
- f"/browsers/{id}/computer/move_mouse",
+ path_template("/browsers/{id}/computer/move_mouse", id=id),
body=maybe_transform(
{
"x": x,
@@ -404,7 +414,7 @@ def press_key(
raise ValueError(f"Expected a non-empty value for `id` but received {id!r}")
extra_headers = {"Accept": "*/*", **(extra_headers or {})}
return self._post(
- f"/browsers/{id}/computer/press_key",
+ path_template("/browsers/{id}/computer/press_key", id=id),
body=maybe_transform(
{
"keys": keys,
@@ -445,7 +455,7 @@ def read_clipboard(
if not id:
raise ValueError(f"Expected a non-empty value for `id` but received {id!r}")
return self._post(
- f"/browsers/{id}/computer/clipboard/read",
+ path_template("/browsers/{id}/computer/clipboard/read", id=id),
options=make_request_options(
extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
),
@@ -476,9 +486,11 @@ def scroll(
y: Y coordinate at which to perform the scroll
- delta_x: Horizontal scroll amount. Positive scrolls right, negative scrolls left.
+ delta_x: Horizontal scroll amount in xdotool "wheel units." Positive scrolls right,
+ negative scrolls left.
- delta_y: Vertical scroll amount. Positive scrolls down, negative scrolls up.
+ delta_y: Vertical scroll amount in xdotool "wheel units." Positive scrolls down, negative
+ scrolls up.
hold_keys: Modifier keys to hold during the scroll
@@ -494,7 +506,7 @@ def scroll(
raise ValueError(f"Expected a non-empty value for `id` but received {id!r}")
extra_headers = {"Accept": "*/*", **(extra_headers or {})}
return self._post(
- f"/browsers/{id}/computer/scroll",
+ path_template("/browsers/{id}/computer/scroll", id=id),
body=maybe_transform(
{
"x": x,
@@ -540,7 +552,7 @@ def set_cursor_visibility(
if not id:
raise ValueError(f"Expected a non-empty value for `id` but received {id!r}")
return self._post(
- f"/browsers/{id}/computer/cursor",
+ path_template("/browsers/{id}/computer/cursor", id=id),
body=maybe_transform(
{"hidden": hidden}, computer_set_cursor_visibility_params.ComputerSetCursorVisibilityParams
),
@@ -583,7 +595,7 @@ def type_text(
raise ValueError(f"Expected a non-empty value for `id` but received {id!r}")
extra_headers = {"Accept": "*/*", **(extra_headers or {})}
return self._post(
- f"/browsers/{id}/computer/type",
+ path_template("/browsers/{id}/computer/type", id=id),
body=maybe_transform(
{
"text": text,
@@ -627,7 +639,7 @@ def write_clipboard(
raise ValueError(f"Expected a non-empty value for `id` but received {id!r}")
extra_headers = {"Accept": "*/*", **(extra_headers or {})}
return self._post(
- f"/browsers/{id}/computer/clipboard/write",
+ path_template("/browsers/{id}/computer/clipboard/write", id=id),
body=maybe_transform({"text": text}, computer_write_clipboard_params.ComputerWriteClipboardParams),
options=make_request_options(
extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
@@ -688,7 +700,7 @@ async def batch(
raise ValueError(f"Expected a non-empty value for `id` but received {id!r}")
extra_headers = {"Accept": "*/*", **(extra_headers or {})}
return await self._post(
- f"/browsers/{id}/computer/batch",
+ path_template("/browsers/{id}/computer/batch", id=id),
body=await async_maybe_transform({"actions": actions}, computer_batch_params.ComputerBatchParams),
options=make_request_options(
extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
@@ -724,7 +736,7 @@ async def capture_screenshot(
raise ValueError(f"Expected a non-empty value for `id` but received {id!r}")
extra_headers = {"Accept": "image/png", **(extra_headers or {})}
return await self._post(
- f"/browsers/{id}/computer/screenshot",
+ path_template("/browsers/{id}/computer/screenshot", id=id),
body=await async_maybe_transform(
{"region": region}, computer_capture_screenshot_params.ComputerCaptureScreenshotParams
),
@@ -779,7 +791,7 @@ async def click_mouse(
raise ValueError(f"Expected a non-empty value for `id` but received {id!r}")
extra_headers = {"Accept": "*/*", **(extra_headers or {})}
return await self._post(
- f"/browsers/{id}/computer/click_mouse",
+ path_template("/browsers/{id}/computer/click_mouse", id=id),
body=await async_maybe_transform(
{
"x": x,
@@ -804,7 +816,9 @@ async def drag_mouse(
path: Iterable[Iterable[int]],
button: Literal["left", "middle", "right"] | Omit = omit,
delay: int | Omit = omit,
+ duration_ms: int | Omit = omit,
hold_keys: SequenceNotStr[str] | Omit = omit,
+ smooth: bool | Omit = omit,
step_delay_ms: int | Omit = omit,
steps_per_segment: int | Omit = omit,
# Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
@@ -825,8 +839,14 @@ async def drag_mouse(
delay: Delay in milliseconds between button down and starting to move along the path.
+ duration_ms: Target total duration in milliseconds for the entire drag movement when
+ smooth=true. Omit for automatic timing based on total path length.
+
hold_keys: Modifier keys to hold during the drag
+ smooth: Use human-like Bezier curves between path waypoints instead of linear
+ interpolation. When true, steps_per_segment and step_delay_ms are ignored.
+
step_delay_ms: Delay in milliseconds between relative steps while dragging (not the initial
delay).
@@ -844,13 +864,15 @@ async def drag_mouse(
raise ValueError(f"Expected a non-empty value for `id` but received {id!r}")
extra_headers = {"Accept": "*/*", **(extra_headers or {})}
return await self._post(
- f"/browsers/{id}/computer/drag_mouse",
+ path_template("/browsers/{id}/computer/drag_mouse", id=id),
body=await async_maybe_transform(
{
"path": path,
"button": button,
"delay": delay,
+ "duration_ms": duration_ms,
"hold_keys": hold_keys,
+ "smooth": smooth,
"step_delay_ms": step_delay_ms,
"steps_per_segment": steps_per_segment,
},
@@ -888,7 +910,7 @@ async def get_mouse_position(
if not id:
raise ValueError(f"Expected a non-empty value for `id` but received {id!r}")
return await self._post(
- f"/browsers/{id}/computer/get_mouse_position",
+ path_template("/browsers/{id}/computer/get_mouse_position", id=id),
options=make_request_options(
extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
),
@@ -938,7 +960,7 @@ async def move_mouse(
raise ValueError(f"Expected a non-empty value for `id` but received {id!r}")
extra_headers = {"Accept": "*/*", **(extra_headers or {})}
return await self._post(
- f"/browsers/{id}/computer/move_mouse",
+ path_template("/browsers/{id}/computer/move_mouse", id=id),
body=await async_maybe_transform(
{
"x": x,
@@ -995,7 +1017,7 @@ async def press_key(
raise ValueError(f"Expected a non-empty value for `id` but received {id!r}")
extra_headers = {"Accept": "*/*", **(extra_headers or {})}
return await self._post(
- f"/browsers/{id}/computer/press_key",
+ path_template("/browsers/{id}/computer/press_key", id=id),
body=await async_maybe_transform(
{
"keys": keys,
@@ -1036,7 +1058,7 @@ async def read_clipboard(
if not id:
raise ValueError(f"Expected a non-empty value for `id` but received {id!r}")
return await self._post(
- f"/browsers/{id}/computer/clipboard/read",
+ path_template("/browsers/{id}/computer/clipboard/read", id=id),
options=make_request_options(
extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
),
@@ -1067,9 +1089,11 @@ async def scroll(
y: Y coordinate at which to perform the scroll
- delta_x: Horizontal scroll amount. Positive scrolls right, negative scrolls left.
+ delta_x: Horizontal scroll amount in xdotool "wheel units." Positive scrolls right,
+ negative scrolls left.
- delta_y: Vertical scroll amount. Positive scrolls down, negative scrolls up.
+ delta_y: Vertical scroll amount in xdotool "wheel units." Positive scrolls down, negative
+ scrolls up.
hold_keys: Modifier keys to hold during the scroll
@@ -1085,7 +1109,7 @@ async def scroll(
raise ValueError(f"Expected a non-empty value for `id` but received {id!r}")
extra_headers = {"Accept": "*/*", **(extra_headers or {})}
return await self._post(
- f"/browsers/{id}/computer/scroll",
+ path_template("/browsers/{id}/computer/scroll", id=id),
body=await async_maybe_transform(
{
"x": x,
@@ -1131,7 +1155,7 @@ async def set_cursor_visibility(
if not id:
raise ValueError(f"Expected a non-empty value for `id` but received {id!r}")
return await self._post(
- f"/browsers/{id}/computer/cursor",
+ path_template("/browsers/{id}/computer/cursor", id=id),
body=await async_maybe_transform(
{"hidden": hidden}, computer_set_cursor_visibility_params.ComputerSetCursorVisibilityParams
),
@@ -1174,7 +1198,7 @@ async def type_text(
raise ValueError(f"Expected a non-empty value for `id` but received {id!r}")
extra_headers = {"Accept": "*/*", **(extra_headers or {})}
return await self._post(
- f"/browsers/{id}/computer/type",
+ path_template("/browsers/{id}/computer/type", id=id),
body=await async_maybe_transform(
{
"text": text,
@@ -1218,7 +1242,7 @@ async def write_clipboard(
raise ValueError(f"Expected a non-empty value for `id` but received {id!r}")
extra_headers = {"Accept": "*/*", **(extra_headers or {})}
return await self._post(
- f"/browsers/{id}/computer/clipboard/write",
+ path_template("/browsers/{id}/computer/clipboard/write", id=id),
body=await async_maybe_transform(
{"text": text}, computer_write_clipboard_params.ComputerWriteClipboardParams
),
diff --git a/src/kernel/resources/browsers/fs/fs.py b/src/kernel/resources/browsers/fs/fs.py
index 1bd16af..f26119f 100644
--- a/src/kernel/resources/browsers/fs/fs.py
+++ b/src/kernel/resources/browsers/fs/fs.py
@@ -30,7 +30,7 @@
omit,
not_given,
)
-from ...._utils import extract_files, maybe_transform, deepcopy_minimal, async_maybe_transform
+from ...._utils import extract_files, path_template, maybe_transform, deepcopy_minimal, async_maybe_transform
from ...._compat import cached_property
from ...._resource import SyncAPIResource, AsyncAPIResource
from ...._response import (
@@ -128,7 +128,7 @@ def create_directory(
raise ValueError(f"Expected a non-empty value for `id` but received {id!r}")
extra_headers = {"Accept": "*/*", **(extra_headers or {})}
return self._put(
- f"/browsers/{id}/fs/create_directory",
+ path_template("/browsers/{id}/fs/create_directory", id=id),
body=maybe_transform(
{
"path": path,
@@ -172,7 +172,7 @@ def delete_directory(
raise ValueError(f"Expected a non-empty value for `id` but received {id!r}")
extra_headers = {"Accept": "*/*", **(extra_headers or {})}
return self._put(
- f"/browsers/{id}/fs/delete_directory",
+ path_template("/browsers/{id}/fs/delete_directory", id=id),
body=maybe_transform({"path": path}, f_delete_directory_params.FDeleteDirectoryParams),
options=make_request_options(
extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
@@ -210,7 +210,7 @@ def delete_file(
raise ValueError(f"Expected a non-empty value for `id` but received {id!r}")
extra_headers = {"Accept": "*/*", **(extra_headers or {})}
return self._put(
- f"/browsers/{id}/fs/delete_file",
+ path_template("/browsers/{id}/fs/delete_file", id=id),
body=maybe_transform({"path": path}, f_delete_file_params.FDeleteFileParams),
options=make_request_options(
extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
@@ -248,7 +248,7 @@ def download_dir_zip(
raise ValueError(f"Expected a non-empty value for `id` but received {id!r}")
extra_headers = {"Accept": "application/zip", **(extra_headers or {})}
return self._get(
- f"/browsers/{id}/fs/download_dir_zip",
+ path_template("/browsers/{id}/fs/download_dir_zip", id=id),
options=make_request_options(
extra_headers=extra_headers,
extra_query=extra_query,
@@ -288,7 +288,7 @@ def file_info(
if not id:
raise ValueError(f"Expected a non-empty value for `id` but received {id!r}")
return self._get(
- f"/browsers/{id}/fs/file_info",
+ path_template("/browsers/{id}/fs/file_info", id=id),
options=make_request_options(
extra_headers=extra_headers,
extra_query=extra_query,
@@ -328,7 +328,7 @@ def list_files(
if not id:
raise ValueError(f"Expected a non-empty value for `id` but received {id!r}")
return self._get(
- f"/browsers/{id}/fs/list_files",
+ path_template("/browsers/{id}/fs/list_files", id=id),
options=make_request_options(
extra_headers=extra_headers,
extra_query=extra_query,
@@ -372,7 +372,7 @@ def move(
raise ValueError(f"Expected a non-empty value for `id` but received {id!r}")
extra_headers = {"Accept": "*/*", **(extra_headers or {})}
return self._put(
- f"/browsers/{id}/fs/move",
+ path_template("/browsers/{id}/fs/move", id=id),
body=maybe_transform(
{
"dest_path": dest_path,
@@ -416,7 +416,7 @@ def read_file(
raise ValueError(f"Expected a non-empty value for `id` but received {id!r}")
extra_headers = {"Accept": "application/octet-stream", **(extra_headers or {})}
return self._get(
- f"/browsers/{id}/fs/read_file",
+ path_template("/browsers/{id}/fs/read_file", id=id),
options=make_request_options(
extra_headers=extra_headers,
extra_query=extra_query,
@@ -466,7 +466,7 @@ def set_file_permissions(
raise ValueError(f"Expected a non-empty value for `id` but received {id!r}")
extra_headers = {"Accept": "*/*", **(extra_headers or {})}
return self._put(
- f"/browsers/{id}/fs/set_file_permissions",
+ path_template("/browsers/{id}/fs/set_file_permissions", id=id),
body=maybe_transform(
{
"mode": mode,
@@ -516,7 +516,7 @@ def upload(
# multipart/form-data; boundary=---abc--
extra_headers["Content-Type"] = "multipart/form-data"
return self._post(
- f"/browsers/{id}/fs/upload",
+ path_template("/browsers/{id}/fs/upload", id=id),
body=maybe_transform(body, f_upload_params.FUploadParams),
files=extracted_files,
options=make_request_options(
@@ -567,7 +567,7 @@ def upload_zip(
# multipart/form-data; boundary=---abc--
extra_headers["Content-Type"] = "multipart/form-data"
return self._post(
- f"/browsers/{id}/fs/upload_zip",
+ path_template("/browsers/{id}/fs/upload_zip", id=id),
body=maybe_transform(body, f_upload_zip_params.FUploadZipParams),
files=files,
options=make_request_options(
@@ -611,7 +611,7 @@ def write_file(
extra_headers = {"Accept": "*/*", **(extra_headers or {})}
extra_headers["Content-Type"] = "application/octet-stream"
return self._put(
- f"/browsers/{id}/fs/write_file",
+ path_template("/browsers/{id}/fs/write_file", id=id),
content=read_file_content(contents) if isinstance(contents, os.PathLike) else contents,
options=make_request_options(
extra_headers=extra_headers,
@@ -690,7 +690,7 @@ async def create_directory(
raise ValueError(f"Expected a non-empty value for `id` but received {id!r}")
extra_headers = {"Accept": "*/*", **(extra_headers or {})}
return await self._put(
- f"/browsers/{id}/fs/create_directory",
+ path_template("/browsers/{id}/fs/create_directory", id=id),
body=await async_maybe_transform(
{
"path": path,
@@ -734,7 +734,7 @@ async def delete_directory(
raise ValueError(f"Expected a non-empty value for `id` but received {id!r}")
extra_headers = {"Accept": "*/*", **(extra_headers or {})}
return await self._put(
- f"/browsers/{id}/fs/delete_directory",
+ path_template("/browsers/{id}/fs/delete_directory", id=id),
body=await async_maybe_transform({"path": path}, f_delete_directory_params.FDeleteDirectoryParams),
options=make_request_options(
extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
@@ -772,7 +772,7 @@ async def delete_file(
raise ValueError(f"Expected a non-empty value for `id` but received {id!r}")
extra_headers = {"Accept": "*/*", **(extra_headers or {})}
return await self._put(
- f"/browsers/{id}/fs/delete_file",
+ path_template("/browsers/{id}/fs/delete_file", id=id),
body=await async_maybe_transform({"path": path}, f_delete_file_params.FDeleteFileParams),
options=make_request_options(
extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
@@ -810,7 +810,7 @@ async def download_dir_zip(
raise ValueError(f"Expected a non-empty value for `id` but received {id!r}")
extra_headers = {"Accept": "application/zip", **(extra_headers or {})}
return await self._get(
- f"/browsers/{id}/fs/download_dir_zip",
+ path_template("/browsers/{id}/fs/download_dir_zip", id=id),
options=make_request_options(
extra_headers=extra_headers,
extra_query=extra_query,
@@ -850,7 +850,7 @@ async def file_info(
if not id:
raise ValueError(f"Expected a non-empty value for `id` but received {id!r}")
return await self._get(
- f"/browsers/{id}/fs/file_info",
+ path_template("/browsers/{id}/fs/file_info", id=id),
options=make_request_options(
extra_headers=extra_headers,
extra_query=extra_query,
@@ -890,7 +890,7 @@ async def list_files(
if not id:
raise ValueError(f"Expected a non-empty value for `id` but received {id!r}")
return await self._get(
- f"/browsers/{id}/fs/list_files",
+ path_template("/browsers/{id}/fs/list_files", id=id),
options=make_request_options(
extra_headers=extra_headers,
extra_query=extra_query,
@@ -934,7 +934,7 @@ async def move(
raise ValueError(f"Expected a non-empty value for `id` but received {id!r}")
extra_headers = {"Accept": "*/*", **(extra_headers or {})}
return await self._put(
- f"/browsers/{id}/fs/move",
+ path_template("/browsers/{id}/fs/move", id=id),
body=await async_maybe_transform(
{
"dest_path": dest_path,
@@ -978,7 +978,7 @@ async def read_file(
raise ValueError(f"Expected a non-empty value for `id` but received {id!r}")
extra_headers = {"Accept": "application/octet-stream", **(extra_headers or {})}
return await self._get(
- f"/browsers/{id}/fs/read_file",
+ path_template("/browsers/{id}/fs/read_file", id=id),
options=make_request_options(
extra_headers=extra_headers,
extra_query=extra_query,
@@ -1028,7 +1028,7 @@ async def set_file_permissions(
raise ValueError(f"Expected a non-empty value for `id` but received {id!r}")
extra_headers = {"Accept": "*/*", **(extra_headers or {})}
return await self._put(
- f"/browsers/{id}/fs/set_file_permissions",
+ path_template("/browsers/{id}/fs/set_file_permissions", id=id),
body=await async_maybe_transform(
{
"mode": mode,
@@ -1078,7 +1078,7 @@ async def upload(
# multipart/form-data; boundary=---abc--
extra_headers["Content-Type"] = "multipart/form-data"
return await self._post(
- f"/browsers/{id}/fs/upload",
+ path_template("/browsers/{id}/fs/upload", id=id),
body=await async_maybe_transform(body, f_upload_params.FUploadParams),
files=extracted_files,
options=make_request_options(
@@ -1129,7 +1129,7 @@ async def upload_zip(
# multipart/form-data; boundary=---abc--
extra_headers["Content-Type"] = "multipart/form-data"
return await self._post(
- f"/browsers/{id}/fs/upload_zip",
+ path_template("/browsers/{id}/fs/upload_zip", id=id),
body=await async_maybe_transform(body, f_upload_zip_params.FUploadZipParams),
files=files,
options=make_request_options(
@@ -1173,7 +1173,7 @@ async def write_file(
extra_headers = {"Accept": "*/*", **(extra_headers or {})}
extra_headers["Content-Type"] = "application/octet-stream"
return await self._put(
- f"/browsers/{id}/fs/write_file",
+ path_template("/browsers/{id}/fs/write_file", id=id),
content=await async_read_file_content(contents) if isinstance(contents, os.PathLike) else contents,
options=make_request_options(
extra_headers=extra_headers,
diff --git a/src/kernel/resources/browsers/fs/watch.py b/src/kernel/resources/browsers/fs/watch.py
index ca43867..bc04605 100644
--- a/src/kernel/resources/browsers/fs/watch.py
+++ b/src/kernel/resources/browsers/fs/watch.py
@@ -5,7 +5,7 @@
import httpx
from ...._types import Body, Omit, Query, Headers, NoneType, NotGiven, omit, not_given
-from ...._utils import maybe_transform, async_maybe_transform
+from ...._utils import path_template, maybe_transform, async_maybe_transform
from ...._compat import cached_property
from ...._resource import SyncAPIResource, AsyncAPIResource
from ...._response import (
@@ -75,7 +75,7 @@ def events(
raise ValueError(f"Expected a non-empty value for `watch_id` but received {watch_id!r}")
extra_headers = {"Accept": "text/event-stream", **(extra_headers or {})}
return self._get(
- f"/browsers/{id}/fs/watch/{watch_id}/events",
+ path_template("/browsers/{id}/fs/watch/{watch_id}/events", id=id, watch_id=watch_id),
options=make_request_options(
extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
),
@@ -116,7 +116,7 @@ def start(
if not id:
raise ValueError(f"Expected a non-empty value for `id` but received {id!r}")
return self._post(
- f"/browsers/{id}/fs/watch",
+ path_template("/browsers/{id}/fs/watch", id=id),
body=maybe_transform(
{
"path": path,
@@ -160,7 +160,7 @@ def stop(
raise ValueError(f"Expected a non-empty value for `watch_id` but received {watch_id!r}")
extra_headers = {"Accept": "*/*", **(extra_headers or {})}
return self._delete(
- f"/browsers/{id}/fs/watch/{watch_id}",
+ path_template("/browsers/{id}/fs/watch/{watch_id}", id=id, watch_id=watch_id),
options=make_request_options(
extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
),
@@ -220,7 +220,7 @@ async def events(
raise ValueError(f"Expected a non-empty value for `watch_id` but received {watch_id!r}")
extra_headers = {"Accept": "text/event-stream", **(extra_headers or {})}
return await self._get(
- f"/browsers/{id}/fs/watch/{watch_id}/events",
+ path_template("/browsers/{id}/fs/watch/{watch_id}/events", id=id, watch_id=watch_id),
options=make_request_options(
extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
),
@@ -261,7 +261,7 @@ async def start(
if not id:
raise ValueError(f"Expected a non-empty value for `id` but received {id!r}")
return await self._post(
- f"/browsers/{id}/fs/watch",
+ path_template("/browsers/{id}/fs/watch", id=id),
body=await async_maybe_transform(
{
"path": path,
@@ -305,7 +305,7 @@ async def stop(
raise ValueError(f"Expected a non-empty value for `watch_id` but received {watch_id!r}")
extra_headers = {"Accept": "*/*", **(extra_headers or {})}
return await self._delete(
- f"/browsers/{id}/fs/watch/{watch_id}",
+ path_template("/browsers/{id}/fs/watch/{watch_id}", id=id, watch_id=watch_id),
options=make_request_options(
extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
),
diff --git a/src/kernel/resources/browsers/logs.py b/src/kernel/resources/browsers/logs.py
index 0132855..35ee66d 100644
--- a/src/kernel/resources/browsers/logs.py
+++ b/src/kernel/resources/browsers/logs.py
@@ -7,7 +7,7 @@
import httpx
from ..._types import Body, Omit, Query, Headers, NotGiven, omit, not_given
-from ..._utils import maybe_transform, async_maybe_transform
+from ..._utils import path_template, maybe_transform, async_maybe_transform
from ..._compat import cached_property
from ..._resource import SyncAPIResource, AsyncAPIResource
from ..._response import (
@@ -81,7 +81,7 @@ def stream(
raise ValueError(f"Expected a non-empty value for `id` but received {id!r}")
extra_headers = {"Accept": "text/event-stream", **(extra_headers or {})}
return self._get(
- f"/browsers/{id}/logs/stream",
+ path_template("/browsers/{id}/logs/stream", id=id),
options=make_request_options(
extra_headers=extra_headers,
extra_query=extra_query,
@@ -160,7 +160,7 @@ async def stream(
raise ValueError(f"Expected a non-empty value for `id` but received {id!r}")
extra_headers = {"Accept": "text/event-stream", **(extra_headers or {})}
return await self._get(
- f"/browsers/{id}/logs/stream",
+ path_template("/browsers/{id}/logs/stream", id=id),
options=make_request_options(
extra_headers=extra_headers,
extra_query=extra_query,
diff --git a/src/kernel/resources/browsers/playwright.py b/src/kernel/resources/browsers/playwright.py
index 6979a9d..8d261ed 100644
--- a/src/kernel/resources/browsers/playwright.py
+++ b/src/kernel/resources/browsers/playwright.py
@@ -5,7 +5,7 @@
import httpx
from ..._types import Body, Omit, Query, Headers, NotGiven, omit, not_given
-from ..._utils import maybe_transform, async_maybe_transform
+from ..._utils import path_template, maybe_transform, async_maybe_transform
from ..._compat import cached_property
from ..._resource import SyncAPIResource, AsyncAPIResource
from ..._response import (
@@ -82,7 +82,7 @@ def execute(
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",
+ path_template("/browsers/{id}/playwright/execute", id=id),
body=maybe_transform(
{
"code": code,
@@ -158,7 +158,7 @@ async def execute(
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",
+ path_template("/browsers/{id}/playwright/execute", id=id),
body=await async_maybe_transform(
{
"code": code,
diff --git a/src/kernel/resources/browsers/process.py b/src/kernel/resources/browsers/process.py
index 86752a5..83827d3 100644
--- a/src/kernel/resources/browsers/process.py
+++ b/src/kernel/resources/browsers/process.py
@@ -8,7 +8,7 @@
import httpx
from ..._types import Body, Omit, Query, Headers, NotGiven, SequenceNotStr, omit, not_given
-from ..._utils import maybe_transform, async_maybe_transform
+from ..._utils import path_template, maybe_transform, async_maybe_transform
from ..._compat import cached_property
from ..._resource import SyncAPIResource, AsyncAPIResource
from ..._response import (
@@ -106,7 +106,7 @@ def exec(
if not id:
raise ValueError(f"Expected a non-empty value for `id` but received {id!r}")
return self._post(
- f"/browsers/{id}/process/exec",
+ path_template("/browsers/{id}/process/exec", id=id),
body=maybe_transform(
{
"command": command,
@@ -157,7 +157,7 @@ def kill(
if not process_id:
raise ValueError(f"Expected a non-empty value for `process_id` but received {process_id!r}")
return self._post(
- f"/browsers/{id}/process/{process_id}/kill",
+ path_template("/browsers/{id}/process/{process_id}/kill", id=id, process_id=process_id),
body=maybe_transform({"signal": signal}, process_kill_params.ProcessKillParams),
options=make_request_options(
extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
@@ -200,7 +200,7 @@ def resize(
if not process_id:
raise ValueError(f"Expected a non-empty value for `process_id` but received {process_id!r}")
return self._post(
- f"/browsers/{id}/process/{process_id}/resize",
+ path_template("/browsers/{id}/process/{process_id}/resize", id=id, process_id=process_id),
body=maybe_transform(
{
"cols": cols,
@@ -270,7 +270,7 @@ def spawn(
if not id:
raise ValueError(f"Expected a non-empty value for `id` but received {id!r}")
return self._post(
- f"/browsers/{id}/process/spawn",
+ path_template("/browsers/{id}/process/spawn", id=id),
body=maybe_transform(
{
"command": command,
@@ -321,7 +321,7 @@ def status(
if not process_id:
raise ValueError(f"Expected a non-empty value for `process_id` but received {process_id!r}")
return self._get(
- f"/browsers/{id}/process/{process_id}/status",
+ path_template("/browsers/{id}/process/{process_id}/status", id=id, process_id=process_id),
options=make_request_options(
extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
),
@@ -360,7 +360,7 @@ def stdin(
if not process_id:
raise ValueError(f"Expected a non-empty value for `process_id` but received {process_id!r}")
return self._post(
- f"/browsers/{id}/process/{process_id}/stdin",
+ path_template("/browsers/{id}/process/{process_id}/stdin", id=id, process_id=process_id),
body=maybe_transform({"data_b64": data_b64}, process_stdin_params.ProcessStdinParams),
options=make_request_options(
extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
@@ -398,7 +398,7 @@ def stdout_stream(
raise ValueError(f"Expected a non-empty value for `process_id` but received {process_id!r}")
extra_headers = {"Accept": "text/event-stream", **(extra_headers or {})}
return self._get(
- f"/browsers/{id}/process/{process_id}/stdout/stream",
+ path_template("/browsers/{id}/process/{process_id}/stdout/stream", id=id, process_id=process_id),
options=make_request_options(
extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
),
@@ -477,7 +477,7 @@ async def exec(
if not id:
raise ValueError(f"Expected a non-empty value for `id` but received {id!r}")
return await self._post(
- f"/browsers/{id}/process/exec",
+ path_template("/browsers/{id}/process/exec", id=id),
body=await async_maybe_transform(
{
"command": command,
@@ -528,7 +528,7 @@ async def kill(
if not process_id:
raise ValueError(f"Expected a non-empty value for `process_id` but received {process_id!r}")
return await self._post(
- f"/browsers/{id}/process/{process_id}/kill",
+ path_template("/browsers/{id}/process/{process_id}/kill", id=id, process_id=process_id),
body=await async_maybe_transform({"signal": signal}, process_kill_params.ProcessKillParams),
options=make_request_options(
extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
@@ -571,7 +571,7 @@ async def resize(
if not process_id:
raise ValueError(f"Expected a non-empty value for `process_id` but received {process_id!r}")
return await self._post(
- f"/browsers/{id}/process/{process_id}/resize",
+ path_template("/browsers/{id}/process/{process_id}/resize", id=id, process_id=process_id),
body=await async_maybe_transform(
{
"cols": cols,
@@ -641,7 +641,7 @@ async def spawn(
if not id:
raise ValueError(f"Expected a non-empty value for `id` but received {id!r}")
return await self._post(
- f"/browsers/{id}/process/spawn",
+ path_template("/browsers/{id}/process/spawn", id=id),
body=await async_maybe_transform(
{
"command": command,
@@ -692,7 +692,7 @@ async def status(
if not process_id:
raise ValueError(f"Expected a non-empty value for `process_id` but received {process_id!r}")
return await self._get(
- f"/browsers/{id}/process/{process_id}/status",
+ path_template("/browsers/{id}/process/{process_id}/status", id=id, process_id=process_id),
options=make_request_options(
extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
),
@@ -731,7 +731,7 @@ async def stdin(
if not process_id:
raise ValueError(f"Expected a non-empty value for `process_id` but received {process_id!r}")
return await self._post(
- f"/browsers/{id}/process/{process_id}/stdin",
+ path_template("/browsers/{id}/process/{process_id}/stdin", id=id, process_id=process_id),
body=await async_maybe_transform({"data_b64": data_b64}, process_stdin_params.ProcessStdinParams),
options=make_request_options(
extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
@@ -769,7 +769,7 @@ async def stdout_stream(
raise ValueError(f"Expected a non-empty value for `process_id` but received {process_id!r}")
extra_headers = {"Accept": "text/event-stream", **(extra_headers or {})}
return await self._get(
- f"/browsers/{id}/process/{process_id}/stdout/stream",
+ path_template("/browsers/{id}/process/{process_id}/stdout/stream", id=id, process_id=process_id),
options=make_request_options(
extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
),
diff --git a/src/kernel/resources/browsers/replays.py b/src/kernel/resources/browsers/replays.py
index 743a666..2b20953 100644
--- a/src/kernel/resources/browsers/replays.py
+++ b/src/kernel/resources/browsers/replays.py
@@ -5,7 +5,7 @@
import httpx
from ..._types import Body, Omit, Query, Headers, NoneType, NotGiven, omit, not_given
-from ..._utils import maybe_transform, async_maybe_transform
+from ..._utils import path_template, maybe_transform, async_maybe_transform
from ..._compat import cached_property
from ..._resource import SyncAPIResource, AsyncAPIResource
from ..._response import (
@@ -78,7 +78,7 @@ def list(
if not id:
raise ValueError(f"Expected a non-empty value for `id` but received {id!r}")
return self._get(
- f"/browsers/{id}/replays",
+ path_template("/browsers/{id}/replays", id=id),
options=make_request_options(
extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
),
@@ -115,7 +115,7 @@ def download(
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}",
+ path_template("/browsers/{id}/replays/{replay_id}", id=id, replay_id=replay_id),
options=make_request_options(
extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
),
@@ -154,7 +154,7 @@ def start(
if not id:
raise ValueError(f"Expected a non-empty value for `id` but received {id!r}")
return self._post(
- f"/browsers/{id}/replays",
+ path_template("/browsers/{id}/replays", id=id),
body=maybe_transform(
{
"framerate": framerate,
@@ -198,7 +198,7 @@ def stop(
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",
+ path_template("/browsers/{id}/replays/{replay_id}/stop", id=id, replay_id=replay_id),
options=make_request_options(
extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
),
@@ -254,7 +254,7 @@ async def list(
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",
+ path_template("/browsers/{id}/replays", id=id),
options=make_request_options(
extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
),
@@ -291,7 +291,7 @@ async def download(
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}",
+ path_template("/browsers/{id}/replays/{replay_id}", id=id, replay_id=replay_id),
options=make_request_options(
extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
),
@@ -330,7 +330,7 @@ async def start(
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",
+ path_template("/browsers/{id}/replays", id=id),
body=await async_maybe_transform(
{
"framerate": framerate,
@@ -374,7 +374,7 @@ async def stop(
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",
+ path_template("/browsers/{id}/replays/{replay_id}/stop", id=id, replay_id=replay_id),
options=make_request_options(
extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
),
diff --git a/src/kernel/resources/credential_providers.py b/src/kernel/resources/credential_providers.py
index c7ad4b0..2dede2c 100644
--- a/src/kernel/resources/credential_providers.py
+++ b/src/kernel/resources/credential_providers.py
@@ -8,7 +8,7 @@
from ..types import credential_provider_create_params, credential_provider_update_params
from .._types import Body, Omit, Query, Headers, NoneType, NotGiven, omit, not_given
-from .._utils import maybe_transform, async_maybe_transform
+from .._utils import path_template, maybe_transform, async_maybe_transform
from .._compat import cached_property
from .._resource import SyncAPIResource, AsyncAPIResource
from .._response import (
@@ -126,7 +126,7 @@ def retrieve(
if not id:
raise ValueError(f"Expected a non-empty value for `id` but received {id!r}")
return self._get(
- f"/org/credential_providers/{id}",
+ path_template("/org/credential_providers/{id}", id=id),
options=make_request_options(
extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
),
@@ -174,7 +174,7 @@ def update(
if not id:
raise ValueError(f"Expected a non-empty value for `id` but received {id!r}")
return self._patch(
- f"/org/credential_providers/{id}",
+ path_template("/org/credential_providers/{id}", id=id),
body=maybe_transform(
{
"token": token,
@@ -237,7 +237,7 @@ def delete(
raise ValueError(f"Expected a non-empty value for `id` but received {id!r}")
extra_headers = {"Accept": "*/*", **(extra_headers or {})}
return self._delete(
- f"/org/credential_providers/{id}",
+ path_template("/org/credential_providers/{id}", id=id),
options=make_request_options(
extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
),
@@ -271,7 +271,7 @@ def list_items(
if not id:
raise ValueError(f"Expected a non-empty value for `id` but received {id!r}")
return self._get(
- f"/org/credential_providers/{id}/items",
+ path_template("/org/credential_providers/{id}/items", id=id),
options=make_request_options(
extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
),
@@ -304,7 +304,7 @@ def test(
if not id:
raise ValueError(f"Expected a non-empty value for `id` but received {id!r}")
return self._post(
- f"/org/credential_providers/{id}/test",
+ path_template("/org/credential_providers/{id}/test", id=id),
options=make_request_options(
extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
),
@@ -412,7 +412,7 @@ async def retrieve(
if not id:
raise ValueError(f"Expected a non-empty value for `id` but received {id!r}")
return await self._get(
- f"/org/credential_providers/{id}",
+ path_template("/org/credential_providers/{id}", id=id),
options=make_request_options(
extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
),
@@ -460,7 +460,7 @@ async def update(
if not id:
raise ValueError(f"Expected a non-empty value for `id` but received {id!r}")
return await self._patch(
- f"/org/credential_providers/{id}",
+ path_template("/org/credential_providers/{id}", id=id),
body=await async_maybe_transform(
{
"token": token,
@@ -523,7 +523,7 @@ async def delete(
raise ValueError(f"Expected a non-empty value for `id` but received {id!r}")
extra_headers = {"Accept": "*/*", **(extra_headers or {})}
return await self._delete(
- f"/org/credential_providers/{id}",
+ path_template("/org/credential_providers/{id}", id=id),
options=make_request_options(
extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
),
@@ -557,7 +557,7 @@ async def list_items(
if not id:
raise ValueError(f"Expected a non-empty value for `id` but received {id!r}")
return await self._get(
- f"/org/credential_providers/{id}/items",
+ path_template("/org/credential_providers/{id}/items", id=id),
options=make_request_options(
extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
),
@@ -590,7 +590,7 @@ async def test(
if not id:
raise ValueError(f"Expected a non-empty value for `id` but received {id!r}")
return await self._post(
- f"/org/credential_providers/{id}/test",
+ path_template("/org/credential_providers/{id}/test", id=id),
options=make_request_options(
extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
),
diff --git a/src/kernel/resources/credentials.py b/src/kernel/resources/credentials.py
index 000c767..093fcc5 100644
--- a/src/kernel/resources/credentials.py
+++ b/src/kernel/resources/credentials.py
@@ -8,7 +8,7 @@
from ..types import credential_list_params, credential_create_params, credential_update_params
from .._types import Body, Omit, Query, Headers, NoneType, NotGiven, omit, not_given
-from .._utils import maybe_transform, async_maybe_transform
+from .._utils import path_template, maybe_transform, async_maybe_transform
from .._compat import cached_property
from .._resource import SyncAPIResource, AsyncAPIResource
from .._response import (
@@ -133,7 +133,7 @@ def retrieve(
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(
- f"/credentials/{id_or_name}",
+ path_template("/credentials/{id_or_name}", id_or_name=id_or_name),
options=make_request_options(
extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
),
@@ -183,7 +183,7 @@ def update(
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._patch(
- f"/credentials/{id_or_name}",
+ path_template("/credentials/{id_or_name}", id_or_name=id_or_name),
body=maybe_transform(
{
"name": name,
@@ -279,7 +279,7 @@ def delete(
raise ValueError(f"Expected a non-empty value for `id_or_name` but received {id_or_name!r}")
extra_headers = {"Accept": "*/*", **(extra_headers or {})}
return self._delete(
- f"/credentials/{id_or_name}",
+ path_template("/credentials/{id_or_name}", id_or_name=id_or_name),
options=make_request_options(
extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
),
@@ -314,7 +314,7 @@ def totp_code(
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(
- f"/credentials/{id_or_name}/totp-code",
+ path_template("/credentials/{id_or_name}/totp-code", id_or_name=id_or_name),
options=make_request_options(
extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
),
@@ -430,7 +430,7 @@ async def retrieve(
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(
- f"/credentials/{id_or_name}",
+ path_template("/credentials/{id_or_name}", id_or_name=id_or_name),
options=make_request_options(
extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
),
@@ -480,7 +480,7 @@ async def update(
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._patch(
- f"/credentials/{id_or_name}",
+ path_template("/credentials/{id_or_name}", id_or_name=id_or_name),
body=await async_maybe_transform(
{
"name": name,
@@ -576,7 +576,7 @@ async def delete(
raise ValueError(f"Expected a non-empty value for `id_or_name` but received {id_or_name!r}")
extra_headers = {"Accept": "*/*", **(extra_headers or {})}
return await self._delete(
- f"/credentials/{id_or_name}",
+ path_template("/credentials/{id_or_name}", id_or_name=id_or_name),
options=make_request_options(
extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
),
@@ -611,7 +611,7 @@ async def totp_code(
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(
- f"/credentials/{id_or_name}/totp-code",
+ path_template("/credentials/{id_or_name}/totp-code", id_or_name=id_or_name),
options=make_request_options(
extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
),
diff --git a/src/kernel/resources/deployments.py b/src/kernel/resources/deployments.py
index b6a72d2..6b2c760 100644
--- a/src/kernel/resources/deployments.py
+++ b/src/kernel/resources/deployments.py
@@ -9,7 +9,7 @@
from ..types import deployment_list_params, deployment_create_params, deployment_follow_params
from .._types import Body, Omit, Query, Headers, NoneType, NotGiven, FileTypes, omit, not_given
-from .._utils import extract_files, maybe_transform, deepcopy_minimal, async_maybe_transform
+from .._utils import extract_files, path_template, maybe_transform, deepcopy_minimal, async_maybe_transform
from .._compat import cached_property
from .._resource import SyncAPIResource, AsyncAPIResource
from .._response import (
@@ -147,7 +147,7 @@ def retrieve(
if not id:
raise ValueError(f"Expected a non-empty value for `id` but received {id!r}")
return self._get(
- f"/deployments/{id}",
+ path_template("/deployments/{id}", id=id),
options=make_request_options(
extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
),
@@ -239,7 +239,7 @@ def delete(
raise ValueError(f"Expected a non-empty value for `id` but received {id!r}")
extra_headers = {"Accept": "*/*", **(extra_headers or {})}
return self._delete(
- f"/deployments/{id}",
+ path_template("/deployments/{id}", id=id),
options=make_request_options(
extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
),
@@ -278,7 +278,7 @@ def follow(
raise ValueError(f"Expected a non-empty value for `id` but received {id!r}")
extra_headers = {"Accept": "text/event-stream", **(extra_headers or {})}
return self._get(
- f"/deployments/{id}/events",
+ path_template("/deployments/{id}/events", id=id),
options=make_request_options(
extra_headers=extra_headers,
extra_query=extra_query,
@@ -412,7 +412,7 @@ async def retrieve(
if not id:
raise ValueError(f"Expected a non-empty value for `id` but received {id!r}")
return await self._get(
- f"/deployments/{id}",
+ path_template("/deployments/{id}", id=id),
options=make_request_options(
extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
),
@@ -504,7 +504,7 @@ async def delete(
raise ValueError(f"Expected a non-empty value for `id` but received {id!r}")
extra_headers = {"Accept": "*/*", **(extra_headers or {})}
return await self._delete(
- f"/deployments/{id}",
+ path_template("/deployments/{id}", id=id),
options=make_request_options(
extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
),
@@ -543,7 +543,7 @@ async def follow(
raise ValueError(f"Expected a non-empty value for `id` but received {id!r}")
extra_headers = {"Accept": "text/event-stream", **(extra_headers or {})}
return await self._get(
- f"/deployments/{id}/events",
+ path_template("/deployments/{id}/events", id=id),
options=make_request_options(
extra_headers=extra_headers,
extra_query=extra_query,
diff --git a/src/kernel/resources/extensions.py b/src/kernel/resources/extensions.py
index ffdef29..c429b3c 100644
--- a/src/kernel/resources/extensions.py
+++ b/src/kernel/resources/extensions.py
@@ -9,7 +9,7 @@
from ..types import extension_upload_params, extension_download_from_chrome_store_params
from .._types import Body, Omit, Query, Headers, NoneType, NotGiven, FileTypes, omit, not_given
-from .._utils import extract_files, maybe_transform, deepcopy_minimal, async_maybe_transform
+from .._utils import extract_files, path_template, maybe_transform, deepcopy_minimal, async_maybe_transform
from .._compat import cached_property
from .._resource import SyncAPIResource, AsyncAPIResource
from .._response import (
@@ -101,7 +101,7 @@ def delete(
raise ValueError(f"Expected a non-empty value for `id_or_name` but received {id_or_name!r}")
extra_headers = {"Accept": "*/*", **(extra_headers or {})}
return self._delete(
- f"/extensions/{id_or_name}",
+ path_template("/extensions/{id_or_name}", id_or_name=id_or_name),
options=make_request_options(
extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
),
@@ -135,7 +135,7 @@ def download(
raise ValueError(f"Expected a non-empty value for `id_or_name` but received {id_or_name!r}")
extra_headers = {"Accept": "application/octet-stream", **(extra_headers or {})}
return self._get(
- f"/extensions/{id_or_name}",
+ path_template("/extensions/{id_or_name}", id_or_name=id_or_name),
options=make_request_options(
extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
),
@@ -310,7 +310,7 @@ async def delete(
raise ValueError(f"Expected a non-empty value for `id_or_name` but received {id_or_name!r}")
extra_headers = {"Accept": "*/*", **(extra_headers or {})}
return await self._delete(
- f"/extensions/{id_or_name}",
+ path_template("/extensions/{id_or_name}", id_or_name=id_or_name),
options=make_request_options(
extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
),
@@ -344,7 +344,7 @@ async def download(
raise ValueError(f"Expected a non-empty value for `id_or_name` but received {id_or_name!r}")
extra_headers = {"Accept": "application/octet-stream", **(extra_headers or {})}
return await self._get(
- f"/extensions/{id_or_name}",
+ path_template("/extensions/{id_or_name}", id_or_name=id_or_name),
options=make_request_options(
extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
),
diff --git a/src/kernel/resources/invocations.py b/src/kernel/resources/invocations.py
index 25be409..6d367e9 100644
--- a/src/kernel/resources/invocations.py
+++ b/src/kernel/resources/invocations.py
@@ -9,7 +9,7 @@
from ..types import invocation_list_params, invocation_create_params, invocation_follow_params, invocation_update_params
from .._types import Body, Omit, Query, Headers, NoneType, NotGiven, omit, not_given
-from .._utils import maybe_transform, async_maybe_transform
+from .._utils import path_template, maybe_transform, async_maybe_transform
from .._compat import cached_property
from .._resource import SyncAPIResource, AsyncAPIResource
from .._response import (
@@ -140,7 +140,7 @@ def retrieve(
if not id:
raise ValueError(f"Expected a non-empty value for `id` but received {id!r}")
return self._get(
- f"/invocations/{id}",
+ path_template("/invocations/{id}", id=id),
options=make_request_options(
extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
),
@@ -181,7 +181,7 @@ def update(
if not id:
raise ValueError(f"Expected a non-empty value for `id` but received {id!r}")
return self._patch(
- f"/invocations/{id}",
+ path_template("/invocations/{id}", id=id),
body=maybe_transform(
{
"status": status,
@@ -296,7 +296,7 @@ def delete_browsers(
raise ValueError(f"Expected a non-empty value for `id` but received {id!r}")
extra_headers = {"Accept": "*/*", **(extra_headers or {})}
return self._delete(
- f"/invocations/{id}/browsers",
+ path_template("/invocations/{id}/browsers", id=id),
options=make_request_options(
extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
),
@@ -335,7 +335,7 @@ def follow(
raise ValueError(f"Expected a non-empty value for `id` but received {id!r}")
extra_headers = {"Accept": "text/event-stream", **(extra_headers or {})}
return self._get(
- f"/invocations/{id}/events",
+ path_template("/invocations/{id}/events", id=id),
options=make_request_options(
extra_headers=extra_headers,
extra_query=extra_query,
@@ -376,7 +376,7 @@ def list_browsers(
if not id:
raise ValueError(f"Expected a non-empty value for `id` but received {id!r}")
return self._get(
- f"/invocations/{id}/browsers",
+ path_template("/invocations/{id}/browsers", id=id),
options=make_request_options(
extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
),
@@ -493,7 +493,7 @@ async def retrieve(
if not id:
raise ValueError(f"Expected a non-empty value for `id` but received {id!r}")
return await self._get(
- f"/invocations/{id}",
+ path_template("/invocations/{id}", id=id),
options=make_request_options(
extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
),
@@ -534,7 +534,7 @@ async def update(
if not id:
raise ValueError(f"Expected a non-empty value for `id` but received {id!r}")
return await self._patch(
- f"/invocations/{id}",
+ path_template("/invocations/{id}", id=id),
body=await async_maybe_transform(
{
"status": status,
@@ -649,7 +649,7 @@ async def delete_browsers(
raise ValueError(f"Expected a non-empty value for `id` but received {id!r}")
extra_headers = {"Accept": "*/*", **(extra_headers or {})}
return await self._delete(
- f"/invocations/{id}/browsers",
+ path_template("/invocations/{id}/browsers", id=id),
options=make_request_options(
extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
),
@@ -688,7 +688,7 @@ async def follow(
raise ValueError(f"Expected a non-empty value for `id` but received {id!r}")
extra_headers = {"Accept": "text/event-stream", **(extra_headers or {})}
return await self._get(
- f"/invocations/{id}/events",
+ path_template("/invocations/{id}/events", id=id),
options=make_request_options(
extra_headers=extra_headers,
extra_query=extra_query,
@@ -729,7 +729,7 @@ async def list_browsers(
if not id:
raise ValueError(f"Expected a non-empty value for `id` but received {id!r}")
return await self._get(
- f"/invocations/{id}/browsers",
+ path_template("/invocations/{id}/browsers", id=id),
options=make_request_options(
extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
),
diff --git a/src/kernel/resources/profiles.py b/src/kernel/resources/profiles.py
index f75569f..ec3d3fc 100644
--- a/src/kernel/resources/profiles.py
+++ b/src/kernel/resources/profiles.py
@@ -6,7 +6,7 @@
from ..types import profile_list_params, profile_create_params
from .._types import Body, Omit, Query, Headers, NoneType, NotGiven, omit, not_given
-from .._utils import maybe_transform, async_maybe_transform
+from .._utils import path_template, maybe_transform, async_maybe_transform
from .._compat import cached_property
from .._resource import SyncAPIResource, AsyncAPIResource
from .._response import (
@@ -113,7 +113,7 @@ def retrieve(
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(
- f"/profiles/{id_or_name}",
+ path_template("/profiles/{id_or_name}", id_or_name=id_or_name),
options=make_request_options(
extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
),
@@ -198,7 +198,7 @@ def delete(
raise ValueError(f"Expected a non-empty value for `id_or_name` but received {id_or_name!r}")
extra_headers = {"Accept": "*/*", **(extra_headers or {})}
return self._delete(
- f"/profiles/{id_or_name}",
+ path_template("/profiles/{id_or_name}", id_or_name=id_or_name),
options=make_request_options(
extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
),
@@ -234,7 +234,7 @@ def download(
raise ValueError(f"Expected a non-empty value for `id_or_name` but received {id_or_name!r}")
extra_headers = {"Accept": "application/octet-stream", **(extra_headers or {})}
return self._get(
- f"/profiles/{id_or_name}/download",
+ path_template("/profiles/{id_or_name}/download", id_or_name=id_or_name),
options=make_request_options(
extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
),
@@ -325,7 +325,7 @@ async def retrieve(
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(
- f"/profiles/{id_or_name}",
+ path_template("/profiles/{id_or_name}", id_or_name=id_or_name),
options=make_request_options(
extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
),
@@ -410,7 +410,7 @@ async def delete(
raise ValueError(f"Expected a non-empty value for `id_or_name` but received {id_or_name!r}")
extra_headers = {"Accept": "*/*", **(extra_headers or {})}
return await self._delete(
- f"/profiles/{id_or_name}",
+ path_template("/profiles/{id_or_name}", id_or_name=id_or_name),
options=make_request_options(
extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
),
@@ -446,7 +446,7 @@ async def download(
raise ValueError(f"Expected a non-empty value for `id_or_name` but received {id_or_name!r}")
extra_headers = {"Accept": "application/octet-stream", **(extra_headers or {})}
return await self._get(
- f"/profiles/{id_or_name}/download",
+ path_template("/profiles/{id_or_name}/download", id_or_name=id_or_name),
options=make_request_options(
extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
),
diff --git a/src/kernel/resources/proxies.py b/src/kernel/resources/proxies.py
index 0c2508c..f259d6e 100644
--- a/src/kernel/resources/proxies.py
+++ b/src/kernel/resources/proxies.py
@@ -8,7 +8,7 @@
from ..types import proxy_create_params
from .._types import Body, Omit, Query, Headers, NoneType, NotGiven, SequenceNotStr, omit, not_given
-from .._utils import maybe_transform, async_maybe_transform
+from .._utils import path_template, maybe_transform, async_maybe_transform
from .._compat import cached_property
from .._resource import SyncAPIResource, AsyncAPIResource
from .._response import (
@@ -130,7 +130,7 @@ def retrieve(
if not id:
raise ValueError(f"Expected a non-empty value for `id` but received {id!r}")
return self._get(
- f"/proxies/{id}",
+ path_template("/proxies/{id}", id=id),
options=make_request_options(
extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
),
@@ -184,7 +184,7 @@ def delete(
raise ValueError(f"Expected a non-empty value for `id` but received {id!r}")
extra_headers = {"Accept": "*/*", **(extra_headers or {})}
return self._delete(
- f"/proxies/{id}",
+ path_template("/proxies/{id}", id=id),
options=make_request_options(
extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
),
@@ -217,7 +217,7 @@ def check(
if not id:
raise ValueError(f"Expected a non-empty value for `id` but received {id!r}")
return self._post(
- f"/proxies/{id}/check",
+ path_template("/proxies/{id}/check", id=id),
options=make_request_options(
extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
),
@@ -329,7 +329,7 @@ async def retrieve(
if not id:
raise ValueError(f"Expected a non-empty value for `id` but received {id!r}")
return await self._get(
- f"/proxies/{id}",
+ path_template("/proxies/{id}", id=id),
options=make_request_options(
extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
),
@@ -383,7 +383,7 @@ async def delete(
raise ValueError(f"Expected a non-empty value for `id` but received {id!r}")
extra_headers = {"Accept": "*/*", **(extra_headers or {})}
return await self._delete(
- f"/proxies/{id}",
+ path_template("/proxies/{id}", id=id),
options=make_request_options(
extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
),
@@ -416,7 +416,7 @@ async def check(
if not id:
raise ValueError(f"Expected a non-empty value for `id` but received {id!r}")
return await self._post(
- f"/proxies/{id}/check",
+ path_template("/proxies/{id}/check", id=id),
options=make_request_options(
extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
),
diff --git a/src/kernel/types/auth/__init__.py b/src/kernel/types/auth/__init__.py
index 51e505b..db89794 100644
--- a/src/kernel/types/auth/__init__.py
+++ b/src/kernel/types/auth/__init__.py
@@ -9,4 +9,5 @@
from .connection_login_params import ConnectionLoginParams as ConnectionLoginParams
from .connection_create_params import ConnectionCreateParams as ConnectionCreateParams
from .connection_submit_params import ConnectionSubmitParams as ConnectionSubmitParams
+from .connection_update_params import ConnectionUpdateParams as ConnectionUpdateParams
from .connection_follow_response import ConnectionFollowResponse as ConnectionFollowResponse
diff --git a/src/kernel/types/auth/connection_follow_response.py b/src/kernel/types/auth/connection_follow_response.py
index 06ffaea..4eeba5c 100644
--- a/src/kernel/types/auth/connection_follow_response.py
+++ b/src/kernel/types/auth/connection_follow_response.py
@@ -15,6 +15,7 @@
"ManagedAuthStateEventDiscoveredField",
"ManagedAuthStateEventMfaOption",
"ManagedAuthStateEventPendingSSOButton",
+ "ManagedAuthStateEventSignInOption",
]
@@ -77,6 +78,22 @@ class ManagedAuthStateEventPendingSSOButton(BaseModel):
"""XPath selector for the button"""
+class ManagedAuthStateEventSignInOption(BaseModel):
+ """A non-MFA choice presented during the auth flow (e.g.
+
+ account selection, org picker)
+ """
+
+ id: str
+ """Unique identifier for this option (used to submit selection back)"""
+
+ label: str
+ """Display text for the option"""
+
+ description: Optional[str] = None
+ """Additional context such as email address or org name"""
+
+
class ManagedAuthStateEvent(BaseModel):
"""An event representing the current state of a managed auth flow."""
@@ -128,6 +145,12 @@ class ManagedAuthStateEvent(BaseModel):
post_login_url: Optional[str] = None
"""URL where the browser landed after successful login."""
+ sign_in_options: Optional[List[ManagedAuthStateEventSignInOption]] = None
+ """
+ Non-MFA choices presented during the auth flow, such as account selection or org
+ pickers (present when flow_step=AWAITING_INPUT).
+ """
+
website_error: Optional[str] = None
"""Visible error message from the website (e.g., 'Incorrect password').
diff --git a/src/kernel/types/auth/connection_submit_params.py b/src/kernel/types/auth/connection_submit_params.py
index 0e2306a..f785b85 100644
--- a/src/kernel/types/auth/connection_submit_params.py
+++ b/src/kernel/types/auth/connection_submit_params.py
@@ -13,7 +13,19 @@ class ConnectionSubmitParams(TypedDict, total=False):
"""Map of field name to value"""
mfa_option_id: str
- """Optional MFA option ID if user selected an MFA method"""
+ """The MFA method type to select (when mfa_options were returned)"""
+
+ sign_in_option_id: str
+ """The sign-in option ID to select (when sign_in_options were returned)"""
sso_button_selector: str
- """Optional XPath selector if user chose to click an SSO button instead"""
+ """XPath selector for the SSO button to click (ODA).
+
+ Use sso_provider instead for CUA.
+ """
+
+ sso_provider: str
+ """
+ SSO provider to click, matching the provider field from pending_sso_buttons
+ (e.g., "google", "github"). Cannot be used with sso_button_selector.
+ """
diff --git a/src/kernel/types/auth/connection_update_params.py b/src/kernel/types/auth/connection_update_params.py
new file mode 100644
index 0000000..77e738b
--- /dev/null
+++ b/src/kernel/types/auth/connection_update_params.py
@@ -0,0 +1,72 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from __future__ import annotations
+
+from typing_extensions import TypedDict
+
+from ..._types import SequenceNotStr
+
+__all__ = ["ConnectionUpdateParams", "Credential", "Proxy"]
+
+
+class ConnectionUpdateParams(TypedDict, total=False):
+ allowed_domains: SequenceNotStr[str]
+ """Additional domains valid for this auth flow (replaces existing list)"""
+
+ credential: Credential
+ """Reference to credentials for the auth connection. Use one of:
+
+ - { name } for Kernel credentials
+ - { provider, path } for external provider item
+ - { provider, auto: true } for external provider domain lookup
+ """
+
+ health_check_interval: int
+ """Interval in seconds between automatic health checks"""
+
+ login_url: str
+ """Login page URL. Set to empty string to clear."""
+
+ proxy: Proxy
+ """Proxy selection.
+
+ Provide either id or name. The proxy must belong to the caller's org.
+ """
+
+ save_credentials: bool
+ """Whether to save credentials after every successful login"""
+
+
+class Credential(TypedDict, total=False):
+ """Reference to credentials for the auth connection.
+
+ Use one of:
+ - { name } for Kernel credentials
+ - { provider, path } for external provider item
+ - { provider, auto: true } for external provider domain lookup
+ """
+
+ auto: bool
+ """If true, lookup by domain from the specified provider"""
+
+ name: str
+ """Kernel credential name"""
+
+ path: str
+ """Provider-specific path (e.g., "VaultName/ItemName" for 1Password)"""
+
+ provider: str
+ """External provider name (e.g., "my-1p")"""
+
+
+class Proxy(TypedDict, total=False):
+ """Proxy selection.
+
+ Provide either id or name. The proxy must belong to the caller's org.
+ """
+
+ id: str
+ """Proxy ID"""
+
+ name: str
+ """Proxy name"""
diff --git a/src/kernel/types/auth/managed_auth.py b/src/kernel/types/auth/managed_auth.py
index de607c9..9f90bc5 100644
--- a/src/kernel/types/auth/managed_auth.py
+++ b/src/kernel/types/auth/managed_auth.py
@@ -6,7 +6,7 @@
from ..._models import BaseModel
-__all__ = ["ManagedAuth", "Credential", "DiscoveredField", "MfaOption", "PendingSSOButton"]
+__all__ = ["ManagedAuth", "Credential", "DiscoveredField", "MfaOption", "PendingSSOButton", "SignInOption"]
class Credential(BaseModel):
@@ -90,6 +90,22 @@ class PendingSSOButton(BaseModel):
"""XPath selector for the button"""
+class SignInOption(BaseModel):
+ """A non-MFA choice presented during the auth flow (e.g.
+
+ account selection, org picker)
+ """
+
+ id: str
+ """Unique identifier for this option (used to submit selection back)"""
+
+ label: str
+ """Display text for the option"""
+
+ description: Optional[str] = None
+ """Additional context such as email address or org name"""
+
+
class ManagedAuth(BaseModel):
"""Managed authentication that keeps a profile logged into a specific domain.
@@ -214,6 +230,12 @@ class ManagedAuth(BaseModel):
proxy_id: Optional[str] = None
"""ID of the proxy associated with this connection, if any."""
+ sign_in_options: Optional[List[SignInOption]] = None
+ """
+ Non-MFA choices presented during the auth flow, such as account selection or org
+ pickers (present when flow_step=awaiting_input).
+ """
+
sso_provider: Optional[str] = None
"""SSO provider being used (e.g., google, github, microsoft)"""
diff --git a/src/kernel/types/browser_create_params.py b/src/kernel/types/browser_create_params.py
index 6df2463..2827b1d 100644
--- a/src/kernel/types/browser_create_params.py
+++ b/src/kernel/types/browser_create_params.py
@@ -21,9 +21,9 @@ class BrowserCreateParams(TypedDict, total=False):
"""
gpu: bool
- """If true, launches a hardware-accelerated browser with GPU rendering.
+ """If true, enables GPU acceleration for the browser session.
- Requires Start-Up or Enterprise plan.
+ Requires Start-Up or Enterprise plan and headless=false.
"""
headless: bool
@@ -73,13 +73,17 @@ class BrowserCreateParams(TypedDict, total=False):
"""
viewport: BrowserViewport
- """Initial browser window size in pixels with optional refresh rate.
-
- If omitted, image defaults apply (1920x1080@25). Arbitrary viewport dimensions
- are accepted, but the following configurations are known-good and fully tested:
- 2560x1440@10, 1920x1080@25, 1920x1200@25, 1440x900@25, 1280x800@60, 1024x768@60,
- 1200x800@60. Viewports outside this list may exhibit unstable live view or
- recording behavior. If refresh_rate is not provided, it will be automatically
- determined based on the resolution (higher resolutions use lower refresh rates
- to keep bandwidth reasonable).
+ """
+ Initial browser window size in pixels with optional refresh rate. If omitted,
+ image defaults apply (1920x1080@25). For GPU images, the default is
+ 1920x1080@60. Arbitrary viewport dimensions and refresh rates are accepted.
+ Known-good presets include: 2560x1440@10, 1920x1080@25, 1920x1200@25,
+ 1440x900@25, 1280x800@60, 1024x768@60, 1200x800@60. For GPU images, recommended
+ presets use one of these resolutions with refresh rates 60, 30, 25, or 10:
+ 800x600, 960x720, 1024x576, 1024x768, 1152x648, 1200x800, 1280x720, 1368x768,
+ 1440x900, 1600x900, 1920x1080, 1920x1200, 390x844, 360x250, 768x1024, 800x1600.
+ Viewports outside this list may exhibit unstable live view or recording
+ behavior. If refresh_rate is not provided, it will be automatically determined
+ based on the resolution (higher resolutions use lower refresh rates to keep
+ bandwidth reasonable).
"""
diff --git a/src/kernel/types/browser_create_response.py b/src/kernel/types/browser_create_response.py
index bbfd9a2..d59a3d0 100644
--- a/src/kernel/types/browser_create_response.py
+++ b/src/kernel/types/browser_create_response.py
@@ -45,7 +45,10 @@ class BrowserCreateResponse(BaseModel):
"""When the browser session was soft-deleted. Only present for deleted sessions."""
gpu: Optional[bool] = None
- """Whether the browser session has hardware-accelerated GPU rendering."""
+ """
+ Whether GPU acceleration is enabled for the browser session (only supported for
+ headful sessions).
+ """
kiosk_mode: Optional[bool] = None
"""Whether the browser session is running in kiosk mode."""
@@ -66,13 +69,17 @@ class BrowserCreateResponse(BaseModel):
"""Session usage metrics."""
viewport: Optional[BrowserViewport] = None
- """Initial browser window size in pixels with optional refresh rate.
-
- If omitted, image defaults apply (1920x1080@25). Arbitrary viewport dimensions
- are accepted, but the following configurations are known-good and fully tested:
- 2560x1440@10, 1920x1080@25, 1920x1200@25, 1440x900@25, 1280x800@60, 1024x768@60,
- 1200x800@60. Viewports outside this list may exhibit unstable live view or
- recording behavior. If refresh_rate is not provided, it will be automatically
- determined based on the resolution (higher resolutions use lower refresh rates
- to keep bandwidth reasonable).
+ """
+ Initial browser window size in pixels with optional refresh rate. If omitted,
+ image defaults apply (1920x1080@25). For GPU images, the default is
+ 1920x1080@60. Arbitrary viewport dimensions and refresh rates are accepted.
+ Known-good presets include: 2560x1440@10, 1920x1080@25, 1920x1200@25,
+ 1440x900@25, 1280x800@60, 1024x768@60, 1200x800@60. For GPU images, recommended
+ presets use one of these resolutions with refresh rates 60, 30, 25, or 10:
+ 800x600, 960x720, 1024x576, 1024x768, 1152x648, 1200x800, 1280x720, 1368x768,
+ 1440x900, 1600x900, 1920x1080, 1920x1200, 390x844, 360x250, 768x1024, 800x1600.
+ Viewports outside this list may exhibit unstable live view or recording
+ behavior. If refresh_rate is not provided, it will be automatically determined
+ based on the resolution (higher resolutions use lower refresh rates to keep
+ bandwidth reasonable).
"""
diff --git a/src/kernel/types/browser_list_response.py b/src/kernel/types/browser_list_response.py
index 915df11..708caa9 100644
--- a/src/kernel/types/browser_list_response.py
+++ b/src/kernel/types/browser_list_response.py
@@ -45,7 +45,10 @@ class BrowserListResponse(BaseModel):
"""When the browser session was soft-deleted. Only present for deleted sessions."""
gpu: Optional[bool] = None
- """Whether the browser session has hardware-accelerated GPU rendering."""
+ """
+ Whether GPU acceleration is enabled for the browser session (only supported for
+ headful sessions).
+ """
kiosk_mode: Optional[bool] = None
"""Whether the browser session is running in kiosk mode."""
@@ -66,13 +69,17 @@ class BrowserListResponse(BaseModel):
"""Session usage metrics."""
viewport: Optional[BrowserViewport] = None
- """Initial browser window size in pixels with optional refresh rate.
-
- If omitted, image defaults apply (1920x1080@25). Arbitrary viewport dimensions
- are accepted, but the following configurations are known-good and fully tested:
- 2560x1440@10, 1920x1080@25, 1920x1200@25, 1440x900@25, 1280x800@60, 1024x768@60,
- 1200x800@60. Viewports outside this list may exhibit unstable live view or
- recording behavior. If refresh_rate is not provided, it will be automatically
- determined based on the resolution (higher resolutions use lower refresh rates
- to keep bandwidth reasonable).
+ """
+ Initial browser window size in pixels with optional refresh rate. If omitted,
+ image defaults apply (1920x1080@25). For GPU images, the default is
+ 1920x1080@60. Arbitrary viewport dimensions and refresh rates are accepted.
+ Known-good presets include: 2560x1440@10, 1920x1080@25, 1920x1200@25,
+ 1440x900@25, 1280x800@60, 1024x768@60, 1200x800@60. For GPU images, recommended
+ presets use one of these resolutions with refresh rates 60, 30, 25, or 10:
+ 800x600, 960x720, 1024x576, 1024x768, 1152x648, 1200x800, 1280x720, 1368x768,
+ 1440x900, 1600x900, 1920x1080, 1920x1200, 390x844, 360x250, 768x1024, 800x1600.
+ Viewports outside this list may exhibit unstable live view or recording
+ behavior. If refresh_rate is not provided, it will be automatically determined
+ based on the resolution (higher resolutions use lower refresh rates to keep
+ bandwidth reasonable).
"""
diff --git a/src/kernel/types/browser_pool.py b/src/kernel/types/browser_pool.py
index fc4e0f1..c6286ac 100644
--- a/src/kernel/types/browser_pool.py
+++ b/src/kernel/types/browser_pool.py
@@ -68,15 +68,19 @@ class BrowserPoolConfig(BaseModel):
"""
viewport: Optional[BrowserViewport] = None
- """Initial browser window size in pixels with optional refresh rate.
-
- If omitted, image defaults apply (1920x1080@25). Arbitrary viewport dimensions
- are accepted, but the following configurations are known-good and fully tested:
- 2560x1440@10, 1920x1080@25, 1920x1200@25, 1440x900@25, 1280x800@60, 1024x768@60,
- 1200x800@60. Viewports outside this list may exhibit unstable live view or
- recording behavior. If refresh_rate is not provided, it will be automatically
- determined based on the resolution (higher resolutions use lower refresh rates
- to keep bandwidth reasonable).
+ """
+ Initial browser window size in pixels with optional refresh rate. If omitted,
+ image defaults apply (1920x1080@25). For GPU images, the default is
+ 1920x1080@60. Arbitrary viewport dimensions and refresh rates are accepted.
+ Known-good presets include: 2560x1440@10, 1920x1080@25, 1920x1200@25,
+ 1440x900@25, 1280x800@60, 1024x768@60, 1200x800@60. For GPU images, recommended
+ presets use one of these resolutions with refresh rates 60, 30, 25, or 10:
+ 800x600, 960x720, 1024x576, 1024x768, 1152x648, 1200x800, 1280x720, 1368x768,
+ 1440x900, 1600x900, 1920x1080, 1920x1200, 390x844, 360x250, 768x1024, 800x1600.
+ Viewports outside this list may exhibit unstable live view or recording
+ behavior. If refresh_rate is not provided, it will be automatically determined
+ based on the resolution (higher resolutions use lower refresh rates to keep
+ bandwidth reasonable).
"""
diff --git a/src/kernel/types/browser_pool_acquire_response.py b/src/kernel/types/browser_pool_acquire_response.py
index 373274c..5ab52b5 100644
--- a/src/kernel/types/browser_pool_acquire_response.py
+++ b/src/kernel/types/browser_pool_acquire_response.py
@@ -45,7 +45,10 @@ class BrowserPoolAcquireResponse(BaseModel):
"""When the browser session was soft-deleted. Only present for deleted sessions."""
gpu: Optional[bool] = None
- """Whether the browser session has hardware-accelerated GPU rendering."""
+ """
+ Whether GPU acceleration is enabled for the browser session (only supported for
+ headful sessions).
+ """
kiosk_mode: Optional[bool] = None
"""Whether the browser session is running in kiosk mode."""
@@ -66,13 +69,17 @@ class BrowserPoolAcquireResponse(BaseModel):
"""Session usage metrics."""
viewport: Optional[BrowserViewport] = None
- """Initial browser window size in pixels with optional refresh rate.
-
- If omitted, image defaults apply (1920x1080@25). Arbitrary viewport dimensions
- are accepted, but the following configurations are known-good and fully tested:
- 2560x1440@10, 1920x1080@25, 1920x1200@25, 1440x900@25, 1280x800@60, 1024x768@60,
- 1200x800@60. Viewports outside this list may exhibit unstable live view or
- recording behavior. If refresh_rate is not provided, it will be automatically
- determined based on the resolution (higher resolutions use lower refresh rates
- to keep bandwidth reasonable).
+ """
+ Initial browser window size in pixels with optional refresh rate. If omitted,
+ image defaults apply (1920x1080@25). For GPU images, the default is
+ 1920x1080@60. Arbitrary viewport dimensions and refresh rates are accepted.
+ Known-good presets include: 2560x1440@10, 1920x1080@25, 1920x1200@25,
+ 1440x900@25, 1280x800@60, 1024x768@60, 1200x800@60. For GPU images, recommended
+ presets use one of these resolutions with refresh rates 60, 30, 25, or 10:
+ 800x600, 960x720, 1024x576, 1024x768, 1152x648, 1200x800, 1280x720, 1368x768,
+ 1440x900, 1600x900, 1920x1080, 1920x1200, 390x844, 360x250, 768x1024, 800x1600.
+ Viewports outside this list may exhibit unstable live view or recording
+ behavior. If refresh_rate is not provided, it will be automatically determined
+ based on the resolution (higher resolutions use lower refresh rates to keep
+ bandwidth reasonable).
"""
diff --git a/src/kernel/types/browser_pool_create_params.py b/src/kernel/types/browser_pool_create_params.py
index 78268a5..63ef712 100644
--- a/src/kernel/types/browser_pool_create_params.py
+++ b/src/kernel/types/browser_pool_create_params.py
@@ -67,13 +67,17 @@ class BrowserPoolCreateParams(TypedDict, total=False):
"""
viewport: BrowserViewport
- """Initial browser window size in pixels with optional refresh rate.
-
- If omitted, image defaults apply (1920x1080@25). Arbitrary viewport dimensions
- are accepted, but the following configurations are known-good and fully tested:
- 2560x1440@10, 1920x1080@25, 1920x1200@25, 1440x900@25, 1280x800@60, 1024x768@60,
- 1200x800@60. Viewports outside this list may exhibit unstable live view or
- recording behavior. If refresh_rate is not provided, it will be automatically
- determined based on the resolution (higher resolutions use lower refresh rates
- to keep bandwidth reasonable).
+ """
+ Initial browser window size in pixels with optional refresh rate. If omitted,
+ image defaults apply (1920x1080@25). For GPU images, the default is
+ 1920x1080@60. Arbitrary viewport dimensions and refresh rates are accepted.
+ Known-good presets include: 2560x1440@10, 1920x1080@25, 1920x1200@25,
+ 1440x900@25, 1280x800@60, 1024x768@60, 1200x800@60. For GPU images, recommended
+ presets use one of these resolutions with refresh rates 60, 30, 25, or 10:
+ 800x600, 960x720, 1024x576, 1024x768, 1152x648, 1200x800, 1280x720, 1368x768,
+ 1440x900, 1600x900, 1920x1080, 1920x1200, 390x844, 360x250, 768x1024, 800x1600.
+ Viewports outside this list may exhibit unstable live view or recording
+ behavior. If refresh_rate is not provided, it will be automatically determined
+ based on the resolution (higher resolutions use lower refresh rates to keep
+ bandwidth reasonable).
"""
diff --git a/src/kernel/types/browser_pool_update_params.py b/src/kernel/types/browser_pool_update_params.py
index 74b76a6..d1f003b 100644
--- a/src/kernel/types/browser_pool_update_params.py
+++ b/src/kernel/types/browser_pool_update_params.py
@@ -73,13 +73,17 @@ class BrowserPoolUpdateParams(TypedDict, total=False):
"""
viewport: BrowserViewport
- """Initial browser window size in pixels with optional refresh rate.
-
- If omitted, image defaults apply (1920x1080@25). Arbitrary viewport dimensions
- are accepted, but the following configurations are known-good and fully tested:
- 2560x1440@10, 1920x1080@25, 1920x1200@25, 1440x900@25, 1280x800@60, 1024x768@60,
- 1200x800@60. Viewports outside this list may exhibit unstable live view or
- recording behavior. If refresh_rate is not provided, it will be automatically
- determined based on the resolution (higher resolutions use lower refresh rates
- to keep bandwidth reasonable).
+ """
+ Initial browser window size in pixels with optional refresh rate. If omitted,
+ image defaults apply (1920x1080@25). For GPU images, the default is
+ 1920x1080@60. Arbitrary viewport dimensions and refresh rates are accepted.
+ Known-good presets include: 2560x1440@10, 1920x1080@25, 1920x1200@25,
+ 1440x900@25, 1280x800@60, 1024x768@60, 1200x800@60. For GPU images, recommended
+ presets use one of these resolutions with refresh rates 60, 30, 25, or 10:
+ 800x600, 960x720, 1024x576, 1024x768, 1152x648, 1200x800, 1280x720, 1368x768,
+ 1440x900, 1600x900, 1920x1080, 1920x1200, 390x844, 360x250, 768x1024, 800x1600.
+ Viewports outside this list may exhibit unstable live view or recording
+ behavior. If refresh_rate is not provided, it will be automatically determined
+ based on the resolution (higher resolutions use lower refresh rates to keep
+ bandwidth reasonable).
"""
diff --git a/src/kernel/types/browser_retrieve_response.py b/src/kernel/types/browser_retrieve_response.py
index 76fc6ce..221eab5 100644
--- a/src/kernel/types/browser_retrieve_response.py
+++ b/src/kernel/types/browser_retrieve_response.py
@@ -45,7 +45,10 @@ class BrowserRetrieveResponse(BaseModel):
"""When the browser session was soft-deleted. Only present for deleted sessions."""
gpu: Optional[bool] = None
- """Whether the browser session has hardware-accelerated GPU rendering."""
+ """
+ Whether GPU acceleration is enabled for the browser session (only supported for
+ headful sessions).
+ """
kiosk_mode: Optional[bool] = None
"""Whether the browser session is running in kiosk mode."""
@@ -66,13 +69,17 @@ class BrowserRetrieveResponse(BaseModel):
"""Session usage metrics."""
viewport: Optional[BrowserViewport] = None
- """Initial browser window size in pixels with optional refresh rate.
-
- If omitted, image defaults apply (1920x1080@25). Arbitrary viewport dimensions
- are accepted, but the following configurations are known-good and fully tested:
- 2560x1440@10, 1920x1080@25, 1920x1200@25, 1440x900@25, 1280x800@60, 1024x768@60,
- 1200x800@60. Viewports outside this list may exhibit unstable live view or
- recording behavior. If refresh_rate is not provided, it will be automatically
- determined based on the resolution (higher resolutions use lower refresh rates
- to keep bandwidth reasonable).
+ """
+ Initial browser window size in pixels with optional refresh rate. If omitted,
+ image defaults apply (1920x1080@25). For GPU images, the default is
+ 1920x1080@60. Arbitrary viewport dimensions and refresh rates are accepted.
+ Known-good presets include: 2560x1440@10, 1920x1080@25, 1920x1200@25,
+ 1440x900@25, 1280x800@60, 1024x768@60, 1200x800@60. For GPU images, recommended
+ presets use one of these resolutions with refresh rates 60, 30, 25, or 10:
+ 800x600, 960x720, 1024x576, 1024x768, 1152x648, 1200x800, 1280x720, 1368x768,
+ 1440x900, 1600x900, 1920x1080, 1920x1200, 390x844, 360x250, 768x1024, 800x1600.
+ Viewports outside this list may exhibit unstable live view or recording
+ behavior. If refresh_rate is not provided, it will be automatically determined
+ based on the resolution (higher resolutions use lower refresh rates to keep
+ bandwidth reasonable).
"""
diff --git a/src/kernel/types/browser_update_response.py b/src/kernel/types/browser_update_response.py
index fdf4fb5..c8a85c3 100644
--- a/src/kernel/types/browser_update_response.py
+++ b/src/kernel/types/browser_update_response.py
@@ -45,7 +45,10 @@ class BrowserUpdateResponse(BaseModel):
"""When the browser session was soft-deleted. Only present for deleted sessions."""
gpu: Optional[bool] = None
- """Whether the browser session has hardware-accelerated GPU rendering."""
+ """
+ Whether GPU acceleration is enabled for the browser session (only supported for
+ headful sessions).
+ """
kiosk_mode: Optional[bool] = None
"""Whether the browser session is running in kiosk mode."""
@@ -66,13 +69,17 @@ class BrowserUpdateResponse(BaseModel):
"""Session usage metrics."""
viewport: Optional[BrowserViewport] = None
- """Initial browser window size in pixels with optional refresh rate.
-
- If omitted, image defaults apply (1920x1080@25). Arbitrary viewport dimensions
- are accepted, but the following configurations are known-good and fully tested:
- 2560x1440@10, 1920x1080@25, 1920x1200@25, 1440x900@25, 1280x800@60, 1024x768@60,
- 1200x800@60. Viewports outside this list may exhibit unstable live view or
- recording behavior. If refresh_rate is not provided, it will be automatically
- determined based on the resolution (higher resolutions use lower refresh rates
- to keep bandwidth reasonable).
+ """
+ Initial browser window size in pixels with optional refresh rate. If omitted,
+ image defaults apply (1920x1080@25). For GPU images, the default is
+ 1920x1080@60. Arbitrary viewport dimensions and refresh rates are accepted.
+ Known-good presets include: 2560x1440@10, 1920x1080@25, 1920x1200@25,
+ 1440x900@25, 1280x800@60, 1024x768@60, 1200x800@60. For GPU images, recommended
+ presets use one of these resolutions with refresh rates 60, 30, 25, or 10:
+ 800x600, 960x720, 1024x576, 1024x768, 1152x648, 1200x800, 1280x720, 1368x768,
+ 1440x900, 1600x900, 1920x1080, 1920x1200, 390x844, 360x250, 768x1024, 800x1600.
+ Viewports outside this list may exhibit unstable live view or recording
+ behavior. If refresh_rate is not provided, it will be automatically determined
+ based on the resolution (higher resolutions use lower refresh rates to keep
+ bandwidth reasonable).
"""
diff --git a/src/kernel/types/browsers/computer_batch_params.py b/src/kernel/types/browsers/computer_batch_params.py
index 9aca724..7fc6abb 100644
--- a/src/kernel/types/browsers/computer_batch_params.py
+++ b/src/kernel/types/browsers/computer_batch_params.py
@@ -59,9 +59,21 @@ class ActionDragMouse(TypedDict, total=False):
delay: int
"""Delay in milliseconds between button down and starting to move along the path."""
+ duration_ms: int
+ """
+ Target total duration in milliseconds for the entire drag movement when
+ smooth=true. Omit for automatic timing based on total path length.
+ """
+
hold_keys: SequenceNotStr[str]
"""Modifier keys to hold during the drag"""
+ smooth: bool
+ """
+ Use human-like Bezier curves between path waypoints instead of linear
+ interpolation. When true, steps_per_segment and step_delay_ms are ignored.
+ """
+
step_delay_ms: int
"""
Delay in milliseconds between relative steps while dragging (not the initial
@@ -119,10 +131,16 @@ class ActionScroll(TypedDict, total=False):
"""Y coordinate at which to perform the scroll"""
delta_x: int
- """Horizontal scroll amount. Positive scrolls right, negative scrolls left."""
+ """
+ Horizontal scroll amount in xdotool "wheel units." Positive scrolls right,
+ negative scrolls left.
+ """
delta_y: int
- """Vertical scroll amount. Positive scrolls down, negative scrolls up."""
+ """
+ Vertical scroll amount in xdotool "wheel units." Positive scrolls down, negative
+ scrolls up.
+ """
hold_keys: SequenceNotStr[str]
"""Modifier keys to hold during the scroll"""
diff --git a/src/kernel/types/browsers/computer_drag_mouse_params.py b/src/kernel/types/browsers/computer_drag_mouse_params.py
index fb03b4b..c0dd4c8 100644
--- a/src/kernel/types/browsers/computer_drag_mouse_params.py
+++ b/src/kernel/types/browsers/computer_drag_mouse_params.py
@@ -23,9 +23,21 @@ class ComputerDragMouseParams(TypedDict, total=False):
delay: int
"""Delay in milliseconds between button down and starting to move along the path."""
+ duration_ms: int
+ """
+ Target total duration in milliseconds for the entire drag movement when
+ smooth=true. Omit for automatic timing based on total path length.
+ """
+
hold_keys: SequenceNotStr[str]
"""Modifier keys to hold during the drag"""
+ smooth: bool
+ """
+ Use human-like Bezier curves between path waypoints instead of linear
+ interpolation. When true, steps_per_segment and step_delay_ms are ignored.
+ """
+
step_delay_ms: int
"""
Delay in milliseconds between relative steps while dragging (not the initial
diff --git a/src/kernel/types/browsers/computer_scroll_params.py b/src/kernel/types/browsers/computer_scroll_params.py
index 110cb30..3af38af 100644
--- a/src/kernel/types/browsers/computer_scroll_params.py
+++ b/src/kernel/types/browsers/computer_scroll_params.py
@@ -17,10 +17,16 @@ class ComputerScrollParams(TypedDict, total=False):
"""Y coordinate at which to perform the scroll"""
delta_x: int
- """Horizontal scroll amount. Positive scrolls right, negative scrolls left."""
+ """
+ Horizontal scroll amount in xdotool "wheel units." Positive scrolls right,
+ negative scrolls left.
+ """
delta_y: int
- """Vertical scroll amount. Positive scrolls down, negative scrolls up."""
+ """
+ Vertical scroll amount in xdotool "wheel units." Positive scrolls down, negative
+ scrolls up.
+ """
hold_keys: SequenceNotStr[str]
"""Modifier keys to hold during the scroll"""
diff --git a/src/kernel/types/invocation_list_browsers_response.py b/src/kernel/types/invocation_list_browsers_response.py
index 673615b..a0fed9a 100644
--- a/src/kernel/types/invocation_list_browsers_response.py
+++ b/src/kernel/types/invocation_list_browsers_response.py
@@ -45,7 +45,10 @@ class Browser(BaseModel):
"""When the browser session was soft-deleted. Only present for deleted sessions."""
gpu: Optional[bool] = None
- """Whether the browser session has hardware-accelerated GPU rendering."""
+ """
+ Whether GPU acceleration is enabled for the browser session (only supported for
+ headful sessions).
+ """
kiosk_mode: Optional[bool] = None
"""Whether the browser session is running in kiosk mode."""
@@ -66,15 +69,19 @@ class Browser(BaseModel):
"""Session usage metrics."""
viewport: Optional[BrowserViewport] = None
- """Initial browser window size in pixels with optional refresh rate.
-
- If omitted, image defaults apply (1920x1080@25). Arbitrary viewport dimensions
- are accepted, but the following configurations are known-good and fully tested:
- 2560x1440@10, 1920x1080@25, 1920x1200@25, 1440x900@25, 1280x800@60, 1024x768@60,
- 1200x800@60. Viewports outside this list may exhibit unstable live view or
- recording behavior. If refresh_rate is not provided, it will be automatically
- determined based on the resolution (higher resolutions use lower refresh rates
- to keep bandwidth reasonable).
+ """
+ Initial browser window size in pixels with optional refresh rate. If omitted,
+ image defaults apply (1920x1080@25). For GPU images, the default is
+ 1920x1080@60. Arbitrary viewport dimensions and refresh rates are accepted.
+ Known-good presets include: 2560x1440@10, 1920x1080@25, 1920x1200@25,
+ 1440x900@25, 1280x800@60, 1024x768@60, 1200x800@60. For GPU images, recommended
+ presets use one of these resolutions with refresh rates 60, 30, 25, or 10:
+ 800x600, 960x720, 1024x576, 1024x768, 1152x648, 1200x800, 1280x720, 1368x768,
+ 1440x900, 1600x900, 1920x1080, 1920x1200, 390x844, 360x250, 768x1024, 800x1600.
+ Viewports outside this list may exhibit unstable live view or recording
+ behavior. If refresh_rate is not provided, it will be automatically determined
+ based on the resolution (higher resolutions use lower refresh rates to keep
+ bandwidth reasonable).
"""
diff --git a/src/kernel/types/shared/browser_viewport.py b/src/kernel/types/shared/browser_viewport.py
index dacac1f..c53505b 100644
--- a/src/kernel/types/shared/browser_viewport.py
+++ b/src/kernel/types/shared/browser_viewport.py
@@ -8,11 +8,15 @@
class BrowserViewport(BaseModel):
- """Initial browser window size in pixels with optional refresh rate.
-
+ """
+ Initial browser window size in pixels with optional refresh rate.
If omitted, image defaults apply (1920x1080@25).
- Arbitrary viewport dimensions are accepted, but the following configurations are known-good and fully tested:
+ For GPU images, the default is 1920x1080@60.
+ Arbitrary viewport dimensions and refresh rates are accepted.
+ Known-good presets include:
2560x1440@10, 1920x1080@25, 1920x1200@25, 1440x900@25, 1280x800@60, 1024x768@60, 1200x800@60.
+ For GPU images, recommended presets use one of these resolutions with refresh rates 60, 30, 25, or 10:
+ 800x600, 960x720, 1024x576, 1024x768, 1152x648, 1200x800, 1280x720, 1368x768, 1440x900, 1600x900, 1920x1080, 1920x1200, 390x844, 360x250, 768x1024, 800x1600.
Viewports outside this list may exhibit unstable live view or recording behavior.
If refresh_rate is not provided, it will be automatically determined based on the resolution
(higher resolutions use lower refresh rates to keep bandwidth reasonable).
diff --git a/src/kernel/types/shared_params/browser_viewport.py b/src/kernel/types/shared_params/browser_viewport.py
index f98ece8..2290f93 100644
--- a/src/kernel/types/shared_params/browser_viewport.py
+++ b/src/kernel/types/shared_params/browser_viewport.py
@@ -8,11 +8,15 @@
class BrowserViewport(TypedDict, total=False):
- """Initial browser window size in pixels with optional refresh rate.
-
+ """
+ Initial browser window size in pixels with optional refresh rate.
If omitted, image defaults apply (1920x1080@25).
- Arbitrary viewport dimensions are accepted, but the following configurations are known-good and fully tested:
+ For GPU images, the default is 1920x1080@60.
+ Arbitrary viewport dimensions and refresh rates are accepted.
+ Known-good presets include:
2560x1440@10, 1920x1080@25, 1920x1200@25, 1440x900@25, 1280x800@60, 1024x768@60, 1200x800@60.
+ For GPU images, recommended presets use one of these resolutions with refresh rates 60, 30, 25, or 10:
+ 800x600, 960x720, 1024x576, 1024x768, 1152x648, 1200x800, 1280x720, 1368x768, 1440x900, 1600x900, 1920x1080, 1920x1200, 390x844, 360x250, 768x1024, 800x1600.
Viewports outside this list may exhibit unstable live view or recording behavior.
If refresh_rate is not provided, it will be automatically determined based on the resolution
(higher resolutions use lower refresh rates to keep bandwidth reasonable).
diff --git a/tests/api_resources/auth/test_connections.py b/tests/api_resources/auth/test_connections.py
index e71a173..f5edd89 100644
--- a/tests/api_resources/auth/test_connections.py
+++ b/tests/api_resources/auth/test_connections.py
@@ -124,6 +124,70 @@ def test_path_params_retrieve(self, client: Kernel) -> None:
"",
)
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ def test_method_update(self, client: Kernel) -> None:
+ connection = client.auth.connections.update(
+ id="id",
+ )
+ assert_matches_type(ManagedAuth, connection, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ def test_method_update_with_all_params(self, client: Kernel) -> None:
+ connection = client.auth.connections.update(
+ id="id",
+ allowed_domains=["login.netflix.com", "auth.netflix.com"],
+ credential={
+ "auto": True,
+ "name": "my-netflix-creds",
+ "path": "Personal/Netflix",
+ "provider": "my-1p",
+ },
+ health_check_interval=3600,
+ login_url="https://netflix.com/login",
+ proxy={
+ "id": "id",
+ "name": "name",
+ },
+ save_credentials=True,
+ )
+ assert_matches_type(ManagedAuth, connection, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ def test_raw_response_update(self, client: Kernel) -> None:
+ response = client.auth.connections.with_raw_response.update(
+ id="id",
+ )
+
+ assert response.is_closed is True
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+ connection = response.parse()
+ assert_matches_type(ManagedAuth, connection, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ def test_streaming_response_update(self, client: Kernel) -> None:
+ with client.auth.connections.with_streaming_response.update(
+ id="id",
+ ) as response:
+ assert not response.is_closed
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+
+ connection = response.parse()
+ assert_matches_type(ManagedAuth, connection, path=["response"])
+
+ assert cast(Any, response.is_closed) is True
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ def test_path_params_update(self, client: Kernel) -> None:
+ with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"):
+ client.auth.connections.with_raw_response.update(
+ id="",
+ )
+
@pytest.mark.skip(reason="Mock server tests are disabled")
@parametrize
def test_method_list(self, client: Kernel) -> None:
@@ -318,7 +382,9 @@ def test_method_submit_with_all_params(self, client: Kernel) -> None:
"password": "secret",
},
mfa_option_id="sms",
+ sign_in_option_id="work-account",
sso_button_selector="xpath=//button[contains(text(), 'Continue with Google')]",
+ sso_provider="google",
)
assert_matches_type(SubmitFieldsResponse, connection, path=["response"])
@@ -464,6 +530,70 @@ async def test_path_params_retrieve(self, async_client: AsyncKernel) -> None:
"",
)
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ async def test_method_update(self, async_client: AsyncKernel) -> None:
+ connection = await async_client.auth.connections.update(
+ id="id",
+ )
+ assert_matches_type(ManagedAuth, connection, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ async def test_method_update_with_all_params(self, async_client: AsyncKernel) -> None:
+ connection = await async_client.auth.connections.update(
+ id="id",
+ allowed_domains=["login.netflix.com", "auth.netflix.com"],
+ credential={
+ "auto": True,
+ "name": "my-netflix-creds",
+ "path": "Personal/Netflix",
+ "provider": "my-1p",
+ },
+ health_check_interval=3600,
+ login_url="https://netflix.com/login",
+ proxy={
+ "id": "id",
+ "name": "name",
+ },
+ save_credentials=True,
+ )
+ assert_matches_type(ManagedAuth, connection, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ async def test_raw_response_update(self, async_client: AsyncKernel) -> None:
+ response = await async_client.auth.connections.with_raw_response.update(
+ id="id",
+ )
+
+ assert response.is_closed is True
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+ connection = await response.parse()
+ assert_matches_type(ManagedAuth, connection, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ async def test_streaming_response_update(self, async_client: AsyncKernel) -> None:
+ async with async_client.auth.connections.with_streaming_response.update(
+ id="id",
+ ) as response:
+ assert not response.is_closed
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+
+ connection = await response.parse()
+ assert_matches_type(ManagedAuth, connection, 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_update(self, async_client: AsyncKernel) -> None:
+ with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"):
+ await async_client.auth.connections.with_raw_response.update(
+ id="",
+ )
+
@pytest.mark.skip(reason="Mock server tests are disabled")
@parametrize
async def test_method_list(self, async_client: AsyncKernel) -> None:
@@ -658,7 +788,9 @@ async def test_method_submit_with_all_params(self, async_client: AsyncKernel) ->
"password": "secret",
},
mfa_option_id="sms",
+ sign_in_option_id="work-account",
sso_button_selector="xpath=//button[contains(text(), 'Continue with Google')]",
+ sso_provider="google",
)
assert_matches_type(SubmitFieldsResponse, connection, path=["response"])
diff --git a/tests/api_resources/browsers/test_computer.py b/tests/api_resources/browsers/test_computer.py
index 09960bf..31974d5 100644
--- a/tests/api_resources/browsers/test_computer.py
+++ b/tests/api_resources/browsers/test_computer.py
@@ -224,7 +224,9 @@ def test_method_drag_mouse_with_all_params(self, client: Kernel) -> None:
path=[[0, 0], [0, 0]],
button="left",
delay=0,
+ duration_ms=50,
hold_keys=["string"],
+ smooth=True,
step_delay_ms=0,
steps_per_segment=1,
)
@@ -887,7 +889,9 @@ async def test_method_drag_mouse_with_all_params(self, async_client: AsyncKernel
path=[[0, 0], [0, 0]],
button="left",
delay=0,
+ duration_ms=50,
hold_keys=["string"],
+ smooth=True,
step_delay_ms=0,
steps_per_segment=1,
)
diff --git a/tests/test_utils/test_path.py b/tests/test_utils/test_path.py
new file mode 100644
index 0000000..80b8680
--- /dev/null
+++ b/tests/test_utils/test_path.py
@@ -0,0 +1,89 @@
+from __future__ import annotations
+
+from typing import Any
+
+import pytest
+
+from kernel._utils._path import path_template
+
+
+@pytest.mark.parametrize(
+ "template, kwargs, expected",
+ [
+ ("/v1/{id}", dict(id="abc"), "/v1/abc"),
+ ("/v1/{a}/{b}", dict(a="x", b="y"), "/v1/x/y"),
+ ("/v1/{a}{b}/path/{c}?val={d}#{e}", dict(a="x", b="y", c="z", d="u", e="v"), "/v1/xy/path/z?val=u#v"),
+ ("/{w}/{w}", dict(w="echo"), "/echo/echo"),
+ ("/v1/static", {}, "/v1/static"),
+ ("", {}, ""),
+ ("/v1/?q={n}&count=10", dict(n=42), "/v1/?q=42&count=10"),
+ ("/v1/{v}", dict(v=None), "/v1/null"),
+ ("/v1/{v}", dict(v=True), "/v1/true"),
+ ("/v1/{v}", dict(v=False), "/v1/false"),
+ ("/v1/{v}", dict(v=".hidden"), "/v1/.hidden"), # dot prefix ok
+ ("/v1/{v}", dict(v="file.txt"), "/v1/file.txt"), # dot in middle ok
+ ("/v1/{v}", dict(v="..."), "/v1/..."), # triple dot ok
+ ("/v1/{a}{b}", dict(a=".", b="txt"), "/v1/.txt"), # dot var combining with adjacent to be ok
+ ("/items?q={v}#{f}", dict(v=".", f=".."), "/items?q=.#.."), # dots in query/fragment are fine
+ (
+ "/v1/{a}?query={b}",
+ dict(a="../../other/endpoint", b="a&bad=true"),
+ "/v1/..%2F..%2Fother%2Fendpoint?query=a%26bad%3Dtrue",
+ ),
+ ("/v1/{val}", dict(val="a/b/c"), "/v1/a%2Fb%2Fc"),
+ ("/v1/{val}", dict(val="a/b/c?query=value"), "/v1/a%2Fb%2Fc%3Fquery=value"),
+ ("/v1/{val}", dict(val="a/b/c?query=value&bad=true"), "/v1/a%2Fb%2Fc%3Fquery=value&bad=true"),
+ ("/v1/{val}", dict(val="%20"), "/v1/%2520"), # escapes escape sequences in input
+ # Query: slash and ? are safe, # is not
+ ("/items?q={v}", dict(v="a/b"), "/items?q=a/b"),
+ ("/items?q={v}", dict(v="a?b"), "/items?q=a?b"),
+ ("/items?q={v}", dict(v="a#b"), "/items?q=a%23b"),
+ ("/items?q={v}", dict(v="a b"), "/items?q=a%20b"),
+ # Fragment: slash and ? are safe
+ ("/docs#{v}", dict(v="a/b"), "/docs#a/b"),
+ ("/docs#{v}", dict(v="a?b"), "/docs#a?b"),
+ # Path: slash, ? and # are all encoded
+ ("/v1/{v}", dict(v="a/b"), "/v1/a%2Fb"),
+ ("/v1/{v}", dict(v="a?b"), "/v1/a%3Fb"),
+ ("/v1/{v}", dict(v="a#b"), "/v1/a%23b"),
+ # same var encoded differently by component
+ (
+ "/v1/{v}?q={v}#{v}",
+ dict(v="a/b?c#d"),
+ "/v1/a%2Fb%3Fc%23d?q=a/b?c%23d#a/b?c%23d",
+ ),
+ ("/v1/{val}", dict(val="x?admin=true"), "/v1/x%3Fadmin=true"), # query injection
+ ("/v1/{val}", dict(val="x#admin"), "/v1/x%23admin"), # fragment injection
+ ],
+)
+def test_interpolation(template: str, kwargs: dict[str, Any], expected: str) -> None:
+ assert path_template(template, **kwargs) == expected
+
+
+def test_missing_kwarg_raises_key_error() -> None:
+ with pytest.raises(KeyError, match="org_id"):
+ path_template("/v1/{org_id}")
+
+
+@pytest.mark.parametrize(
+ "template, kwargs",
+ [
+ ("{a}/path", dict(a=".")),
+ ("{a}/path", dict(a="..")),
+ ("/v1/{a}", dict(a=".")),
+ ("/v1/{a}", dict(a="..")),
+ ("/v1/{a}/path", dict(a=".")),
+ ("/v1/{a}/path", dict(a="..")),
+ ("/v1/{a}{b}", dict(a=".", b=".")), # adjacent vars → ".."
+ ("/v1/{a}.", dict(a=".")), # var + static → ".."
+ ("/v1/{a}{b}", dict(a="", b=".")), # empty + dot → "."
+ ("/v1/%2e/{x}", dict(x="ok")), # encoded dot in static text
+ ("/v1/%2e./{x}", dict(x="ok")), # mixed encoded ".." in static
+ ("/v1/.%2E/{x}", dict(x="ok")), # mixed encoded ".." in static
+ ("/v1/{v}?q=1", dict(v="..")),
+ ("/v1/{v}#frag", dict(v="..")),
+ ],
+)
+def test_dot_segment_rejected(template: str, kwargs: dict[str, Any]) -> None:
+ with pytest.raises(ValueError, match="dot-segment"):
+ path_template(template, **kwargs)