From ad4df347086518a52fe7f34ad43ab5608b1583a3 Mon Sep 17 00:00:00 2001
From: "stainless-app[bot]"
<142633134+stainless-app[bot]@users.noreply.github.com>
Date: Wed, 13 May 2026 02:41:09 +0000
Subject: [PATCH 001/366] ci: pin GitHub Actions to commit SHAs
Pin all GitHub Actions referenced in generated workflows (both
first-party `actions/*` and third-party) to immutable commit SHAs.
Updating pinned actions is now a deliberate codegen-side bump rather
than implicit on every workflow run.
---
.github/workflows/ci.yml | 8 ++++----
.github/workflows/publish-pypi.yml | 2 +-
.github/workflows/release-doctor.yml | 2 +-
3 files changed, 6 insertions(+), 6 deletions(-)
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index fafebca7..ae545691 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -21,7 +21,7 @@ jobs:
runs-on: ${{ github.repository == 'stainless-sdks/kernel-python' && 'depot-ubuntu-24.04' || 'ubuntu-latest' }}
if: (github.event_name == 'push' || github.event.pull_request.head.repo.fork) && (github.event_name != 'push' || github.event.head_commit.message != 'codegen metadata')
steps:
- - uses: actions/checkout@v6
+ - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Install Rye
run: |
@@ -46,7 +46,7 @@ jobs:
id-token: write
runs-on: ${{ github.repository == 'stainless-sdks/kernel-python' && 'depot-ubuntu-24.04' || 'ubuntu-latest' }}
steps:
- - uses: actions/checkout@v6
+ - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Install Rye
run: |
@@ -67,7 +67,7 @@ jobs:
github.repository == 'stainless-sdks/kernel-python' &&
!startsWith(github.ref, 'refs/heads/stl/')
id: github-oidc
- uses: actions/github-script@v8
+ uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
with:
script: core.setOutput('github_token', await core.getIDToken());
@@ -87,7 +87,7 @@ jobs:
runs-on: ${{ github.repository == 'stainless-sdks/kernel-python' && 'depot-ubuntu-24.04' || 'ubuntu-latest' }}
if: github.event_name == 'push' || github.event.pull_request.head.repo.fork
steps:
- - uses: actions/checkout@v6
+ - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Install Rye
run: |
diff --git a/.github/workflows/publish-pypi.yml b/.github/workflows/publish-pypi.yml
index 2e95b5a9..2e7e7190 100644
--- a/.github/workflows/publish-pypi.yml
+++ b/.github/workflows/publish-pypi.yml
@@ -14,7 +14,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- - uses: actions/checkout@v6
+ - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Install Rye
run: |
diff --git a/.github/workflows/release-doctor.yml b/.github/workflows/release-doctor.yml
index 48941b69..057d4a7f 100644
--- a/.github/workflows/release-doctor.yml
+++ b/.github/workflows/release-doctor.yml
@@ -12,7 +12,7 @@ jobs:
if: github.repository == 'kernel/kernel-python-sdk' && (github.event_name == 'push' || github.event_name == 'workflow_dispatch' || startsWith(github.head_ref, 'release-please') || github.head_ref == 'next')
steps:
- - uses: actions/checkout@v6
+ - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Check release environment
run: |
From eda8f0dc702c684140ce8d2c0aa5360bd427395c Mon Sep 17 00:00:00 2001
From: "stainless-app[bot]"
<142633134+stainless-app[bot]@users.noreply.github.com>
Date: Thu, 14 May 2026 18:20:27 +0000
Subject: [PATCH 002/366] feat: Polish start URL OpenAPI descriptions
---
.stats.yml | 4 +--
src/kernel/resources/browser_pools.py | 32 +++++++------------
src/kernel/resources/browsers/browsers.py | 16 ++++------
src/kernel/types/browser_create_params.py | 8 ++---
src/kernel/types/browser_create_response.py | 5 +--
src/kernel/types/browser_list_response.py | 5 +--
src/kernel/types/browser_pool.py | 8 ++---
.../types/browser_pool_acquire_response.py | 5 +--
.../types/browser_pool_create_params.py | 8 ++---
.../types/browser_pool_update_params.py | 8 ++---
src/kernel/types/browser_retrieve_response.py | 5 +--
src/kernel/types/browser_update_response.py | 5 +--
.../invocation_list_browsers_response.py | 5 +--
13 files changed, 38 insertions(+), 76 deletions(-)
diff --git a/.stats.yml b/.stats.yml
index 7915e042..35cab471 100644
--- a/.stats.yml
+++ b/.stats.yml
@@ -1,4 +1,4 @@
configured_endpoints: 112
-openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel/kernel-a33e59aa1758ba51f13538838ecd70b0a23ed69739b3022e8c2ce0622e42b904.yml
-openapi_spec_hash: c042d2f6880c927be09aa9fa79d7241e
+openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel/kernel-b7a19ff1fbd93322c8cffcd0b397ce2536ca8bff91594e0081bd030d4bec879f.yml
+openapi_spec_hash: 490520e6f0a8b1ebc89e9c0add46082d
config_hash: 08d55086449943a8fec212b870061a3f
diff --git a/src/kernel/resources/browser_pools.py b/src/kernel/resources/browser_pools.py
index 10b23923..682b2936 100644
--- a/src/kernel/resources/browser_pools.py
+++ b/src/kernel/resources/browser_pools.py
@@ -110,11 +110,9 @@ def create(
proxy_id: Optional proxy to associate to the browser session. Must reference a proxy
belonging to the caller's org.
- start_url: Optional URL to navigate to when a new browser is warmed into the pool.
- Best-effort: failures to navigate do not fail pool fill. Only applied to
- newly-warmed browsers — browsers reused via release/acquire keep whatever URL
- the previous lease left them on. Accepts any URL Chromium can resolve, including
- chrome:// pages.
+ start_url: Optional URL to open when a browser is created for the pool. Navigation is
+ best-effort, so navigation failures do not prevent the pool from filling. Reused
+ browsers keep the page left by the previous lease.
stealth: If true, launches the browser in stealth mode to reduce detection by anti-bot
mechanisms.
@@ -261,11 +259,9 @@ def update(
proxy_id: Optional proxy to associate to the browser session. Must reference a proxy
belonging to the caller's org.
- start_url: Optional URL to navigate to when a new browser is warmed into the pool.
- Best-effort: failures to navigate do not fail pool fill. Only applied to
- newly-warmed browsers — browsers reused via release/acquire keep whatever URL
- the previous lease left them on. Accepts any URL Chromium can resolve, including
- chrome:// pages.
+ start_url: Optional URL to open when a browser is created for the pool. Navigation is
+ best-effort, so navigation failures do not prevent the pool from filling. Reused
+ browsers keep the page left by the previous lease.
stealth: If true, launches the browser in stealth mode to reduce detection by anti-bot
mechanisms.
@@ -588,11 +584,9 @@ async def create(
proxy_id: Optional proxy to associate to the browser session. Must reference a proxy
belonging to the caller's org.
- start_url: Optional URL to navigate to when a new browser is warmed into the pool.
- Best-effort: failures to navigate do not fail pool fill. Only applied to
- newly-warmed browsers — browsers reused via release/acquire keep whatever URL
- the previous lease left them on. Accepts any URL Chromium can resolve, including
- chrome:// pages.
+ start_url: Optional URL to open when a browser is created for the pool. Navigation is
+ best-effort, so navigation failures do not prevent the pool from filling. Reused
+ browsers keep the page left by the previous lease.
stealth: If true, launches the browser in stealth mode to reduce detection by anti-bot
mechanisms.
@@ -739,11 +733,9 @@ async def update(
proxy_id: Optional proxy to associate to the browser session. Must reference a proxy
belonging to the caller's org.
- start_url: Optional URL to navigate to when a new browser is warmed into the pool.
- Best-effort: failures to navigate do not fail pool fill. Only applied to
- newly-warmed browsers — browsers reused via release/acquire keep whatever URL
- the previous lease left them on. Accepts any URL Chromium can resolve, including
- chrome:// pages.
+ start_url: Optional URL to open when a browser is created for the pool. Navigation is
+ best-effort, so navigation failures do not prevent the pool from filling. Reused
+ browsers keep the page left by the previous lease.
stealth: If true, launches the browser in stealth mode to reduce detection by anti-bot
mechanisms.
diff --git a/src/kernel/resources/browsers/browsers.py b/src/kernel/resources/browsers/browsers.py
index d1dba086..82ee8b85 100644
--- a/src/kernel/resources/browsers/browsers.py
+++ b/src/kernel/resources/browsers/browsers.py
@@ -197,11 +197,9 @@ def create(
proxy_id: Optional proxy to associate to the browser session. Must reference a proxy
belonging to the caller's org.
- start_url: Optional URL to navigate to immediately after the browser is created.
- Best-effort: failures to navigate do not fail browser creation. Any pre-existing
- tabs are reduced to a single tab which is then navigated. Accepts any URL
- Chromium can resolve, including chrome:// pages. Ignored when reusing an
- existing persistent session.
+ start_url: Optional URL to open when the browser session is created. Navigation is
+ best-effort, so navigation failures do not prevent the session from being
+ created.
stealth: If true, launches the browser in stealth mode to reduce detection by anti-bot
mechanisms.
@@ -763,11 +761,9 @@ async def create(
proxy_id: Optional proxy to associate to the browser session. Must reference a proxy
belonging to the caller's org.
- start_url: Optional URL to navigate to immediately after the browser is created.
- Best-effort: failures to navigate do not fail browser creation. Any pre-existing
- tabs are reduced to a single tab which is then navigated. Accepts any URL
- Chromium can resolve, including chrome:// pages. Ignored when reusing an
- existing persistent session.
+ start_url: Optional URL to open when the browser session is created. Navigation is
+ best-effort, so navigation failures do not prevent the session from being
+ created.
stealth: If true, launches the browser in stealth mode to reduce detection by anti-bot
mechanisms.
diff --git a/src/kernel/types/browser_create_params.py b/src/kernel/types/browser_create_params.py
index 1a4493f0..399ded4c 100644
--- a/src/kernel/types/browser_create_params.py
+++ b/src/kernel/types/browser_create_params.py
@@ -58,12 +58,10 @@ class BrowserCreateParams(TypedDict, total=False):
"""
start_url: str
- """Optional URL to navigate to immediately after the browser is created.
+ """Optional URL to open when the browser session is created.
- Best-effort: failures to navigate do not fail browser creation. Any pre-existing
- tabs are reduced to a single tab which is then navigated. Accepts any URL
- Chromium can resolve, including chrome:// pages. Ignored when reusing an
- existing persistent session.
+ Navigation is best-effort, so navigation failures do not prevent the session
+ from being created.
"""
stealth: bool
diff --git a/src/kernel/types/browser_create_response.py b/src/kernel/types/browser_create_response.py
index e63a6896..1c30ee63 100644
--- a/src/kernel/types/browser_create_response.py
+++ b/src/kernel/types/browser_create_response.py
@@ -69,10 +69,7 @@ class BrowserCreateResponse(BaseModel):
"""ID of the proxy associated with this browser session, if any."""
start_url: Optional[str] = None
- """URL the session was asked to navigate to on creation, if any.
-
- Recorded for debugging — navigation is best-effort and may have failed.
- """
+ """Start URL requested for the session, if provided."""
usage: Optional[BrowserUsage] = None
"""Session usage metrics."""
diff --git a/src/kernel/types/browser_list_response.py b/src/kernel/types/browser_list_response.py
index de37e26f..2a172671 100644
--- a/src/kernel/types/browser_list_response.py
+++ b/src/kernel/types/browser_list_response.py
@@ -69,10 +69,7 @@ class BrowserListResponse(BaseModel):
"""ID of the proxy associated with this browser session, if any."""
start_url: Optional[str] = None
- """URL the session was asked to navigate to on creation, if any.
-
- Recorded for debugging — navigation is best-effort and may have failed.
- """
+ """Start URL requested for the session, if provided."""
usage: Optional[BrowserUsage] = None
"""Session usage metrics."""
diff --git a/src/kernel/types/browser_pool.py b/src/kernel/types/browser_pool.py
index a691d0eb..e456fca2 100644
--- a/src/kernel/types/browser_pool.py
+++ b/src/kernel/types/browser_pool.py
@@ -64,12 +64,10 @@ class BrowserPoolConfig(BaseModel):
"""
start_url: Optional[str] = None
- """Optional URL to navigate to when a new browser is warmed into the pool.
+ """Optional URL to open when a browser is created for the pool.
- Best-effort: failures to navigate do not fail pool fill. Only applied to
- newly-warmed browsers — browsers reused via release/acquire keep whatever URL
- the previous lease left them on. Accepts any URL Chromium can resolve, including
- chrome:// pages.
+ Navigation is best-effort, so navigation failures do not prevent the pool from
+ filling. Reused browsers keep the page left by the previous lease.
"""
stealth: Optional[bool] = None
diff --git a/src/kernel/types/browser_pool_acquire_response.py b/src/kernel/types/browser_pool_acquire_response.py
index cff8286d..91ea02e7 100644
--- a/src/kernel/types/browser_pool_acquire_response.py
+++ b/src/kernel/types/browser_pool_acquire_response.py
@@ -69,10 +69,7 @@ class BrowserPoolAcquireResponse(BaseModel):
"""ID of the proxy associated with this browser session, if any."""
start_url: Optional[str] = None
- """URL the session was asked to navigate to on creation, if any.
-
- Recorded for debugging — navigation is best-effort and may have failed.
- """
+ """Start URL requested for the session, if provided."""
usage: Optional[BrowserUsage] = None
"""Session usage metrics."""
diff --git a/src/kernel/types/browser_pool_create_params.py b/src/kernel/types/browser_pool_create_params.py
index 99d6c9cd..bc08c3f2 100644
--- a/src/kernel/types/browser_pool_create_params.py
+++ b/src/kernel/types/browser_pool_create_params.py
@@ -63,12 +63,10 @@ class BrowserPoolCreateParams(TypedDict, total=False):
"""
start_url: str
- """Optional URL to navigate to when a new browser is warmed into the pool.
+ """Optional URL to open when a browser is created for the pool.
- Best-effort: failures to navigate do not fail pool fill. Only applied to
- newly-warmed browsers — browsers reused via release/acquire keep whatever URL
- the previous lease left them on. Accepts any URL Chromium can resolve, including
- chrome:// pages.
+ Navigation is best-effort, so navigation failures do not prevent the pool from
+ filling. Reused browsers keep the page left by the previous lease.
"""
stealth: bool
diff --git a/src/kernel/types/browser_pool_update_params.py b/src/kernel/types/browser_pool_update_params.py
index 5b069c4c..043eaf85 100644
--- a/src/kernel/types/browser_pool_update_params.py
+++ b/src/kernel/types/browser_pool_update_params.py
@@ -69,12 +69,10 @@ class BrowserPoolUpdateParams(TypedDict, total=False):
"""
start_url: str
- """Optional URL to navigate to when a new browser is warmed into the pool.
+ """Optional URL to open when a browser is created for the pool.
- Best-effort: failures to navigate do not fail pool fill. Only applied to
- newly-warmed browsers — browsers reused via release/acquire keep whatever URL
- the previous lease left them on. Accepts any URL Chromium can resolve, including
- chrome:// pages.
+ Navigation is best-effort, so navigation failures do not prevent the pool from
+ filling. Reused browsers keep the page left by the previous lease.
"""
stealth: bool
diff --git a/src/kernel/types/browser_retrieve_response.py b/src/kernel/types/browser_retrieve_response.py
index 80a96d36..63f4da9d 100644
--- a/src/kernel/types/browser_retrieve_response.py
+++ b/src/kernel/types/browser_retrieve_response.py
@@ -69,10 +69,7 @@ class BrowserRetrieveResponse(BaseModel):
"""ID of the proxy associated with this browser session, if any."""
start_url: Optional[str] = None
- """URL the session was asked to navigate to on creation, if any.
-
- Recorded for debugging — navigation is best-effort and may have failed.
- """
+ """Start URL requested for the session, if provided."""
usage: Optional[BrowserUsage] = None
"""Session usage metrics."""
diff --git a/src/kernel/types/browser_update_response.py b/src/kernel/types/browser_update_response.py
index 4d1f61bc..2e30f49d 100644
--- a/src/kernel/types/browser_update_response.py
+++ b/src/kernel/types/browser_update_response.py
@@ -69,10 +69,7 @@ class BrowserUpdateResponse(BaseModel):
"""ID of the proxy associated with this browser session, if any."""
start_url: Optional[str] = None
- """URL the session was asked to navigate to on creation, if any.
-
- Recorded for debugging — navigation is best-effort and may have failed.
- """
+ """Start URL requested for the session, if provided."""
usage: Optional[BrowserUsage] = None
"""Session usage metrics."""
diff --git a/src/kernel/types/invocation_list_browsers_response.py b/src/kernel/types/invocation_list_browsers_response.py
index 71c22a7a..c7b9e394 100644
--- a/src/kernel/types/invocation_list_browsers_response.py
+++ b/src/kernel/types/invocation_list_browsers_response.py
@@ -69,10 +69,7 @@ class Browser(BaseModel):
"""ID of the proxy associated with this browser session, if any."""
start_url: Optional[str] = None
- """URL the session was asked to navigate to on creation, if any.
-
- Recorded for debugging — navigation is best-effort and may have failed.
- """
+ """Start URL requested for the session, if provided."""
usage: Optional[BrowserUsage] = None
"""Session usage metrics."""
From edc271ffc1aff8715112274b51383ad0e66db887 Mon Sep 17 00:00:00 2001
From: "stainless-app[bot]"
<142633134+stainless-app[bot]@users.noreply.github.com>
Date: Fri, 15 May 2026 20:32:29 +0000
Subject: [PATCH 003/366] feat: Add health check and auto-reauth controls for
managed auth connections
---
.stats.yml | 2 +-
src/kernel/resources/auth/connections.py | 70 +++++++++++++++++++
.../types/auth/connection_create_params.py | 20 ++++++
.../types/auth/connection_update_params.py | 20 ++++++
src/kernel/types/auth/managed_auth.py | 21 ++++++
tests/api_resources/auth/test_connections.py | 8 +++
6 files changed, 140 insertions(+), 1 deletion(-)
diff --git a/.stats.yml b/.stats.yml
index 35cab471..6b78eac1 100644
--- a/.stats.yml
+++ b/.stats.yml
@@ -1,4 +1,4 @@
configured_endpoints: 112
openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel/kernel-b7a19ff1fbd93322c8cffcd0b397ce2536ca8bff91594e0081bd030d4bec879f.yml
-openapi_spec_hash: 490520e6f0a8b1ebc89e9c0add46082d
+openapi_spec_hash: 9dd204b37a357b19032aea9eb4496645
config_hash: 08d55086449943a8fec212b870061a3f
diff --git a/src/kernel/resources/auth/connections.py b/src/kernel/resources/auth/connections.py
index 4befc6e9..dc912aba 100644
--- a/src/kernel/resources/auth/connections.py
+++ b/src/kernel/resources/auth/connections.py
@@ -62,8 +62,10 @@ def create(
domain: str,
profile_name: str,
allowed_domains: SequenceNotStr[str] | Omit = omit,
+ auto_reauth: bool | Omit = omit,
credential: connection_create_params.Credential | Omit = omit,
health_check_interval: int | Omit = omit,
+ health_checks: bool | Omit = omit,
login_url: str | Omit = omit,
proxy: connection_create_params.Proxy | Omit = omit,
record_session: bool | Omit = omit,
@@ -105,6 +107,15 @@ def create(
- OneLogin: \\**.onelogin.com
- Ping Identity: _.pingone.com, _.pingidentity.com
+ auto_reauth: Whether to permit automatic re-authentication when a scheduled health check
+ detects an expired session. This is an opt-in flag only — it does not check
+ whether re-auth is actually feasible. Even when true, re-auth only runs when the
+ system has what it needs to perform it (for example, saved credentials for the
+ required login fields), and only after a scheduled health check detects an
+ expired session — so this flag has no effect when `health_checks` is false. When
+ false, expired sessions are marked as `NEEDS_AUTH` instead of attempting
+ re-auth. Defaults to true.
+
credential:
Reference to credentials for the auth connection. Use one of:
@@ -118,6 +129,11 @@ def create(
depends on your plan: Enterprise: 300 (5 minutes), Startup: 1200 (20 minutes),
Hobbyist: 3600 (1 hour).
+ health_checks: Whether to enable periodic health checks. When false, the system will not
+ automatically verify authentication status, and `auto_reauth` has no effect on
+ the automatic flow (since re-auth is only triggered by a failed scheduled health
+ check). Defaults to true.
+
login_url: Optional login page URL to skip discovery
proxy: Proxy selection. Provide either id or name. The proxy must belong to the
@@ -144,8 +160,10 @@ def create(
"domain": domain,
"profile_name": profile_name,
"allowed_domains": allowed_domains,
+ "auto_reauth": auto_reauth,
"credential": credential,
"health_check_interval": health_check_interval,
+ "health_checks": health_checks,
"login_url": login_url,
"proxy": proxy,
"record_session": record_session,
@@ -199,8 +217,10 @@ def update(
id: str,
*,
allowed_domains: SequenceNotStr[str] | Omit = omit,
+ auto_reauth: bool | Omit = omit,
credential: connection_update_params.Credential | Omit = omit,
health_check_interval: int | Omit = omit,
+ health_checks: bool | Omit = omit,
login_url: str | Omit = omit,
proxy: connection_update_params.Proxy | Omit = omit,
record_session: bool | Omit = omit,
@@ -220,6 +240,14 @@ def update(
Args:
allowed_domains: Additional domains valid for this auth flow (replaces existing list)
+ auto_reauth: Whether automatic re-authentication is permitted for this connection. This is an
+ opt-in flag only — it does not check whether re-auth is actually feasible. Even
+ when true, re-auth only runs when the system has what it needs to perform it
+ (for example, saved credentials for the required login fields), and only after a
+ scheduled health check detects an expired session — so this flag has no effect
+ when `health_checks` is false. When false, expired sessions detected by a health
+ check are marked as `NEEDS_AUTH` instead of attempting re-auth.
+
credential:
Reference to credentials for the auth connection. Use one of:
@@ -229,6 +257,11 @@ def update(
health_check_interval: Interval in seconds between automatic health checks
+ health_checks: Whether periodic health checks are enabled. When set to false, the system will
+ not automatically verify authentication status, and `auto_reauth` has no effect
+ on the automatic flow (since re-auth is only triggered by a failed scheduled
+ health check).
+
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
@@ -253,8 +286,10 @@ def update(
body=maybe_transform(
{
"allowed_domains": allowed_domains,
+ "auto_reauth": auto_reauth,
"credential": credential,
"health_check_interval": health_check_interval,
+ "health_checks": health_checks,
"login_url": login_url,
"proxy": proxy,
"record_session": record_session,
@@ -544,8 +579,10 @@ async def create(
domain: str,
profile_name: str,
allowed_domains: SequenceNotStr[str] | Omit = omit,
+ auto_reauth: bool | Omit = omit,
credential: connection_create_params.Credential | Omit = omit,
health_check_interval: int | Omit = omit,
+ health_checks: bool | Omit = omit,
login_url: str | Omit = omit,
proxy: connection_create_params.Proxy | Omit = omit,
record_session: bool | Omit = omit,
@@ -587,6 +624,15 @@ async def create(
- OneLogin: \\**.onelogin.com
- Ping Identity: _.pingone.com, _.pingidentity.com
+ auto_reauth: Whether to permit automatic re-authentication when a scheduled health check
+ detects an expired session. This is an opt-in flag only — it does not check
+ whether re-auth is actually feasible. Even when true, re-auth only runs when the
+ system has what it needs to perform it (for example, saved credentials for the
+ required login fields), and only after a scheduled health check detects an
+ expired session — so this flag has no effect when `health_checks` is false. When
+ false, expired sessions are marked as `NEEDS_AUTH` instead of attempting
+ re-auth. Defaults to true.
+
credential:
Reference to credentials for the auth connection. Use one of:
@@ -600,6 +646,11 @@ async def create(
depends on your plan: Enterprise: 300 (5 minutes), Startup: 1200 (20 minutes),
Hobbyist: 3600 (1 hour).
+ health_checks: Whether to enable periodic health checks. When false, the system will not
+ automatically verify authentication status, and `auto_reauth` has no effect on
+ the automatic flow (since re-auth is only triggered by a failed scheduled health
+ check). Defaults to true.
+
login_url: Optional login page URL to skip discovery
proxy: Proxy selection. Provide either id or name. The proxy must belong to the
@@ -626,8 +677,10 @@ async def create(
"domain": domain,
"profile_name": profile_name,
"allowed_domains": allowed_domains,
+ "auto_reauth": auto_reauth,
"credential": credential,
"health_check_interval": health_check_interval,
+ "health_checks": health_checks,
"login_url": login_url,
"proxy": proxy,
"record_session": record_session,
@@ -681,8 +734,10 @@ async def update(
id: str,
*,
allowed_domains: SequenceNotStr[str] | Omit = omit,
+ auto_reauth: bool | Omit = omit,
credential: connection_update_params.Credential | Omit = omit,
health_check_interval: int | Omit = omit,
+ health_checks: bool | Omit = omit,
login_url: str | Omit = omit,
proxy: connection_update_params.Proxy | Omit = omit,
record_session: bool | Omit = omit,
@@ -702,6 +757,14 @@ async def update(
Args:
allowed_domains: Additional domains valid for this auth flow (replaces existing list)
+ auto_reauth: Whether automatic re-authentication is permitted for this connection. This is an
+ opt-in flag only — it does not check whether re-auth is actually feasible. Even
+ when true, re-auth only runs when the system has what it needs to perform it
+ (for example, saved credentials for the required login fields), and only after a
+ scheduled health check detects an expired session — so this flag has no effect
+ when `health_checks` is false. When false, expired sessions detected by a health
+ check are marked as `NEEDS_AUTH` instead of attempting re-auth.
+
credential:
Reference to credentials for the auth connection. Use one of:
@@ -711,6 +774,11 @@ async def update(
health_check_interval: Interval in seconds between automatic health checks
+ health_checks: Whether periodic health checks are enabled. When set to false, the system will
+ not automatically verify authentication status, and `auto_reauth` has no effect
+ on the automatic flow (since re-auth is only triggered by a failed scheduled
+ health check).
+
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
@@ -735,8 +803,10 @@ async def update(
body=await async_maybe_transform(
{
"allowed_domains": allowed_domains,
+ "auto_reauth": auto_reauth,
"credential": credential,
"health_check_interval": health_check_interval,
+ "health_checks": health_checks,
"login_url": login_url,
"proxy": proxy,
"record_session": record_session,
diff --git a/src/kernel/types/auth/connection_create_params.py b/src/kernel/types/auth/connection_create_params.py
index bdd22681..acde944c 100644
--- a/src/kernel/types/auth/connection_create_params.py
+++ b/src/kernel/types/auth/connection_create_params.py
@@ -40,6 +40,18 @@ class ConnectionCreateParams(TypedDict, total=False):
- Ping Identity: _.pingone.com, _.pingidentity.com
"""
+ auto_reauth: bool
+ """
+ Whether to permit automatic re-authentication when a scheduled health check
+ detects an expired session. This is an opt-in flag only — it does not check
+ whether re-auth is actually feasible. Even when true, re-auth only runs when the
+ system has what it needs to perform it (for example, saved credentials for the
+ required login fields), and only after a scheduled health check detects an
+ expired session — so this flag has no effect when `health_checks` is false. When
+ false, expired sessions are marked as `NEEDS_AUTH` instead of attempting
+ re-auth. Defaults to true.
+ """
+
credential: Credential
"""Reference to credentials for the auth connection. Use one of:
@@ -57,6 +69,14 @@ class ConnectionCreateParams(TypedDict, total=False):
Startup: 1200 (20 minutes), Hobbyist: 3600 (1 hour).
"""
+ health_checks: bool
+ """Whether to enable periodic health checks.
+
+ When false, the system will not automatically verify authentication status, and
+ `auto_reauth` has no effect on the automatic flow (since re-auth is only
+ triggered by a failed scheduled health check). Defaults to true.
+ """
+
login_url: str
"""Optional login page URL to skip discovery"""
diff --git a/src/kernel/types/auth/connection_update_params.py b/src/kernel/types/auth/connection_update_params.py
index 23832778..8875f609 100644
--- a/src/kernel/types/auth/connection_update_params.py
+++ b/src/kernel/types/auth/connection_update_params.py
@@ -13,6 +13,18 @@ class ConnectionUpdateParams(TypedDict, total=False):
allowed_domains: SequenceNotStr[str]
"""Additional domains valid for this auth flow (replaces existing list)"""
+ auto_reauth: bool
+ """Whether automatic re-authentication is permitted for this connection.
+
+ This is an opt-in flag only — it does not check whether re-auth is actually
+ feasible. Even when true, re-auth only runs when the system has what it needs to
+ perform it (for example, saved credentials for the required login fields), and
+ only after a scheduled health check detects an expired session — so this flag
+ has no effect when `health_checks` is false. When false, expired sessions
+ detected by a health check are marked as `NEEDS_AUTH` instead of attempting
+ re-auth.
+ """
+
credential: Credential
"""Reference to credentials for the auth connection. Use one of:
@@ -24,6 +36,14 @@ class ConnectionUpdateParams(TypedDict, total=False):
health_check_interval: int
"""Interval in seconds between automatic health checks"""
+ health_checks: bool
+ """Whether periodic health checks are enabled.
+
+ When set to false, the system will not automatically verify authentication
+ status, and `auto_reauth` has no effect on the automatic flow (since re-auth is
+ only triggered by a failed scheduled health check).
+ """
+
login_url: str
"""Login page URL. Set to empty string to clear."""
diff --git a/src/kernel/types/auth/managed_auth.py b/src/kernel/types/auth/managed_auth.py
index 980d0207..893631d4 100644
--- a/src/kernel/types/auth/managed_auth.py
+++ b/src/kernel/types/auth/managed_auth.py
@@ -166,6 +166,18 @@ class ManagedAuth(BaseModel):
- Ping Identity: _.pingone.com, _.pingidentity.com
"""
+ auto_reauth: Optional[bool] = None
+ """Whether automatic re-authentication is permitted for this connection.
+
+ This is an opt-in flag only — it does not check whether re-auth is actually
+ feasible. Even when true, re-auth only runs when the system has what it needs to
+ perform it (for example, saved credentials for the required login fields), and
+ only after a scheduled health check detects an expired session — so this flag
+ has no effect when `health_checks` is false. When false, expired sessions
+ detected by a health check are marked as `NEEDS_AUTH` instead of attempting
+ re-auth.
+ """
+
browser_session_id: Optional[str] = None
"""
ID of the underlying browser session driving the current flow (present when flow
@@ -236,6 +248,15 @@ class ManagedAuth(BaseModel):
Startup: 1200 (20 minutes), Hobbyist: 3600 (1 hour).
"""
+ health_checks: Optional[bool] = None
+ """Whether periodic health checks are enabled for this connection.
+
+ When false, the system will not automatically verify authentication status, and
+ `auto_reauth` has no effect on the automatic flow (since re-auth is only
+ triggered by a failed scheduled health check). Manually triggering a health
+ check via the API still works regardless of this setting.
+ """
+
hosted_url: Optional[str] = None
"""URL to redirect user to for hosted login (present when flow in progress)"""
diff --git a/tests/api_resources/auth/test_connections.py b/tests/api_resources/auth/test_connections.py
index e3da167f..702207fe 100644
--- a/tests/api_resources/auth/test_connections.py
+++ b/tests/api_resources/auth/test_connections.py
@@ -38,6 +38,7 @@ def test_method_create_with_all_params(self, client: Kernel) -> None:
domain="netflix.com",
profile_name="user-123",
allowed_domains=["login.netflix.com", "auth.netflix.com"],
+ auto_reauth=True,
credential={
"auto": True,
"name": "my-netflix-creds",
@@ -45,6 +46,7 @@ def test_method_create_with_all_params(self, client: Kernel) -> None:
"provider": "my-1p",
},
health_check_interval=3600,
+ health_checks=True,
login_url="https://netflix.com/login",
proxy={
"id": "id",
@@ -139,6 +141,7 @@ 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"],
+ auto_reauth=True,
credential={
"auto": True,
"name": "my-netflix-creds",
@@ -146,6 +149,7 @@ def test_method_update_with_all_params(self, client: Kernel) -> None:
"provider": "my-1p",
},
health_check_interval=3600,
+ health_checks=True,
login_url="https://netflix.com/login",
proxy={
"id": "id",
@@ -447,6 +451,7 @@ async def test_method_create_with_all_params(self, async_client: AsyncKernel) ->
domain="netflix.com",
profile_name="user-123",
allowed_domains=["login.netflix.com", "auth.netflix.com"],
+ auto_reauth=True,
credential={
"auto": True,
"name": "my-netflix-creds",
@@ -454,6 +459,7 @@ async def test_method_create_with_all_params(self, async_client: AsyncKernel) ->
"provider": "my-1p",
},
health_check_interval=3600,
+ health_checks=True,
login_url="https://netflix.com/login",
proxy={
"id": "id",
@@ -548,6 +554,7 @@ async def test_method_update_with_all_params(self, async_client: AsyncKernel) ->
connection = await async_client.auth.connections.update(
id="id",
allowed_domains=["login.netflix.com", "auth.netflix.com"],
+ auto_reauth=True,
credential={
"auto": True,
"name": "my-netflix-creds",
@@ -555,6 +562,7 @@ async def test_method_update_with_all_params(self, async_client: AsyncKernel) ->
"provider": "my-1p",
},
health_check_interval=3600,
+ health_checks=True,
login_url="https://netflix.com/login",
proxy={
"id": "id",
From 9a3e19eb5b2f50640d70b0341b3a7d1477ee0153 Mon Sep 17 00:00:00 2001
From: "stainless-app[bot]"
<142633134+stainless-app[bot]@users.noreply.github.com>
Date: Fri, 15 May 2026 20:46:35 +0000
Subject: [PATCH 004/366] release: 0.55.0
---
.release-please-manifest.json | 2 +-
CHANGELOG.md | 9 +++++++++
pyproject.toml | 2 +-
src/kernel/_version.py | 2 +-
4 files changed, 12 insertions(+), 3 deletions(-)
diff --git a/.release-please-manifest.json b/.release-please-manifest.json
index c3e01e1e..d940b600 100644
--- a/.release-please-manifest.json
+++ b/.release-please-manifest.json
@@ -1,3 +1,3 @@
{
- ".": "0.53.0"
+ ".": "0.55.0"
}
\ No newline at end of file
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 122a4e88..c5b40016 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,14 @@
# Changelog
+## 0.55.0 (2026-05-15)
+
+Full Changelog: [v0.53.0...v0.55.0](https://github.com/kernel/kernel-python-sdk/compare/v0.53.0...v0.55.0)
+
+### Features
+
+* Add health check and auto-reauth controls for managed auth connections ([9a0655e](https://github.com/kernel/kernel-python-sdk/commit/9a0655efbeef25e4929397b04f492644339f839e))
+* Polish start URL OpenAPI descriptions ([4f16fb0](https://github.com/kernel/kernel-python-sdk/commit/4f16fb073e0d295fd962e8a8aabd55bdf3144e00))
+
## 0.53.0 (2026-05-12)
Full Changelog: [v0.52.0...v0.53.0](https://github.com/kernel/kernel-python-sdk/compare/v0.52.0...v0.53.0)
diff --git a/pyproject.toml b/pyproject.toml
index 10375b3b..353f3cd9 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -1,6 +1,6 @@
[project]
name = "kernel"
-version = "0.53.0"
+version = "0.55.0"
description = "The official Python library for the kernel API"
dynamic = ["readme"]
license = "Apache-2.0"
diff --git a/src/kernel/_version.py b/src/kernel/_version.py
index 173fc2c2..b2f051b4 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.53.0" # x-release-please-version
+__version__ = "0.55.0" # x-release-please-version
From 29bf36fd13d5f0e8042cc914b51389e583f7504d Mon Sep 17 00:00:00 2001
From: "stainless-app[bot]"
<142633134+stainless-app[bot]@users.noreply.github.com>
Date: Sat, 16 May 2026 02:13:51 +0000
Subject: [PATCH 005/366] codegen metadata
---
.stats.yml | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/.stats.yml b/.stats.yml
index 6b78eac1..e7238e0a 100644
--- a/.stats.yml
+++ b/.stats.yml
@@ -1,4 +1,4 @@
configured_endpoints: 112
-openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel/kernel-b7a19ff1fbd93322c8cffcd0b397ce2536ca8bff91594e0081bd030d4bec879f.yml
+openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel/kernel-85604d59d55171797ed7e5e60f46532e7825ccfcaf6ee1319284d513f582a9cf.yml
openapi_spec_hash: 9dd204b37a357b19032aea9eb4496645
config_hash: 08d55086449943a8fec212b870061a3f
From d9d7d302db8d24eb3bdcb89b7393a5ab0fce9726 Mon Sep 17 00:00:00 2001
From: "stainless-app[bot]"
<142633134+stainless-app[bot]@users.noreply.github.com>
Date: Mon, 18 May 2026 13:14:44 +0000
Subject: [PATCH 006/366] feat: Expose POST /projects in public API
---
.stats.yml | 4 ++--
src/kernel/resources/projects/projects.py | 12 ++++--------
2 files changed, 6 insertions(+), 10 deletions(-)
diff --git a/.stats.yml b/.stats.yml
index e7238e0a..e0a033ee 100644
--- a/.stats.yml
+++ b/.stats.yml
@@ -1,4 +1,4 @@
configured_endpoints: 112
-openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel/kernel-85604d59d55171797ed7e5e60f46532e7825ccfcaf6ee1319284d513f582a9cf.yml
-openapi_spec_hash: 9dd204b37a357b19032aea9eb4496645
+openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel/kernel-982efd191c23737c9e1cdbcbf9237fa2231b9f74e0a25db2870293bdf9951c21.yml
+openapi_spec_hash: eeb27952a4cc939316c24fc0ce2c9e3a
config_hash: 08d55086449943a8fec212b870061a3f
diff --git a/src/kernel/resources/projects/projects.py b/src/kernel/resources/projects/projects.py
index f73e70d7..87fae1b8 100644
--- a/src/kernel/resources/projects/projects.py
+++ b/src/kernel/resources/projects/projects.py
@@ -70,10 +70,8 @@ def create(
extra_body: Body | None = None,
timeout: float | httpx.Timeout | None | NotGiven = not_given,
) -> Project:
- """Create a new project within the authenticated organization.
-
- Requires the
- projects feature flag.
+ """
+ Create a new project within the authenticated organization.
Args:
name: Project name (1-255 characters)
@@ -299,10 +297,8 @@ async def create(
extra_body: Body | None = None,
timeout: float | httpx.Timeout | None | NotGiven = not_given,
) -> Project:
- """Create a new project within the authenticated organization.
-
- Requires the
- projects feature flag.
+ """
+ Create a new project within the authenticated organization.
Args:
name: Project name (1-255 characters)
From eec2c4b176421a8ab875786997be8fe62711580e Mon Sep 17 00:00:00 2001
From: "stainless-app[bot]"
<142633134+stainless-app[bot]@users.noreply.github.com>
Date: Mon, 18 May 2026 13:15:31 +0000
Subject: [PATCH 007/366] release: 0.56.0
---
.release-please-manifest.json | 2 +-
CHANGELOG.md | 8 ++++++++
pyproject.toml | 2 +-
src/kernel/_version.py | 2 +-
4 files changed, 11 insertions(+), 3 deletions(-)
diff --git a/.release-please-manifest.json b/.release-please-manifest.json
index d940b600..87d3d84c 100644
--- a/.release-please-manifest.json
+++ b/.release-please-manifest.json
@@ -1,3 +1,3 @@
{
- ".": "0.55.0"
+ ".": "0.56.0"
}
\ No newline at end of file
diff --git a/CHANGELOG.md b/CHANGELOG.md
index c5b40016..2a3eba14 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,13 @@
# Changelog
+## 0.56.0 (2026-05-18)
+
+Full Changelog: [v0.55.0...v0.56.0](https://github.com/kernel/kernel-python-sdk/compare/v0.55.0...v0.56.0)
+
+### Features
+
+* Expose POST /projects in public API ([4d70271](https://github.com/kernel/kernel-python-sdk/commit/4d702711a74e050ab121f0ce7b2bbe276904a5de))
+
## 0.55.0 (2026-05-15)
Full Changelog: [v0.53.0...v0.55.0](https://github.com/kernel/kernel-python-sdk/compare/v0.53.0...v0.55.0)
diff --git a/pyproject.toml b/pyproject.toml
index 353f3cd9..c98902d7 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -1,6 +1,6 @@
[project]
name = "kernel"
-version = "0.55.0"
+version = "0.56.0"
description = "The official Python library for the kernel API"
dynamic = ["readme"]
license = "Apache-2.0"
diff --git a/src/kernel/_version.py b/src/kernel/_version.py
index b2f051b4..a4d2a572 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.55.0" # x-release-please-version
+__version__ = "0.56.0" # x-release-please-version
From 36a77c4a9f4c65cc0a15451837262042581fbb3b Mon Sep 17 00:00:00 2001
From: "stainless-app[bot]"
<142633134+stainless-app[bot]@users.noreply.github.com>
Date: Tue, 19 May 2026 00:02:33 +0000
Subject: [PATCH 008/366] feat: browsers: accept chrome_policy on POST
/browsers (KERNEL-1216)
---
.stats.yml | 4 ++--
src/kernel/resources/browsers/browsers.py | 16 ++++++++++++++++
src/kernel/types/browser_create_params.py | 11 ++++++++++-
src/kernel/types/browser_create_response.py | 9 ++++++++-
src/kernel/types/browser_list_response.py | 9 ++++++++-
.../types/browser_pool_acquire_response.py | 9 ++++++++-
src/kernel/types/browser_retrieve_response.py | 9 ++++++++-
src/kernel/types/browser_update_response.py | 9 ++++++++-
.../types/invocation_list_browsers_response.py | 9 ++++++++-
tests/api_resources/test_browsers.py | 2 ++
10 files changed, 78 insertions(+), 9 deletions(-)
diff --git a/.stats.yml b/.stats.yml
index e0a033ee..542eaa0d 100644
--- a/.stats.yml
+++ b/.stats.yml
@@ -1,4 +1,4 @@
configured_endpoints: 112
-openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel/kernel-982efd191c23737c9e1cdbcbf9237fa2231b9f74e0a25db2870293bdf9951c21.yml
-openapi_spec_hash: eeb27952a4cc939316c24fc0ce2c9e3a
+openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel/kernel-e564e74a7aae1744d4aa070a63c387f456c4719a48747dc6229b58a986255b65.yml
+openapi_spec_hash: 62beb1f20708652aaee31bbffb6cfbe9
config_hash: 08d55086449943a8fec212b870061a3f
diff --git a/src/kernel/resources/browsers/browsers.py b/src/kernel/resources/browsers/browsers.py
index 82ee8b85..a6dbd353 100644
--- a/src/kernel/resources/browsers/browsers.py
+++ b/src/kernel/resources/browsers/browsers.py
@@ -152,6 +152,7 @@ def with_streaming_response(self) -> BrowsersResourceWithStreamingResponse:
def create(
self,
*,
+ chrome_policy: Dict[str, object] | Omit = omit,
extensions: Iterable[BrowserExtension] | Omit = omit,
gpu: bool | Omit = omit,
headless: bool | Omit = omit,
@@ -175,6 +176,12 @@ def create(
Create a new browser session from within an action.
Args:
+ chrome_policy: Custom Chrome enterprise policy overrides applied to this browser session. Keys
+ are Chrome enterprise policy names; values must match their expected types.
+ Blocked: kernel-managed policies (extensions, proxy, CDP/automation). Ignored
+ when reusing an existing persistent session. See
+ https://chromeenterprise.google/policies/
+
extensions: List of browser extensions to load into the session. Provide each by id or name.
gpu: If true, enables GPU acceleration for the browser session. Requires Start-Up or
@@ -235,6 +242,7 @@ def create(
"/browsers",
body=maybe_transform(
{
+ "chrome_policy": chrome_policy,
"extensions": extensions,
"gpu": gpu,
"headless": headless,
@@ -716,6 +724,7 @@ def with_streaming_response(self) -> AsyncBrowsersResourceWithStreamingResponse:
async def create(
self,
*,
+ chrome_policy: Dict[str, object] | Omit = omit,
extensions: Iterable[BrowserExtension] | Omit = omit,
gpu: bool | Omit = omit,
headless: bool | Omit = omit,
@@ -739,6 +748,12 @@ async def create(
Create a new browser session from within an action.
Args:
+ chrome_policy: Custom Chrome enterprise policy overrides applied to this browser session. Keys
+ are Chrome enterprise policy names; values must match their expected types.
+ Blocked: kernel-managed policies (extensions, proxy, CDP/automation). Ignored
+ when reusing an existing persistent session. See
+ https://chromeenterprise.google/policies/
+
extensions: List of browser extensions to load into the session. Provide each by id or name.
gpu: If true, enables GPU acceleration for the browser session. Requires Start-Up or
@@ -799,6 +814,7 @@ async def create(
"/browsers",
body=await async_maybe_transform(
{
+ "chrome_policy": chrome_policy,
"extensions": extensions,
"gpu": gpu,
"headless": headless,
diff --git a/src/kernel/types/browser_create_params.py b/src/kernel/types/browser_create_params.py
index 399ded4c..c05fd94e 100644
--- a/src/kernel/types/browser_create_params.py
+++ b/src/kernel/types/browser_create_params.py
@@ -2,7 +2,7 @@
from __future__ import annotations
-from typing import Iterable
+from typing import Dict, Iterable
from typing_extensions import TypedDict
from .browser_persistence_param import BrowserPersistenceParam
@@ -14,6 +14,15 @@
class BrowserCreateParams(TypedDict, total=False):
+ chrome_policy: Dict[str, object]
+ """Custom Chrome enterprise policy overrides applied to this browser session.
+
+ Keys are Chrome enterprise policy names; values must match their expected types.
+ Blocked: kernel-managed policies (extensions, proxy, CDP/automation). Ignored
+ when reusing an existing persistent session. See
+ https://chromeenterprise.google/policies/
+ """
+
extensions: Iterable[BrowserExtension]
"""List of browser extensions to load into the session.
diff --git a/src/kernel/types/browser_create_response.py b/src/kernel/types/browser_create_response.py
index 1c30ee63..2e2e5dbc 100644
--- a/src/kernel/types/browser_create_response.py
+++ b/src/kernel/types/browser_create_response.py
@@ -1,6 +1,6 @@
# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
-from typing import Optional
+from typing import Dict, Optional
from datetime import datetime
from .profile import Profile
@@ -44,6 +44,13 @@ class BrowserCreateResponse(BaseModel):
Only available for non-headless browsers.
"""
+ chrome_policy: Optional[Dict[str, object]] = None
+ """
+ Custom Chrome enterprise policy overrides that were applied to this browser
+ session, if any. Echoed back for verification. Keys are Chrome enterprise policy
+ names.
+ """
+
deleted_at: Optional[datetime] = None
"""When the browser session was soft-deleted. Only present for deleted sessions."""
diff --git a/src/kernel/types/browser_list_response.py b/src/kernel/types/browser_list_response.py
index 2a172671..27e409f4 100644
--- a/src/kernel/types/browser_list_response.py
+++ b/src/kernel/types/browser_list_response.py
@@ -1,6 +1,6 @@
# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
-from typing import Optional
+from typing import Dict, Optional
from datetime import datetime
from .profile import Profile
@@ -44,6 +44,13 @@ class BrowserListResponse(BaseModel):
Only available for non-headless browsers.
"""
+ chrome_policy: Optional[Dict[str, object]] = None
+ """
+ Custom Chrome enterprise policy overrides that were applied to this browser
+ session, if any. Echoed back for verification. Keys are Chrome enterprise policy
+ names.
+ """
+
deleted_at: Optional[datetime] = None
"""When the browser session was soft-deleted. Only present for deleted sessions."""
diff --git a/src/kernel/types/browser_pool_acquire_response.py b/src/kernel/types/browser_pool_acquire_response.py
index 91ea02e7..9cf914ee 100644
--- a/src/kernel/types/browser_pool_acquire_response.py
+++ b/src/kernel/types/browser_pool_acquire_response.py
@@ -1,6 +1,6 @@
# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
-from typing import Optional
+from typing import Dict, Optional
from datetime import datetime
from .profile import Profile
@@ -44,6 +44,13 @@ class BrowserPoolAcquireResponse(BaseModel):
Only available for non-headless browsers.
"""
+ chrome_policy: Optional[Dict[str, object]] = None
+ """
+ Custom Chrome enterprise policy overrides that were applied to this browser
+ session, if any. Echoed back for verification. Keys are Chrome enterprise policy
+ names.
+ """
+
deleted_at: Optional[datetime] = None
"""When the browser session was soft-deleted. Only present for deleted sessions."""
diff --git a/src/kernel/types/browser_retrieve_response.py b/src/kernel/types/browser_retrieve_response.py
index 63f4da9d..95cfa9ad 100644
--- a/src/kernel/types/browser_retrieve_response.py
+++ b/src/kernel/types/browser_retrieve_response.py
@@ -1,6 +1,6 @@
# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
-from typing import Optional
+from typing import Dict, Optional
from datetime import datetime
from .profile import Profile
@@ -44,6 +44,13 @@ class BrowserRetrieveResponse(BaseModel):
Only available for non-headless browsers.
"""
+ chrome_policy: Optional[Dict[str, object]] = None
+ """
+ Custom Chrome enterprise policy overrides that were applied to this browser
+ session, if any. Echoed back for verification. Keys are Chrome enterprise policy
+ names.
+ """
+
deleted_at: Optional[datetime] = None
"""When the browser session was soft-deleted. Only present for deleted sessions."""
diff --git a/src/kernel/types/browser_update_response.py b/src/kernel/types/browser_update_response.py
index 2e30f49d..209d58c4 100644
--- a/src/kernel/types/browser_update_response.py
+++ b/src/kernel/types/browser_update_response.py
@@ -1,6 +1,6 @@
# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
-from typing import Optional
+from typing import Dict, Optional
from datetime import datetime
from .profile import Profile
@@ -44,6 +44,13 @@ class BrowserUpdateResponse(BaseModel):
Only available for non-headless browsers.
"""
+ chrome_policy: Optional[Dict[str, object]] = None
+ """
+ Custom Chrome enterprise policy overrides that were applied to this browser
+ session, if any. Echoed back for verification. Keys are Chrome enterprise policy
+ names.
+ """
+
deleted_at: Optional[datetime] = None
"""When the browser session was soft-deleted. Only present for deleted sessions."""
diff --git a/src/kernel/types/invocation_list_browsers_response.py b/src/kernel/types/invocation_list_browsers_response.py
index c7b9e394..7ff4910a 100644
--- a/src/kernel/types/invocation_list_browsers_response.py
+++ b/src/kernel/types/invocation_list_browsers_response.py
@@ -1,6 +1,6 @@
# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
-from typing import List, Optional
+from typing import Dict, List, Optional
from datetime import datetime
from .profile import Profile
@@ -44,6 +44,13 @@ class Browser(BaseModel):
Only available for non-headless browsers.
"""
+ chrome_policy: Optional[Dict[str, object]] = None
+ """
+ Custom Chrome enterprise policy overrides that were applied to this browser
+ session, if any. Echoed back for verification. Keys are Chrome enterprise policy
+ names.
+ """
+
deleted_at: Optional[datetime] = None
"""When the browser session was soft-deleted. Only present for deleted sessions."""
diff --git a/tests/api_resources/test_browsers.py b/tests/api_resources/test_browsers.py
index 94c52654..3d1ac191 100644
--- a/tests/api_resources/test_browsers.py
+++ b/tests/api_resources/test_browsers.py
@@ -36,6 +36,7 @@ def test_method_create(self, client: Kernel) -> None:
@parametrize
def test_method_create_with_all_params(self, client: Kernel) -> None:
browser = client.browsers.create(
+ chrome_policy={"foo": "bar"},
extensions=[
{
"id": "id",
@@ -462,6 +463,7 @@ async def test_method_create(self, async_client: AsyncKernel) -> None:
@parametrize
async def test_method_create_with_all_params(self, async_client: AsyncKernel) -> None:
browser = await async_client.browsers.create(
+ chrome_policy={"foo": "bar"},
extensions=[
{
"id": "id",
From 32bbbd5126ffea9bdacfc3b362633280bc9441f0 Mon Sep 17 00:00:00 2001
From: "stainless-app[bot]"
<142633134+stainless-app[bot]@users.noreply.github.com>
Date: Thu, 21 May 2026 18:53:01 +0000
Subject: [PATCH 009/366] feat: [kernel-1116] browser events api integration
---
.stats.yml | 8 +-
api.md | 44 ++++
src/kernel/resources/browser_pools.py | 32 +--
src/kernel/resources/browsers/__init__.py | 14 ++
src/kernel/resources/browsers/browsers.py | 65 ++++++
src/kernel/resources/browsers/telemetry.py | 195 ++++++++++++++++++
src/kernel/resources/proxies.py | 8 +-
src/kernel/types/__init__.py | 17 ++
src/kernel/types/browser_create_params.py | 10 +-
src/kernel/types/browser_create_response.py | 12 +-
src/kernel/types/browser_list_response.py | 12 +-
src/kernel/types/browser_pool.py | 8 +-
.../types/browser_pool_acquire_response.py | 12 +-
.../types/browser_pool_create_params.py | 8 +-
.../types/browser_pool_update_params.py | 8 +-
src/kernel/types/browser_retrieve_response.py | 12 +-
src/kernel/types/browser_update_params.py | 10 +
src/kernel/types/browser_update_response.py | 12 +-
src/kernel/types/browsers/__init__.py | 44 ++++
.../types/browsers/browser_call_stack.py | 43 ++++
.../browsers/browser_console_error_event.py | 87 ++++++++
.../browsers/browser_console_log_event.py | 64 ++++++
.../types/browsers/browser_event_context.py | 42 ++++
.../types/browsers/browser_event_source.py | 30 +++
.../types/browsers/browser_http_headers.py | 8 +
.../browser_interaction_click_event.py | 54 +++++
.../browsers/browser_interaction_key_event.py | 48 +++++
...rowser_interaction_scroll_settled_event.py | 56 +++++
.../browser_monitor_disconnected_event.py | 34 +++
.../browser_monitor_init_failed_event.py | 31 +++
.../browser_monitor_reconnect_failed_event.py | 37 ++++
.../browser_monitor_reconnected_event.py | 33 +++
.../browser_monitor_screenshot_event.py | 31 +++
.../browsers/browser_network_idle_event.py | 34 +++
.../browser_network_loading_failed_event.py | 58 ++++++
.../browsers/browser_network_request_event.py | 74 +++++++
.../browser_network_response_event.py | 71 +++++++
.../browser_page_dom_content_loaded_event.py | 45 ++++
.../browser_page_layout_settled_event.py | 34 +++
.../browser_page_layout_shift_event.py | 69 +++++++
.../types/browsers/browser_page_lcp_event.py | 75 +++++++
.../types/browsers/browser_page_load_event.py | 45 ++++
.../browsers/browser_page_navigation_event.py | 55 +++++
.../browser_page_navigation_settled_event.py | 34 +++
.../browsers/browser_page_tab_opened_event.py | 45 ++++
.../browser_telemetry_categories_config.py | 32 +++
...owser_telemetry_categories_config_param.py | 33 +++
.../browser_telemetry_category_config.py | 14 ++
...browser_telemetry_category_config_param.py | 14 ++
.../browsers/browser_telemetry_config.py | 15 ++
.../browser_telemetry_config_param.py | 16 ++
.../types/browsers/browser_telemetry_event.py | 61 ++++++
.../browsers/telemetry_stream_response.py | 34 +++
.../invocation_list_browsers_response.py | 12 +-
src/kernel/types/proxy_check_params.py | 5 +-
.../api_resources/browsers/test_telemetry.py | 122 +++++++++++
tests/api_resources/test_browsers.py | 32 +++
57 files changed, 2123 insertions(+), 35 deletions(-)
create mode 100644 src/kernel/resources/browsers/telemetry.py
create mode 100644 src/kernel/types/browsers/browser_call_stack.py
create mode 100644 src/kernel/types/browsers/browser_console_error_event.py
create mode 100644 src/kernel/types/browsers/browser_console_log_event.py
create mode 100644 src/kernel/types/browsers/browser_event_context.py
create mode 100644 src/kernel/types/browsers/browser_event_source.py
create mode 100644 src/kernel/types/browsers/browser_http_headers.py
create mode 100644 src/kernel/types/browsers/browser_interaction_click_event.py
create mode 100644 src/kernel/types/browsers/browser_interaction_key_event.py
create mode 100644 src/kernel/types/browsers/browser_interaction_scroll_settled_event.py
create mode 100644 src/kernel/types/browsers/browser_monitor_disconnected_event.py
create mode 100644 src/kernel/types/browsers/browser_monitor_init_failed_event.py
create mode 100644 src/kernel/types/browsers/browser_monitor_reconnect_failed_event.py
create mode 100644 src/kernel/types/browsers/browser_monitor_reconnected_event.py
create mode 100644 src/kernel/types/browsers/browser_monitor_screenshot_event.py
create mode 100644 src/kernel/types/browsers/browser_network_idle_event.py
create mode 100644 src/kernel/types/browsers/browser_network_loading_failed_event.py
create mode 100644 src/kernel/types/browsers/browser_network_request_event.py
create mode 100644 src/kernel/types/browsers/browser_network_response_event.py
create mode 100644 src/kernel/types/browsers/browser_page_dom_content_loaded_event.py
create mode 100644 src/kernel/types/browsers/browser_page_layout_settled_event.py
create mode 100644 src/kernel/types/browsers/browser_page_layout_shift_event.py
create mode 100644 src/kernel/types/browsers/browser_page_lcp_event.py
create mode 100644 src/kernel/types/browsers/browser_page_load_event.py
create mode 100644 src/kernel/types/browsers/browser_page_navigation_event.py
create mode 100644 src/kernel/types/browsers/browser_page_navigation_settled_event.py
create mode 100644 src/kernel/types/browsers/browser_page_tab_opened_event.py
create mode 100644 src/kernel/types/browsers/browser_telemetry_categories_config.py
create mode 100644 src/kernel/types/browsers/browser_telemetry_categories_config_param.py
create mode 100644 src/kernel/types/browsers/browser_telemetry_category_config.py
create mode 100644 src/kernel/types/browsers/browser_telemetry_category_config_param.py
create mode 100644 src/kernel/types/browsers/browser_telemetry_config.py
create mode 100644 src/kernel/types/browsers/browser_telemetry_config_param.py
create mode 100644 src/kernel/types/browsers/browser_telemetry_event.py
create mode 100644 src/kernel/types/browsers/telemetry_stream_response.py
create mode 100644 tests/api_resources/browsers/test_telemetry.py
diff --git a/.stats.yml b/.stats.yml
index 542eaa0d..3dfe6c63 100644
--- a/.stats.yml
+++ b/.stats.yml
@@ -1,4 +1,4 @@
-configured_endpoints: 112
-openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel/kernel-e564e74a7aae1744d4aa070a63c387f456c4719a48747dc6229b58a986255b65.yml
-openapi_spec_hash: 62beb1f20708652aaee31bbffb6cfbe9
-config_hash: 08d55086449943a8fec212b870061a3f
+configured_endpoints: 113
+openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel/kernel-a9c7e806132001dbfbd4e8ae6c7d0935e503f457d63385fc800c862e3d064375.yml
+openapi_spec_hash: b048dcb0c5401bc0a301c3d30cb8ecba
+config_hash: 37661d89120558d34b6cc184292632f2
diff --git a/api.md b/api.md
index 3dea16a3..d0b1d720 100644
--- a/api.md
+++ b/api.md
@@ -103,6 +103,50 @@ Methods:
- client.browsers.delete_by_id(id) -> None
- client.browsers.load_extensions(id, \*\*params) -> None
+## Telemetry
+
+Types:
+
+```python
+from kernel.types.browsers import (
+ BrowserCallStack,
+ BrowserConsoleErrorEvent,
+ BrowserConsoleLogEvent,
+ BrowserEventContext,
+ BrowserEventSource,
+ BrowserHTTPHeaders,
+ BrowserInteractionClickEvent,
+ BrowserInteractionKeyEvent,
+ BrowserInteractionScrollSettledEvent,
+ BrowserMonitorDisconnectedEvent,
+ BrowserMonitorInitFailedEvent,
+ BrowserMonitorReconnectFailedEvent,
+ BrowserMonitorReconnectedEvent,
+ BrowserMonitorScreenshotEvent,
+ BrowserNetworkIdleEvent,
+ BrowserNetworkLoadingFailedEvent,
+ BrowserNetworkRequestEvent,
+ BrowserNetworkResponseEvent,
+ BrowserPageDomContentLoadedEvent,
+ BrowserPageLayoutSettledEvent,
+ BrowserPageLayoutShiftEvent,
+ BrowserPageLcpEvent,
+ BrowserPageLoadEvent,
+ BrowserPageNavigationEvent,
+ BrowserPageNavigationSettledEvent,
+ BrowserPageTabOpenedEvent,
+ BrowserTelemetryCategoriesConfig,
+ BrowserTelemetryCategoryConfig,
+ BrowserTelemetryConfig,
+ BrowserTelemetryEvent,
+ TelemetryStreamResponse,
+)
+```
+
+Methods:
+
+- client.browsers.telemetry.stream(id) -> TelemetryStreamResponse
+
## Replays
Types:
diff --git a/src/kernel/resources/browser_pools.py b/src/kernel/resources/browser_pools.py
index 682b2936..db8ebbeb 100644
--- a/src/kernel/resources/browser_pools.py
+++ b/src/kernel/resources/browser_pools.py
@@ -110,9 +110,11 @@ def create(
proxy_id: Optional proxy to associate to the browser session. Must reference a proxy
belonging to the caller's org.
- start_url: Optional URL to open when a browser is created for the pool. Navigation is
- best-effort, so navigation failures do not prevent the pool from filling. Reused
- browsers keep the page left by the previous lease.
+ start_url: Optional URL to navigate to when a new browser is warmed into the pool.
+ Best-effort: failures to navigate do not fail pool fill. Only applied to
+ newly-warmed browsers; browsers reused via release/acquire keep whatever URL the
+ previous lease left them on. Accepts any URL Chromium can resolve, including
+ chrome:// pages.
stealth: If true, launches the browser in stealth mode to reduce detection by anti-bot
mechanisms.
@@ -259,9 +261,11 @@ def update(
proxy_id: Optional proxy to associate to the browser session. Must reference a proxy
belonging to the caller's org.
- start_url: Optional URL to open when a browser is created for the pool. Navigation is
- best-effort, so navigation failures do not prevent the pool from filling. Reused
- browsers keep the page left by the previous lease.
+ start_url: Optional URL to navigate to when a new browser is warmed into the pool.
+ Best-effort: failures to navigate do not fail pool fill. Only applied to
+ newly-warmed browsers; browsers reused via release/acquire keep whatever URL the
+ previous lease left them on. Accepts any URL Chromium can resolve, including
+ chrome:// pages.
stealth: If true, launches the browser in stealth mode to reduce detection by anti-bot
mechanisms.
@@ -584,9 +588,11 @@ async def create(
proxy_id: Optional proxy to associate to the browser session. Must reference a proxy
belonging to the caller's org.
- start_url: Optional URL to open when a browser is created for the pool. Navigation is
- best-effort, so navigation failures do not prevent the pool from filling. Reused
- browsers keep the page left by the previous lease.
+ start_url: Optional URL to navigate to when a new browser is warmed into the pool.
+ Best-effort: failures to navigate do not fail pool fill. Only applied to
+ newly-warmed browsers; browsers reused via release/acquire keep whatever URL the
+ previous lease left them on. Accepts any URL Chromium can resolve, including
+ chrome:// pages.
stealth: If true, launches the browser in stealth mode to reduce detection by anti-bot
mechanisms.
@@ -733,9 +739,11 @@ async def update(
proxy_id: Optional proxy to associate to the browser session. Must reference a proxy
belonging to the caller's org.
- start_url: Optional URL to open when a browser is created for the pool. Navigation is
- best-effort, so navigation failures do not prevent the pool from filling. Reused
- browsers keep the page left by the previous lease.
+ start_url: Optional URL to navigate to when a new browser is warmed into the pool.
+ Best-effort: failures to navigate do not fail pool fill. Only applied to
+ newly-warmed browsers; browsers reused via release/acquire keep whatever URL the
+ previous lease left them on. Accepts any URL Chromium can resolve, including
+ chrome:// pages.
stealth: If true, launches the browser in stealth mode to reduce detection by anti-bot
mechanisms.
diff --git a/src/kernel/resources/browsers/__init__.py b/src/kernel/resources/browsers/__init__.py
index a1acee20..928d4553 100644
--- a/src/kernel/resources/browsers/__init__.py
+++ b/src/kernel/resources/browsers/__init__.py
@@ -48,6 +48,14 @@
ComputerResourceWithStreamingResponse,
AsyncComputerResourceWithStreamingResponse,
)
+from .telemetry import (
+ TelemetryResource,
+ AsyncTelemetryResource,
+ TelemetryResourceWithRawResponse,
+ AsyncTelemetryResourceWithRawResponse,
+ TelemetryResourceWithStreamingResponse,
+ AsyncTelemetryResourceWithStreamingResponse,
+)
from .playwright import (
PlaywrightResource,
AsyncPlaywrightResource,
@@ -58,6 +66,12 @@
)
__all__ = [
+ "TelemetryResource",
+ "AsyncTelemetryResource",
+ "TelemetryResourceWithRawResponse",
+ "AsyncTelemetryResourceWithRawResponse",
+ "TelemetryResourceWithStreamingResponse",
+ "AsyncTelemetryResourceWithStreamingResponse",
"ReplaysResource",
"AsyncReplaysResource",
"ReplaysResourceWithRawResponse",
diff --git a/src/kernel/resources/browsers/browsers.py b/src/kernel/resources/browsers/browsers.py
index a6dbd353..f7136a1a 100644
--- a/src/kernel/resources/browsers/browsers.py
+++ b/src/kernel/resources/browsers/browsers.py
@@ -62,6 +62,14 @@
AsyncComputerResourceWithStreamingResponse,
)
from ..._compat import cached_property
+from .telemetry import (
+ TelemetryResource,
+ AsyncTelemetryResource,
+ TelemetryResourceWithRawResponse,
+ AsyncTelemetryResourceWithRawResponse,
+ TelemetryResourceWithStreamingResponse,
+ AsyncTelemetryResourceWithStreamingResponse,
+)
from .playwright import (
PlaywrightResource,
AsyncPlaywrightResource,
@@ -94,6 +102,7 @@
from ...types.shared_params.browser_profile import BrowserProfile
from ...types.shared_params.browser_viewport import BrowserViewport
from ...types.shared_params.browser_extension import BrowserExtension
+from ...types.browsers.browser_telemetry_config_param import BrowserTelemetryConfigParam
__all__ = ["BrowsersResource", "AsyncBrowsersResource"]
@@ -101,6 +110,11 @@
class BrowsersResource(SyncAPIResource):
"""Create and manage browser sessions."""
+ @cached_property
+ def telemetry(self) -> TelemetryResource:
+ """Stream live telemetry events from a browser session."""
+ return TelemetryResource(self._client)
+
@cached_property
def replays(self) -> ReplaysResource:
"""Record and manage browser session video replays."""
@@ -163,6 +177,7 @@ def create(
proxy_id: str | Omit = omit,
start_url: str | Omit = omit,
stealth: bool | Omit = omit,
+ telemetry: Optional[BrowserTelemetryConfigParam] | Omit = omit,
timeout_seconds: int | Omit = omit,
viewport: BrowserViewport | Omit = omit,
# Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
@@ -211,6 +226,10 @@ def create(
stealth: If true, launches the browser in stealth mode to reduce detection by anti-bot
mechanisms.
+ telemetry: Telemetry configuration for the browser session. If provided, telemetry capture
+ starts with the specified category filter when the session is created. If
+ omitted, no telemetry capture is started.
+
timeout_seconds: The number of seconds of inactivity before the browser session is terminated.
Activity includes CDP connections and live view connections. Defaults to 60
seconds. Minimum allowed is 10 seconds. Maximum allowed is 259200 (72 hours). We
@@ -253,6 +272,7 @@ def create(
"proxy_id": proxy_id,
"start_url": start_url,
"stealth": stealth,
+ "telemetry": telemetry,
"timeout_seconds": timeout_seconds,
"viewport": viewport,
},
@@ -313,6 +333,7 @@ def update(
disable_default_proxy: bool | Omit = omit,
profile: BrowserProfile | Omit = omit,
proxy_id: Optional[str] | Omit = omit,
+ telemetry: Optional[BrowserTelemetryConfigParam] | Omit = omit,
viewport: browser_update_params.Viewport | 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.
@@ -334,6 +355,11 @@ def update(
proxy_id: ID of the proxy to use. Omit to leave unchanged, set to empty string to remove
proxy.
+ telemetry: Telemetry configuration. Omit, set to null, or set to an empty object ({}) to
+ leave the existing configuration unchanged (no-op). To enable capture for all
+ categories using VM defaults, set browser to an empty object ({"browser": {}}).
+ To stop capture, set every category's enabled to false.
+
viewport: Viewport configuration to apply to the browser session.
extra_headers: Send extra headers
@@ -353,6 +379,7 @@ def update(
"disable_default_proxy": disable_default_proxy,
"profile": profile,
"proxy_id": proxy_id,
+ "telemetry": telemetry,
"viewport": viewport,
},
browser_update_params.BrowserUpdateParams,
@@ -673,6 +700,11 @@ def load_extensions(
class AsyncBrowsersResource(AsyncAPIResource):
"""Create and manage browser sessions."""
+ @cached_property
+ def telemetry(self) -> AsyncTelemetryResource:
+ """Stream live telemetry events from a browser session."""
+ return AsyncTelemetryResource(self._client)
+
@cached_property
def replays(self) -> AsyncReplaysResource:
"""Record and manage browser session video replays."""
@@ -735,6 +767,7 @@ async def create(
proxy_id: str | Omit = omit,
start_url: str | Omit = omit,
stealth: bool | Omit = omit,
+ telemetry: Optional[BrowserTelemetryConfigParam] | Omit = omit,
timeout_seconds: int | Omit = omit,
viewport: BrowserViewport | Omit = omit,
# Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
@@ -783,6 +816,10 @@ async def create(
stealth: If true, launches the browser in stealth mode to reduce detection by anti-bot
mechanisms.
+ telemetry: Telemetry configuration for the browser session. If provided, telemetry capture
+ starts with the specified category filter when the session is created. If
+ omitted, no telemetry capture is started.
+
timeout_seconds: The number of seconds of inactivity before the browser session is terminated.
Activity includes CDP connections and live view connections. Defaults to 60
seconds. Minimum allowed is 10 seconds. Maximum allowed is 259200 (72 hours). We
@@ -825,6 +862,7 @@ async def create(
"proxy_id": proxy_id,
"start_url": start_url,
"stealth": stealth,
+ "telemetry": telemetry,
"timeout_seconds": timeout_seconds,
"viewport": viewport,
},
@@ -885,6 +923,7 @@ async def update(
disable_default_proxy: bool | Omit = omit,
profile: BrowserProfile | Omit = omit,
proxy_id: Optional[str] | Omit = omit,
+ telemetry: Optional[BrowserTelemetryConfigParam] | Omit = omit,
viewport: browser_update_params.Viewport | 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.
@@ -906,6 +945,11 @@ async def update(
proxy_id: ID of the proxy to use. Omit to leave unchanged, set to empty string to remove
proxy.
+ telemetry: Telemetry configuration. Omit, set to null, or set to an empty object ({}) to
+ leave the existing configuration unchanged (no-op). To enable capture for all
+ categories using VM defaults, set browser to an empty object ({"browser": {}}).
+ To stop capture, set every category's enabled to false.
+
viewport: Viewport configuration to apply to the browser session.
extra_headers: Send extra headers
@@ -925,6 +969,7 @@ async def update(
"disable_default_proxy": disable_default_proxy,
"profile": profile,
"proxy_id": proxy_id,
+ "telemetry": telemetry,
"viewport": viewport,
},
browser_update_params.BrowserUpdateParams,
@@ -1275,6 +1320,11 @@ def __init__(self, browsers: BrowsersResource) -> None:
browsers.load_extensions,
)
+ @cached_property
+ def telemetry(self) -> TelemetryResourceWithRawResponse:
+ """Stream live telemetry events from a browser session."""
+ return TelemetryResourceWithRawResponse(self._browsers.telemetry)
+
@cached_property
def replays(self) -> ReplaysResourceWithRawResponse:
"""Record and manage browser session video replays."""
@@ -1336,6 +1386,11 @@ def __init__(self, browsers: AsyncBrowsersResource) -> None:
browsers.load_extensions,
)
+ @cached_property
+ def telemetry(self) -> AsyncTelemetryResourceWithRawResponse:
+ """Stream live telemetry events from a browser session."""
+ return AsyncTelemetryResourceWithRawResponse(self._browsers.telemetry)
+
@cached_property
def replays(self) -> AsyncReplaysResourceWithRawResponse:
"""Record and manage browser session video replays."""
@@ -1397,6 +1452,11 @@ def __init__(self, browsers: BrowsersResource) -> None:
browsers.load_extensions,
)
+ @cached_property
+ def telemetry(self) -> TelemetryResourceWithStreamingResponse:
+ """Stream live telemetry events from a browser session."""
+ return TelemetryResourceWithStreamingResponse(self._browsers.telemetry)
+
@cached_property
def replays(self) -> ReplaysResourceWithStreamingResponse:
"""Record and manage browser session video replays."""
@@ -1458,6 +1518,11 @@ def __init__(self, browsers: AsyncBrowsersResource) -> None:
browsers.load_extensions,
)
+ @cached_property
+ def telemetry(self) -> AsyncTelemetryResourceWithStreamingResponse:
+ """Stream live telemetry events from a browser session."""
+ return AsyncTelemetryResourceWithStreamingResponse(self._browsers.telemetry)
+
@cached_property
def replays(self) -> AsyncReplaysResourceWithStreamingResponse:
"""Record and manage browser session video replays."""
diff --git a/src/kernel/resources/browsers/telemetry.py b/src/kernel/resources/browsers/telemetry.py
new file mode 100644
index 00000000..cb95b2d1
--- /dev/null
+++ b/src/kernel/resources/browsers/telemetry.py
@@ -0,0 +1,195 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from __future__ import annotations
+
+import httpx
+
+from ..._types import Body, Omit, Query, Headers, NotGiven, omit, not_given
+from ..._utils import path_template, strip_not_given
+from ..._compat import cached_property
+from ..._resource import SyncAPIResource, AsyncAPIResource
+from ..._response import (
+ to_raw_response_wrapper,
+ to_streamed_response_wrapper,
+ async_to_raw_response_wrapper,
+ async_to_streamed_response_wrapper,
+)
+from ..._streaming import Stream, AsyncStream
+from ..._base_client import make_request_options
+from ...types.browsers.telemetry_stream_response import TelemetryStreamResponse
+
+__all__ = ["TelemetryResource", "AsyncTelemetryResource"]
+
+
+class TelemetryResource(SyncAPIResource):
+ """Stream live telemetry events from a browser session."""
+
+ @cached_property
+ def with_raw_response(self) -> TelemetryResourceWithRawResponse:
+ """
+ This property can be used as a prefix for any HTTP method call to return
+ the raw response object instead of the parsed content.
+
+ For more information, see https://www.github.com/kernel/kernel-python-sdk#accessing-raw-response-data-eg-headers
+ """
+ return TelemetryResourceWithRawResponse(self)
+
+ @cached_property
+ def with_streaming_response(self) -> TelemetryResourceWithStreamingResponse:
+ """
+ An alternative to `.with_raw_response` that doesn't eagerly read the response body.
+
+ For more information, see https://www.github.com/kernel/kernel-python-sdk#with_streaming_response
+ """
+ return TelemetryResourceWithStreamingResponse(self)
+
+ def stream(
+ self,
+ id: str,
+ *,
+ last_event_id: str | Omit = omit,
+ # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
+ # The extra values given here take precedence over values defined on the client or passed to this method.
+ extra_headers: Headers | None = None,
+ extra_query: Query | None = None,
+ extra_body: Body | None = None,
+ timeout: float | httpx.Timeout | None | NotGiven = not_given,
+ ) -> Stream[TelemetryStreamResponse]:
+ """Streams browser telemetry events as a server-sent events (SSE) stream.
+
+ The
+ stream closes when the browser session terminates. Each event frame includes an
+ id: field containing a monotonically increasing sequence number; pass it as
+ Last-Event-ID on reconnect to resume without gaps. The event: field is never
+ set; all frames carry JSON in the data: field. A keepalive comment frame is sent
+ every 15 seconds when no events arrive. Returns 404 if the browser session does
+ not exist. If telemetry was not enabled on the session, the stream opens but no
+ events are delivered.
+
+ Args:
+ extra_headers: Send extra headers
+
+ extra_query: Add additional query parameters to the request
+
+ extra_body: Add additional JSON properties to the request
+
+ timeout: Override the client-level default timeout for this request, in seconds
+ """
+ if not id:
+ raise ValueError(f"Expected a non-empty value for `id` but received {id!r}")
+ extra_headers = {"Accept": "text/event-stream", **(extra_headers or {})}
+ extra_headers = {**strip_not_given({"Last-Event-ID": last_event_id}), **(extra_headers or {})}
+ return self._get(
+ path_template("/browsers/{id}/telemetry", id=id),
+ options=make_request_options(
+ extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
+ ),
+ cast_to=TelemetryStreamResponse,
+ stream=True,
+ stream_cls=Stream[TelemetryStreamResponse],
+ )
+
+
+class AsyncTelemetryResource(AsyncAPIResource):
+ """Stream live telemetry events from a browser session."""
+
+ @cached_property
+ def with_raw_response(self) -> AsyncTelemetryResourceWithRawResponse:
+ """
+ This property can be used as a prefix for any HTTP method call to return
+ the raw response object instead of the parsed content.
+
+ For more information, see https://www.github.com/kernel/kernel-python-sdk#accessing-raw-response-data-eg-headers
+ """
+ return AsyncTelemetryResourceWithRawResponse(self)
+
+ @cached_property
+ def with_streaming_response(self) -> AsyncTelemetryResourceWithStreamingResponse:
+ """
+ An alternative to `.with_raw_response` that doesn't eagerly read the response body.
+
+ For more information, see https://www.github.com/kernel/kernel-python-sdk#with_streaming_response
+ """
+ return AsyncTelemetryResourceWithStreamingResponse(self)
+
+ async def stream(
+ self,
+ id: str,
+ *,
+ last_event_id: str | Omit = omit,
+ # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
+ # The extra values given here take precedence over values defined on the client or passed to this method.
+ extra_headers: Headers | None = None,
+ extra_query: Query | None = None,
+ extra_body: Body | None = None,
+ timeout: float | httpx.Timeout | None | NotGiven = not_given,
+ ) -> AsyncStream[TelemetryStreamResponse]:
+ """Streams browser telemetry events as a server-sent events (SSE) stream.
+
+ The
+ stream closes when the browser session terminates. Each event frame includes an
+ id: field containing a monotonically increasing sequence number; pass it as
+ Last-Event-ID on reconnect to resume without gaps. The event: field is never
+ set; all frames carry JSON in the data: field. A keepalive comment frame is sent
+ every 15 seconds when no events arrive. Returns 404 if the browser session does
+ not exist. If telemetry was not enabled on the session, the stream opens but no
+ events are delivered.
+
+ Args:
+ extra_headers: Send extra headers
+
+ extra_query: Add additional query parameters to the request
+
+ extra_body: Add additional JSON properties to the request
+
+ timeout: Override the client-level default timeout for this request, in seconds
+ """
+ if not id:
+ raise ValueError(f"Expected a non-empty value for `id` but received {id!r}")
+ extra_headers = {"Accept": "text/event-stream", **(extra_headers or {})}
+ extra_headers = {**strip_not_given({"Last-Event-ID": last_event_id}), **(extra_headers or {})}
+ return await self._get(
+ path_template("/browsers/{id}/telemetry", id=id),
+ options=make_request_options(
+ extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
+ ),
+ cast_to=TelemetryStreamResponse,
+ stream=True,
+ stream_cls=AsyncStream[TelemetryStreamResponse],
+ )
+
+
+class TelemetryResourceWithRawResponse:
+ def __init__(self, telemetry: TelemetryResource) -> None:
+ self._telemetry = telemetry
+
+ self.stream = to_raw_response_wrapper(
+ telemetry.stream,
+ )
+
+
+class AsyncTelemetryResourceWithRawResponse:
+ def __init__(self, telemetry: AsyncTelemetryResource) -> None:
+ self._telemetry = telemetry
+
+ self.stream = async_to_raw_response_wrapper(
+ telemetry.stream,
+ )
+
+
+class TelemetryResourceWithStreamingResponse:
+ def __init__(self, telemetry: TelemetryResource) -> None:
+ self._telemetry = telemetry
+
+ self.stream = to_streamed_response_wrapper(
+ telemetry.stream,
+ )
+
+
+class AsyncTelemetryResourceWithStreamingResponse:
+ def __init__(self, telemetry: AsyncTelemetryResource) -> None:
+ self._telemetry = telemetry
+
+ self.stream = async_to_streamed_response_wrapper(
+ telemetry.stream,
+ )
diff --git a/src/kernel/resources/proxies.py b/src/kernel/resources/proxies.py
index bacdd570..501a2e88 100644
--- a/src/kernel/resources/proxies.py
+++ b/src/kernel/resources/proxies.py
@@ -222,7 +222,9 @@ def check(
between requests, so a successful check validates proxy configuration but does
not guarantee that a subsequent browser session will use the same exit IP or
reach the same site — it is useful for verifying credentials and connectivity,
- not for predicting site-specific behavior.
+ not for predicting site-specific behavior. When provided, the check result does
+ not update the proxy's health status, since a failure may indicate a problem
+ with the target site rather than the proxy itself.
extra_headers: Send extra headers
@@ -440,7 +442,9 @@ async def check(
between requests, so a successful check validates proxy configuration but does
not guarantee that a subsequent browser session will use the same exit IP or
reach the same site — it is useful for verifying credentials and connectivity,
- not for predicting site-specific behavior.
+ not for predicting site-specific behavior. When provided, the check result does
+ not update the proxy's health status, since a failure may indicate a problem
+ with the target site rather than the proxy itself.
extra_headers: Send extra headers
diff --git a/src/kernel/types/__init__.py b/src/kernel/types/__init__.py
index 3838047e..1a7d19c3 100644
--- a/src/kernel/types/__init__.py
+++ b/src/kernel/types/__init__.py
@@ -2,6 +2,8 @@
from __future__ import annotations
+from . import browsers
+from .. import _compat
from .shared import (
LogEvent as LogEvent,
AppAction as AppAction,
@@ -91,3 +93,18 @@
from .extension_download_from_chrome_store_params import (
ExtensionDownloadFromChromeStoreParams as ExtensionDownloadFromChromeStoreParams,
)
+
+# Rebuild cyclical models only after all modules are imported.
+# This ensures that, when building the deferred (due to cyclical references) model schema,
+# Pydantic can resolve the necessary references.
+# See: https://github.com/pydantic/pydantic/issues/11250 for more context.
+if _compat.PYDANTIC_V1:
+ browsers.browser_call_stack.BrowserCallStack.update_forward_refs() # type: ignore
+ browsers.browser_console_error_event.BrowserConsoleErrorEvent.update_forward_refs() # type: ignore
+ browsers.browser_console_log_event.BrowserConsoleLogEvent.update_forward_refs() # type: ignore
+ browsers.telemetry_stream_response.TelemetryStreamResponse.update_forward_refs() # type: ignore
+else:
+ browsers.browser_call_stack.BrowserCallStack.model_rebuild(_parent_namespace_depth=0)
+ browsers.browser_console_error_event.BrowserConsoleErrorEvent.model_rebuild(_parent_namespace_depth=0)
+ browsers.browser_console_log_event.BrowserConsoleLogEvent.model_rebuild(_parent_namespace_depth=0)
+ browsers.telemetry_stream_response.TelemetryStreamResponse.model_rebuild(_parent_namespace_depth=0)
diff --git a/src/kernel/types/browser_create_params.py b/src/kernel/types/browser_create_params.py
index c05fd94e..ca184fc7 100644
--- a/src/kernel/types/browser_create_params.py
+++ b/src/kernel/types/browser_create_params.py
@@ -2,13 +2,14 @@
from __future__ import annotations
-from typing import Dict, Iterable
+from typing import Dict, Iterable, Optional
from typing_extensions import TypedDict
from .browser_persistence_param import BrowserPersistenceParam
from .shared_params.browser_profile import BrowserProfile
from .shared_params.browser_viewport import BrowserViewport
from .shared_params.browser_extension import BrowserExtension
+from .browsers.browser_telemetry_config_param import BrowserTelemetryConfigParam
__all__ = ["BrowserCreateParams"]
@@ -79,6 +80,13 @@ class BrowserCreateParams(TypedDict, total=False):
mechanisms.
"""
+ telemetry: Optional[BrowserTelemetryConfigParam]
+ """Telemetry configuration for the browser session.
+
+ If provided, telemetry capture starts with the specified category filter when
+ the session is created. If omitted, no telemetry capture is started.
+ """
+
timeout_seconds: int
"""The number of seconds of inactivity before the browser session is terminated.
diff --git a/src/kernel/types/browser_create_response.py b/src/kernel/types/browser_create_response.py
index 2e2e5dbc..bbb2287e 100644
--- a/src/kernel/types/browser_create_response.py
+++ b/src/kernel/types/browser_create_response.py
@@ -9,6 +9,7 @@
from .browser_pool_ref import BrowserPoolRef
from .browser_persistence import BrowserPersistence
from .shared.browser_viewport import BrowserViewport
+from .browsers.browser_telemetry_config import BrowserTelemetryConfig
__all__ = ["BrowserCreateResponse"]
@@ -76,7 +77,16 @@ class BrowserCreateResponse(BaseModel):
"""ID of the proxy associated with this browser session, if any."""
start_url: Optional[str] = None
- """Start URL requested for the session, if provided."""
+ """URL the session was asked to navigate to on creation, if any.
+
+ Recorded for debugging. Navigation is fire-and-forget — the URL is dispatched to
+ the browser without waiting for it to load, and any errors (DNS failure, bad
+ status, timeout) are silently dropped. Captures what was requested, not what the
+ browser actually loaded.
+ """
+
+ telemetry: Optional[BrowserTelemetryConfig] = None
+ """Active telemetry configuration for the session, if any."""
usage: Optional[BrowserUsage] = None
"""Session usage metrics."""
diff --git a/src/kernel/types/browser_list_response.py b/src/kernel/types/browser_list_response.py
index 27e409f4..ed14251a 100644
--- a/src/kernel/types/browser_list_response.py
+++ b/src/kernel/types/browser_list_response.py
@@ -9,6 +9,7 @@
from .browser_pool_ref import BrowserPoolRef
from .browser_persistence import BrowserPersistence
from .shared.browser_viewport import BrowserViewport
+from .browsers.browser_telemetry_config import BrowserTelemetryConfig
__all__ = ["BrowserListResponse"]
@@ -76,7 +77,16 @@ class BrowserListResponse(BaseModel):
"""ID of the proxy associated with this browser session, if any."""
start_url: Optional[str] = None
- """Start URL requested for the session, if provided."""
+ """URL the session was asked to navigate to on creation, if any.
+
+ Recorded for debugging. Navigation is fire-and-forget — the URL is dispatched to
+ the browser without waiting for it to load, and any errors (DNS failure, bad
+ status, timeout) are silently dropped. Captures what was requested, not what the
+ browser actually loaded.
+ """
+
+ telemetry: Optional[BrowserTelemetryConfig] = None
+ """Active telemetry configuration for the session, if any."""
usage: Optional[BrowserUsage] = None
"""Session usage metrics."""
diff --git a/src/kernel/types/browser_pool.py b/src/kernel/types/browser_pool.py
index e456fca2..8ead6807 100644
--- a/src/kernel/types/browser_pool.py
+++ b/src/kernel/types/browser_pool.py
@@ -64,10 +64,12 @@ class BrowserPoolConfig(BaseModel):
"""
start_url: Optional[str] = None
- """Optional URL to open when a browser is created for the pool.
+ """Optional URL to navigate to when a new browser is warmed into the pool.
- Navigation is best-effort, so navigation failures do not prevent the pool from
- filling. Reused browsers keep the page left by the previous lease.
+ Best-effort: failures to navigate do not fail pool fill. Only applied to
+ newly-warmed browsers; browsers reused via release/acquire keep whatever URL the
+ previous lease left them on. Accepts any URL Chromium can resolve, including
+ chrome:// pages.
"""
stealth: Optional[bool] = None
diff --git a/src/kernel/types/browser_pool_acquire_response.py b/src/kernel/types/browser_pool_acquire_response.py
index 9cf914ee..20ab1fc5 100644
--- a/src/kernel/types/browser_pool_acquire_response.py
+++ b/src/kernel/types/browser_pool_acquire_response.py
@@ -9,6 +9,7 @@
from .browser_pool_ref import BrowserPoolRef
from .browser_persistence import BrowserPersistence
from .shared.browser_viewport import BrowserViewport
+from .browsers.browser_telemetry_config import BrowserTelemetryConfig
__all__ = ["BrowserPoolAcquireResponse"]
@@ -76,7 +77,16 @@ class BrowserPoolAcquireResponse(BaseModel):
"""ID of the proxy associated with this browser session, if any."""
start_url: Optional[str] = None
- """Start URL requested for the session, if provided."""
+ """URL the session was asked to navigate to on creation, if any.
+
+ Recorded for debugging. Navigation is fire-and-forget — the URL is dispatched to
+ the browser without waiting for it to load, and any errors (DNS failure, bad
+ status, timeout) are silently dropped. Captures what was requested, not what the
+ browser actually loaded.
+ """
+
+ telemetry: Optional[BrowserTelemetryConfig] = None
+ """Active telemetry configuration for the session, if any."""
usage: Optional[BrowserUsage] = None
"""Session usage metrics."""
diff --git a/src/kernel/types/browser_pool_create_params.py b/src/kernel/types/browser_pool_create_params.py
index bc08c3f2..1ffeeb75 100644
--- a/src/kernel/types/browser_pool_create_params.py
+++ b/src/kernel/types/browser_pool_create_params.py
@@ -63,10 +63,12 @@ class BrowserPoolCreateParams(TypedDict, total=False):
"""
start_url: str
- """Optional URL to open when a browser is created for the pool.
+ """Optional URL to navigate to when a new browser is warmed into the pool.
- Navigation is best-effort, so navigation failures do not prevent the pool from
- filling. Reused browsers keep the page left by the previous lease.
+ Best-effort: failures to navigate do not fail pool fill. Only applied to
+ newly-warmed browsers; browsers reused via release/acquire keep whatever URL the
+ previous lease left them on. Accepts any URL Chromium can resolve, including
+ chrome:// pages.
"""
stealth: bool
diff --git a/src/kernel/types/browser_pool_update_params.py b/src/kernel/types/browser_pool_update_params.py
index 043eaf85..4efc9ced 100644
--- a/src/kernel/types/browser_pool_update_params.py
+++ b/src/kernel/types/browser_pool_update_params.py
@@ -69,10 +69,12 @@ class BrowserPoolUpdateParams(TypedDict, total=False):
"""
start_url: str
- """Optional URL to open when a browser is created for the pool.
+ """Optional URL to navigate to when a new browser is warmed into the pool.
- Navigation is best-effort, so navigation failures do not prevent the pool from
- filling. Reused browsers keep the page left by the previous lease.
+ Best-effort: failures to navigate do not fail pool fill. Only applied to
+ newly-warmed browsers; browsers reused via release/acquire keep whatever URL the
+ previous lease left them on. Accepts any URL Chromium can resolve, including
+ chrome:// pages.
"""
stealth: bool
diff --git a/src/kernel/types/browser_retrieve_response.py b/src/kernel/types/browser_retrieve_response.py
index 95cfa9ad..6c79b519 100644
--- a/src/kernel/types/browser_retrieve_response.py
+++ b/src/kernel/types/browser_retrieve_response.py
@@ -9,6 +9,7 @@
from .browser_pool_ref import BrowserPoolRef
from .browser_persistence import BrowserPersistence
from .shared.browser_viewport import BrowserViewport
+from .browsers.browser_telemetry_config import BrowserTelemetryConfig
__all__ = ["BrowserRetrieveResponse"]
@@ -76,7 +77,16 @@ class BrowserRetrieveResponse(BaseModel):
"""ID of the proxy associated with this browser session, if any."""
start_url: Optional[str] = None
- """Start URL requested for the session, if provided."""
+ """URL the session was asked to navigate to on creation, if any.
+
+ Recorded for debugging. Navigation is fire-and-forget — the URL is dispatched to
+ the browser without waiting for it to load, and any errors (DNS failure, bad
+ status, timeout) are silently dropped. Captures what was requested, not what the
+ browser actually loaded.
+ """
+
+ telemetry: Optional[BrowserTelemetryConfig] = None
+ """Active telemetry configuration for the session, if any."""
usage: Optional[BrowserUsage] = None
"""Session usage metrics."""
diff --git a/src/kernel/types/browser_update_params.py b/src/kernel/types/browser_update_params.py
index e0f7588e..297d0388 100644
--- a/src/kernel/types/browser_update_params.py
+++ b/src/kernel/types/browser_update_params.py
@@ -7,6 +7,7 @@
from .shared_params.browser_profile import BrowserProfile
from .shared_params.browser_viewport import BrowserViewport
+from .browsers.browser_telemetry_config_param import BrowserTelemetryConfigParam
__all__ = ["BrowserUpdateParams", "Viewport"]
@@ -30,6 +31,15 @@ class BrowserUpdateParams(TypedDict, total=False):
Omit to leave unchanged, set to empty string to remove proxy.
"""
+ telemetry: Optional[BrowserTelemetryConfigParam]
+ """Telemetry configuration.
+
+ Omit, set to null, or set to an empty object ({}) to leave the existing
+ configuration unchanged (no-op). To enable capture for all categories using VM
+ defaults, set browser to an empty object ({"browser": {}}). To stop capture, set
+ every category's enabled to false.
+ """
+
viewport: Viewport
"""Viewport configuration to apply to the browser session."""
diff --git a/src/kernel/types/browser_update_response.py b/src/kernel/types/browser_update_response.py
index 209d58c4..4f237cf5 100644
--- a/src/kernel/types/browser_update_response.py
+++ b/src/kernel/types/browser_update_response.py
@@ -9,6 +9,7 @@
from .browser_pool_ref import BrowserPoolRef
from .browser_persistence import BrowserPersistence
from .shared.browser_viewport import BrowserViewport
+from .browsers.browser_telemetry_config import BrowserTelemetryConfig
__all__ = ["BrowserUpdateResponse"]
@@ -76,7 +77,16 @@ class BrowserUpdateResponse(BaseModel):
"""ID of the proxy associated with this browser session, if any."""
start_url: Optional[str] = None
- """Start URL requested for the session, if provided."""
+ """URL the session was asked to navigate to on creation, if any.
+
+ Recorded for debugging. Navigation is fire-and-forget — the URL is dispatched to
+ the browser without waiting for it to load, and any errors (DNS failure, bad
+ status, timeout) are silently dropped. Captures what was requested, not what the
+ browser actually loaded.
+ """
+
+ telemetry: Optional[BrowserTelemetryConfig] = None
+ """Active telemetry configuration for the session, if any."""
usage: Optional[BrowserUsage] = None
"""Session usage metrics."""
diff --git a/src/kernel/types/browsers/__init__.py b/src/kernel/types/browsers/__init__.py
index 1e47205d..a3bca412 100644
--- a/src/kernel/types/browsers/__init__.py
+++ b/src/kernel/types/browsers/__init__.py
@@ -5,6 +5,7 @@
from .f_move_params import FMoveParams as FMoveParams
from .f_upload_params import FUploadParams as FUploadParams
from .log_stream_params import LogStreamParams as LogStreamParams
+from .browser_call_stack import BrowserCallStack as BrowserCallStack
from .f_file_info_params import FFileInfoParams as FFileInfoParams
from .f_read_file_params import FReadFileParams as FReadFileParams
from .f_list_files_params import FListFilesParams as FListFilesParams
@@ -13,41 +14,84 @@
from .process_exec_params import ProcessExecParams as ProcessExecParams
from .process_kill_params import ProcessKillParams as ProcessKillParams
from .replay_start_params import ReplayStartParams as ReplayStartParams
+from .browser_event_source import BrowserEventSource as BrowserEventSource
+from .browser_http_headers import BrowserHTTPHeaders as BrowserHTTPHeaders
from .f_delete_file_params import FDeleteFileParams as FDeleteFileParams
from .f_file_info_response import FFileInfoResponse as FFileInfoResponse
from .process_spawn_params import ProcessSpawnParams as ProcessSpawnParams
from .process_stdin_params import ProcessStdinParams as ProcessStdinParams
from .replay_list_response import ReplayListResponse as ReplayListResponse
+from .browser_event_context import BrowserEventContext as BrowserEventContext
from .computer_batch_params import ComputerBatchParams as ComputerBatchParams
from .f_list_files_response import FListFilesResponse as FListFilesResponse
from .process_exec_response import ProcessExecResponse as ProcessExecResponse
from .process_kill_response import ProcessKillResponse as ProcessKillResponse
from .process_resize_params import ProcessResizeParams as ProcessResizeParams
from .replay_start_response import ReplayStartResponse as ReplayStartResponse
+from .browser_page_lcp_event import BrowserPageLcpEvent as BrowserPageLcpEvent
from .computer_scroll_params import ComputerScrollParams as ComputerScrollParams
from .process_spawn_response import ProcessSpawnResponse as ProcessSpawnResponse
from .process_stdin_response import ProcessStdinResponse as ProcessStdinResponse
+from .browser_page_load_event import BrowserPageLoadEvent as BrowserPageLoadEvent
+from .browser_telemetry_event import BrowserTelemetryEvent as BrowserTelemetryEvent
from .process_resize_response import ProcessResizeResponse as ProcessResizeResponse
from .process_status_response import ProcessStatusResponse as ProcessStatusResponse
+from .browser_telemetry_config import BrowserTelemetryConfig as BrowserTelemetryConfig
+from .browser_console_log_event import BrowserConsoleLogEvent as BrowserConsoleLogEvent
from .computer_press_key_params import ComputerPressKeyParams as ComputerPressKeyParams
from .computer_type_text_params import ComputerTypeTextParams as ComputerTypeTextParams
from .f_create_directory_params import FCreateDirectoryParams as FCreateDirectoryParams
from .f_delete_directory_params import FDeleteDirectoryParams as FDeleteDirectoryParams
from .f_download_dir_zip_params import FDownloadDirZipParams as FDownloadDirZipParams
from .playwright_execute_params import PlaywrightExecuteParams as PlaywrightExecuteParams
+from .telemetry_stream_response import TelemetryStreamResponse as TelemetryStreamResponse
+from .browser_network_idle_event import BrowserNetworkIdleEvent as BrowserNetworkIdleEvent
from .computer_drag_mouse_params import ComputerDragMouseParams as ComputerDragMouseParams
from .computer_move_mouse_params import ComputerMoveMouseParams as ComputerMoveMouseParams
+from .browser_console_error_event import BrowserConsoleErrorEvent as BrowserConsoleErrorEvent
from .computer_click_mouse_params import ComputerClickMouseParams as ComputerClickMouseParams
from .playwright_execute_response import PlaywrightExecuteResponse as PlaywrightExecuteResponse
+from .browser_interaction_key_event import BrowserInteractionKeyEvent as BrowserInteractionKeyEvent
+from .browser_network_request_event import BrowserNetworkRequestEvent as BrowserNetworkRequestEvent
+from .browser_page_navigation_event import BrowserPageNavigationEvent as BrowserPageNavigationEvent
+from .browser_page_tab_opened_event import BrowserPageTabOpenedEvent as BrowserPageTabOpenedEvent
from .f_set_file_permissions_params import FSetFilePermissionsParams as FSetFilePermissionsParams
+from .browser_network_response_event import BrowserNetworkResponseEvent as BrowserNetworkResponseEvent
+from .browser_telemetry_config_param import BrowserTelemetryConfigParam as BrowserTelemetryConfigParam
from .process_stdout_stream_response import ProcessStdoutStreamResponse as ProcessStdoutStreamResponse
+from .browser_interaction_click_event import BrowserInteractionClickEvent as BrowserInteractionClickEvent
+from .browser_page_layout_shift_event import BrowserPageLayoutShiftEvent as BrowserPageLayoutShiftEvent
from .computer_write_clipboard_params import ComputerWriteClipboardParams as ComputerWriteClipboardParams
+from .browser_monitor_screenshot_event import BrowserMonitorScreenshotEvent as BrowserMonitorScreenshotEvent
from .computer_read_clipboard_response import ComputerReadClipboardResponse as ComputerReadClipboardResponse
+from .browser_monitor_init_failed_event import BrowserMonitorInitFailedEvent as BrowserMonitorInitFailedEvent
+from .browser_monitor_reconnected_event import BrowserMonitorReconnectedEvent as BrowserMonitorReconnectedEvent
+from .browser_page_layout_settled_event import BrowserPageLayoutSettledEvent as BrowserPageLayoutSettledEvent
+from .browser_telemetry_category_config import BrowserTelemetryCategoryConfig as BrowserTelemetryCategoryConfig
+from .browser_monitor_disconnected_event import BrowserMonitorDisconnectedEvent as BrowserMonitorDisconnectedEvent
from .computer_capture_screenshot_params import ComputerCaptureScreenshotParams as ComputerCaptureScreenshotParams
+from .browser_telemetry_categories_config import BrowserTelemetryCategoriesConfig as BrowserTelemetryCategoriesConfig
+from .browser_network_loading_failed_event import BrowserNetworkLoadingFailedEvent as BrowserNetworkLoadingFailedEvent
from .computer_get_mouse_position_response import ComputerGetMousePositionResponse as ComputerGetMousePositionResponse
+from .browser_page_dom_content_loaded_event import BrowserPageDomContentLoadedEvent as BrowserPageDomContentLoadedEvent
+from .browser_page_navigation_settled_event import (
+ BrowserPageNavigationSettledEvent as BrowserPageNavigationSettledEvent,
+)
from .computer_set_cursor_visibility_params import (
ComputerSetCursorVisibilityParams as ComputerSetCursorVisibilityParams,
)
+from .browser_monitor_reconnect_failed_event import (
+ BrowserMonitorReconnectFailedEvent as BrowserMonitorReconnectFailedEvent,
+)
+from .browser_telemetry_category_config_param import (
+ BrowserTelemetryCategoryConfigParam as BrowserTelemetryCategoryConfigParam,
+)
from .computer_set_cursor_visibility_response import (
ComputerSetCursorVisibilityResponse as ComputerSetCursorVisibilityResponse,
)
+from .browser_interaction_scroll_settled_event import (
+ BrowserInteractionScrollSettledEvent as BrowserInteractionScrollSettledEvent,
+)
+from .browser_telemetry_categories_config_param import (
+ BrowserTelemetryCategoriesConfigParam as BrowserTelemetryCategoriesConfigParam,
+)
diff --git a/src/kernel/types/browsers/browser_call_stack.py b/src/kernel/types/browsers/browser_call_stack.py
new file mode 100644
index 00000000..d9e97ffd
--- /dev/null
+++ b/src/kernel/types/browsers/browser_call_stack.py
@@ -0,0 +1,43 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from __future__ import annotations
+
+from typing import List, Optional
+
+from pydantic import Field as FieldInfo
+
+from ..._models import BaseModel
+
+__all__ = ["BrowserCallStack", "CallFrame"]
+
+
+class CallFrame(BaseModel):
+ column_number: int = FieldInfo(alias="columnNumber")
+ """Zero-based column number within the line."""
+
+ function_name: str = FieldInfo(alias="functionName")
+ """JavaScript function name, or empty string for anonymous functions."""
+
+ line_number: int = FieldInfo(alias="lineNumber")
+ """Zero-based line number within the script."""
+
+ script_id: str = FieldInfo(alias="scriptId")
+ """CDP script identifier."""
+
+ url: str
+ """URL or name of the script file."""
+
+
+class BrowserCallStack(BaseModel):
+ """
+ CDP Runtime.StackTrace representing the JavaScript call stack at the time of an event. Fields use CDP naming conventions rather than snake_case to match the Chrome DevTools Protocol wire format.
+ """
+
+ call_frames: List[CallFrame] = FieldInfo(alias="callFrames")
+ """Ordered list of call frames, outermost first."""
+
+ description: Optional[str] = None
+ """Optional label for the stack trace (e.g. async cause)."""
+
+ parent: Optional["BrowserCallStack"] = None
+ """Parent stack trace for async stacks."""
diff --git a/src/kernel/types/browsers/browser_console_error_event.py b/src/kernel/types/browsers/browser_console_error_event.py
new file mode 100644
index 00000000..a152f944
--- /dev/null
+++ b/src/kernel/types/browsers/browser_console_error_event.py
@@ -0,0 +1,87 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from __future__ import annotations
+
+from typing import List, Optional
+from typing_extensions import Literal
+
+from ..._models import BaseModel
+from .browser_event_source import BrowserEventSource
+from .browser_event_context import BrowserEventContext
+
+__all__ = ["BrowserConsoleErrorEvent", "Data"]
+
+
+class Data(BrowserEventContext):
+ """Browser event context stamped by the browser monitor onto all CDP-sourced events.
+
+ Identifies the target, frame, and navigation epoch in which the event occurred.
+ """
+
+ text: str
+ """Error message text. Present in both source paths."""
+
+ args: Optional[List[str]] = None
+ """All console arguments coerced to strings.
+
+ Present only when sourced from Runtime.consoleAPICalled.
+ """
+
+ column: Optional[int] = None
+ """Column number in the script where the exception was thrown.
+
+ Present only when sourced from Runtime.exceptionThrown.
+ """
+
+ level: Optional[str] = None
+ """CDP console type value, always "error".
+
+ Present only when sourced from Runtime.consoleAPICalled.
+ """
+
+ line: Optional[int] = None
+ """Line number in the script where the exception was thrown.
+
+ Present only when sourced from Runtime.exceptionThrown.
+ """
+
+ source_url: Optional[str] = None
+ """URL of the script file that threw the exception.
+
+ Present only when sourced from Runtime.exceptionThrown.
+ """
+
+ stack_trace: Optional["BrowserCallStack"] = None
+ """
+ CDP Runtime.StackTrace representing the JavaScript call stack at the time of an
+ event. Fields use CDP naming conventions rather than snake_case to match the
+ Chrome DevTools Protocol wire format.
+ """
+
+
+class BrowserConsoleErrorEvent(BaseModel):
+ """A browser console error or uncaught JavaScript exception event.
+
+ Emitted from two distinct CDP sources with different data shapes. Runtime.consoleAPICalled (console.error calls) produces level, text, args, and stack_trace. Runtime.exceptionThrown (uncaught exceptions) produces text, line, column, source_url, and stack_trace. Fields not applicable to the source are absent.
+ """
+
+ source: BrowserEventSource
+ """Provenance metadata identifying which producer emitted the event."""
+
+ ts: int
+ """Event timestamp in Unix microseconds."""
+
+ type: Literal["console_error"]
+
+ data: Optional[Data] = None
+ """Browser event context stamped by the browser monitor onto all CDP-sourced
+ events.
+
+ Identifies the target, frame, and navigation epoch in which the event occurred.
+ """
+
+ truncated: Optional[bool] = None
+ """True if the data field was truncated due to size limits."""
+
+
+from .browser_call_stack import BrowserCallStack
diff --git a/src/kernel/types/browsers/browser_console_log_event.py b/src/kernel/types/browsers/browser_console_log_event.py
new file mode 100644
index 00000000..5994c0be
--- /dev/null
+++ b/src/kernel/types/browsers/browser_console_log_event.py
@@ -0,0 +1,64 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from __future__ import annotations
+
+from typing import List, Optional
+from typing_extensions import Literal
+
+from ..._models import BaseModel
+from .browser_event_source import BrowserEventSource
+from .browser_event_context import BrowserEventContext
+
+__all__ = ["BrowserConsoleLogEvent", "Data"]
+
+
+class Data(BrowserEventContext):
+ """Browser event context stamped by the browser monitor onto all CDP-sourced events.
+
+ Identifies the target, frame, and navigation epoch in which the event occurred.
+ """
+
+ args: Optional[List[str]] = None
+ """All console arguments coerced to strings."""
+
+ level: Optional[str] = None
+ """CDP Runtime.consoleAPICalled type, passed through unfiltered from Chrome.
+
+ error is routed to console_error events instead; all other CDP console types
+ appear here. See CDP spec for the full enum.
+ """
+
+ stack_trace: Optional["BrowserCallStack"] = None
+ """
+ CDP Runtime.StackTrace representing the JavaScript call stack at the time of an
+ event. Fields use CDP naming conventions rather than snake_case to match the
+ Chrome DevTools Protocol wire format.
+ """
+
+ text: Optional[str] = None
+ """First console argument coerced to string."""
+
+
+class BrowserConsoleLogEvent(BaseModel):
+ """A browser console log event (console.log, console.info, console.warn, etc.)."""
+
+ source: BrowserEventSource
+ """Provenance metadata identifying which producer emitted the event."""
+
+ ts: int
+ """Event timestamp in Unix microseconds."""
+
+ type: Literal["console_log"]
+
+ data: Optional[Data] = None
+ """Browser event context stamped by the browser monitor onto all CDP-sourced
+ events.
+
+ Identifies the target, frame, and navigation epoch in which the event occurred.
+ """
+
+ truncated: Optional[bool] = None
+ """True if the data field was truncated due to size limits."""
+
+
+from .browser_call_stack import BrowserCallStack
diff --git a/src/kernel/types/browsers/browser_event_context.py b/src/kernel/types/browsers/browser_event_context.py
new file mode 100644
index 00000000..6f2e9549
--- /dev/null
+++ b/src/kernel/types/browsers/browser_event_context.py
@@ -0,0 +1,42 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from typing import Optional
+from typing_extensions import Literal
+
+from ..._models import BaseModel
+
+__all__ = ["BrowserEventContext"]
+
+
+class BrowserEventContext(BaseModel):
+ """Browser event context stamped by the browser monitor onto all CDP-sourced events.
+
+ Identifies the target, frame, and navigation epoch in which the event occurred.
+ """
+
+ frame_id: Optional[str] = None
+ """CDP frame identifier within the target."""
+
+ loader_id: Optional[str] = None
+ """CDP document loader identifier, reset on each navigation."""
+
+ nav_seq: Optional[int] = None
+ """
+ Monotonically increasing navigation sequence number, incremented on each
+ top-level navigation within the target.
+ """
+
+ session_id: Optional[str] = None
+ """CDP session identifier for the target connection."""
+
+ target_id: Optional[str] = None
+ """Browser target identifier (stable across navigations within a tab)."""
+
+ target_type: Optional[Literal["page", "background_page", "service_worker", "shared_worker", "other"]] = None
+ """CDP target type of the page that produced the event."""
+
+ url: Optional[str] = None
+ """
+ URL relevant to this event — page URL for navigation and page events, request
+ URL for network events.
+ """
diff --git a/src/kernel/types/browsers/browser_event_source.py b/src/kernel/types/browsers/browser_event_source.py
new file mode 100644
index 00000000..40125b6b
--- /dev/null
+++ b/src/kernel/types/browsers/browser_event_source.py
@@ -0,0 +1,30 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from typing import Dict, Optional
+from typing_extensions import Literal
+
+from ..._models import BaseModel
+
+__all__ = ["BrowserEventSource"]
+
+
+class BrowserEventSource(BaseModel):
+ """Provenance metadata identifying which producer emitted the event."""
+
+ kind: Literal["cdp", "kernel_api", "extension", "local_process"]
+ """Event producer.
+
+ cdp: Chrome DevTools Protocol events from the browser. kernel_api: Kernel API
+ server. extension: injected Chrome extension. local_process: system process
+ running alongside the browser.
+ """
+
+ event: Optional[str] = None
+ """Producer-specific event name (e.g.
+
+ Runtime.consoleAPICalled for CDP-sourced console events, Runtime.exceptionThrown
+ for uncaught exceptions).
+ """
+
+ metadata: Optional[Dict[str, str]] = None
+ """Producer-specific context (e.g. CDP target/session/frame IDs)."""
diff --git a/src/kernel/types/browsers/browser_http_headers.py b/src/kernel/types/browsers/browser_http_headers.py
new file mode 100644
index 00000000..b55ae855
--- /dev/null
+++ b/src/kernel/types/browsers/browser_http_headers.py
@@ -0,0 +1,8 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from typing import Dict
+from typing_extensions import TypeAlias
+
+__all__ = ["BrowserHTTPHeaders"]
+
+BrowserHTTPHeaders: TypeAlias = Dict[str, object]
diff --git a/src/kernel/types/browsers/browser_interaction_click_event.py b/src/kernel/types/browsers/browser_interaction_click_event.py
new file mode 100644
index 00000000..efc76b34
--- /dev/null
+++ b/src/kernel/types/browsers/browser_interaction_click_event.py
@@ -0,0 +1,54 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from typing import Optional
+from typing_extensions import Literal
+
+from ..._models import BaseModel
+from .browser_event_source import BrowserEventSource
+from .browser_event_context import BrowserEventContext
+
+__all__ = ["BrowserInteractionClickEvent", "Data"]
+
+
+class Data(BrowserEventContext):
+ """Browser event context stamped by the browser monitor onto all CDP-sourced events.
+
+ Identifies the target, frame, and navigation epoch in which the event occurred.
+ """
+
+ selector: Optional[str] = None
+ """CSS selector path to the clicked element."""
+
+ tag: Optional[str] = None
+ """HTML tag name of the clicked element in uppercase (e.g. BUTTON, A, DIV)."""
+
+ text: Optional[str] = None
+ """Visible text content of the clicked element, trimmed."""
+
+ x: Optional[int] = None
+ """Viewport x-coordinate of the click in CSS pixels."""
+
+ y: Optional[int] = None
+ """Viewport y-coordinate of the click in CSS pixels."""
+
+
+class BrowserInteractionClickEvent(BaseModel):
+ """A browser user click event captured via injected page script."""
+
+ source: BrowserEventSource
+ """Provenance metadata identifying which producer emitted the event."""
+
+ ts: int
+ """Event timestamp in Unix microseconds."""
+
+ type: Literal["interaction_click"]
+
+ data: Optional[Data] = None
+ """Browser event context stamped by the browser monitor onto all CDP-sourced
+ events.
+
+ Identifies the target, frame, and navigation epoch in which the event occurred.
+ """
+
+ truncated: Optional[bool] = None
+ """True if the data field was truncated due to size limits."""
diff --git a/src/kernel/types/browsers/browser_interaction_key_event.py b/src/kernel/types/browsers/browser_interaction_key_event.py
new file mode 100644
index 00000000..e8860330
--- /dev/null
+++ b/src/kernel/types/browsers/browser_interaction_key_event.py
@@ -0,0 +1,48 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from typing import Optional
+from typing_extensions import Literal
+
+from ..._models import BaseModel
+from .browser_event_source import BrowserEventSource
+from .browser_event_context import BrowserEventContext
+
+__all__ = ["BrowserInteractionKeyEvent", "Data"]
+
+
+class Data(BrowserEventContext):
+ """Browser event context stamped by the browser monitor onto all CDP-sourced events.
+
+ Identifies the target, frame, and navigation epoch in which the event occurred.
+ """
+
+ key: Optional[str] = None
+ """Key value from the KeyboardEvent (e.g. Enter, Backspace, a)."""
+
+ selector: Optional[str] = None
+ """CSS selector path to the element that had focus when the key was pressed."""
+
+ tag: Optional[str] = None
+ """HTML tag name of the focused element in uppercase (e.g. INPUT, TEXTAREA, DIV)."""
+
+
+class BrowserInteractionKeyEvent(BaseModel):
+ """A browser keyboard event captured via injected page script."""
+
+ source: BrowserEventSource
+ """Provenance metadata identifying which producer emitted the event."""
+
+ ts: int
+ """Event timestamp in Unix microseconds."""
+
+ type: Literal["interaction_key"]
+
+ data: Optional[Data] = None
+ """Browser event context stamped by the browser monitor onto all CDP-sourced
+ events.
+
+ Identifies the target, frame, and navigation epoch in which the event occurred.
+ """
+
+ truncated: Optional[bool] = None
+ """True if the data field was truncated due to size limits."""
diff --git a/src/kernel/types/browsers/browser_interaction_scroll_settled_event.py b/src/kernel/types/browsers/browser_interaction_scroll_settled_event.py
new file mode 100644
index 00000000..16b7536b
--- /dev/null
+++ b/src/kernel/types/browsers/browser_interaction_scroll_settled_event.py
@@ -0,0 +1,56 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from typing import Optional
+from typing_extensions import Literal
+
+from ..._models import BaseModel
+from .browser_event_source import BrowserEventSource
+from .browser_event_context import BrowserEventContext
+
+__all__ = ["BrowserInteractionScrollSettledEvent", "Data"]
+
+
+class Data(BrowserEventContext):
+ """Browser event context stamped by the browser monitor onto all CDP-sourced events.
+
+ Identifies the target, frame, and navigation epoch in which the event occurred.
+ """
+
+ from_x: Optional[int] = None
+ """Scroll x-position at the start of the scroll gesture in CSS pixels."""
+
+ from_y: Optional[int] = None
+ """Scroll y-position at the start of the scroll gesture in CSS pixels."""
+
+ target_selector: Optional[str] = None
+ """CSS selector path to the scrolled element."""
+
+ to_x: Optional[int] = None
+ """Final scroll x-position after the gesture settled in CSS pixels."""
+
+ to_y: Optional[int] = None
+ """Final scroll y-position after the gesture settled in CSS pixels."""
+
+
+class BrowserInteractionScrollSettledEvent(BaseModel):
+ """
+ A browser scroll settled event emitted after scroll position stops changing, captured via injected page script.
+ """
+
+ source: BrowserEventSource
+ """Provenance metadata identifying which producer emitted the event."""
+
+ ts: int
+ """Event timestamp in Unix microseconds."""
+
+ type: Literal["interaction_scroll_settled"]
+
+ data: Optional[Data] = None
+ """Browser event context stamped by the browser monitor onto all CDP-sourced
+ events.
+
+ Identifies the target, frame, and navigation epoch in which the event occurred.
+ """
+
+ truncated: Optional[bool] = None
+ """True if the data field was truncated due to size limits."""
diff --git a/src/kernel/types/browsers/browser_monitor_disconnected_event.py b/src/kernel/types/browsers/browser_monitor_disconnected_event.py
new file mode 100644
index 00000000..b329ab13
--- /dev/null
+++ b/src/kernel/types/browsers/browser_monitor_disconnected_event.py
@@ -0,0 +1,34 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from typing import Optional
+from typing_extensions import Literal
+
+from ..._models import BaseModel
+from .browser_event_source import BrowserEventSource
+
+__all__ = ["BrowserMonitorDisconnectedEvent", "Data"]
+
+
+class Data(BaseModel):
+ reason: Optional[Literal["chrome_restarted"]] = None
+ """Reason for the disconnection. chrome_restarted: Chrome process restarted."""
+
+
+class BrowserMonitorDisconnectedEvent(BaseModel):
+ """The CDP connection to Chrome was lost.
+
+ Telemetry events may be dropped until monitor_reconnected arrives. Treat any in-progress computed state (network_idle, page_layout_settled) as unreliable until then.
+ """
+
+ source: BrowserEventSource
+ """Provenance metadata identifying which producer emitted the event."""
+
+ ts: int
+ """Event timestamp in Unix microseconds."""
+
+ type: Literal["monitor_disconnected"]
+
+ data: Optional[Data] = None
+
+ truncated: Optional[bool] = None
+ """True if the data field was truncated due to size limits."""
diff --git a/src/kernel/types/browsers/browser_monitor_init_failed_event.py b/src/kernel/types/browsers/browser_monitor_init_failed_event.py
new file mode 100644
index 00000000..a745fb8d
--- /dev/null
+++ b/src/kernel/types/browsers/browser_monitor_init_failed_event.py
@@ -0,0 +1,31 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from typing import Optional
+from typing_extensions import Literal
+
+from ..._models import BaseModel
+from .browser_event_source import BrowserEventSource
+
+__all__ = ["BrowserMonitorInitFailedEvent", "Data"]
+
+
+class Data(BaseModel):
+ step: Optional[str] = None
+ """The CDP method or initialization step that failed (e.g. Target.setAutoAttach)."""
+
+
+class BrowserMonitorInitFailedEvent(BaseModel):
+ """The CDP session could not be initialized."""
+
+ source: BrowserEventSource
+ """Provenance metadata identifying which producer emitted the event."""
+
+ ts: int
+ """Event timestamp in Unix microseconds."""
+
+ type: Literal["monitor_init_failed"]
+
+ data: Optional[Data] = None
+
+ truncated: Optional[bool] = None
+ """True if the data field was truncated due to size limits."""
diff --git a/src/kernel/types/browsers/browser_monitor_reconnect_failed_event.py b/src/kernel/types/browsers/browser_monitor_reconnect_failed_event.py
new file mode 100644
index 00000000..79308d2e
--- /dev/null
+++ b/src/kernel/types/browsers/browser_monitor_reconnect_failed_event.py
@@ -0,0 +1,37 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from typing import Optional
+from typing_extensions import Literal
+
+from ..._models import BaseModel
+from .browser_event_source import BrowserEventSource
+
+__all__ = ["BrowserMonitorReconnectFailedEvent", "Data"]
+
+
+class Data(BaseModel):
+ reason: Optional[Literal["reconnect_exhausted"]] = None
+ """Reason for the reconnection failure.
+
+ reconnect_exhausted: all retry attempts were used up without successfully
+ restoring the CDP connection.
+ """
+
+
+class BrowserMonitorReconnectFailedEvent(BaseModel):
+ """
+ The CDP connection to Chrome could not be re-established after exhausting all reconnection attempts. No further telemetry events will arrive on this session.
+ """
+
+ source: BrowserEventSource
+ """Provenance metadata identifying which producer emitted the event."""
+
+ ts: int
+ """Event timestamp in Unix microseconds."""
+
+ type: Literal["monitor_reconnect_failed"]
+
+ data: Optional[Data] = None
+
+ truncated: Optional[bool] = None
+ """True if the data field was truncated due to size limits."""
diff --git a/src/kernel/types/browsers/browser_monitor_reconnected_event.py b/src/kernel/types/browsers/browser_monitor_reconnected_event.py
new file mode 100644
index 00000000..a49ad351
--- /dev/null
+++ b/src/kernel/types/browsers/browser_monitor_reconnected_event.py
@@ -0,0 +1,33 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from typing import Optional
+from typing_extensions import Literal
+
+from ..._models import BaseModel
+from .browser_event_source import BrowserEventSource
+
+__all__ = ["BrowserMonitorReconnectedEvent", "Data"]
+
+
+class Data(BaseModel):
+ reconnect_duration_ms: Optional[int] = None
+ """Wall-clock time in milliseconds taken to reconnect after the disconnection."""
+
+
+class BrowserMonitorReconnectedEvent(BaseModel):
+ """
+ The CDP connection to Chrome was successfully re-established after a disconnection. Events emitted during the gap are lost. Computed state is reset, so navigation and network tracking restart fresh from this point.
+ """
+
+ source: BrowserEventSource
+ """Provenance metadata identifying which producer emitted the event."""
+
+ ts: int
+ """Event timestamp in Unix microseconds."""
+
+ type: Literal["monitor_reconnected"]
+
+ data: Optional[Data] = None
+
+ truncated: Optional[bool] = None
+ """True if the data field was truncated due to size limits."""
diff --git a/src/kernel/types/browsers/browser_monitor_screenshot_event.py b/src/kernel/types/browsers/browser_monitor_screenshot_event.py
new file mode 100644
index 00000000..9b446fa9
--- /dev/null
+++ b/src/kernel/types/browsers/browser_monitor_screenshot_event.py
@@ -0,0 +1,31 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from typing import Optional
+from typing_extensions import Literal
+
+from ..._models import BaseModel
+from .browser_event_source import BrowserEventSource
+
+__all__ = ["BrowserMonitorScreenshotEvent", "Data"]
+
+
+class Data(BaseModel):
+ png: Optional[str] = None
+ """Base64-encoded PNG screenshot of the browser viewport."""
+
+
+class BrowserMonitorScreenshotEvent(BaseModel):
+ """A periodic screenshot of the browser viewport."""
+
+ source: BrowserEventSource
+ """Provenance metadata identifying which producer emitted the event."""
+
+ ts: int
+ """Event timestamp in Unix microseconds."""
+
+ type: Literal["monitor_screenshot"]
+
+ data: Optional[Data] = None
+
+ truncated: Optional[bool] = None
+ """True if the data field was truncated due to size limits."""
diff --git a/src/kernel/types/browsers/browser_network_idle_event.py b/src/kernel/types/browsers/browser_network_idle_event.py
new file mode 100644
index 00000000..4b4fae6b
--- /dev/null
+++ b/src/kernel/types/browsers/browser_network_idle_event.py
@@ -0,0 +1,34 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from typing import Optional
+from typing_extensions import Literal
+
+from ..._models import BaseModel
+from .browser_event_source import BrowserEventSource
+from .browser_event_context import BrowserEventContext
+
+__all__ = ["BrowserNetworkIdleEvent"]
+
+
+class BrowserNetworkIdleEvent(BaseModel):
+ """
+ A browser network idle event emitted after a 500ms quiet period with no in-flight HTTP requests.
+ """
+
+ source: BrowserEventSource
+ """Provenance metadata identifying which producer emitted the event."""
+
+ ts: int
+ """Event timestamp in Unix microseconds."""
+
+ type: Literal["network_idle"]
+
+ data: Optional[BrowserEventContext] = None
+ """Browser event context stamped by the browser monitor onto all CDP-sourced
+ events.
+
+ Identifies the target, frame, and navigation epoch in which the event occurred.
+ """
+
+ truncated: Optional[bool] = None
+ """True if the data field was truncated due to size limits."""
diff --git a/src/kernel/types/browsers/browser_network_loading_failed_event.py b/src/kernel/types/browsers/browser_network_loading_failed_event.py
new file mode 100644
index 00000000..bf566a6b
--- /dev/null
+++ b/src/kernel/types/browsers/browser_network_loading_failed_event.py
@@ -0,0 +1,58 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from typing import Optional
+from typing_extensions import Literal
+
+from ..._models import BaseModel
+from .browser_event_source import BrowserEventSource
+from .browser_event_context import BrowserEventContext
+
+__all__ = ["BrowserNetworkLoadingFailedEvent", "Data"]
+
+
+class Data(BrowserEventContext):
+ """Browser event context stamped by the browser monitor onto all CDP-sourced events.
+
+ Identifies the target, frame, and navigation epoch in which the event occurred.
+ """
+
+ canceled: Optional[bool] = None
+ """True if the request was canceled by the browser or page script."""
+
+ error_text: Optional[str] = None
+ """Network error description (e.g. net::ERR_CONNECTION_REFUSED)."""
+
+ request_id: Optional[str] = None
+ """CDP request identifier matching the originating network_request event."""
+
+ resource_type: Optional[str] = None
+ """CDP Network.ResourceType for the request, passed through as-is from Chrome.
+
+ Known values include Document, Fetch, XHR, Script, Stylesheet, Image, Media,
+ Font, TextTrack, EventSource, WebSocket, Manifest, Prefetch, Other, and more.
+ """
+
+
+class BrowserNetworkLoadingFailedEvent(BaseModel):
+ """A browser network loading failed event.
+
+ If the request was already in flight when CDP attached (no prior network_request was emitted for it), url, frame_id, loader_id, and resource_type are absent; BrowserEventContext is partially populated in that case.
+ """
+
+ source: BrowserEventSource
+ """Provenance metadata identifying which producer emitted the event."""
+
+ ts: int
+ """Event timestamp in Unix microseconds."""
+
+ type: Literal["network_loading_failed"]
+
+ data: Optional[Data] = None
+ """Browser event context stamped by the browser monitor onto all CDP-sourced
+ events.
+
+ Identifies the target, frame, and navigation epoch in which the event occurred.
+ """
+
+ truncated: Optional[bool] = None
+ """True if the data field was truncated due to size limits."""
diff --git a/src/kernel/types/browsers/browser_network_request_event.py b/src/kernel/types/browsers/browser_network_request_event.py
new file mode 100644
index 00000000..e1bd9428
--- /dev/null
+++ b/src/kernel/types/browsers/browser_network_request_event.py
@@ -0,0 +1,74 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from typing import Optional
+from typing_extensions import Literal
+
+from ..._models import BaseModel
+from .browser_event_source import BrowserEventSource
+from .browser_http_headers import BrowserHTTPHeaders
+from .browser_event_context import BrowserEventContext
+
+__all__ = ["BrowserNetworkRequestEvent", "Data"]
+
+
+class Data(BrowserEventContext):
+ """Browser event context stamped by the browser monitor onto all CDP-sourced events.
+
+ Identifies the target, frame, and navigation epoch in which the event occurred.
+ """
+
+ document_url: Optional[str] = None
+ """URL of the document that initiated the request."""
+
+ headers: Optional[BrowserHTTPHeaders] = None
+ """Request headers."""
+
+ initiator_type: Optional[str] = None
+ """
+ CDP Initiator.type indicating what caused the request, passed through as-is from
+ Chrome. Known values include script, parser, preload, and other.
+ """
+
+ is_redirect: Optional[bool] = None
+ """True if this request is the result of a redirect."""
+
+ method: Optional[str] = None
+ """HTTP method as sent on the wire (e.g. GET, POST)."""
+
+ post_data: Optional[str] = None
+ """Request body for POST/PUT requests, if available."""
+
+ redirect_url: Optional[str] = None
+ """Original URL before the redirect, present when is_redirect is true."""
+
+ request_id: Optional[str] = None
+ """CDP request identifier, unique within the session."""
+
+ resource_type: Optional[str] = None
+ """CDP Network.ResourceType for the request, passed through as-is from Chrome.
+
+ Known values include Document, Fetch, XHR, Script, Stylesheet, Image, Media,
+ Font, TextTrack, EventSource, WebSocket, Manifest, Prefetch, Other, and more.
+ """
+
+
+class BrowserNetworkRequestEvent(BaseModel):
+ """A browser network request sent event."""
+
+ source: BrowserEventSource
+ """Provenance metadata identifying which producer emitted the event."""
+
+ ts: int
+ """Event timestamp in Unix microseconds."""
+
+ type: Literal["network_request"]
+
+ data: Optional[Data] = None
+ """Browser event context stamped by the browser monitor onto all CDP-sourced
+ events.
+
+ Identifies the target, frame, and navigation epoch in which the event occurred.
+ """
+
+ truncated: Optional[bool] = None
+ """True if the data field was truncated due to size limits."""
diff --git a/src/kernel/types/browsers/browser_network_response_event.py b/src/kernel/types/browsers/browser_network_response_event.py
new file mode 100644
index 00000000..8a71d24a
--- /dev/null
+++ b/src/kernel/types/browsers/browser_network_response_event.py
@@ -0,0 +1,71 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from typing import Optional
+from typing_extensions import Literal
+
+from ..._models import BaseModel
+from .browser_event_source import BrowserEventSource
+from .browser_http_headers import BrowserHTTPHeaders
+from .browser_event_context import BrowserEventContext
+
+__all__ = ["BrowserNetworkResponseEvent", "Data"]
+
+
+class Data(BrowserEventContext):
+ """Browser event context stamped by the browser monitor onto all CDP-sourced events.
+
+ Identifies the target, frame, and navigation epoch in which the event occurred.
+ """
+
+ body: Optional[str] = None
+ """Truncated response body, present only for text MIME types."""
+
+ headers: Optional[BrowserHTTPHeaders] = None
+ """Response headers."""
+
+ method: Optional[str] = None
+ """HTTP method of the original request."""
+
+ mime_type: Optional[str] = None
+ """MIME type of the response (e.g. text/html, application/json)."""
+
+ request_id: Optional[str] = None
+ """CDP request identifier matching the originating network_request event."""
+
+ resource_type: Optional[str] = None
+ """CDP Network.ResourceType for the request, passed through as-is from Chrome.
+
+ Known values include Document, Fetch, XHR, Script, Stylesheet, Image, Media,
+ Font, TextTrack, EventSource, WebSocket, Manifest, Prefetch, Other, and more.
+ """
+
+ status: Optional[int] = None
+ """HTTP response status code."""
+
+ status_text: Optional[str] = None
+ """HTTP response status text (e.g. OK, Not Found)."""
+
+
+class BrowserNetworkResponseEvent(BaseModel):
+ """A browser network response received event.
+
+ Fired after the response body is fully received, not when headers arrive.
+ """
+
+ source: BrowserEventSource
+ """Provenance metadata identifying which producer emitted the event."""
+
+ ts: int
+ """Event timestamp in Unix microseconds."""
+
+ type: Literal["network_response"]
+
+ data: Optional[Data] = None
+ """Browser event context stamped by the browser monitor onto all CDP-sourced
+ events.
+
+ Identifies the target, frame, and navigation epoch in which the event occurred.
+ """
+
+ truncated: Optional[bool] = None
+ """True if the data field was truncated due to size limits."""
diff --git a/src/kernel/types/browsers/browser_page_dom_content_loaded_event.py b/src/kernel/types/browsers/browser_page_dom_content_loaded_event.py
new file mode 100644
index 00000000..e917ca32
--- /dev/null
+++ b/src/kernel/types/browsers/browser_page_dom_content_loaded_event.py
@@ -0,0 +1,45 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from typing import Optional
+from typing_extensions import Literal
+
+from ..._models import BaseModel
+from .browser_event_source import BrowserEventSource
+from .browser_event_context import BrowserEventContext
+
+__all__ = ["BrowserPageDomContentLoadedEvent", "Data"]
+
+
+class Data(BrowserEventContext):
+ """Browser event context stamped by the browser monitor onto all CDP-sourced events.
+
+ Identifies the target, frame, and navigation epoch in which the event occurred.
+ """
+
+ cdp_timestamp: Optional[float] = None
+ """
+ Chrome monotonic clock value in seconds at which DOMContentLoaded fired,
+ relative to browser process start (not Unix epoch). Use ts for wall-clock time.
+ """
+
+
+class BrowserPageDomContentLoadedEvent(BaseModel):
+ """A browser DOMContentLoaded event (CDP Page.domContentEventFired)."""
+
+ source: BrowserEventSource
+ """Provenance metadata identifying which producer emitted the event."""
+
+ ts: int
+ """Event timestamp in Unix microseconds."""
+
+ type: Literal["page_dom_content_loaded"]
+
+ data: Optional[Data] = None
+ """Browser event context stamped by the browser monitor onto all CDP-sourced
+ events.
+
+ Identifies the target, frame, and navigation epoch in which the event occurred.
+ """
+
+ truncated: Optional[bool] = None
+ """True if the data field was truncated due to size limits."""
diff --git a/src/kernel/types/browsers/browser_page_layout_settled_event.py b/src/kernel/types/browsers/browser_page_layout_settled_event.py
new file mode 100644
index 00000000..289b53a5
--- /dev/null
+++ b/src/kernel/types/browsers/browser_page_layout_settled_event.py
@@ -0,0 +1,34 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from typing import Optional
+from typing_extensions import Literal
+
+from ..._models import BaseModel
+from .browser_event_source import BrowserEventSource
+from .browser_event_context import BrowserEventContext
+
+__all__ = ["BrowserPageLayoutSettledEvent"]
+
+
+class BrowserPageLayoutSettledEvent(BaseModel):
+ """
+ A browser layout settled event emitted 1 second after page load with no intervening layout shifts, indicating visual stability. Each layout shift resets the 1-second timer.
+ """
+
+ source: BrowserEventSource
+ """Provenance metadata identifying which producer emitted the event."""
+
+ ts: int
+ """Event timestamp in Unix microseconds."""
+
+ type: Literal["page_layout_settled"]
+
+ data: Optional[BrowserEventContext] = None
+ """Browser event context stamped by the browser monitor onto all CDP-sourced
+ events.
+
+ Identifies the target, frame, and navigation epoch in which the event occurred.
+ """
+
+ truncated: Optional[bool] = None
+ """True if the data field was truncated due to size limits."""
diff --git a/src/kernel/types/browsers/browser_page_layout_shift_event.py b/src/kernel/types/browsers/browser_page_layout_shift_event.py
new file mode 100644
index 00000000..336bfce2
--- /dev/null
+++ b/src/kernel/types/browsers/browser_page_layout_shift_event.py
@@ -0,0 +1,69 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from typing import Optional
+from typing_extensions import Literal
+
+from ..._models import BaseModel
+from .browser_event_source import BrowserEventSource
+from .browser_event_context import BrowserEventContext
+
+__all__ = ["BrowserPageLayoutShiftEvent", "Data", "DataLayoutShiftDetails"]
+
+
+class DataLayoutShiftDetails(BaseModel):
+ """PerformanceLayoutShift attributes from the Performance Timeline entry."""
+
+ had_recent_input: Optional[bool] = None
+ """
+ True if the layout shift was preceded by user input within 500ms, excluding it
+ from CLS.
+ """
+
+ value: Optional[float] = None
+ """Layout shift score for this entry (contribution to CLS)."""
+
+
+class Data(BrowserEventContext):
+ """Browser event context stamped by the browser monitor onto all CDP-sourced events.
+
+ Identifies the target, frame, and navigation epoch in which the event occurred.
+ """
+
+ duration: Optional[float] = None
+ """
+ Duration of the layout shift entry in milliseconds (always 0 for layout shifts
+ per spec).
+ """
+
+ layout_shift_details: Optional[DataLayoutShiftDetails] = None
+ """PerformanceLayoutShift attributes from the Performance Timeline entry."""
+
+ source_frame_id: Optional[str] = None
+ """CDP frame identifier of the frame where the layout shift occurred."""
+
+ time: Optional[float] = None
+ """Performance Timeline timestamp of the layout shift in milliseconds."""
+
+
+class BrowserPageLayoutShiftEvent(BaseModel):
+ """
+ A browser cumulative layout shift (CLS) event from the Performance Timeline API.
+ """
+
+ source: BrowserEventSource
+ """Provenance metadata identifying which producer emitted the event."""
+
+ ts: int
+ """Event timestamp in Unix microseconds."""
+
+ type: Literal["page_layout_shift"]
+
+ data: Optional[Data] = None
+ """Browser event context stamped by the browser monitor onto all CDP-sourced
+ events.
+
+ Identifies the target, frame, and navigation epoch in which the event occurred.
+ """
+
+ truncated: Optional[bool] = None
+ """True if the data field was truncated due to size limits."""
diff --git a/src/kernel/types/browsers/browser_page_lcp_event.py b/src/kernel/types/browsers/browser_page_lcp_event.py
new file mode 100644
index 00000000..13b9c270
--- /dev/null
+++ b/src/kernel/types/browsers/browser_page_lcp_event.py
@@ -0,0 +1,75 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from typing import Optional
+from typing_extensions import Literal
+
+from ..._models import BaseModel
+from .browser_event_source import BrowserEventSource
+from .browser_event_context import BrowserEventContext
+
+__all__ = ["BrowserPageLcpEvent", "Data", "DataLcpDetails"]
+
+
+class DataLcpDetails(BaseModel):
+ """LargestContentfulPaint attributes from the Performance Timeline entry."""
+
+ element_id: Optional[str] = None
+ """id attribute of the LCP element, if present."""
+
+ load_time: Optional[float] = None
+ """Load time of the LCP element in milliseconds."""
+
+ node_id: Optional[int] = None
+ """CDP DOM node identifier of the LCP element."""
+
+ render_time: Optional[float] = None
+ """
+ Render time of the LCP element in milliseconds; 0 for cross-origin images
+ without Timing-Allow-Origin.
+ """
+
+ size: Optional[float] = None
+ """Visible area of the LCP element in pixels squared."""
+
+ url: Optional[str] = None
+ """URL of the LCP element for image or video elements."""
+
+
+class Data(BrowserEventContext):
+ """Browser event context stamped by the browser monitor onto all CDP-sourced events.
+
+ Identifies the target, frame, and navigation epoch in which the event occurred.
+ """
+
+ lcp_details: Optional[DataLcpDetails] = None
+ """LargestContentfulPaint attributes from the Performance Timeline entry."""
+
+ source_frame_id: Optional[str] = None
+ """CDP frame identifier of the frame where the LCP element was rendered."""
+
+ time: Optional[float] = None
+ """Performance Timeline timestamp of the LCP entry in milliseconds."""
+
+
+class BrowserPageLcpEvent(BaseModel):
+ """
+ A browser Largest Contentful Paint (LCP) event from the Performance Timeline API.
+ """
+
+ source: BrowserEventSource
+ """Provenance metadata identifying which producer emitted the event."""
+
+ ts: int
+ """Event timestamp in Unix microseconds."""
+
+ type: Literal["page_lcp"]
+
+ data: Optional[Data] = None
+ """Browser event context stamped by the browser monitor onto all CDP-sourced
+ events.
+
+ Identifies the target, frame, and navigation epoch in which the event occurred.
+ """
+
+ truncated: Optional[bool] = None
+ """True if the data field was truncated due to size limits."""
diff --git a/src/kernel/types/browsers/browser_page_load_event.py b/src/kernel/types/browsers/browser_page_load_event.py
new file mode 100644
index 00000000..b4d85c8f
--- /dev/null
+++ b/src/kernel/types/browsers/browser_page_load_event.py
@@ -0,0 +1,45 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from typing import Optional
+from typing_extensions import Literal
+
+from ..._models import BaseModel
+from .browser_event_source import BrowserEventSource
+from .browser_event_context import BrowserEventContext
+
+__all__ = ["BrowserPageLoadEvent", "Data"]
+
+
+class Data(BrowserEventContext):
+ """Browser event context stamped by the browser monitor onto all CDP-sourced events.
+
+ Identifies the target, frame, and navigation epoch in which the event occurred.
+ """
+
+ cdp_timestamp: Optional[float] = None
+ """
+ Chrome monotonic clock value in seconds at which the load event fired, relative
+ to browser process start (not Unix epoch). Use ts for wall-clock time.
+ """
+
+
+class BrowserPageLoadEvent(BaseModel):
+ """A browser page load event (CDP Page.loadEventFired)."""
+
+ source: BrowserEventSource
+ """Provenance metadata identifying which producer emitted the event."""
+
+ ts: int
+ """Event timestamp in Unix microseconds."""
+
+ type: Literal["page_load"]
+
+ data: Optional[Data] = None
+ """Browser event context stamped by the browser monitor onto all CDP-sourced
+ events.
+
+ Identifies the target, frame, and navigation epoch in which the event occurred.
+ """
+
+ truncated: Optional[bool] = None
+ """True if the data field was truncated due to size limits."""
diff --git a/src/kernel/types/browsers/browser_page_navigation_event.py b/src/kernel/types/browsers/browser_page_navigation_event.py
new file mode 100644
index 00000000..f857398c
--- /dev/null
+++ b/src/kernel/types/browsers/browser_page_navigation_event.py
@@ -0,0 +1,55 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from typing import Optional
+from typing_extensions import Literal
+
+from ..._models import BaseModel
+from .browser_event_source import BrowserEventSource
+
+__all__ = ["BrowserPageNavigationEvent", "Data"]
+
+
+class Data(BaseModel):
+ frame_id: Optional[str] = None
+ """CDP frame identifier of the navigated frame."""
+
+ loader_id: Optional[str] = None
+ """New CDP document loader identifier assigned for this navigation."""
+
+ parent_frame_id: Optional[str] = None
+ """
+ Parent frame identifier for subframe navigations; absent for top-level
+ navigations.
+ """
+
+ session_id: Optional[str] = None
+ """CDP session identifier."""
+
+ target_id: Optional[str] = None
+ """Browser target identifier."""
+
+ target_type: Optional[Literal["page", "background_page", "service_worker", "shared_worker", "other"]] = None
+ """CDP target type of the page that produced the event."""
+
+ url: Optional[str] = None
+ """URL navigated to."""
+
+
+class BrowserPageNavigationEvent(BaseModel):
+ """A browser page navigation started event (CDP Page.frameNavigated).
+
+ Carries nav context fields inline but not nav_seq, as this event resets the navigation epoch.
+ """
+
+ source: BrowserEventSource
+ """Provenance metadata identifying which producer emitted the event."""
+
+ ts: int
+ """Event timestamp in Unix microseconds."""
+
+ type: Literal["page_navigation"]
+
+ data: Optional[Data] = None
+
+ truncated: Optional[bool] = None
+ """True if the data field was truncated due to size limits."""
diff --git a/src/kernel/types/browsers/browser_page_navigation_settled_event.py b/src/kernel/types/browsers/browser_page_navigation_settled_event.py
new file mode 100644
index 00000000..b5227ff4
--- /dev/null
+++ b/src/kernel/types/browsers/browser_page_navigation_settled_event.py
@@ -0,0 +1,34 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from typing import Optional
+from typing_extensions import Literal
+
+from ..._models import BaseModel
+from .browser_event_source import BrowserEventSource
+from .browser_event_context import BrowserEventContext
+
+__all__ = ["BrowserPageNavigationSettledEvent"]
+
+
+class BrowserPageNavigationSettledEvent(BaseModel):
+ """
+ Emitted when page_dom_content_loaded and page_layout_settled have both fired for the same navigation, indicating the page is loaded and visually stable. Independent of network_idle; a single pending request does not block it.
+ """
+
+ source: BrowserEventSource
+ """Provenance metadata identifying which producer emitted the event."""
+
+ ts: int
+ """Event timestamp in Unix microseconds."""
+
+ type: Literal["page_navigation_settled"]
+
+ data: Optional[BrowserEventContext] = None
+ """Browser event context stamped by the browser monitor onto all CDP-sourced
+ events.
+
+ Identifies the target, frame, and navigation epoch in which the event occurred.
+ """
+
+ truncated: Optional[bool] = None
+ """True if the data field was truncated due to size limits."""
diff --git a/src/kernel/types/browsers/browser_page_tab_opened_event.py b/src/kernel/types/browsers/browser_page_tab_opened_event.py
new file mode 100644
index 00000000..9831aee1
--- /dev/null
+++ b/src/kernel/types/browsers/browser_page_tab_opened_event.py
@@ -0,0 +1,45 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from typing import Optional
+from typing_extensions import Literal
+
+from ..._models import BaseModel
+from .browser_event_source import BrowserEventSource
+
+__all__ = ["BrowserPageTabOpenedEvent", "Data"]
+
+
+class Data(BaseModel):
+ opener_id: Optional[str] = None
+ """Target identifier of the tab that opened this one, if any."""
+
+ target_id: Optional[str] = None
+ """CDP target identifier for the newly opened tab."""
+
+ target_type: Optional[Literal["page", "background_page", "service_worker", "shared_worker", "other"]] = None
+ """CDP target type of the page that produced the event."""
+
+ title: Optional[str] = None
+ """Initial page title of the new tab."""
+
+ url: Optional[str] = None
+ """Initial URL of the new tab."""
+
+
+class BrowserPageTabOpenedEvent(BaseModel):
+ """
+ A new browser tab or target was opened (CDP Target.attachedToTarget for page targets). Fires before a CDP session is attached to the new target, so session_id, frame_id, loader_id, and nav_seq are absent; this event does not compose BrowserEventContext. Consumers reading context fields generically should treat it as a special case.
+ """
+
+ source: BrowserEventSource
+ """Provenance metadata identifying which producer emitted the event."""
+
+ ts: int
+ """Event timestamp in Unix microseconds."""
+
+ type: Literal["page_tab_opened"]
+
+ data: Optional[Data] = None
+
+ truncated: Optional[bool] = None
+ """True if the data field was truncated due to size limits."""
diff --git a/src/kernel/types/browsers/browser_telemetry_categories_config.py b/src/kernel/types/browsers/browser_telemetry_categories_config.py
new file mode 100644
index 00000000..82cdb1db
--- /dev/null
+++ b/src/kernel/types/browsers/browser_telemetry_categories_config.py
@@ -0,0 +1,32 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from typing import Optional
+
+from ..._models import BaseModel
+from .browser_telemetry_category_config import BrowserTelemetryCategoryConfig
+
+__all__ = ["BrowserTelemetryCategoriesConfig"]
+
+
+class BrowserTelemetryCategoriesConfig(BaseModel):
+ """Per-category telemetry capture settings."""
+
+ console: Optional[BrowserTelemetryCategoryConfig] = None
+ """Console output (log, warn, error) and uncaught exceptions."""
+
+ interaction: Optional[BrowserTelemetryCategoryConfig] = None
+ """User interaction events including clicks, keydowns, and scroll-settled events."""
+
+ network: Optional[BrowserTelemetryCategoryConfig] = None
+ """
+ HTTP request and response metadata including URL, method, status code, and
+ timing. Request post data is forwarded as-is from CDP. Text response bodies are
+ truncated at 8 KB for structured types (JSON, XML, form data) and 4 KB for other
+ text types. Binary responses (images, fonts, media) are excluded.
+ """
+
+ page: Optional[BrowserTelemetryCategoryConfig] = None
+ """
+ Page lifecycle events including navigation, DOMContentLoaded, load, layout
+ shifts, and LCP.
+ """
diff --git a/src/kernel/types/browsers/browser_telemetry_categories_config_param.py b/src/kernel/types/browsers/browser_telemetry_categories_config_param.py
new file mode 100644
index 00000000..75a3a797
--- /dev/null
+++ b/src/kernel/types/browsers/browser_telemetry_categories_config_param.py
@@ -0,0 +1,33 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from __future__ import annotations
+
+from typing_extensions import TypedDict
+
+from .browser_telemetry_category_config_param import BrowserTelemetryCategoryConfigParam
+
+__all__ = ["BrowserTelemetryCategoriesConfigParam"]
+
+
+class BrowserTelemetryCategoriesConfigParam(TypedDict, total=False):
+ """Per-category telemetry capture settings."""
+
+ console: BrowserTelemetryCategoryConfigParam
+ """Console output (log, warn, error) and uncaught exceptions."""
+
+ interaction: BrowserTelemetryCategoryConfigParam
+ """User interaction events including clicks, keydowns, and scroll-settled events."""
+
+ network: BrowserTelemetryCategoryConfigParam
+ """
+ HTTP request and response metadata including URL, method, status code, and
+ timing. Request post data is forwarded as-is from CDP. Text response bodies are
+ truncated at 8 KB for structured types (JSON, XML, form data) and 4 KB for other
+ text types. Binary responses (images, fonts, media) are excluded.
+ """
+
+ page: BrowserTelemetryCategoryConfigParam
+ """
+ Page lifecycle events including navigation, DOMContentLoaded, load, layout
+ shifts, and LCP.
+ """
diff --git a/src/kernel/types/browsers/browser_telemetry_category_config.py b/src/kernel/types/browsers/browser_telemetry_category_config.py
new file mode 100644
index 00000000..4a2da2cb
--- /dev/null
+++ b/src/kernel/types/browsers/browser_telemetry_category_config.py
@@ -0,0 +1,14 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from typing import Optional
+
+from ..._models import BaseModel
+
+__all__ = ["BrowserTelemetryCategoryConfig"]
+
+
+class BrowserTelemetryCategoryConfig(BaseModel):
+ """Per-category telemetry configuration."""
+
+ enabled: Optional[bool] = None
+ """Whether this category is captured. Defaults to true if omitted."""
diff --git a/src/kernel/types/browsers/browser_telemetry_category_config_param.py b/src/kernel/types/browsers/browser_telemetry_category_config_param.py
new file mode 100644
index 00000000..3824b4c8
--- /dev/null
+++ b/src/kernel/types/browsers/browser_telemetry_category_config_param.py
@@ -0,0 +1,14 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from __future__ import annotations
+
+from typing_extensions import TypedDict
+
+__all__ = ["BrowserTelemetryCategoryConfigParam"]
+
+
+class BrowserTelemetryCategoryConfigParam(TypedDict, total=False):
+ """Per-category telemetry configuration."""
+
+ enabled: bool
+ """Whether this category is captured. Defaults to true if omitted."""
diff --git a/src/kernel/types/browsers/browser_telemetry_config.py b/src/kernel/types/browsers/browser_telemetry_config.py
new file mode 100644
index 00000000..d4365eeb
--- /dev/null
+++ b/src/kernel/types/browsers/browser_telemetry_config.py
@@ -0,0 +1,15 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from typing import Optional
+
+from ..._models import BaseModel
+from .browser_telemetry_categories_config import BrowserTelemetryCategoriesConfig
+
+__all__ = ["BrowserTelemetryConfig"]
+
+
+class BrowserTelemetryConfig(BaseModel):
+ """Telemetry configuration for a browser session."""
+
+ browser: Optional[BrowserTelemetryCategoriesConfig] = None
+ """Per-category enable/disable flags. If omitted, all categories are captured."""
diff --git a/src/kernel/types/browsers/browser_telemetry_config_param.py b/src/kernel/types/browsers/browser_telemetry_config_param.py
new file mode 100644
index 00000000..33464340
--- /dev/null
+++ b/src/kernel/types/browsers/browser_telemetry_config_param.py
@@ -0,0 +1,16 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from __future__ import annotations
+
+from typing_extensions import TypedDict
+
+from .browser_telemetry_categories_config_param import BrowserTelemetryCategoriesConfigParam
+
+__all__ = ["BrowserTelemetryConfigParam"]
+
+
+class BrowserTelemetryConfigParam(TypedDict, total=False):
+ """Telemetry configuration for a browser session."""
+
+ browser: BrowserTelemetryCategoriesConfigParam
+ """Per-category enable/disable flags. If omitted, all categories are captured."""
diff --git a/src/kernel/types/browsers/browser_telemetry_event.py b/src/kernel/types/browsers/browser_telemetry_event.py
new file mode 100644
index 00000000..c27297bc
--- /dev/null
+++ b/src/kernel/types/browsers/browser_telemetry_event.py
@@ -0,0 +1,61 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from __future__ import annotations
+
+from typing import Union
+from typing_extensions import Annotated, TypeAlias
+
+from ..._utils import PropertyInfo
+from .browser_page_lcp_event import BrowserPageLcpEvent
+from .browser_page_load_event import BrowserPageLoadEvent
+from .browser_network_idle_event import BrowserNetworkIdleEvent
+from .browser_interaction_key_event import BrowserInteractionKeyEvent
+from .browser_network_request_event import BrowserNetworkRequestEvent
+from .browser_page_navigation_event import BrowserPageNavigationEvent
+from .browser_page_tab_opened_event import BrowserPageTabOpenedEvent
+from .browser_network_response_event import BrowserNetworkResponseEvent
+from .browser_interaction_click_event import BrowserInteractionClickEvent
+from .browser_page_layout_shift_event import BrowserPageLayoutShiftEvent
+from .browser_monitor_screenshot_event import BrowserMonitorScreenshotEvent
+from .browser_monitor_init_failed_event import BrowserMonitorInitFailedEvent
+from .browser_monitor_reconnected_event import BrowserMonitorReconnectedEvent
+from .browser_page_layout_settled_event import BrowserPageLayoutSettledEvent
+from .browser_monitor_disconnected_event import BrowserMonitorDisconnectedEvent
+from .browser_network_loading_failed_event import BrowserNetworkLoadingFailedEvent
+from .browser_page_dom_content_loaded_event import BrowserPageDomContentLoadedEvent
+from .browser_page_navigation_settled_event import BrowserPageNavigationSettledEvent
+from .browser_monitor_reconnect_failed_event import BrowserMonitorReconnectFailedEvent
+from .browser_interaction_scroll_settled_event import BrowserInteractionScrollSettledEvent
+
+__all__ = ["BrowserTelemetryEvent"]
+
+BrowserTelemetryEvent: TypeAlias = Annotated[
+ Union[
+ "BrowserConsoleLogEvent",
+ "BrowserConsoleErrorEvent",
+ BrowserNetworkRequestEvent,
+ BrowserNetworkResponseEvent,
+ BrowserNetworkLoadingFailedEvent,
+ BrowserNetworkIdleEvent,
+ BrowserPageNavigationEvent,
+ BrowserPageDomContentLoadedEvent,
+ BrowserPageLoadEvent,
+ BrowserPageTabOpenedEvent,
+ BrowserPageLayoutShiftEvent,
+ BrowserPageLcpEvent,
+ BrowserPageLayoutSettledEvent,
+ BrowserPageNavigationSettledEvent,
+ BrowserInteractionClickEvent,
+ BrowserInteractionKeyEvent,
+ BrowserInteractionScrollSettledEvent,
+ BrowserMonitorScreenshotEvent,
+ BrowserMonitorDisconnectedEvent,
+ BrowserMonitorReconnectedEvent,
+ BrowserMonitorReconnectFailedEvent,
+ BrowserMonitorInitFailedEvent,
+ ],
+ PropertyInfo(discriminator="type"),
+]
+
+from .browser_console_log_event import BrowserConsoleLogEvent
+from .browser_console_error_event import BrowserConsoleErrorEvent
diff --git a/src/kernel/types/browsers/telemetry_stream_response.py b/src/kernel/types/browsers/telemetry_stream_response.py
new file mode 100644
index 00000000..fef04b3f
--- /dev/null
+++ b/src/kernel/types/browsers/telemetry_stream_response.py
@@ -0,0 +1,34 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from __future__ import annotations
+
+from ..._models import BaseModel
+
+__all__ = ["TelemetryStreamResponse"]
+
+
+class TelemetryStreamResponse(BaseModel):
+ """Envelope wrapping a browser telemetry event with its monotonic sequence number.
+
+ Each SSE data: frame carries one envelope as JSON. The seq value is also emitted as the SSE id: field so clients can pass it as Last-Event-ID on reconnect.
+ """
+
+ event: "BrowserTelemetryEvent"
+ """Union type representing any browser telemetry event.
+
+ Discriminated on `type`. Events with a `monitor_` prefix (monitor_screenshot,
+ monitor_disconnected, monitor_reconnected, monitor_reconnect_failed,
+ monitor_init_failed) are always emitted regardless of the category configuration
+ in BrowserTelemetryConfig. All other event types are controlled by the
+ per-category enable/disable flags.
+ """
+
+ seq: int
+ """Process-monotonic sequence number assigned by the browser VM.
+
+ Pass as Last-Event-ID on reconnect to resume without gaps. Gaps in received seq
+ values indicate dropped events.
+ """
+
+
+from .browser_telemetry_event import BrowserTelemetryEvent
diff --git a/src/kernel/types/invocation_list_browsers_response.py b/src/kernel/types/invocation_list_browsers_response.py
index 7ff4910a..7c60ae22 100644
--- a/src/kernel/types/invocation_list_browsers_response.py
+++ b/src/kernel/types/invocation_list_browsers_response.py
@@ -9,6 +9,7 @@
from .browser_pool_ref import BrowserPoolRef
from .browser_persistence import BrowserPersistence
from .shared.browser_viewport import BrowserViewport
+from .browsers.browser_telemetry_config import BrowserTelemetryConfig
__all__ = ["InvocationListBrowsersResponse", "Browser"]
@@ -76,7 +77,16 @@ class Browser(BaseModel):
"""ID of the proxy associated with this browser session, if any."""
start_url: Optional[str] = None
- """Start URL requested for the session, if provided."""
+ """URL the session was asked to navigate to on creation, if any.
+
+ Recorded for debugging. Navigation is fire-and-forget — the URL is dispatched to
+ the browser without waiting for it to load, and any errors (DNS failure, bad
+ status, timeout) are silently dropped. Captures what was requested, not what the
+ browser actually loaded.
+ """
+
+ telemetry: Optional[BrowserTelemetryConfig] = None
+ """Active telemetry configuration for the session, if any."""
usage: Optional[BrowserUsage] = None
"""Session usage metrics."""
diff --git a/src/kernel/types/proxy_check_params.py b/src/kernel/types/proxy_check_params.py
index de3a8c3d..47f99f04 100644
--- a/src/kernel/types/proxy_check_params.py
+++ b/src/kernel/types/proxy_check_params.py
@@ -19,5 +19,8 @@ class ProxyCheckParams(TypedDict, total=False):
proxies, the exit node changes between requests, so a successful check validates
proxy configuration but does not guarantee that a subsequent browser session
will use the same exit IP or reach the same site — it is useful for verifying
- credentials and connectivity, not for predicting site-specific behavior.
+ credentials and connectivity, not for predicting site-specific behavior. When
+ provided, the check result does not update the proxy's health status, since a
+ failure may indicate a problem with the target site rather than the proxy
+ itself.
"""
diff --git a/tests/api_resources/browsers/test_telemetry.py b/tests/api_resources/browsers/test_telemetry.py
new file mode 100644
index 00000000..0fcd2715
--- /dev/null
+++ b/tests/api_resources/browsers/test_telemetry.py
@@ -0,0 +1,122 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from __future__ import annotations
+
+import os
+from typing import Any, cast
+
+import pytest
+
+from kernel import Kernel, AsyncKernel
+
+base_url = os.environ.get("TEST_API_BASE_URL", "http://127.0.0.1:4010")
+
+
+class TestTelemetry:
+ parametrize = pytest.mark.parametrize("client", [False, True], indirect=True, ids=["loose", "strict"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ def test_method_stream(self, client: Kernel) -> None:
+ telemetry_stream = client.browsers.telemetry.stream(
+ id="id",
+ )
+ telemetry_stream.response.close()
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ def test_method_stream_with_all_params(self, client: Kernel) -> None:
+ telemetry_stream = client.browsers.telemetry.stream(
+ id="id",
+ last_event_id="Last-Event-ID",
+ )
+ telemetry_stream.response.close()
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ def test_raw_response_stream(self, client: Kernel) -> None:
+ response = client.browsers.telemetry.with_raw_response.stream(
+ id="id",
+ )
+
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+ stream = response.parse()
+ stream.close()
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ def test_streaming_response_stream(self, client: Kernel) -> None:
+ with client.browsers.telemetry.with_streaming_response.stream(
+ id="id",
+ ) as response:
+ assert not response.is_closed
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+
+ stream = response.parse()
+ stream.close()
+
+ assert cast(Any, response.is_closed) is True
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ def test_path_params_stream(self, client: Kernel) -> None:
+ with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"):
+ client.browsers.telemetry.with_raw_response.stream(
+ id="",
+ )
+
+
+class TestAsyncTelemetry:
+ parametrize = pytest.mark.parametrize(
+ "async_client", [False, True, {"http_client": "aiohttp"}], indirect=True, ids=["loose", "strict", "aiohttp"]
+ )
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ async def test_method_stream(self, async_client: AsyncKernel) -> None:
+ telemetry_stream = await async_client.browsers.telemetry.stream(
+ id="id",
+ )
+ await telemetry_stream.response.aclose()
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ async def test_method_stream_with_all_params(self, async_client: AsyncKernel) -> None:
+ telemetry_stream = await async_client.browsers.telemetry.stream(
+ id="id",
+ last_event_id="Last-Event-ID",
+ )
+ await telemetry_stream.response.aclose()
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ async def test_raw_response_stream(self, async_client: AsyncKernel) -> None:
+ response = await async_client.browsers.telemetry.with_raw_response.stream(
+ id="id",
+ )
+
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+ stream = await response.parse()
+ await stream.close()
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ async def test_streaming_response_stream(self, async_client: AsyncKernel) -> None:
+ async with async_client.browsers.telemetry.with_streaming_response.stream(
+ id="id",
+ ) as response:
+ assert not response.is_closed
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+
+ stream = await response.parse()
+ await stream.close()
+
+ assert cast(Any, response.is_closed) is True
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ async def test_path_params_stream(self, async_client: AsyncKernel) -> None:
+ with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"):
+ await async_client.browsers.telemetry.with_raw_response.stream(
+ id="",
+ )
diff --git a/tests/api_resources/test_browsers.py b/tests/api_resources/test_browsers.py
index 3d1ac191..dcd20977 100644
--- a/tests/api_resources/test_browsers.py
+++ b/tests/api_resources/test_browsers.py
@@ -56,6 +56,14 @@ def test_method_create_with_all_params(self, client: Kernel) -> None:
proxy_id="proxy_id",
start_url="https://example.com",
stealth=True,
+ telemetry={
+ "browser": {
+ "console": {"enabled": True},
+ "interaction": {"enabled": True},
+ "network": {"enabled": True},
+ "page": {"enabled": True},
+ }
+ },
timeout_seconds=10,
viewport={
"height": 800,
@@ -158,6 +166,14 @@ def test_method_update_with_all_params(self, client: Kernel) -> None:
"save_changes": True,
},
proxy_id="proxy_id",
+ telemetry={
+ "browser": {
+ "console": {"enabled": True},
+ "interaction": {"enabled": True},
+ "network": {"enabled": True},
+ "page": {"enabled": True},
+ }
+ },
viewport={
"height": 800,
"width": 1280,
@@ -483,6 +499,14 @@ async def test_method_create_with_all_params(self, async_client: AsyncKernel) ->
proxy_id="proxy_id",
start_url="https://example.com",
stealth=True,
+ telemetry={
+ "browser": {
+ "console": {"enabled": True},
+ "interaction": {"enabled": True},
+ "network": {"enabled": True},
+ "page": {"enabled": True},
+ }
+ },
timeout_seconds=10,
viewport={
"height": 800,
@@ -585,6 +609,14 @@ async def test_method_update_with_all_params(self, async_client: AsyncKernel) ->
"save_changes": True,
},
proxy_id="proxy_id",
+ telemetry={
+ "browser": {
+ "console": {"enabled": True},
+ "interaction": {"enabled": True},
+ "network": {"enabled": True},
+ "page": {"enabled": True},
+ }
+ },
viewport={
"height": 800,
"width": 1280,
From 4387bef6502803fd2505bf19e50565c1dc059d4a Mon Sep 17 00:00:00 2001
From: "stainless-app[bot]"
<142633134+stainless-app[bot]@users.noreply.github.com>
Date: Fri, 22 May 2026 16:38:06 +0000
Subject: [PATCH 010/366] feat(api): type can_reauth_reason as an enum on
ManagedAuth
---
.stats.yml | 4 +-
src/kernel/types/auth/managed_auth.py | 59 ++++++++++++++++++++++++---
2 files changed, 56 insertions(+), 7 deletions(-)
diff --git a/.stats.yml b/.stats.yml
index 3dfe6c63..01ff0714 100644
--- a/.stats.yml
+++ b/.stats.yml
@@ -1,4 +1,4 @@
configured_endpoints: 113
-openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel/kernel-a9c7e806132001dbfbd4e8ae6c7d0935e503f457d63385fc800c862e3d064375.yml
-openapi_spec_hash: b048dcb0c5401bc0a301c3d30cb8ecba
+openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel/kernel-2118a79fa452fad5922776130ea4877059e9568b603ef146171b7bb6a0f8458a.yml
+openapi_spec_hash: 686e8c5855ed88b2944b9d9ec21d22ce
config_hash: 37661d89120558d34b6cc184292632f2
diff --git a/src/kernel/types/auth/managed_auth.py b/src/kernel/types/auth/managed_auth.py
index 893631d4..3addc5e0 100644
--- a/src/kernel/types/auth/managed_auth.py
+++ b/src/kernel/types/auth/managed_auth.py
@@ -187,12 +187,61 @@ class ManagedAuth(BaseModel):
can_reauth: Optional[bool] = None
"""
- Whether automatic re-authentication is possible (has credential, selectors, and
- login_url)
+ Whether Kernel can automatically re-authenticate this connection when the
+ session expires. Requires a prior successful login plus either a Kernel
+ credential or an external credential reference. See `can_reauth_reason` for the
+ specific outcome.
+ """
+
+ can_reauth_reason: Optional[
+ Literal[
+ "external_credential",
+ "cua_has_credential",
+ "has_credential",
+ "viable_plans_found",
+ "no_requirements_recorded",
+ "requirements_satisfiable",
+ "no_prior_successful_login",
+ "no_credential",
+ "no_viable_plans",
+ "viable_plans_require_external_action",
+ "requires_external_action",
+ "requires_totp_without_secret",
+ "requires_sms_code",
+ "requires_email_code",
+ ]
+ ] = None
+ """
+ Machine-readable reason for the current value of `can_reauth`. Affirmative
+ values (re-auth is possible):
+
+ - `external_credential` — an external credential provider is attached
+ - `cua_has_credential` — CUA flow with a stored credential
+ - `has_credential` — Kernel credential is attached (optimistic; plan viability
+ not checked)
+ - `viable_plans_found` — at least one stored login plan can be replayed
+ - `no_requirements_recorded` — no recorded credential requirements to fail
+ against
+ - `requirements_satisfiable` — recorded requirements can be met by the attached
+ credential
+
+ Negative values (a human must complete the login flow):
+
+ - `no_prior_successful_login` — connection has never completed a successful
+ login
+ - `no_credential` — no Kernel or external credential attached
+ - `no_viable_plans` — credential attached but no replayable login plan exists
+ yet
+ - `viable_plans_require_external_action` — stored plans need an external step
+ (email link, push, etc.)
+ - `requires_external_action` — recorded requirements include an external step
+ - `requires_totp_without_secret` — flow needs a TOTP code but no TOTP secret is
+ stored
+ - `requires_sms_code` — flow needs an SMS code that cannot be received
+ automatically
+ - `requires_email_code` — flow needs an email code that cannot be received
+ automatically
"""
-
- can_reauth_reason: Optional[str] = None
- """Reason why automatic re-authentication is or is not possible"""
credential: Optional[Credential] = None
"""Reference to credentials for the auth connection. Use one of:
From 0c10a4c845ac8b11d8763878b9b4a2ab0f00970f Mon Sep 17 00:00:00 2001
From: "stainless-app[bot]"
<142633134+stainless-app[bot]@users.noreply.github.com>
Date: Fri, 22 May 2026 16:48:38 +0000
Subject: [PATCH 011/366] feat: EOL persistent browsers: openapi limits
---
.stats.yml | 8 +-
README.md | 4 +-
api.md | 2 -
src/kernel/resources/browsers/browsers.py | 121 +-----------------
src/kernel/resources/projects/limits.py | 10 --
src/kernel/types/__init__.py | 3 -
src/kernel/types/browser_create_params.py | 7 +-
src/kernel/types/browser_create_response.py | 4 -
src/kernel/types/browser_delete_params.py | 12 --
src/kernel/types/browser_list_response.py | 4 -
src/kernel/types/browser_persistence.py | 12 --
src/kernel/types/browser_persistence_param.py | 14 --
.../types/browser_pool_acquire_response.py | 4 -
src/kernel/types/browser_retrieve_response.py | 4 -
src/kernel/types/browser_update_response.py | 4 -
.../invocation_list_browsers_response.py | 4 -
.../types/projects/limit_update_params.py | 6 -
src/kernel/types/projects/project_limits.py | 6 -
tests/api_resources/projects/test_limits.py | 2 -
tests/api_resources/test_browsers.py | 80 ------------
20 files changed, 9 insertions(+), 302 deletions(-)
delete mode 100644 src/kernel/types/browser_delete_params.py
delete mode 100644 src/kernel/types/browser_persistence.py
delete mode 100644 src/kernel/types/browser_persistence_param.py
diff --git a/.stats.yml b/.stats.yml
index 01ff0714..6007fff2 100644
--- a/.stats.yml
+++ b/.stats.yml
@@ -1,4 +1,4 @@
-configured_endpoints: 113
-openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel/kernel-2118a79fa452fad5922776130ea4877059e9568b603ef146171b7bb6a0f8458a.yml
-openapi_spec_hash: 686e8c5855ed88b2944b9d9ec21d22ce
-config_hash: 37661d89120558d34b6cc184292632f2
+configured_endpoints: 112
+openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel/kernel-346b1affebf16e7f943bcf4db307a5589fcae636c9147dd0afbe5c02cf12ad30.yml
+openapi_spec_hash: 348df436759d220264f12450919211c3
+config_hash: ae3dea7997fb5d36fa41979f9585ed78
diff --git a/README.md b/README.md
index 2ac7afba..d7639da5 100644
--- a/README.md
+++ b/README.md
@@ -203,9 +203,9 @@ from kernel import Kernel
client = Kernel()
browser = client.browsers.create(
- persistence={"id": "my-awesome-browser-for-user-1234"},
+ profile={},
)
-print(browser.persistence)
+print(browser.profile)
```
## File uploads
diff --git a/api.md b/api.md
index d0b1d720..b652f86d 100644
--- a/api.md
+++ b/api.md
@@ -80,7 +80,6 @@ Types:
```python
from kernel.types import (
- BrowserPersistence,
BrowserPoolRef,
BrowserUsage,
Profile,
@@ -98,7 +97,6 @@ Methods:
- client.browsers.retrieve(id, \*\*params) -> BrowserRetrieveResponse
- client.browsers.update(id, \*\*params) -> BrowserUpdateResponse
- client.browsers.list(\*\*params) -> SyncOffsetPagination[BrowserListResponse]
-- client.browsers.delete(\*\*params) -> None
- client.browsers.curl(id, \*\*params) -> BrowserCurlResponse
- client.browsers.delete_by_id(id) -> None
- client.browsers.load_extensions(id, \*\*params) -> None
diff --git a/src/kernel/resources/browsers/browsers.py b/src/kernel/resources/browsers/browsers.py
index f7136a1a..d346a708 100644
--- a/src/kernel/resources/browsers/browsers.py
+++ b/src/kernel/resources/browsers/browsers.py
@@ -2,7 +2,6 @@
from __future__ import annotations
-import typing_extensions
from typing import Dict, Mapping, Iterable, Iterator, Optional, AsyncIterator, cast
from contextlib import contextmanager, asynccontextmanager
from typing_extensions import Literal
@@ -29,7 +28,6 @@
browser_curl_params,
browser_list_params,
browser_create_params,
- browser_delete_params,
browser_update_params,
browser_retrieve_params,
browser_load_extensions_params,
@@ -97,7 +95,6 @@
)
from ...types.browser_create_response import BrowserCreateResponse
from ...types.browser_update_response import BrowserUpdateResponse
-from ...types.browser_persistence_param import BrowserPersistenceParam
from ...types.browser_retrieve_response import BrowserRetrieveResponse
from ...types.shared_params.browser_profile import BrowserProfile
from ...types.shared_params.browser_viewport import BrowserViewport
@@ -172,7 +169,6 @@ def create(
headless: bool | Omit = omit,
invocation_id: str | Omit = omit,
kiosk_mode: bool | Omit = omit,
- persistence: BrowserPersistenceParam | Omit = omit,
profile: BrowserProfile | Omit = omit,
proxy_id: str | Omit = omit,
start_url: str | Omit = omit,
@@ -193,8 +189,7 @@ def create(
Args:
chrome_policy: Custom Chrome enterprise policy overrides applied to this browser session. Keys
are Chrome enterprise policy names; values must match their expected types.
- Blocked: kernel-managed policies (extensions, proxy, CDP/automation). Ignored
- when reusing an existing persistent session. See
+ Blocked: kernel-managed policies (extensions, proxy, CDP/automation). See
https://chromeenterprise.google/policies/
extensions: List of browser extensions to load into the session. Provide each by id or name.
@@ -210,8 +205,6 @@ def create(
kiosk_mode: If true, launches the browser in kiosk mode to hide address bar and tabs in live
view.
- persistence: DEPRECATED: Use timeout_seconds (up to 72 hours) and Profiles instead.
-
profile: Profile selection for the browser session. Provide either id or name. If
specified, the matching profile will be loaded into the browser session.
Profiles must be created beforehand.
@@ -267,7 +260,6 @@ def create(
"headless": headless,
"invocation_id": invocation_id,
"kiosk_mode": kiosk_mode,
- "persistence": persistence,
"profile": profile,
"proxy_id": proxy_id,
"start_url": start_url,
@@ -453,47 +445,6 @@ def list(
model=BrowserListResponse,
)
- @typing_extensions.deprecated("deprecated")
- def delete(
- self,
- *,
- persistent_id: str,
- # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
- # The extra values given here take precedence over values defined on the client or passed to this method.
- extra_headers: Headers | None = None,
- extra_query: Query | None = None,
- extra_body: Body | None = None,
- timeout: float | httpx.Timeout | None | NotGiven = not_given,
- ) -> None:
- """DEPRECATED: Use DELETE /browsers/{id} instead.
-
- Delete a persistent browser
- session by its persistent_id.
-
- Args:
- persistent_id: Persistent browser identifier
-
- 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
- """
- extra_headers = {"Accept": "*/*", **(extra_headers or {})}
- return self._delete(
- "/browsers",
- options=make_request_options(
- extra_headers=extra_headers,
- extra_query=extra_query,
- extra_body=extra_body,
- timeout=timeout,
- query=maybe_transform({"persistent_id": persistent_id}, browser_delete_params.BrowserDeleteParams),
- ),
- cast_to=NoneType,
- )
-
def curl(
self,
id: str,
@@ -762,7 +713,6 @@ async def create(
headless: bool | Omit = omit,
invocation_id: str | Omit = omit,
kiosk_mode: bool | Omit = omit,
- persistence: BrowserPersistenceParam | Omit = omit,
profile: BrowserProfile | Omit = omit,
proxy_id: str | Omit = omit,
start_url: str | Omit = omit,
@@ -783,8 +733,7 @@ async def create(
Args:
chrome_policy: Custom Chrome enterprise policy overrides applied to this browser session. Keys
are Chrome enterprise policy names; values must match their expected types.
- Blocked: kernel-managed policies (extensions, proxy, CDP/automation). Ignored
- when reusing an existing persistent session. See
+ Blocked: kernel-managed policies (extensions, proxy, CDP/automation). See
https://chromeenterprise.google/policies/
extensions: List of browser extensions to load into the session. Provide each by id or name.
@@ -800,8 +749,6 @@ async def create(
kiosk_mode: If true, launches the browser in kiosk mode to hide address bar and tabs in live
view.
- persistence: DEPRECATED: Use timeout_seconds (up to 72 hours) and Profiles instead.
-
profile: Profile selection for the browser session. Provide either id or name. If
specified, the matching profile will be loaded into the browser session.
Profiles must be created beforehand.
@@ -857,7 +804,6 @@ async def create(
"headless": headless,
"invocation_id": invocation_id,
"kiosk_mode": kiosk_mode,
- "persistence": persistence,
"profile": profile,
"proxy_id": proxy_id,
"start_url": start_url,
@@ -1043,49 +989,6 @@ def list(
model=BrowserListResponse,
)
- @typing_extensions.deprecated("deprecated")
- async def delete(
- self,
- *,
- persistent_id: str,
- # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
- # The extra values given here take precedence over values defined on the client or passed to this method.
- extra_headers: Headers | None = None,
- extra_query: Query | None = None,
- extra_body: Body | None = None,
- timeout: float | httpx.Timeout | None | NotGiven = not_given,
- ) -> None:
- """DEPRECATED: Use DELETE /browsers/{id} instead.
-
- Delete a persistent browser
- session by its persistent_id.
-
- Args:
- persistent_id: Persistent browser identifier
-
- 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
- """
- extra_headers = {"Accept": "*/*", **(extra_headers or {})}
- return await self._delete(
- "/browsers",
- options=make_request_options(
- extra_headers=extra_headers,
- extra_query=extra_query,
- extra_body=extra_body,
- timeout=timeout,
- query=await async_maybe_transform(
- {"persistent_id": persistent_id}, browser_delete_params.BrowserDeleteParams
- ),
- ),
- cast_to=NoneType,
- )
-
async def curl(
self,
id: str,
@@ -1305,11 +1208,6 @@ def __init__(self, browsers: BrowsersResource) -> None:
self.list = to_raw_response_wrapper(
browsers.list,
)
- self.delete = ( # pyright: ignore[reportDeprecated]
- to_raw_response_wrapper(
- browsers.delete, # pyright: ignore[reportDeprecated],
- )
- )
self.curl = to_raw_response_wrapper(
browsers.curl,
)
@@ -1371,11 +1269,6 @@ def __init__(self, browsers: AsyncBrowsersResource) -> None:
self.list = async_to_raw_response_wrapper(
browsers.list,
)
- self.delete = ( # pyright: ignore[reportDeprecated]
- async_to_raw_response_wrapper(
- browsers.delete, # pyright: ignore[reportDeprecated],
- )
- )
self.curl = async_to_raw_response_wrapper(
browsers.curl,
)
@@ -1437,11 +1330,6 @@ def __init__(self, browsers: BrowsersResource) -> None:
self.list = to_streamed_response_wrapper(
browsers.list,
)
- self.delete = ( # pyright: ignore[reportDeprecated]
- to_streamed_response_wrapper(
- browsers.delete, # pyright: ignore[reportDeprecated],
- )
- )
self.curl = to_streamed_response_wrapper(
browsers.curl,
)
@@ -1503,11 +1391,6 @@ def __init__(self, browsers: AsyncBrowsersResource) -> None:
self.list = async_to_streamed_response_wrapper(
browsers.list,
)
- self.delete = ( # pyright: ignore[reportDeprecated]
- async_to_streamed_response_wrapper(
- browsers.delete, # pyright: ignore[reportDeprecated],
- )
- )
self.curl = async_to_streamed_response_wrapper(
browsers.curl,
)
diff --git a/src/kernel/resources/projects/limits.py b/src/kernel/resources/projects/limits.py
index eeff5930..04ac3148 100644
--- a/src/kernel/resources/projects/limits.py
+++ b/src/kernel/resources/projects/limits.py
@@ -86,7 +86,6 @@ def update(
*,
max_concurrent_invocations: Optional[int] | Omit = omit,
max_concurrent_sessions: Optional[int] | Omit = omit,
- max_persistent_sessions: Optional[int] | Omit = omit,
max_pooled_sessions: Optional[int] | Omit = omit,
# Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
# The extra values given here take precedence over values defined on the client or passed to this method.
@@ -108,9 +107,6 @@ def update(
max_concurrent_sessions: Maximum concurrent browser sessions for this project. Set to 0 to remove the
cap; omit to leave unchanged.
- max_persistent_sessions: Maximum persistent browser sessions for this project. Set to 0 to remove the
- cap; omit to leave unchanged.
-
max_pooled_sessions: Maximum pooled sessions capacity for this project. Set to 0 to remove the cap;
omit to leave unchanged.
@@ -130,7 +126,6 @@ def update(
{
"max_concurrent_invocations": max_concurrent_invocations,
"max_concurrent_sessions": max_concurrent_sessions,
- "max_persistent_sessions": max_persistent_sessions,
"max_pooled_sessions": max_pooled_sessions,
},
limit_update_params.LimitUpdateParams,
@@ -205,7 +200,6 @@ async def update(
*,
max_concurrent_invocations: Optional[int] | Omit = omit,
max_concurrent_sessions: Optional[int] | Omit = omit,
- max_persistent_sessions: Optional[int] | Omit = omit,
max_pooled_sessions: Optional[int] | Omit = omit,
# Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
# The extra values given here take precedence over values defined on the client or passed to this method.
@@ -227,9 +221,6 @@ async def update(
max_concurrent_sessions: Maximum concurrent browser sessions for this project. Set to 0 to remove the
cap; omit to leave unchanged.
- max_persistent_sessions: Maximum persistent browser sessions for this project. Set to 0 to remove the
- cap; omit to leave unchanged.
-
max_pooled_sessions: Maximum pooled sessions capacity for this project. Set to 0 to remove the cap;
omit to leave unchanged.
@@ -249,7 +240,6 @@ async def update(
{
"max_concurrent_invocations": max_concurrent_invocations,
"max_concurrent_sessions": max_concurrent_sessions,
- "max_persistent_sessions": max_persistent_sessions,
"max_pooled_sessions": max_pooled_sessions,
},
limit_update_params.LimitUpdateParams,
diff --git a/src/kernel/types/__init__.py b/src/kernel/types/__init__.py
index 1a7d19c3..49346e2f 100644
--- a/src/kernel/types/__init__.py
+++ b/src/kernel/types/__init__.py
@@ -26,7 +26,6 @@
from .proxy_check_params import ProxyCheckParams as ProxyCheckParams
from .browser_curl_params import BrowserCurlParams as BrowserCurlParams
from .browser_list_params import BrowserListParams as BrowserListParams
-from .browser_persistence import BrowserPersistence as BrowserPersistence
from .credential_provider import CredentialProvider as CredentialProvider
from .profile_list_params import ProfileListParams as ProfileListParams
from .project_list_params import ProjectListParams as ProjectListParams
@@ -35,7 +34,6 @@
from .proxy_check_response import ProxyCheckResponse as ProxyCheckResponse
from .browser_create_params import BrowserCreateParams as BrowserCreateParams
from .browser_curl_response import BrowserCurlResponse as BrowserCurlResponse
-from .browser_delete_params import BrowserDeleteParams as BrowserDeleteParams
from .browser_list_response import BrowserListResponse as BrowserListResponse
from .browser_update_params import BrowserUpdateParams as BrowserUpdateParams
from .profile_create_params import ProfileCreateParams as ProfileCreateParams
@@ -63,7 +61,6 @@
from .invocation_follow_params import InvocationFollowParams as InvocationFollowParams
from .invocation_list_response import InvocationListResponse as InvocationListResponse
from .invocation_update_params import InvocationUpdateParams as InvocationUpdateParams
-from .browser_persistence_param import BrowserPersistenceParam as BrowserPersistenceParam
from .browser_retrieve_response import BrowserRetrieveResponse as BrowserRetrieveResponse
from .extension_upload_response import ExtensionUploadResponse as ExtensionUploadResponse
from .browser_pool_create_params import BrowserPoolCreateParams as BrowserPoolCreateParams
diff --git a/src/kernel/types/browser_create_params.py b/src/kernel/types/browser_create_params.py
index ca184fc7..1c2ed5e9 100644
--- a/src/kernel/types/browser_create_params.py
+++ b/src/kernel/types/browser_create_params.py
@@ -5,7 +5,6 @@
from typing import Dict, Iterable, Optional
from typing_extensions import TypedDict
-from .browser_persistence_param import BrowserPersistenceParam
from .shared_params.browser_profile import BrowserProfile
from .shared_params.browser_viewport import BrowserViewport
from .shared_params.browser_extension import BrowserExtension
@@ -19,8 +18,7 @@ class BrowserCreateParams(TypedDict, total=False):
"""Custom Chrome enterprise policy overrides applied to this browser session.
Keys are Chrome enterprise policy names; values must match their expected types.
- Blocked: kernel-managed policies (extensions, proxy, CDP/automation). Ignored
- when reusing an existing persistent session. See
+ Blocked: kernel-managed policies (extensions, proxy, CDP/automation). See
https://chromeenterprise.google/policies/
"""
@@ -51,9 +49,6 @@ class BrowserCreateParams(TypedDict, total=False):
view.
"""
- persistence: BrowserPersistenceParam
- """DEPRECATED: Use timeout_seconds (up to 72 hours) and Profiles instead."""
-
profile: BrowserProfile
"""Profile selection for the browser session.
diff --git a/src/kernel/types/browser_create_response.py b/src/kernel/types/browser_create_response.py
index bbb2287e..651216ab 100644
--- a/src/kernel/types/browser_create_response.py
+++ b/src/kernel/types/browser_create_response.py
@@ -7,7 +7,6 @@
from .._models import BaseModel
from .browser_usage import BrowserUsage
from .browser_pool_ref import BrowserPoolRef
-from .browser_persistence import BrowserPersistence
from .shared.browser_viewport import BrowserViewport
from .browsers.browser_telemetry_config import BrowserTelemetryConfig
@@ -64,9 +63,6 @@ class BrowserCreateResponse(BaseModel):
kiosk_mode: Optional[bool] = None
"""Whether the browser session is running in kiosk mode."""
- persistence: Optional[BrowserPersistence] = None
- """DEPRECATED: Use timeout_seconds (up to 72 hours) and Profiles instead."""
-
pool: Optional[BrowserPoolRef] = None
"""Browser pool this session was acquired from, if any."""
diff --git a/src/kernel/types/browser_delete_params.py b/src/kernel/types/browser_delete_params.py
deleted file mode 100644
index 4c5b1c6a..00000000
--- a/src/kernel/types/browser_delete_params.py
+++ /dev/null
@@ -1,12 +0,0 @@
-# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
-
-from __future__ import annotations
-
-from typing_extensions import Required, TypedDict
-
-__all__ = ["BrowserDeleteParams"]
-
-
-class BrowserDeleteParams(TypedDict, total=False):
- persistent_id: Required[str]
- """Persistent browser identifier"""
diff --git a/src/kernel/types/browser_list_response.py b/src/kernel/types/browser_list_response.py
index ed14251a..79db0cfc 100644
--- a/src/kernel/types/browser_list_response.py
+++ b/src/kernel/types/browser_list_response.py
@@ -7,7 +7,6 @@
from .._models import BaseModel
from .browser_usage import BrowserUsage
from .browser_pool_ref import BrowserPoolRef
-from .browser_persistence import BrowserPersistence
from .shared.browser_viewport import BrowserViewport
from .browsers.browser_telemetry_config import BrowserTelemetryConfig
@@ -64,9 +63,6 @@ class BrowserListResponse(BaseModel):
kiosk_mode: Optional[bool] = None
"""Whether the browser session is running in kiosk mode."""
- persistence: Optional[BrowserPersistence] = None
- """DEPRECATED: Use timeout_seconds (up to 72 hours) and Profiles instead."""
-
pool: Optional[BrowserPoolRef] = None
"""Browser pool this session was acquired from, if any."""
diff --git a/src/kernel/types/browser_persistence.py b/src/kernel/types/browser_persistence.py
deleted file mode 100644
index 381d6306..00000000
--- a/src/kernel/types/browser_persistence.py
+++ /dev/null
@@ -1,12 +0,0 @@
-# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
-
-from .._models import BaseModel
-
-__all__ = ["BrowserPersistence"]
-
-
-class BrowserPersistence(BaseModel):
- """DEPRECATED: Use timeout_seconds (up to 72 hours) and Profiles instead."""
-
- id: str
- """DEPRECATED: Unique identifier for the persistent browser session."""
diff --git a/src/kernel/types/browser_persistence_param.py b/src/kernel/types/browser_persistence_param.py
deleted file mode 100644
index 6109abfd..00000000
--- a/src/kernel/types/browser_persistence_param.py
+++ /dev/null
@@ -1,14 +0,0 @@
-# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
-
-from __future__ import annotations
-
-from typing_extensions import Required, TypedDict
-
-__all__ = ["BrowserPersistenceParam"]
-
-
-class BrowserPersistenceParam(TypedDict, total=False):
- """DEPRECATED: Use timeout_seconds (up to 72 hours) and Profiles instead."""
-
- id: Required[str]
- """DEPRECATED: Unique identifier for the persistent browser session."""
diff --git a/src/kernel/types/browser_pool_acquire_response.py b/src/kernel/types/browser_pool_acquire_response.py
index 20ab1fc5..b3cbabfa 100644
--- a/src/kernel/types/browser_pool_acquire_response.py
+++ b/src/kernel/types/browser_pool_acquire_response.py
@@ -7,7 +7,6 @@
from .._models import BaseModel
from .browser_usage import BrowserUsage
from .browser_pool_ref import BrowserPoolRef
-from .browser_persistence import BrowserPersistence
from .shared.browser_viewport import BrowserViewport
from .browsers.browser_telemetry_config import BrowserTelemetryConfig
@@ -64,9 +63,6 @@ class BrowserPoolAcquireResponse(BaseModel):
kiosk_mode: Optional[bool] = None
"""Whether the browser session is running in kiosk mode."""
- persistence: Optional[BrowserPersistence] = None
- """DEPRECATED: Use timeout_seconds (up to 72 hours) and Profiles instead."""
-
pool: Optional[BrowserPoolRef] = None
"""Browser pool this session was acquired from, if any."""
diff --git a/src/kernel/types/browser_retrieve_response.py b/src/kernel/types/browser_retrieve_response.py
index 6c79b519..86b5fbef 100644
--- a/src/kernel/types/browser_retrieve_response.py
+++ b/src/kernel/types/browser_retrieve_response.py
@@ -7,7 +7,6 @@
from .._models import BaseModel
from .browser_usage import BrowserUsage
from .browser_pool_ref import BrowserPoolRef
-from .browser_persistence import BrowserPersistence
from .shared.browser_viewport import BrowserViewport
from .browsers.browser_telemetry_config import BrowserTelemetryConfig
@@ -64,9 +63,6 @@ class BrowserRetrieveResponse(BaseModel):
kiosk_mode: Optional[bool] = None
"""Whether the browser session is running in kiosk mode."""
- persistence: Optional[BrowserPersistence] = None
- """DEPRECATED: Use timeout_seconds (up to 72 hours) and Profiles instead."""
-
pool: Optional[BrowserPoolRef] = None
"""Browser pool this session was acquired from, if any."""
diff --git a/src/kernel/types/browser_update_response.py b/src/kernel/types/browser_update_response.py
index 4f237cf5..60e95176 100644
--- a/src/kernel/types/browser_update_response.py
+++ b/src/kernel/types/browser_update_response.py
@@ -7,7 +7,6 @@
from .._models import BaseModel
from .browser_usage import BrowserUsage
from .browser_pool_ref import BrowserPoolRef
-from .browser_persistence import BrowserPersistence
from .shared.browser_viewport import BrowserViewport
from .browsers.browser_telemetry_config import BrowserTelemetryConfig
@@ -64,9 +63,6 @@ class BrowserUpdateResponse(BaseModel):
kiosk_mode: Optional[bool] = None
"""Whether the browser session is running in kiosk mode."""
- persistence: Optional[BrowserPersistence] = None
- """DEPRECATED: Use timeout_seconds (up to 72 hours) and Profiles instead."""
-
pool: Optional[BrowserPoolRef] = None
"""Browser pool this session was acquired from, if any."""
diff --git a/src/kernel/types/invocation_list_browsers_response.py b/src/kernel/types/invocation_list_browsers_response.py
index 7c60ae22..0a1700c2 100644
--- a/src/kernel/types/invocation_list_browsers_response.py
+++ b/src/kernel/types/invocation_list_browsers_response.py
@@ -7,7 +7,6 @@
from .._models import BaseModel
from .browser_usage import BrowserUsage
from .browser_pool_ref import BrowserPoolRef
-from .browser_persistence import BrowserPersistence
from .shared.browser_viewport import BrowserViewport
from .browsers.browser_telemetry_config import BrowserTelemetryConfig
@@ -64,9 +63,6 @@ class Browser(BaseModel):
kiosk_mode: Optional[bool] = None
"""Whether the browser session is running in kiosk mode."""
- persistence: Optional[BrowserPersistence] = None
- """DEPRECATED: Use timeout_seconds (up to 72 hours) and Profiles instead."""
-
pool: Optional[BrowserPoolRef] = None
"""Browser pool this session was acquired from, if any."""
diff --git a/src/kernel/types/projects/limit_update_params.py b/src/kernel/types/projects/limit_update_params.py
index 6f0ec8ae..387b4c64 100644
--- a/src/kernel/types/projects/limit_update_params.py
+++ b/src/kernel/types/projects/limit_update_params.py
@@ -21,12 +21,6 @@ class LimitUpdateParams(TypedDict, total=False):
Set to 0 to remove the cap; omit to leave unchanged.
"""
- max_persistent_sessions: Optional[int]
- """Maximum persistent browser sessions for this project.
-
- Set to 0 to remove the cap; omit to leave unchanged.
- """
-
max_pooled_sessions: Optional[int]
"""Maximum pooled sessions capacity for this project.
diff --git a/src/kernel/types/projects/project_limits.py b/src/kernel/types/projects/project_limits.py
index bf49b2a2..99868fc8 100644
--- a/src/kernel/types/projects/project_limits.py
+++ b/src/kernel/types/projects/project_limits.py
@@ -20,12 +20,6 @@ class ProjectLimits(BaseModel):
Null means no project-level cap.
"""
- max_persistent_sessions: Optional[int] = None
- """Maximum persistent browser sessions for this project.
-
- Null means no project-level cap.
- """
-
max_pooled_sessions: Optional[int] = None
"""Maximum pooled sessions capacity for this project.
diff --git a/tests/api_resources/projects/test_limits.py b/tests/api_resources/projects/test_limits.py
index 9df6a0df..a42a0bde 100644
--- a/tests/api_resources/projects/test_limits.py
+++ b/tests/api_resources/projects/test_limits.py
@@ -74,7 +74,6 @@ def test_method_update_with_all_params(self, client: Kernel) -> None:
id="id",
max_concurrent_invocations=0,
max_concurrent_sessions=0,
- max_persistent_sessions=0,
max_pooled_sessions=0,
)
assert_matches_type(ProjectLimits, limit, path=["response"])
@@ -176,7 +175,6 @@ async def test_method_update_with_all_params(self, async_client: AsyncKernel) ->
id="id",
max_concurrent_invocations=0,
max_concurrent_sessions=0,
- max_persistent_sessions=0,
max_pooled_sessions=0,
)
assert_matches_type(ProjectLimits, limit, path=["response"])
diff --git a/tests/api_resources/test_browsers.py b/tests/api_resources/test_browsers.py
index dcd20977..57aa89fe 100644
--- a/tests/api_resources/test_browsers.py
+++ b/tests/api_resources/test_browsers.py
@@ -18,8 +18,6 @@
)
from kernel.pagination import SyncOffsetPagination, AsyncOffsetPagination
-# pyright: reportDeprecated=false
-
base_url = os.environ.get("TEST_API_BASE_URL", "http://127.0.0.1:4010")
@@ -47,7 +45,6 @@ def test_method_create_with_all_params(self, client: Kernel) -> None:
headless=False,
invocation_id="rr33xuugxj9h0bkf1rdt2bet",
kiosk_mode=True,
- persistence={"id": "my-awesome-browser-for-user-1234"},
profile={
"id": "id",
"name": "name",
@@ -257,44 +254,6 @@ def test_streaming_response_list(self, client: Kernel) -> None:
assert cast(Any, response.is_closed) is True
- @pytest.mark.skip(reason="Mock server tests are disabled")
- @parametrize
- def test_method_delete(self, client: Kernel) -> None:
- with pytest.warns(DeprecationWarning):
- browser = client.browsers.delete(
- persistent_id="persistent_id",
- )
-
- assert browser is None
-
- @pytest.mark.skip(reason="Mock server tests are disabled")
- @parametrize
- def test_raw_response_delete(self, client: Kernel) -> None:
- with pytest.warns(DeprecationWarning):
- response = client.browsers.with_raw_response.delete(
- persistent_id="persistent_id",
- )
-
- assert response.is_closed is True
- assert response.http_request.headers.get("X-Stainless-Lang") == "python"
- browser = response.parse()
- assert browser is None
-
- @pytest.mark.skip(reason="Mock server tests are disabled")
- @parametrize
- def test_streaming_response_delete(self, client: Kernel) -> None:
- with pytest.warns(DeprecationWarning):
- with client.browsers.with_streaming_response.delete(
- persistent_id="persistent_id",
- ) as response:
- assert not response.is_closed
- assert response.http_request.headers.get("X-Stainless-Lang") == "python"
-
- browser = response.parse()
- assert browser is None
-
- assert cast(Any, response.is_closed) is True
-
@pytest.mark.skip(reason="Mock server tests are disabled")
@parametrize
def test_method_curl(self, client: Kernel) -> None:
@@ -490,7 +449,6 @@ async def test_method_create_with_all_params(self, async_client: AsyncKernel) ->
headless=False,
invocation_id="rr33xuugxj9h0bkf1rdt2bet",
kiosk_mode=True,
- persistence={"id": "my-awesome-browser-for-user-1234"},
profile={
"id": "id",
"name": "name",
@@ -700,44 +658,6 @@ async def test_streaming_response_list(self, async_client: AsyncKernel) -> None:
assert cast(Any, response.is_closed) is True
- @pytest.mark.skip(reason="Mock server tests are disabled")
- @parametrize
- async def test_method_delete(self, async_client: AsyncKernel) -> None:
- with pytest.warns(DeprecationWarning):
- browser = await async_client.browsers.delete(
- persistent_id="persistent_id",
- )
-
- assert browser is None
-
- @pytest.mark.skip(reason="Mock server tests are disabled")
- @parametrize
- async def test_raw_response_delete(self, async_client: AsyncKernel) -> None:
- with pytest.warns(DeprecationWarning):
- response = await async_client.browsers.with_raw_response.delete(
- persistent_id="persistent_id",
- )
-
- assert response.is_closed is True
- assert response.http_request.headers.get("X-Stainless-Lang") == "python"
- browser = await response.parse()
- assert browser is None
-
- @pytest.mark.skip(reason="Mock server tests are disabled")
- @parametrize
- async def test_streaming_response_delete(self, async_client: AsyncKernel) -> None:
- with pytest.warns(DeprecationWarning):
- async with async_client.browsers.with_streaming_response.delete(
- persistent_id="persistent_id",
- ) as response:
- assert not response.is_closed
- assert response.http_request.headers.get("X-Stainless-Lang") == "python"
-
- browser = await response.parse()
- assert browser is None
-
- assert cast(Any, response.is_closed) is True
-
@pytest.mark.skip(reason="Mock server tests are disabled")
@parametrize
async def test_method_curl(self, async_client: AsyncKernel) -> None:
From 4abdd6364ece2e195f254973222645085f4f9e8e Mon Sep 17 00:00:00 2001
From: "stainless-app[bot]"
<142633134+stainless-app[bot]@users.noreply.github.com>
Date: Fri, 22 May 2026 19:17:13 +0000
Subject: [PATCH 012/366] codegen metadata
---
.stats.yml | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/.stats.yml b/.stats.yml
index 6007fff2..6f5d31f0 100644
--- a/.stats.yml
+++ b/.stats.yml
@@ -1,4 +1,4 @@
configured_endpoints: 112
-openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel/kernel-346b1affebf16e7f943bcf4db307a5589fcae636c9147dd0afbe5c02cf12ad30.yml
-openapi_spec_hash: 348df436759d220264f12450919211c3
+openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel/kernel-5d96fec7a84722f300bd99db7352d6284141826f3412f6d370ac0926edf03d42.yml
+openapi_spec_hash: d4e1a29ac06f9543e0ef69372eb3ff35
config_hash: ae3dea7997fb5d36fa41979f9585ed78
From 02d7702908c7d9ce14df13340e8295a6e241912c Mon Sep 17 00:00:00 2001
From: "stainless-app[bot]"
<142633134+stainless-app[bot]@users.noreply.github.com>
Date: Fri, 22 May 2026 19:17:43 +0000
Subject: [PATCH 013/366] release: 0.57.0
---
.release-please-manifest.json | 2 +-
CHANGELOG.md | 11 +++++++++++
pyproject.toml | 2 +-
src/kernel/_version.py | 2 +-
4 files changed, 14 insertions(+), 3 deletions(-)
diff --git a/.release-please-manifest.json b/.release-please-manifest.json
index 87d3d84c..2afb750c 100644
--- a/.release-please-manifest.json
+++ b/.release-please-manifest.json
@@ -1,3 +1,3 @@
{
- ".": "0.56.0"
+ ".": "0.57.0"
}
\ No newline at end of file
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 2a3eba14..96493998 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,16 @@
# Changelog
+## 0.57.0 (2026-05-22)
+
+Full Changelog: [v0.56.0...v0.57.0](https://github.com/kernel/kernel-python-sdk/compare/v0.56.0...v0.57.0)
+
+### Features
+
+* [kernel-1116] browser events api integration ([bfd4043](https://github.com/kernel/kernel-python-sdk/commit/bfd404368adf8a4305aabe2fad448e7eeba40322))
+* **api:** type can_reauth_reason as an enum on ManagedAuth ([c9a6c75](https://github.com/kernel/kernel-python-sdk/commit/c9a6c7571ca9eb18b3ef11974571fa1426d2dd61))
+* browsers: accept chrome_policy on POST /browsers (KERNEL-1216) ([b1807a2](https://github.com/kernel/kernel-python-sdk/commit/b1807a2d690f206e7482f5862acde36e692d07aa))
+* EOL persistent browsers: openapi limits ([253be85](https://github.com/kernel/kernel-python-sdk/commit/253be85c497bf65329c484e28a3732b3bd5b3208))
+
## 0.56.0 (2026-05-18)
Full Changelog: [v0.55.0...v0.56.0](https://github.com/kernel/kernel-python-sdk/compare/v0.55.0...v0.56.0)
diff --git a/pyproject.toml b/pyproject.toml
index c98902d7..6956b281 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -1,6 +1,6 @@
[project]
name = "kernel"
-version = "0.56.0"
+version = "0.57.0"
description = "The official Python library for the kernel API"
dynamic = ["readme"]
license = "Apache-2.0"
diff --git a/src/kernel/_version.py b/src/kernel/_version.py
index a4d2a572..fb252c57 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.56.0" # x-release-please-version
+__version__ = "0.57.0" # x-release-please-version
From 6ae8ac9a904434dd2e8e2f1adae9843539a639c1 Mon Sep 17 00:00:00 2001
From: Ilyssd
Date: Fri, 22 May 2026 15:44:18 -0400
Subject: [PATCH 014/366] Fix telemetry forward refs
---
src/kernel/types/browsers/browser_telemetry_event.py | 9 ++++-----
1 file changed, 4 insertions(+), 5 deletions(-)
diff --git a/src/kernel/types/browsers/browser_telemetry_event.py b/src/kernel/types/browsers/browser_telemetry_event.py
index c27297bc..d8506bf7 100644
--- a/src/kernel/types/browsers/browser_telemetry_event.py
+++ b/src/kernel/types/browsers/browser_telemetry_event.py
@@ -8,7 +8,9 @@
from ..._utils import PropertyInfo
from .browser_page_lcp_event import BrowserPageLcpEvent
from .browser_page_load_event import BrowserPageLoadEvent
+from .browser_console_log_event import BrowserConsoleLogEvent
from .browser_network_idle_event import BrowserNetworkIdleEvent
+from .browser_console_error_event import BrowserConsoleErrorEvent
from .browser_interaction_key_event import BrowserInteractionKeyEvent
from .browser_network_request_event import BrowserNetworkRequestEvent
from .browser_page_navigation_event import BrowserPageNavigationEvent
@@ -31,8 +33,8 @@
BrowserTelemetryEvent: TypeAlias = Annotated[
Union[
- "BrowserConsoleLogEvent",
- "BrowserConsoleErrorEvent",
+ BrowserConsoleLogEvent,
+ BrowserConsoleErrorEvent,
BrowserNetworkRequestEvent,
BrowserNetworkResponseEvent,
BrowserNetworkLoadingFailedEvent,
@@ -56,6 +58,3 @@
],
PropertyInfo(discriminator="type"),
]
-
-from .browser_console_log_event import BrowserConsoleLogEvent
-from .browser_console_error_event import BrowserConsoleErrorEvent
From cb90c73c36e974e9e8321faa0518393b284d6af2 Mon Sep 17 00:00:00 2001
From: "stainless-app[bot]"
<142633134+stainless-app[bot]@users.noreply.github.com>
Date: Sun, 24 May 2026 21:26:08 +0000
Subject: [PATCH 015/366] feat: api: dual-route /projects under /org/projects,
deprecate /projects
---
.stats.yml | 6 +++---
api.md | 14 +++++++-------
src/kernel/resources/projects/limits.py | 8 ++++----
src/kernel/resources/projects/projects.py | 20 ++++++++++----------
4 files changed, 24 insertions(+), 24 deletions(-)
diff --git a/.stats.yml b/.stats.yml
index 6f5d31f0..dbf097fb 100644
--- a/.stats.yml
+++ b/.stats.yml
@@ -1,4 +1,4 @@
configured_endpoints: 112
-openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel/kernel-5d96fec7a84722f300bd99db7352d6284141826f3412f6d370ac0926edf03d42.yml
-openapi_spec_hash: d4e1a29ac06f9543e0ef69372eb3ff35
-config_hash: ae3dea7997fb5d36fa41979f9585ed78
+openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel/kernel-a0ad160eb0fb11e201e240de8487c12992a474076aeed1c1167ee54ade43edfb.yml
+openapi_spec_hash: 84a7075fddbe17b1446a759e8cc39047
+config_hash: 26beac3050665664d5d74d2bbfe9e808
diff --git a/api.md b/api.md
index b652f86d..4785c2d9 100644
--- a/api.md
+++ b/api.md
@@ -395,11 +395,11 @@ from kernel.types import CreateProjectRequest, Project, UpdateProjectRequest
Methods:
-- client.projects.create(\*\*params) -> Project
-- client.projects.retrieve(id) -> Project
-- client.projects.update(id, \*\*params) -> Project
-- client.projects.list(\*\*params) -> SyncOffsetPagination[Project]
-- client.projects.delete(id) -> None
+- client.projects.create(\*\*params) -> Project
+- client.projects.retrieve(id) -> Project
+- client.projects.update(id, \*\*params) -> Project
+- client.projects.list(\*\*params) -> SyncOffsetPagination[Project]
+- client.projects.delete(id) -> None
## Limits
@@ -411,8 +411,8 @@ from kernel.types.projects import ProjectLimits, UpdateProjectLimitsRequest
Methods:
-- client.projects.limits.retrieve(id) -> ProjectLimits
-- client.projects.limits.update(id, \*\*params) -> ProjectLimits
+- client.projects.limits.retrieve(id) -> ProjectLimits
+- client.projects.limits.update(id, \*\*params) -> ProjectLimits
# CredentialProviders
diff --git a/src/kernel/resources/projects/limits.py b/src/kernel/resources/projects/limits.py
index 04ac3148..ade9302c 100644
--- a/src/kernel/resources/projects/limits.py
+++ b/src/kernel/resources/projects/limits.py
@@ -73,7 +73,7 @@ def retrieve(
if not id:
raise ValueError(f"Expected a non-empty value for `id` but received {id!r}")
return self._get(
- path_template("/projects/{id}/limits", id=id),
+ path_template("/org/projects/{id}/limits", id=id),
options=make_request_options(
extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
),
@@ -121,7 +121,7 @@ def update(
if not id:
raise ValueError(f"Expected a non-empty value for `id` but received {id!r}")
return self._patch(
- path_template("/projects/{id}/limits", id=id),
+ path_template("/org/projects/{id}/limits", id=id),
body=maybe_transform(
{
"max_concurrent_invocations": max_concurrent_invocations,
@@ -187,7 +187,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(
- path_template("/projects/{id}/limits", id=id),
+ path_template("/org/projects/{id}/limits", id=id),
options=make_request_options(
extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
),
@@ -235,7 +235,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(
- path_template("/projects/{id}/limits", id=id),
+ path_template("/org/projects/{id}/limits", id=id),
body=await async_maybe_transform(
{
"max_concurrent_invocations": max_concurrent_invocations,
diff --git a/src/kernel/resources/projects/projects.py b/src/kernel/resources/projects/projects.py
index 87fae1b8..ea977689 100644
--- a/src/kernel/resources/projects/projects.py
+++ b/src/kernel/resources/projects/projects.py
@@ -85,7 +85,7 @@ def create(
timeout: Override the client-level default timeout for this request, in seconds
"""
return self._post(
- "/projects",
+ "/org/projects",
body=maybe_transform({"name": name}, project_create_params.ProjectCreateParams),
options=make_request_options(
extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
@@ -119,7 +119,7 @@ def retrieve(
if not id:
raise ValueError(f"Expected a non-empty value for `id` but received {id!r}")
return self._get(
- path_template("/projects/{id}", id=id),
+ path_template("/org/projects/{id}", id=id),
options=make_request_options(
extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
),
@@ -158,7 +158,7 @@ def update(
if not id:
raise ValueError(f"Expected a non-empty value for `id` but received {id!r}")
return self._patch(
- path_template("/projects/{id}", id=id),
+ path_template("/org/projects/{id}", id=id),
body=maybe_transform(
{
"name": name,
@@ -204,7 +204,7 @@ def list(
timeout: Override the client-level default timeout for this request, in seconds
"""
return self._get_api_list(
- "/projects",
+ "/org/projects",
page=SyncOffsetPagination[Project],
options=make_request_options(
extra_headers=extra_headers,
@@ -251,7 +251,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(
- path_template("/projects/{id}", id=id),
+ path_template("/org/projects/{id}", id=id),
options=make_request_options(
extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
),
@@ -312,7 +312,7 @@ async def create(
timeout: Override the client-level default timeout for this request, in seconds
"""
return await self._post(
- "/projects",
+ "/org/projects",
body=await async_maybe_transform({"name": name}, project_create_params.ProjectCreateParams),
options=make_request_options(
extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
@@ -346,7 +346,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(
- path_template("/projects/{id}", id=id),
+ path_template("/org/projects/{id}", id=id),
options=make_request_options(
extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
),
@@ -385,7 +385,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(
- path_template("/projects/{id}", id=id),
+ path_template("/org/projects/{id}", id=id),
body=await async_maybe_transform(
{
"name": name,
@@ -431,7 +431,7 @@ def list(
timeout: Override the client-level default timeout for this request, in seconds
"""
return self._get_api_list(
- "/projects",
+ "/org/projects",
page=AsyncOffsetPagination[Project],
options=make_request_options(
extra_headers=extra_headers,
@@ -478,7 +478,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(
- path_template("/projects/{id}", id=id),
+ path_template("/org/projects/{id}", id=id),
options=make_request_options(
extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
),
From 3a3f587f49cf3a64e5b5c658494af931c7e6b795 Mon Sep 17 00:00:00 2001
From: "stainless-app[bot]"
<142633134+stainless-app[bot]@users.noreply.github.com>
Date: Tue, 26 May 2026 17:42:46 +0000
Subject: [PATCH 016/366] feat: Support telemetry enabled request config
---
.stats.yml | 6 +--
api.md | 1 +
src/kernel/resources/browsers/browsers.py | 40 +++++++++++--------
src/kernel/types/browser_create_params.py | 10 +++--
src/kernel/types/browser_update_params.py | 11 ++---
src/kernel/types/browsers/__init__.py | 4 +-
.../browsers/browser_telemetry_config.py | 4 +-
.../browser_telemetry_config_param.py | 16 --------
.../browser_telemetry_request_config_param.py | 28 +++++++++++++
tests/api_resources/test_browsers.py | 12 ++++--
10 files changed, 80 insertions(+), 52 deletions(-)
delete mode 100644 src/kernel/types/browsers/browser_telemetry_config_param.py
create mode 100644 src/kernel/types/browsers/browser_telemetry_request_config_param.py
diff --git a/.stats.yml b/.stats.yml
index dbf097fb..47ab7c16 100644
--- a/.stats.yml
+++ b/.stats.yml
@@ -1,4 +1,4 @@
configured_endpoints: 112
-openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel/kernel-a0ad160eb0fb11e201e240de8487c12992a474076aeed1c1167ee54ade43edfb.yml
-openapi_spec_hash: 84a7075fddbe17b1446a759e8cc39047
-config_hash: 26beac3050665664d5d74d2bbfe9e808
+openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel/kernel-e9c99662d29710f105847d461f8919e06f6aa2e43b0e1a6285d0b137643a7907.yml
+openapi_spec_hash: 4415cb4790c7a5ec892f4e3521217cb4
+config_hash: 27b38657d9a3b33328be930eeb319628
diff --git a/api.md b/api.md
index 4785c2d9..106e3cd8 100644
--- a/api.md
+++ b/api.md
@@ -137,6 +137,7 @@ from kernel.types.browsers import (
BrowserTelemetryCategoryConfig,
BrowserTelemetryConfig,
BrowserTelemetryEvent,
+ BrowserTelemetryRequestConfig,
TelemetryStreamResponse,
)
```
diff --git a/src/kernel/resources/browsers/browsers.py b/src/kernel/resources/browsers/browsers.py
index d346a708..ad8087fb 100644
--- a/src/kernel/resources/browsers/browsers.py
+++ b/src/kernel/resources/browsers/browsers.py
@@ -99,7 +99,7 @@
from ...types.shared_params.browser_profile import BrowserProfile
from ...types.shared_params.browser_viewport import BrowserViewport
from ...types.shared_params.browser_extension import BrowserExtension
-from ...types.browsers.browser_telemetry_config_param import BrowserTelemetryConfigParam
+from ...types.browsers.browser_telemetry_request_config_param import BrowserTelemetryRequestConfigParam
__all__ = ["BrowsersResource", "AsyncBrowsersResource"]
@@ -173,7 +173,7 @@ def create(
proxy_id: str | Omit = omit,
start_url: str | Omit = omit,
stealth: bool | Omit = omit,
- telemetry: Optional[BrowserTelemetryConfigParam] | Omit = omit,
+ telemetry: Optional[BrowserTelemetryRequestConfigParam] | Omit = omit,
timeout_seconds: int | Omit = omit,
viewport: BrowserViewport | Omit = omit,
# Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
@@ -219,9 +219,11 @@ def create(
stealth: If true, launches the browser in stealth mode to reduce detection by anti-bot
mechanisms.
- telemetry: Telemetry configuration for the browser session. If provided, telemetry capture
- starts with the specified category filter when the session is created. If
- omitted, no telemetry capture is started.
+ telemetry: Telemetry configuration for the browser session. Set enabled to true to start
+ capture using VM defaults, or provide browser category settings. If omitted,
+ null, set to an empty object ({}), set to enabled: false without browser
+ category settings, or all four categories are explicitly disabled, capture is
+ not started.
timeout_seconds: The number of seconds of inactivity before the browser session is terminated.
Activity includes CDP connections and live view connections. Defaults to 60
@@ -325,7 +327,7 @@ def update(
disable_default_proxy: bool | Omit = omit,
profile: BrowserProfile | Omit = omit,
proxy_id: Optional[str] | Omit = omit,
- telemetry: Optional[BrowserTelemetryConfigParam] | Omit = omit,
+ telemetry: Optional[BrowserTelemetryRequestConfigParam] | Omit = omit,
viewport: browser_update_params.Viewport | 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.
@@ -348,9 +350,10 @@ def update(
proxy.
telemetry: Telemetry configuration. Omit, set to null, or set to an empty object ({}) to
- leave the existing configuration unchanged (no-op). To enable capture for all
- categories using VM defaults, set browser to an empty object ({"browser": {}}).
- To stop capture, set every category's enabled to false.
+ leave the existing configuration unchanged. Set enabled to true to enable
+ capture using VM defaults. Set enabled to false to stop capture. Provide browser
+ category settings for per-category updates. Explicitly disabling all four
+ categories also stops capture.
viewport: Viewport configuration to apply to the browser session.
@@ -717,7 +720,7 @@ async def create(
proxy_id: str | Omit = omit,
start_url: str | Omit = omit,
stealth: bool | Omit = omit,
- telemetry: Optional[BrowserTelemetryConfigParam] | Omit = omit,
+ telemetry: Optional[BrowserTelemetryRequestConfigParam] | Omit = omit,
timeout_seconds: int | Omit = omit,
viewport: BrowserViewport | Omit = omit,
# Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
@@ -763,9 +766,11 @@ async def create(
stealth: If true, launches the browser in stealth mode to reduce detection by anti-bot
mechanisms.
- telemetry: Telemetry configuration for the browser session. If provided, telemetry capture
- starts with the specified category filter when the session is created. If
- omitted, no telemetry capture is started.
+ telemetry: Telemetry configuration for the browser session. Set enabled to true to start
+ capture using VM defaults, or provide browser category settings. If omitted,
+ null, set to an empty object ({}), set to enabled: false without browser
+ category settings, or all four categories are explicitly disabled, capture is
+ not started.
timeout_seconds: The number of seconds of inactivity before the browser session is terminated.
Activity includes CDP connections and live view connections. Defaults to 60
@@ -869,7 +874,7 @@ async def update(
disable_default_proxy: bool | Omit = omit,
profile: BrowserProfile | Omit = omit,
proxy_id: Optional[str] | Omit = omit,
- telemetry: Optional[BrowserTelemetryConfigParam] | Omit = omit,
+ telemetry: Optional[BrowserTelemetryRequestConfigParam] | Omit = omit,
viewport: browser_update_params.Viewport | 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.
@@ -892,9 +897,10 @@ async def update(
proxy.
telemetry: Telemetry configuration. Omit, set to null, or set to an empty object ({}) to
- leave the existing configuration unchanged (no-op). To enable capture for all
- categories using VM defaults, set browser to an empty object ({"browser": {}}).
- To stop capture, set every category's enabled to false.
+ leave the existing configuration unchanged. Set enabled to true to enable
+ capture using VM defaults. Set enabled to false to stop capture. Provide browser
+ category settings for per-category updates. Explicitly disabling all four
+ categories also stops capture.
viewport: Viewport configuration to apply to the browser session.
diff --git a/src/kernel/types/browser_create_params.py b/src/kernel/types/browser_create_params.py
index 1c2ed5e9..82406a5a 100644
--- a/src/kernel/types/browser_create_params.py
+++ b/src/kernel/types/browser_create_params.py
@@ -8,7 +8,7 @@
from .shared_params.browser_profile import BrowserProfile
from .shared_params.browser_viewport import BrowserViewport
from .shared_params.browser_extension import BrowserExtension
-from .browsers.browser_telemetry_config_param import BrowserTelemetryConfigParam
+from .browsers.browser_telemetry_request_config_param import BrowserTelemetryRequestConfigParam
__all__ = ["BrowserCreateParams"]
@@ -75,11 +75,13 @@ class BrowserCreateParams(TypedDict, total=False):
mechanisms.
"""
- telemetry: Optional[BrowserTelemetryConfigParam]
+ telemetry: Optional[BrowserTelemetryRequestConfigParam]
"""Telemetry configuration for the browser session.
- If provided, telemetry capture starts with the specified category filter when
- the session is created. If omitted, no telemetry capture is started.
+ Set enabled to true to start capture using VM defaults, or provide browser
+ category settings. If omitted, null, set to an empty object ({}), set to
+ enabled: false without browser category settings, or all four categories are
+ explicitly disabled, capture is not started.
"""
timeout_seconds: int
diff --git a/src/kernel/types/browser_update_params.py b/src/kernel/types/browser_update_params.py
index 297d0388..c883089d 100644
--- a/src/kernel/types/browser_update_params.py
+++ b/src/kernel/types/browser_update_params.py
@@ -7,7 +7,7 @@
from .shared_params.browser_profile import BrowserProfile
from .shared_params.browser_viewport import BrowserViewport
-from .browsers.browser_telemetry_config_param import BrowserTelemetryConfigParam
+from .browsers.browser_telemetry_request_config_param import BrowserTelemetryRequestConfigParam
__all__ = ["BrowserUpdateParams", "Viewport"]
@@ -31,13 +31,14 @@ class BrowserUpdateParams(TypedDict, total=False):
Omit to leave unchanged, set to empty string to remove proxy.
"""
- telemetry: Optional[BrowserTelemetryConfigParam]
+ telemetry: Optional[BrowserTelemetryRequestConfigParam]
"""Telemetry configuration.
Omit, set to null, or set to an empty object ({}) to leave the existing
- configuration unchanged (no-op). To enable capture for all categories using VM
- defaults, set browser to an empty object ({"browser": {}}). To stop capture, set
- every category's enabled to false.
+ configuration unchanged. Set enabled to true to enable capture using VM
+ defaults. Set enabled to false to stop capture. Provide browser category
+ settings for per-category updates. Explicitly disabling all four categories also
+ stops capture.
"""
viewport: Viewport
diff --git a/src/kernel/types/browsers/__init__.py b/src/kernel/types/browsers/__init__.py
index a3bca412..619a59c4 100644
--- a/src/kernel/types/browsers/__init__.py
+++ b/src/kernel/types/browsers/__init__.py
@@ -57,7 +57,6 @@
from .browser_page_tab_opened_event import BrowserPageTabOpenedEvent as BrowserPageTabOpenedEvent
from .f_set_file_permissions_params import FSetFilePermissionsParams as FSetFilePermissionsParams
from .browser_network_response_event import BrowserNetworkResponseEvent as BrowserNetworkResponseEvent
-from .browser_telemetry_config_param import BrowserTelemetryConfigParam as BrowserTelemetryConfigParam
from .process_stdout_stream_response import ProcessStdoutStreamResponse as ProcessStdoutStreamResponse
from .browser_interaction_click_event import BrowserInteractionClickEvent as BrowserInteractionClickEvent
from .browser_page_layout_shift_event import BrowserPageLayoutShiftEvent as BrowserPageLayoutShiftEvent
@@ -83,6 +82,9 @@
from .browser_monitor_reconnect_failed_event import (
BrowserMonitorReconnectFailedEvent as BrowserMonitorReconnectFailedEvent,
)
+from .browser_telemetry_request_config_param import (
+ BrowserTelemetryRequestConfigParam as BrowserTelemetryRequestConfigParam,
+)
from .browser_telemetry_category_config_param import (
BrowserTelemetryCategoryConfigParam as BrowserTelemetryCategoryConfigParam,
)
diff --git a/src/kernel/types/browsers/browser_telemetry_config.py b/src/kernel/types/browsers/browser_telemetry_config.py
index d4365eeb..2fae7a3d 100644
--- a/src/kernel/types/browsers/browser_telemetry_config.py
+++ b/src/kernel/types/browsers/browser_telemetry_config.py
@@ -9,7 +9,7 @@
class BrowserTelemetryConfig(BaseModel):
- """Telemetry configuration for a browser session."""
+ """Active telemetry configuration for a browser session."""
browser: Optional[BrowserTelemetryCategoriesConfig] = None
- """Per-category enable/disable flags. If omitted, all categories are captured."""
+ """Per-category enable/disable flags."""
diff --git a/src/kernel/types/browsers/browser_telemetry_config_param.py b/src/kernel/types/browsers/browser_telemetry_config_param.py
deleted file mode 100644
index 33464340..00000000
--- a/src/kernel/types/browsers/browser_telemetry_config_param.py
+++ /dev/null
@@ -1,16 +0,0 @@
-# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
-
-from __future__ import annotations
-
-from typing_extensions import TypedDict
-
-from .browser_telemetry_categories_config_param import BrowserTelemetryCategoriesConfigParam
-
-__all__ = ["BrowserTelemetryConfigParam"]
-
-
-class BrowserTelemetryConfigParam(TypedDict, total=False):
- """Telemetry configuration for a browser session."""
-
- browser: BrowserTelemetryCategoriesConfigParam
- """Per-category enable/disable flags. If omitted, all categories are captured."""
diff --git a/src/kernel/types/browsers/browser_telemetry_request_config_param.py b/src/kernel/types/browsers/browser_telemetry_request_config_param.py
new file mode 100644
index 00000000..d901a74e
--- /dev/null
+++ b/src/kernel/types/browsers/browser_telemetry_request_config_param.py
@@ -0,0 +1,28 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from __future__ import annotations
+
+from typing_extensions import TypedDict
+
+from .browser_telemetry_categories_config_param import BrowserTelemetryCategoriesConfigParam
+
+__all__ = ["BrowserTelemetryRequestConfigParam"]
+
+
+class BrowserTelemetryRequestConfigParam(TypedDict, total=False):
+ """Telemetry request configuration for a browser session."""
+
+ browser: BrowserTelemetryCategoriesConfigParam
+ """Per-category enable/disable flags.
+
+ If enabled is true and browser is omitted or empty, the VM default category set
+ is used. Explicitly disabling all four categories stops capture on update and
+ starts no capture on create.
+ """
+
+ enabled: bool
+ """Request shortcut for browser telemetry capture.
+
+ True enables capture using VM defaults. False stops capture on update and starts
+ no capture on create. Cannot be combined with browser category settings.
+ """
diff --git a/tests/api_resources/test_browsers.py b/tests/api_resources/test_browsers.py
index 57aa89fe..c845e52b 100644
--- a/tests/api_resources/test_browsers.py
+++ b/tests/api_resources/test_browsers.py
@@ -59,7 +59,8 @@ def test_method_create_with_all_params(self, client: Kernel) -> None:
"interaction": {"enabled": True},
"network": {"enabled": True},
"page": {"enabled": True},
- }
+ },
+ "enabled": True,
},
timeout_seconds=10,
viewport={
@@ -169,7 +170,8 @@ def test_method_update_with_all_params(self, client: Kernel) -> None:
"interaction": {"enabled": True},
"network": {"enabled": True},
"page": {"enabled": True},
- }
+ },
+ "enabled": True,
},
viewport={
"height": 800,
@@ -463,7 +465,8 @@ async def test_method_create_with_all_params(self, async_client: AsyncKernel) ->
"interaction": {"enabled": True},
"network": {"enabled": True},
"page": {"enabled": True},
- }
+ },
+ "enabled": True,
},
timeout_seconds=10,
viewport={
@@ -573,7 +576,8 @@ async def test_method_update_with_all_params(self, async_client: AsyncKernel) ->
"interaction": {"enabled": True},
"network": {"enabled": True},
"page": {"enabled": True},
- }
+ },
+ "enabled": True,
},
viewport={
"height": 800,
From dee2c6365fec37a35da296c90cbaa23e53ee0f25 Mon Sep 17 00:00:00 2001
From: "stainless-app[bot]"
<142633134+stainless-app[bot]@users.noreply.github.com>
Date: Tue, 26 May 2026 17:59:58 +0000
Subject: [PATCH 017/366] release: 0.57.0
---
CHANGELOG.md | 9 +++++++++
1 file changed, 9 insertions(+)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 96493998..4df91e67 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,14 @@
# Changelog
+## 0.57.0 (2026-05-26)
+
+Full Changelog: [v0.57.0...v0.57.0](https://github.com/kernel/kernel-python-sdk/compare/v0.57.0...v0.57.0)
+
+### Features
+
+* api: dual-route /projects under /org/projects, deprecate /projects ([c5f21ec](https://github.com/kernel/kernel-python-sdk/commit/c5f21ecf3e1081c774ba58ad32b10e8cd5b0a94a))
+* Support telemetry enabled request config ([f48cbee](https://github.com/kernel/kernel-python-sdk/commit/f48cbee40aee158615972e6f39b92179e06efbce))
+
## 0.57.0 (2026-05-22)
Full Changelog: [v0.56.0...v0.57.0](https://github.com/kernel/kernel-python-sdk/compare/v0.56.0...v0.57.0)
From d7baba3976667e5741a10b8e9a98f9d246fc1960 Mon Sep 17 00:00:00 2001
From: IlyaasK <86218345+IlyaasK@users.noreply.github.com>
Date: Tue, 26 May 2026 14:12:15 -0400
Subject: [PATCH 018/366] Fix browser routing project path mock
---
tests/test_browser_routing.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/tests/test_browser_routing.py b/tests/test_browser_routing.py
index 810de800..ac841521 100644
--- a/tests/test_browser_routing.py
+++ b/tests/test_browser_routing.py
@@ -180,7 +180,7 @@ async def test_async_raw_browser_create_warms_route_cache() -> None:
@respx.mock
def test_only_browser_metadata_endpoints_warm_route_cache() -> None:
- projects_route = respx.get(f"{base_url}/projects").mock(return_value=httpx.Response(200, json=_fake_browser()))
+ projects_route = respx.get(f"{base_url}/org/projects").mock(return_value=httpx.Response(200, json=_fake_browser()))
with Kernel(base_url=base_url, api_key=api_key, _strict_response_validation=True) as client:
response = client.projects.with_raw_response.list()
with pytest.raises(ValueError, match="route cache"):
From a1054a6b72e79e0fffc88b1b46661683a8a953b8 Mon Sep 17 00:00:00 2001
From: "stainless-app[bot]"
<142633134+stainless-app[bot]@users.noreply.github.com>
Date: Tue, 26 May 2026 18:33:59 +0000
Subject: [PATCH 019/366] feat: Support telemetry enabled request config and
fix SDK metadata
---
.stats.yml | 6 ++--
api.md | 1 -
src/kernel/resources/browsers/browsers.py | 9 +++---
src/kernel/types/browser_create_params.py | 29 +++++++++++++++++--
src/kernel/types/browser_update_params.py | 29 +++++++++++++++++--
src/kernel/types/browsers/__init__.py | 3 --
.../browser_telemetry_request_config_param.py | 28 ------------------
7 files changed, 59 insertions(+), 46 deletions(-)
delete mode 100644 src/kernel/types/browsers/browser_telemetry_request_config_param.py
diff --git a/.stats.yml b/.stats.yml
index 47ab7c16..7fdb7bfb 100644
--- a/.stats.yml
+++ b/.stats.yml
@@ -1,4 +1,4 @@
configured_endpoints: 112
-openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel/kernel-e9c99662d29710f105847d461f8919e06f6aa2e43b0e1a6285d0b137643a7907.yml
-openapi_spec_hash: 4415cb4790c7a5ec892f4e3521217cb4
-config_hash: 27b38657d9a3b33328be930eeb319628
+openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel/kernel-0a186c486b56f555cab374ea5f2adbef2d718b5c9190a48c862f0fdf1232324f.yml
+openapi_spec_hash: fad386b8e8712e6639ed9689e9dfc070
+config_hash: 5dde8b5de321a7bb96f695a69eb21c23
diff --git a/api.md b/api.md
index 106e3cd8..4785c2d9 100644
--- a/api.md
+++ b/api.md
@@ -137,7 +137,6 @@ from kernel.types.browsers import (
BrowserTelemetryCategoryConfig,
BrowserTelemetryConfig,
BrowserTelemetryEvent,
- BrowserTelemetryRequestConfig,
TelemetryStreamResponse,
)
```
diff --git a/src/kernel/resources/browsers/browsers.py b/src/kernel/resources/browsers/browsers.py
index ad8087fb..c020c288 100644
--- a/src/kernel/resources/browsers/browsers.py
+++ b/src/kernel/resources/browsers/browsers.py
@@ -99,7 +99,6 @@
from ...types.shared_params.browser_profile import BrowserProfile
from ...types.shared_params.browser_viewport import BrowserViewport
from ...types.shared_params.browser_extension import BrowserExtension
-from ...types.browsers.browser_telemetry_request_config_param import BrowserTelemetryRequestConfigParam
__all__ = ["BrowsersResource", "AsyncBrowsersResource"]
@@ -173,7 +172,7 @@ def create(
proxy_id: str | Omit = omit,
start_url: str | Omit = omit,
stealth: bool | Omit = omit,
- telemetry: Optional[BrowserTelemetryRequestConfigParam] | Omit = omit,
+ telemetry: Optional[browser_create_params.Telemetry] | Omit = omit,
timeout_seconds: int | Omit = omit,
viewport: BrowserViewport | Omit = omit,
# Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
@@ -327,7 +326,7 @@ def update(
disable_default_proxy: bool | Omit = omit,
profile: BrowserProfile | Omit = omit,
proxy_id: Optional[str] | Omit = omit,
- telemetry: Optional[BrowserTelemetryRequestConfigParam] | Omit = omit,
+ telemetry: Optional[browser_update_params.Telemetry] | Omit = omit,
viewport: browser_update_params.Viewport | 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.
@@ -720,7 +719,7 @@ async def create(
proxy_id: str | Omit = omit,
start_url: str | Omit = omit,
stealth: bool | Omit = omit,
- telemetry: Optional[BrowserTelemetryRequestConfigParam] | Omit = omit,
+ telemetry: Optional[browser_create_params.Telemetry] | Omit = omit,
timeout_seconds: int | Omit = omit,
viewport: BrowserViewport | Omit = omit,
# Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
@@ -874,7 +873,7 @@ async def update(
disable_default_proxy: bool | Omit = omit,
profile: BrowserProfile | Omit = omit,
proxy_id: Optional[str] | Omit = omit,
- telemetry: Optional[BrowserTelemetryRequestConfigParam] | Omit = omit,
+ telemetry: Optional[browser_update_params.Telemetry] | Omit = omit,
viewport: browser_update_params.Viewport | 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.
diff --git a/src/kernel/types/browser_create_params.py b/src/kernel/types/browser_create_params.py
index 82406a5a..c94071cf 100644
--- a/src/kernel/types/browser_create_params.py
+++ b/src/kernel/types/browser_create_params.py
@@ -8,9 +8,9 @@
from .shared_params.browser_profile import BrowserProfile
from .shared_params.browser_viewport import BrowserViewport
from .shared_params.browser_extension import BrowserExtension
-from .browsers.browser_telemetry_request_config_param import BrowserTelemetryRequestConfigParam
+from .browsers.browser_telemetry_categories_config_param import BrowserTelemetryCategoriesConfigParam
-__all__ = ["BrowserCreateParams"]
+__all__ = ["BrowserCreateParams", "Telemetry"]
class BrowserCreateParams(TypedDict, total=False):
@@ -75,7 +75,7 @@ class BrowserCreateParams(TypedDict, total=False):
mechanisms.
"""
- telemetry: Optional[BrowserTelemetryRequestConfigParam]
+ telemetry: Optional[Telemetry]
"""Telemetry configuration for the browser session.
Set enabled to true to start capture using VM defaults, or provide browser
@@ -108,3 +108,26 @@ class BrowserCreateParams(TypedDict, total=False):
based on the resolution (higher resolutions use lower refresh rates to keep
bandwidth reasonable).
"""
+
+
+class Telemetry(TypedDict, total=False):
+ """Telemetry configuration for the browser session.
+
+ Set enabled to true to start capture using VM defaults, or provide browser category settings. If omitted, null, set to an empty object ({}), set to enabled: false without browser category settings, or all four categories are explicitly disabled, capture is not started.
+ """
+
+ browser: BrowserTelemetryCategoriesConfigParam
+ """Per-category enable/disable flags.
+
+ If enabled is true and browser is omitted or empty, the VM default category set
+ is used. Explicitly disabling all four categories stops capture on update and
+ starts no capture on create.
+ """
+
+ enabled: bool
+ """Request shortcut for browser telemetry capture.
+
+ True enables capture using VM defaults unless browser category settings are
+ provided. False stops capture on update and starts no capture on create.
+ enabled=false cannot be combined with browser category settings.
+ """
diff --git a/src/kernel/types/browser_update_params.py b/src/kernel/types/browser_update_params.py
index c883089d..837c1784 100644
--- a/src/kernel/types/browser_update_params.py
+++ b/src/kernel/types/browser_update_params.py
@@ -7,9 +7,9 @@
from .shared_params.browser_profile import BrowserProfile
from .shared_params.browser_viewport import BrowserViewport
-from .browsers.browser_telemetry_request_config_param import BrowserTelemetryRequestConfigParam
+from .browsers.browser_telemetry_categories_config_param import BrowserTelemetryCategoriesConfigParam
-__all__ = ["BrowserUpdateParams", "Viewport"]
+__all__ = ["BrowserUpdateParams", "Telemetry", "Viewport"]
class BrowserUpdateParams(TypedDict, total=False):
@@ -31,7 +31,7 @@ class BrowserUpdateParams(TypedDict, total=False):
Omit to leave unchanged, set to empty string to remove proxy.
"""
- telemetry: Optional[BrowserTelemetryRequestConfigParam]
+ telemetry: Optional[Telemetry]
"""Telemetry configuration.
Omit, set to null, or set to an empty object ({}) to leave the existing
@@ -45,6 +45,29 @@ class BrowserUpdateParams(TypedDict, total=False):
"""Viewport configuration to apply to the browser session."""
+class Telemetry(TypedDict, total=False):
+ """Telemetry configuration.
+
+ Omit, set to null, or set to an empty object ({}) to leave the existing configuration unchanged. Set enabled to true to enable capture using VM defaults. Set enabled to false to stop capture. Provide browser category settings for per-category updates. Explicitly disabling all four categories also stops capture.
+ """
+
+ browser: BrowserTelemetryCategoriesConfigParam
+ """Per-category enable/disable flags.
+
+ If enabled is true and browser is omitted or empty, the VM default category set
+ is used. Explicitly disabling all four categories stops capture on update and
+ starts no capture on create.
+ """
+
+ enabled: bool
+ """Request shortcut for browser telemetry capture.
+
+ True enables capture using VM defaults unless browser category settings are
+ provided. False stops capture on update and starts no capture on create.
+ enabled=false cannot be combined with browser category settings.
+ """
+
+
class Viewport(BrowserViewport, total=False):
"""Viewport configuration to apply to the browser session."""
diff --git a/src/kernel/types/browsers/__init__.py b/src/kernel/types/browsers/__init__.py
index 619a59c4..34f4fd64 100644
--- a/src/kernel/types/browsers/__init__.py
+++ b/src/kernel/types/browsers/__init__.py
@@ -82,9 +82,6 @@
from .browser_monitor_reconnect_failed_event import (
BrowserMonitorReconnectFailedEvent as BrowserMonitorReconnectFailedEvent,
)
-from .browser_telemetry_request_config_param import (
- BrowserTelemetryRequestConfigParam as BrowserTelemetryRequestConfigParam,
-)
from .browser_telemetry_category_config_param import (
BrowserTelemetryCategoryConfigParam as BrowserTelemetryCategoryConfigParam,
)
diff --git a/src/kernel/types/browsers/browser_telemetry_request_config_param.py b/src/kernel/types/browsers/browser_telemetry_request_config_param.py
deleted file mode 100644
index d901a74e..00000000
--- a/src/kernel/types/browsers/browser_telemetry_request_config_param.py
+++ /dev/null
@@ -1,28 +0,0 @@
-# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
-
-from __future__ import annotations
-
-from typing_extensions import TypedDict
-
-from .browser_telemetry_categories_config_param import BrowserTelemetryCategoriesConfigParam
-
-__all__ = ["BrowserTelemetryRequestConfigParam"]
-
-
-class BrowserTelemetryRequestConfigParam(TypedDict, total=False):
- """Telemetry request configuration for a browser session."""
-
- browser: BrowserTelemetryCategoriesConfigParam
- """Per-category enable/disable flags.
-
- If enabled is true and browser is omitted or empty, the VM default category set
- is used. Explicitly disabling all four categories stops capture on update and
- starts no capture on create.
- """
-
- enabled: bool
- """Request shortcut for browser telemetry capture.
-
- True enables capture using VM defaults. False stops capture on update and starts
- no capture on create. Cannot be combined with browser category settings.
- """
From 41b3aa243c39af3614154af0059b0299c4e41664 Mon Sep 17 00:00:00 2001
From: "stainless-app[bot]"
<142633134+stainless-app[bot]@users.noreply.github.com>
Date: Wed, 27 May 2026 15:07:50 +0000
Subject: [PATCH 020/366] feat: [codex] Expose API keys in SDK config
---
.stats.yml | 6 +-
api.md | 16 +
src/kernel/_client.py | 44 ++
src/kernel/resources/__init__.py | 14 +
src/kernel/resources/api_keys.py | 557 ++++++++++++++++++++++
src/kernel/types/__init__.py | 5 +
src/kernel/types/api_key.py | 47 ++
src/kernel/types/api_key_create_params.py | 19 +
src/kernel/types/api_key_list_params.py | 15 +
src/kernel/types/api_key_update_params.py | 12 +
src/kernel/types/created_api_key.py | 12 +
tests/api_resources/test_api_keys.py | 447 +++++++++++++++++
12 files changed, 1191 insertions(+), 3 deletions(-)
create mode 100644 src/kernel/resources/api_keys.py
create mode 100644 src/kernel/types/api_key.py
create mode 100644 src/kernel/types/api_key_create_params.py
create mode 100644 src/kernel/types/api_key_list_params.py
create mode 100644 src/kernel/types/api_key_update_params.py
create mode 100644 src/kernel/types/created_api_key.py
create mode 100644 tests/api_resources/test_api_keys.py
diff --git a/.stats.yml b/.stats.yml
index 7fdb7bfb..e86e3342 100644
--- a/.stats.yml
+++ b/.stats.yml
@@ -1,4 +1,4 @@
-configured_endpoints: 112
-openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel/kernel-0a186c486b56f555cab374ea5f2adbef2d718b5c9190a48c862f0fdf1232324f.yml
+configured_endpoints: 117
+openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel/kernel-54d9016df9730e65881df9f694df4f33620b0c032f177849d36ccc426cd42fa8.yml
openapi_spec_hash: fad386b8e8712e6639ed9689e9dfc070
-config_hash: 5dde8b5de321a7bb96f695a69eb21c23
+config_hash: 58396d6c1fafaf72b26596b9117edf48
diff --git a/api.md b/api.md
index 4785c2d9..1dc6fece 100644
--- a/api.md
+++ b/api.md
@@ -414,6 +414,22 @@ Methods:
- client.projects.limits.retrieve(id) -> ProjectLimits
- client.projects.limits.update(id, \*\*params) -> ProjectLimits
+# APIKeys
+
+Types:
+
+```python
+from kernel.types import APIKey, CreateAPIKeyRequest, CreatedAPIKey, UpdateAPIKeyRequest
+```
+
+Methods:
+
+- client.api_keys.create(\*\*params) -> CreatedAPIKey
+- client.api_keys.retrieve(id) -> APIKey
+- client.api_keys.update(id, \*\*params) -> APIKey
+- client.api_keys.list(\*\*params) -> SyncOffsetPagination[APIKey]
+- client.api_keys.delete(id) -> None
+
# CredentialProviders
Types:
diff --git a/src/kernel/_client.py b/src/kernel/_client.py
index 6c3e3aae..537d1598 100644
--- a/src/kernel/_client.py
+++ b/src/kernel/_client.py
@@ -50,6 +50,7 @@
apps,
auth,
proxies,
+ api_keys,
browsers,
profiles,
projects,
@@ -62,6 +63,7 @@
)
from .resources.apps import AppsResource, AsyncAppsResource
from .resources.proxies import ProxiesResource, AsyncProxiesResource
+ from .resources.api_keys import APIKeysResource, AsyncAPIKeysResource
from .resources.profiles import ProfilesResource, AsyncProfilesResource
from .resources.auth.auth import AuthResource, AsyncAuthResource
from .resources.extensions import ExtensionsResource, AsyncExtensionsResource
@@ -260,6 +262,13 @@ def projects(self) -> ProjectsResource:
return ProjectsResource(self)
+ @cached_property
+ def api_keys(self) -> APIKeysResource:
+ """Create and manage API keys for organization and project-scoped access."""
+ from .resources.api_keys import APIKeysResource
+
+ return APIKeysResource(self)
+
@cached_property
def credential_providers(self) -> CredentialProvidersResource:
"""Configure external credential providers like 1Password."""
@@ -584,6 +593,13 @@ def projects(self) -> AsyncProjectsResource:
return AsyncProjectsResource(self)
+ @cached_property
+ def api_keys(self) -> AsyncAPIKeysResource:
+ """Create and manage API keys for organization and project-scoped access."""
+ from .resources.api_keys import AsyncAPIKeysResource
+
+ return AsyncAPIKeysResource(self)
+
@cached_property
def credential_providers(self) -> AsyncCredentialProvidersResource:
"""Configure external credential providers like 1Password."""
@@ -821,6 +837,13 @@ def projects(self) -> projects.ProjectsResourceWithRawResponse:
return ProjectsResourceWithRawResponse(self._client.projects)
+ @cached_property
+ def api_keys(self) -> api_keys.APIKeysResourceWithRawResponse:
+ """Create and manage API keys for organization and project-scoped access."""
+ from .resources.api_keys import APIKeysResourceWithRawResponse
+
+ return APIKeysResourceWithRawResponse(self._client.api_keys)
+
@cached_property
def credential_providers(self) -> credential_providers.CredentialProvidersResourceWithRawResponse:
"""Configure external credential providers like 1Password."""
@@ -911,6 +934,13 @@ def projects(self) -> projects.AsyncProjectsResourceWithRawResponse:
return AsyncProjectsResourceWithRawResponse(self._client.projects)
+ @cached_property
+ def api_keys(self) -> api_keys.AsyncAPIKeysResourceWithRawResponse:
+ """Create and manage API keys for organization and project-scoped access."""
+ from .resources.api_keys import AsyncAPIKeysResourceWithRawResponse
+
+ return AsyncAPIKeysResourceWithRawResponse(self._client.api_keys)
+
@cached_property
def credential_providers(self) -> credential_providers.AsyncCredentialProvidersResourceWithRawResponse:
"""Configure external credential providers like 1Password."""
@@ -1001,6 +1031,13 @@ def projects(self) -> projects.ProjectsResourceWithStreamingResponse:
return ProjectsResourceWithStreamingResponse(self._client.projects)
+ @cached_property
+ def api_keys(self) -> api_keys.APIKeysResourceWithStreamingResponse:
+ """Create and manage API keys for organization and project-scoped access."""
+ from .resources.api_keys import APIKeysResourceWithStreamingResponse
+
+ return APIKeysResourceWithStreamingResponse(self._client.api_keys)
+
@cached_property
def credential_providers(self) -> credential_providers.CredentialProvidersResourceWithStreamingResponse:
"""Configure external credential providers like 1Password."""
@@ -1091,6 +1128,13 @@ def projects(self) -> projects.AsyncProjectsResourceWithStreamingResponse:
return AsyncProjectsResourceWithStreamingResponse(self._client.projects)
+ @cached_property
+ def api_keys(self) -> api_keys.AsyncAPIKeysResourceWithStreamingResponse:
+ """Create and manage API keys for organization and project-scoped access."""
+ from .resources.api_keys import AsyncAPIKeysResourceWithStreamingResponse
+
+ return AsyncAPIKeysResourceWithStreamingResponse(self._client.api_keys)
+
@cached_property
def credential_providers(self) -> credential_providers.AsyncCredentialProvidersResourceWithStreamingResponse:
"""Configure external credential providers like 1Password."""
diff --git a/src/kernel/resources/__init__.py b/src/kernel/resources/__init__.py
index f078a03b..64b98338 100644
--- a/src/kernel/resources/__init__.py
+++ b/src/kernel/resources/__init__.py
@@ -24,6 +24,14 @@
ProxiesResourceWithStreamingResponse,
AsyncProxiesResourceWithStreamingResponse,
)
+from .api_keys import (
+ APIKeysResource,
+ AsyncAPIKeysResource,
+ APIKeysResourceWithRawResponse,
+ AsyncAPIKeysResourceWithRawResponse,
+ APIKeysResourceWithStreamingResponse,
+ AsyncAPIKeysResourceWithStreamingResponse,
+)
from .browsers import (
BrowsersResource,
AsyncBrowsersResource,
@@ -164,6 +172,12 @@
"AsyncProjectsResourceWithRawResponse",
"ProjectsResourceWithStreamingResponse",
"AsyncProjectsResourceWithStreamingResponse",
+ "APIKeysResource",
+ "AsyncAPIKeysResource",
+ "APIKeysResourceWithRawResponse",
+ "AsyncAPIKeysResourceWithRawResponse",
+ "APIKeysResourceWithStreamingResponse",
+ "AsyncAPIKeysResourceWithStreamingResponse",
"CredentialProvidersResource",
"AsyncCredentialProvidersResource",
"CredentialProvidersResourceWithRawResponse",
diff --git a/src/kernel/resources/api_keys.py b/src/kernel/resources/api_keys.py
new file mode 100644
index 00000000..aae96794
--- /dev/null
+++ b/src/kernel/resources/api_keys.py
@@ -0,0 +1,557 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from __future__ import annotations
+
+from typing import Optional
+
+import httpx
+
+from ..types import api_key_list_params, api_key_create_params, api_key_update_params
+from .._types import Body, Omit, Query, Headers, NoneType, NotGiven, omit, not_given
+from .._utils import path_template, maybe_transform, async_maybe_transform
+from .._compat import cached_property
+from .._resource import SyncAPIResource, AsyncAPIResource
+from .._response import (
+ to_raw_response_wrapper,
+ to_streamed_response_wrapper,
+ async_to_raw_response_wrapper,
+ async_to_streamed_response_wrapper,
+)
+from ..pagination import SyncOffsetPagination, AsyncOffsetPagination
+from .._base_client import AsyncPaginator, make_request_options
+from ..types.api_key import APIKey
+from ..types.created_api_key import CreatedAPIKey
+
+__all__ = ["APIKeysResource", "AsyncAPIKeysResource"]
+
+
+class APIKeysResource(SyncAPIResource):
+ """Create and manage API keys for organization and project-scoped access."""
+
+ @cached_property
+ def with_raw_response(self) -> APIKeysResourceWithRawResponse:
+ """
+ This property can be used as a prefix for any HTTP method call to return
+ the raw response object instead of the parsed content.
+
+ For more information, see https://www.github.com/kernel/kernel-python-sdk#accessing-raw-response-data-eg-headers
+ """
+ return APIKeysResourceWithRawResponse(self)
+
+ @cached_property
+ def with_streaming_response(self) -> APIKeysResourceWithStreamingResponse:
+ """
+ An alternative to `.with_raw_response` that doesn't eagerly read the response body.
+
+ For more information, see https://www.github.com/kernel/kernel-python-sdk#with_streaming_response
+ """
+ return APIKeysResourceWithStreamingResponse(self)
+
+ def create(
+ self,
+ *,
+ name: str,
+ days_to_expire: Optional[int] | Omit = omit,
+ project_id: Optional[str] | Omit = omit,
+ # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
+ # The extra values given here take precedence over values defined on the client or passed to this method.
+ extra_headers: Headers | None = None,
+ extra_query: Query | None = None,
+ extra_body: Body | None = None,
+ timeout: float | httpx.Timeout | None | NotGiven = not_given,
+ ) -> CreatedAPIKey:
+ """
+ Create a new API key within the authenticated organization.
+
+ Args:
+ name: API key name (1-255 characters)
+
+ days_to_expire: Number of days until expiry, up to 3650. Use null for never.
+
+ project_id: Unique project identifier
+
+ 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
+ """
+ return self._post(
+ "/org/api_keys",
+ body=maybe_transform(
+ {
+ "name": name,
+ "days_to_expire": days_to_expire,
+ "project_id": project_id,
+ },
+ api_key_create_params.APIKeyCreateParams,
+ ),
+ options=make_request_options(
+ extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
+ ),
+ cast_to=CreatedAPIKey,
+ )
+
+ def retrieve(
+ self,
+ id: str,
+ *,
+ # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
+ # The extra values given here take precedence over values defined on the client or passed to this method.
+ extra_headers: Headers | None = None,
+ extra_query: Query | None = None,
+ extra_body: Body | None = None,
+ timeout: float | httpx.Timeout | None | NotGiven = not_given,
+ ) -> APIKey:
+ """Retrieve an API key by ID for the authenticated organization.
+
+ API keys are
+ masked.
+
+ Args:
+ extra_headers: Send extra headers
+
+ extra_query: Add additional query parameters to the request
+
+ extra_body: Add additional JSON properties to the request
+
+ timeout: Override the client-level default timeout for this request, in seconds
+ """
+ if not id:
+ raise ValueError(f"Expected a non-empty value for `id` but received {id!r}")
+ return self._get(
+ path_template("/org/api_keys/{id}", id=id),
+ options=make_request_options(
+ extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
+ ),
+ cast_to=APIKey,
+ )
+
+ def update(
+ self,
+ id: str,
+ *,
+ name: str,
+ # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
+ # The extra values given here take precedence over values defined on the client or passed to this method.
+ extra_headers: Headers | None = None,
+ extra_query: Query | None = None,
+ extra_body: Body | None = None,
+ timeout: float | httpx.Timeout | None | NotGiven = not_given,
+ ) -> APIKey:
+ """
+ Update an API key's name.
+
+ Args:
+ name: New API key name
+
+ 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("/org/api_keys/{id}", id=id),
+ body=maybe_transform({"name": name}, api_key_update_params.APIKeyUpdateParams),
+ options=make_request_options(
+ extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
+ ),
+ cast_to=APIKey,
+ )
+
+ def list(
+ self,
+ *,
+ limit: int | Omit = omit,
+ offset: int | Omit = omit,
+ # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
+ # The extra values given here take precedence over values defined on the client or passed to this method.
+ extra_headers: Headers | None = None,
+ extra_query: Query | None = None,
+ extra_body: Body | None = None,
+ timeout: float | httpx.Timeout | None | NotGiven = not_given,
+ ) -> SyncOffsetPagination[APIKey]:
+ """List API keys for the authenticated organization.
+
+ API keys are masked.
+
+ Args:
+ limit: Maximum number of results to return
+
+ offset: Number of results to skip
+
+ 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
+ """
+ return self._get_api_list(
+ "/org/api_keys",
+ page=SyncOffsetPagination[APIKey],
+ options=make_request_options(
+ extra_headers=extra_headers,
+ extra_query=extra_query,
+ extra_body=extra_body,
+ timeout=timeout,
+ query=maybe_transform(
+ {
+ "limit": limit,
+ "offset": offset,
+ },
+ api_key_list_params.APIKeyListParams,
+ ),
+ ),
+ model=APIKey,
+ )
+
+ def delete(
+ self,
+ id: str,
+ *,
+ # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
+ # The extra values given here take precedence over values defined on the client or passed to this method.
+ extra_headers: Headers | None = None,
+ extra_query: Query | None = None,
+ extra_body: Body | None = None,
+ timeout: float | httpx.Timeout | None | NotGiven = not_given,
+ ) -> None:
+ """
+ Delete an API key.
+
+ Args:
+ extra_headers: Send extra headers
+
+ extra_query: Add additional query parameters to the request
+
+ extra_body: Add additional JSON properties to the request
+
+ timeout: Override the client-level default timeout for this request, in seconds
+ """
+ if not id:
+ raise ValueError(f"Expected a non-empty value for `id` but received {id!r}")
+ extra_headers = {"Accept": "*/*", **(extra_headers or {})}
+ return self._delete(
+ path_template("/org/api_keys/{id}", id=id),
+ options=make_request_options(
+ extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
+ ),
+ cast_to=NoneType,
+ )
+
+
+class AsyncAPIKeysResource(AsyncAPIResource):
+ """Create and manage API keys for organization and project-scoped access."""
+
+ @cached_property
+ def with_raw_response(self) -> AsyncAPIKeysResourceWithRawResponse:
+ """
+ This property can be used as a prefix for any HTTP method call to return
+ the raw response object instead of the parsed content.
+
+ For more information, see https://www.github.com/kernel/kernel-python-sdk#accessing-raw-response-data-eg-headers
+ """
+ return AsyncAPIKeysResourceWithRawResponse(self)
+
+ @cached_property
+ def with_streaming_response(self) -> AsyncAPIKeysResourceWithStreamingResponse:
+ """
+ An alternative to `.with_raw_response` that doesn't eagerly read the response body.
+
+ For more information, see https://www.github.com/kernel/kernel-python-sdk#with_streaming_response
+ """
+ return AsyncAPIKeysResourceWithStreamingResponse(self)
+
+ async def create(
+ self,
+ *,
+ name: str,
+ days_to_expire: Optional[int] | Omit = omit,
+ project_id: Optional[str] | Omit = omit,
+ # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
+ # The extra values given here take precedence over values defined on the client or passed to this method.
+ extra_headers: Headers | None = None,
+ extra_query: Query | None = None,
+ extra_body: Body | None = None,
+ timeout: float | httpx.Timeout | None | NotGiven = not_given,
+ ) -> CreatedAPIKey:
+ """
+ Create a new API key within the authenticated organization.
+
+ Args:
+ name: API key name (1-255 characters)
+
+ days_to_expire: Number of days until expiry, up to 3650. Use null for never.
+
+ project_id: Unique project identifier
+
+ 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
+ """
+ return await self._post(
+ "/org/api_keys",
+ body=await async_maybe_transform(
+ {
+ "name": name,
+ "days_to_expire": days_to_expire,
+ "project_id": project_id,
+ },
+ api_key_create_params.APIKeyCreateParams,
+ ),
+ options=make_request_options(
+ extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
+ ),
+ cast_to=CreatedAPIKey,
+ )
+
+ async def retrieve(
+ self,
+ id: str,
+ *,
+ # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
+ # The extra values given here take precedence over values defined on the client or passed to this method.
+ extra_headers: Headers | None = None,
+ extra_query: Query | None = None,
+ extra_body: Body | None = None,
+ timeout: float | httpx.Timeout | None | NotGiven = not_given,
+ ) -> APIKey:
+ """Retrieve an API key by ID for the authenticated organization.
+
+ API keys are
+ masked.
+
+ Args:
+ extra_headers: Send extra headers
+
+ extra_query: Add additional query parameters to the request
+
+ extra_body: Add additional JSON properties to the request
+
+ timeout: Override the client-level default timeout for this request, in seconds
+ """
+ if not id:
+ raise ValueError(f"Expected a non-empty value for `id` but received {id!r}")
+ return await self._get(
+ path_template("/org/api_keys/{id}", id=id),
+ options=make_request_options(
+ extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
+ ),
+ cast_to=APIKey,
+ )
+
+ async def update(
+ self,
+ id: str,
+ *,
+ name: str,
+ # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
+ # The extra values given here take precedence over values defined on the client or passed to this method.
+ extra_headers: Headers | None = None,
+ extra_query: Query | None = None,
+ extra_body: Body | None = None,
+ timeout: float | httpx.Timeout | None | NotGiven = not_given,
+ ) -> APIKey:
+ """
+ Update an API key's name.
+
+ Args:
+ name: New API key name
+
+ 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("/org/api_keys/{id}", id=id),
+ body=await async_maybe_transform({"name": name}, api_key_update_params.APIKeyUpdateParams),
+ options=make_request_options(
+ extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
+ ),
+ cast_to=APIKey,
+ )
+
+ def list(
+ self,
+ *,
+ limit: int | Omit = omit,
+ offset: int | Omit = omit,
+ # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
+ # The extra values given here take precedence over values defined on the client or passed to this method.
+ extra_headers: Headers | None = None,
+ extra_query: Query | None = None,
+ extra_body: Body | None = None,
+ timeout: float | httpx.Timeout | None | NotGiven = not_given,
+ ) -> AsyncPaginator[APIKey, AsyncOffsetPagination[APIKey]]:
+ """List API keys for the authenticated organization.
+
+ API keys are masked.
+
+ Args:
+ limit: Maximum number of results to return
+
+ offset: Number of results to skip
+
+ 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
+ """
+ return self._get_api_list(
+ "/org/api_keys",
+ page=AsyncOffsetPagination[APIKey],
+ options=make_request_options(
+ extra_headers=extra_headers,
+ extra_query=extra_query,
+ extra_body=extra_body,
+ timeout=timeout,
+ query=maybe_transform(
+ {
+ "limit": limit,
+ "offset": offset,
+ },
+ api_key_list_params.APIKeyListParams,
+ ),
+ ),
+ model=APIKey,
+ )
+
+ async def delete(
+ self,
+ id: str,
+ *,
+ # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
+ # The extra values given here take precedence over values defined on the client or passed to this method.
+ extra_headers: Headers | None = None,
+ extra_query: Query | None = None,
+ extra_body: Body | None = None,
+ timeout: float | httpx.Timeout | None | NotGiven = not_given,
+ ) -> None:
+ """
+ Delete an API key.
+
+ Args:
+ extra_headers: Send extra headers
+
+ extra_query: Add additional query parameters to the request
+
+ extra_body: Add additional JSON properties to the request
+
+ timeout: Override the client-level default timeout for this request, in seconds
+ """
+ if not id:
+ raise ValueError(f"Expected a non-empty value for `id` but received {id!r}")
+ extra_headers = {"Accept": "*/*", **(extra_headers or {})}
+ return await self._delete(
+ path_template("/org/api_keys/{id}", id=id),
+ options=make_request_options(
+ extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
+ ),
+ cast_to=NoneType,
+ )
+
+
+class APIKeysResourceWithRawResponse:
+ def __init__(self, api_keys: APIKeysResource) -> None:
+ self._api_keys = api_keys
+
+ self.create = to_raw_response_wrapper(
+ api_keys.create,
+ )
+ self.retrieve = to_raw_response_wrapper(
+ api_keys.retrieve,
+ )
+ self.update = to_raw_response_wrapper(
+ api_keys.update,
+ )
+ self.list = to_raw_response_wrapper(
+ api_keys.list,
+ )
+ self.delete = to_raw_response_wrapper(
+ api_keys.delete,
+ )
+
+
+class AsyncAPIKeysResourceWithRawResponse:
+ def __init__(self, api_keys: AsyncAPIKeysResource) -> None:
+ self._api_keys = api_keys
+
+ self.create = async_to_raw_response_wrapper(
+ api_keys.create,
+ )
+ self.retrieve = async_to_raw_response_wrapper(
+ api_keys.retrieve,
+ )
+ self.update = async_to_raw_response_wrapper(
+ api_keys.update,
+ )
+ self.list = async_to_raw_response_wrapper(
+ api_keys.list,
+ )
+ self.delete = async_to_raw_response_wrapper(
+ api_keys.delete,
+ )
+
+
+class APIKeysResourceWithStreamingResponse:
+ def __init__(self, api_keys: APIKeysResource) -> None:
+ self._api_keys = api_keys
+
+ self.create = to_streamed_response_wrapper(
+ api_keys.create,
+ )
+ self.retrieve = to_streamed_response_wrapper(
+ api_keys.retrieve,
+ )
+ self.update = to_streamed_response_wrapper(
+ api_keys.update,
+ )
+ self.list = to_streamed_response_wrapper(
+ api_keys.list,
+ )
+ self.delete = to_streamed_response_wrapper(
+ api_keys.delete,
+ )
+
+
+class AsyncAPIKeysResourceWithStreamingResponse:
+ def __init__(self, api_keys: AsyncAPIKeysResource) -> None:
+ self._api_keys = api_keys
+
+ self.create = async_to_streamed_response_wrapper(
+ api_keys.create,
+ )
+ self.retrieve = async_to_streamed_response_wrapper(
+ api_keys.retrieve,
+ )
+ self.update = async_to_streamed_response_wrapper(
+ api_keys.update,
+ )
+ self.list = async_to_streamed_response_wrapper(
+ api_keys.list,
+ )
+ self.delete = async_to_streamed_response_wrapper(
+ api_keys.delete,
+ )
diff --git a/src/kernel/types/__init__.py b/src/kernel/types/__init__.py
index 49346e2f..fb6fd589 100644
--- a/src/kernel/types/__init__.py
+++ b/src/kernel/types/__init__.py
@@ -15,15 +15,18 @@
BrowserViewport as BrowserViewport,
BrowserExtension as BrowserExtension,
)
+from .api_key import APIKey as APIKey
from .profile import Profile as Profile
from .project import Project as Project
from .credential import Credential as Credential
from .browser_pool import BrowserPool as BrowserPool
from .browser_usage import BrowserUsage as BrowserUsage
from .app_list_params import AppListParams as AppListParams
+from .created_api_key import CreatedAPIKey as CreatedAPIKey
from .browser_pool_ref import BrowserPoolRef as BrowserPoolRef
from .app_list_response import AppListResponse as AppListResponse
from .proxy_check_params import ProxyCheckParams as ProxyCheckParams
+from .api_key_list_params import APIKeyListParams as APIKeyListParams
from .browser_curl_params import BrowserCurlParams as BrowserCurlParams
from .browser_list_params import BrowserListParams as BrowserListParams
from .credential_provider import CredentialProvider as CredentialProvider
@@ -32,6 +35,8 @@
from .proxy_create_params import ProxyCreateParams as ProxyCreateParams
from .proxy_list_response import ProxyListResponse as ProxyListResponse
from .proxy_check_response import ProxyCheckResponse as ProxyCheckResponse
+from .api_key_create_params import APIKeyCreateParams as APIKeyCreateParams
+from .api_key_update_params import APIKeyUpdateParams as APIKeyUpdateParams
from .browser_create_params import BrowserCreateParams as BrowserCreateParams
from .browser_curl_response import BrowserCurlResponse as BrowserCurlResponse
from .browser_list_response import BrowserListResponse as BrowserListResponse
diff --git a/src/kernel/types/api_key.py b/src/kernel/types/api_key.py
new file mode 100644
index 00000000..6df577f8
--- /dev/null
+++ b/src/kernel/types/api_key.py
@@ -0,0 +1,47 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from typing import Optional
+from datetime import datetime
+
+from .._models import BaseModel
+
+__all__ = ["APIKey", "CreatedBy"]
+
+
+class CreatedBy(BaseModel):
+ id: str
+ """Kernel user ID of the creator."""
+
+ email: str
+ """Email address of the creator."""
+
+ name: Optional[str] = None
+ """Display name of the creator, if available."""
+
+
+class APIKey(BaseModel):
+ id: str
+ """Unique API key identifier"""
+
+ created_at: datetime
+ """When the API key was created"""
+
+ created_by: CreatedBy
+
+ expires_at: Optional[datetime] = None
+ """When the API key expires"""
+
+ masked_key: str
+ """Masked version of the API key"""
+
+ name: str
+ """API key name"""
+
+ project_id: Optional[str] = None
+ """Project identifier for project-scoped API keys. Null means org-wide."""
+
+ project_name: Optional[str] = None
+ """Project name for project-scoped API keys.
+
+ Null means the key is org-wide or the project name is unavailable.
+ """
diff --git a/src/kernel/types/api_key_create_params.py b/src/kernel/types/api_key_create_params.py
new file mode 100644
index 00000000..7705108f
--- /dev/null
+++ b/src/kernel/types/api_key_create_params.py
@@ -0,0 +1,19 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from __future__ import annotations
+
+from typing import Optional
+from typing_extensions import Required, TypedDict
+
+__all__ = ["APIKeyCreateParams"]
+
+
+class APIKeyCreateParams(TypedDict, total=False):
+ name: Required[str]
+ """API key name (1-255 characters)"""
+
+ days_to_expire: Optional[int]
+ """Number of days until expiry, up to 3650. Use null for never."""
+
+ project_id: Optional[str]
+ """Unique project identifier"""
diff --git a/src/kernel/types/api_key_list_params.py b/src/kernel/types/api_key_list_params.py
new file mode 100644
index 00000000..e7d807f2
--- /dev/null
+++ b/src/kernel/types/api_key_list_params.py
@@ -0,0 +1,15 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from __future__ import annotations
+
+from typing_extensions import TypedDict
+
+__all__ = ["APIKeyListParams"]
+
+
+class APIKeyListParams(TypedDict, total=False):
+ limit: int
+ """Maximum number of results to return"""
+
+ offset: int
+ """Number of results to skip"""
diff --git a/src/kernel/types/api_key_update_params.py b/src/kernel/types/api_key_update_params.py
new file mode 100644
index 00000000..2e917607
--- /dev/null
+++ b/src/kernel/types/api_key_update_params.py
@@ -0,0 +1,12 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from __future__ import annotations
+
+from typing_extensions import Required, TypedDict
+
+__all__ = ["APIKeyUpdateParams"]
+
+
+class APIKeyUpdateParams(TypedDict, total=False):
+ name: Required[str]
+ """New API key name"""
diff --git a/src/kernel/types/created_api_key.py b/src/kernel/types/created_api_key.py
new file mode 100644
index 00000000..674943d5
--- /dev/null
+++ b/src/kernel/types/created_api_key.py
@@ -0,0 +1,12 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from .api_key import APIKey
+
+__all__ = ["CreatedAPIKey"]
+
+
+class CreatedAPIKey(APIKey):
+ """API key returned immediately after creation. Includes the plaintext key once."""
+
+ key: str
+ """Plaintext API key. Only returned once when the key is created."""
diff --git a/tests/api_resources/test_api_keys.py b/tests/api_resources/test_api_keys.py
new file mode 100644
index 00000000..da854b23
--- /dev/null
+++ b/tests/api_resources/test_api_keys.py
@@ -0,0 +1,447 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from __future__ import annotations
+
+import os
+from typing import Any, cast
+
+import pytest
+
+from kernel import Kernel, AsyncKernel
+from tests.utils import assert_matches_type
+from kernel.types import APIKey, CreatedAPIKey
+from kernel.pagination import SyncOffsetPagination, AsyncOffsetPagination
+
+base_url = os.environ.get("TEST_API_BASE_URL", "http://127.0.0.1:4010")
+
+
+class TestAPIKeys:
+ parametrize = pytest.mark.parametrize("client", [False, True], indirect=True, ids=["loose", "strict"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ def test_method_create(self, client: Kernel) -> None:
+ api_key = client.api_keys.create(
+ name="staging",
+ )
+ assert_matches_type(CreatedAPIKey, api_key, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ def test_method_create_with_all_params(self, client: Kernel) -> None:
+ api_key = client.api_keys.create(
+ name="staging",
+ days_to_expire=30,
+ project_id="proj_abc123",
+ )
+ assert_matches_type(CreatedAPIKey, api_key, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ def test_raw_response_create(self, client: Kernel) -> None:
+ response = client.api_keys.with_raw_response.create(
+ name="staging",
+ )
+
+ assert response.is_closed is True
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+ api_key = response.parse()
+ assert_matches_type(CreatedAPIKey, api_key, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ def test_streaming_response_create(self, client: Kernel) -> None:
+ with client.api_keys.with_streaming_response.create(
+ name="staging",
+ ) as response:
+ assert not response.is_closed
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+
+ api_key = response.parse()
+ assert_matches_type(CreatedAPIKey, api_key, path=["response"])
+
+ assert cast(Any, response.is_closed) is True
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ def test_method_retrieve(self, client: Kernel) -> None:
+ api_key = client.api_keys.retrieve(
+ "id",
+ )
+ assert_matches_type(APIKey, api_key, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ def test_raw_response_retrieve(self, client: Kernel) -> None:
+ response = client.api_keys.with_raw_response.retrieve(
+ "id",
+ )
+
+ assert response.is_closed is True
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+ api_key = response.parse()
+ assert_matches_type(APIKey, api_key, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ def test_streaming_response_retrieve(self, client: Kernel) -> None:
+ with client.api_keys.with_streaming_response.retrieve(
+ "id",
+ ) as response:
+ assert not response.is_closed
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+
+ api_key = response.parse()
+ assert_matches_type(APIKey, api_key, path=["response"])
+
+ assert cast(Any, response.is_closed) is True
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ def test_path_params_retrieve(self, client: Kernel) -> None:
+ with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"):
+ client.api_keys.with_raw_response.retrieve(
+ "",
+ )
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ def test_method_update(self, client: Kernel) -> None:
+ api_key = client.api_keys.update(
+ id="id",
+ name="new-api-name",
+ )
+ assert_matches_type(APIKey, api_key, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ def test_raw_response_update(self, client: Kernel) -> None:
+ response = client.api_keys.with_raw_response.update(
+ id="id",
+ name="new-api-name",
+ )
+
+ assert response.is_closed is True
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+ api_key = response.parse()
+ assert_matches_type(APIKey, api_key, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ def test_streaming_response_update(self, client: Kernel) -> None:
+ with client.api_keys.with_streaming_response.update(
+ id="id",
+ name="new-api-name",
+ ) as response:
+ assert not response.is_closed
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+
+ api_key = response.parse()
+ assert_matches_type(APIKey, api_key, 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.api_keys.with_raw_response.update(
+ id="",
+ name="new-api-name",
+ )
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ def test_method_list(self, client: Kernel) -> None:
+ api_key = client.api_keys.list()
+ assert_matches_type(SyncOffsetPagination[APIKey], api_key, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ def test_method_list_with_all_params(self, client: Kernel) -> None:
+ api_key = client.api_keys.list(
+ limit=100,
+ offset=0,
+ )
+ assert_matches_type(SyncOffsetPagination[APIKey], api_key, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ def test_raw_response_list(self, client: Kernel) -> None:
+ response = client.api_keys.with_raw_response.list()
+
+ assert response.is_closed is True
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+ api_key = response.parse()
+ assert_matches_type(SyncOffsetPagination[APIKey], api_key, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ def test_streaming_response_list(self, client: Kernel) -> None:
+ with client.api_keys.with_streaming_response.list() as response:
+ assert not response.is_closed
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+
+ api_key = response.parse()
+ assert_matches_type(SyncOffsetPagination[APIKey], api_key, path=["response"])
+
+ assert cast(Any, response.is_closed) is True
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ def test_method_delete(self, client: Kernel) -> None:
+ api_key = client.api_keys.delete(
+ "id",
+ )
+ assert api_key is None
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ def test_raw_response_delete(self, client: Kernel) -> None:
+ response = client.api_keys.with_raw_response.delete(
+ "id",
+ )
+
+ assert response.is_closed is True
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+ api_key = response.parse()
+ assert api_key is None
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ def test_streaming_response_delete(self, client: Kernel) -> None:
+ with client.api_keys.with_streaming_response.delete(
+ "id",
+ ) as response:
+ assert not response.is_closed
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+
+ api_key = response.parse()
+ assert api_key is None
+
+ assert cast(Any, response.is_closed) is True
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ def test_path_params_delete(self, client: Kernel) -> None:
+ with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"):
+ client.api_keys.with_raw_response.delete(
+ "",
+ )
+
+
+class TestAsyncAPIKeys:
+ parametrize = pytest.mark.parametrize(
+ "async_client", [False, True, {"http_client": "aiohttp"}], indirect=True, ids=["loose", "strict", "aiohttp"]
+ )
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ async def test_method_create(self, async_client: AsyncKernel) -> None:
+ api_key = await async_client.api_keys.create(
+ name="staging",
+ )
+ assert_matches_type(CreatedAPIKey, api_key, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ async def test_method_create_with_all_params(self, async_client: AsyncKernel) -> None:
+ api_key = await async_client.api_keys.create(
+ name="staging",
+ days_to_expire=30,
+ project_id="proj_abc123",
+ )
+ assert_matches_type(CreatedAPIKey, api_key, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ async def test_raw_response_create(self, async_client: AsyncKernel) -> None:
+ response = await async_client.api_keys.with_raw_response.create(
+ name="staging",
+ )
+
+ assert response.is_closed is True
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+ api_key = await response.parse()
+ assert_matches_type(CreatedAPIKey, api_key, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ async def test_streaming_response_create(self, async_client: AsyncKernel) -> None:
+ async with async_client.api_keys.with_streaming_response.create(
+ name="staging",
+ ) as response:
+ assert not response.is_closed
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+
+ api_key = await response.parse()
+ assert_matches_type(CreatedAPIKey, api_key, path=["response"])
+
+ assert cast(Any, response.is_closed) is True
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ async def test_method_retrieve(self, async_client: AsyncKernel) -> None:
+ api_key = await async_client.api_keys.retrieve(
+ "id",
+ )
+ assert_matches_type(APIKey, api_key, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ async def test_raw_response_retrieve(self, async_client: AsyncKernel) -> None:
+ response = await async_client.api_keys.with_raw_response.retrieve(
+ "id",
+ )
+
+ assert response.is_closed is True
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+ api_key = await response.parse()
+ assert_matches_type(APIKey, api_key, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ async def test_streaming_response_retrieve(self, async_client: AsyncKernel) -> None:
+ async with async_client.api_keys.with_streaming_response.retrieve(
+ "id",
+ ) as response:
+ assert not response.is_closed
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+
+ api_key = await response.parse()
+ assert_matches_type(APIKey, api_key, 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_retrieve(self, async_client: AsyncKernel) -> None:
+ with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"):
+ await async_client.api_keys.with_raw_response.retrieve(
+ "",
+ )
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ async def test_method_update(self, async_client: AsyncKernel) -> None:
+ api_key = await async_client.api_keys.update(
+ id="id",
+ name="new-api-name",
+ )
+ assert_matches_type(APIKey, api_key, 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.api_keys.with_raw_response.update(
+ id="id",
+ name="new-api-name",
+ )
+
+ assert response.is_closed is True
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+ api_key = await response.parse()
+ assert_matches_type(APIKey, api_key, 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.api_keys.with_streaming_response.update(
+ id="id",
+ name="new-api-name",
+ ) as response:
+ assert not response.is_closed
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+
+ api_key = await response.parse()
+ assert_matches_type(APIKey, api_key, 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.api_keys.with_raw_response.update(
+ id="",
+ name="new-api-name",
+ )
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ async def test_method_list(self, async_client: AsyncKernel) -> None:
+ api_key = await async_client.api_keys.list()
+ assert_matches_type(AsyncOffsetPagination[APIKey], api_key, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ async def test_method_list_with_all_params(self, async_client: AsyncKernel) -> None:
+ api_key = await async_client.api_keys.list(
+ limit=100,
+ offset=0,
+ )
+ assert_matches_type(AsyncOffsetPagination[APIKey], api_key, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ async def test_raw_response_list(self, async_client: AsyncKernel) -> None:
+ response = await async_client.api_keys.with_raw_response.list()
+
+ assert response.is_closed is True
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+ api_key = await response.parse()
+ assert_matches_type(AsyncOffsetPagination[APIKey], api_key, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ async def test_streaming_response_list(self, async_client: AsyncKernel) -> None:
+ async with async_client.api_keys.with_streaming_response.list() as response:
+ assert not response.is_closed
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+
+ api_key = await response.parse()
+ assert_matches_type(AsyncOffsetPagination[APIKey], api_key, path=["response"])
+
+ assert cast(Any, response.is_closed) is True
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ async def test_method_delete(self, async_client: AsyncKernel) -> None:
+ api_key = await async_client.api_keys.delete(
+ "id",
+ )
+ assert api_key is None
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ async def test_raw_response_delete(self, async_client: AsyncKernel) -> None:
+ response = await async_client.api_keys.with_raw_response.delete(
+ "id",
+ )
+
+ assert response.is_closed is True
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+ api_key = await response.parse()
+ assert api_key is None
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ async def test_streaming_response_delete(self, async_client: AsyncKernel) -> None:
+ async with async_client.api_keys.with_streaming_response.delete(
+ "id",
+ ) as response:
+ assert not response.is_closed
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+
+ api_key = await response.parse()
+ assert api_key is None
+
+ assert cast(Any, response.is_closed) is True
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ async def test_path_params_delete(self, async_client: AsyncKernel) -> None:
+ with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"):
+ await async_client.api_keys.with_raw_response.delete(
+ "",
+ )
From 3ced474265441099b9fd26f26050ec799e804fac Mon Sep 17 00:00:00 2001
From: "stainless-app[bot]"
<142633134+stainless-app[bot]@users.noreply.github.com>
Date: Wed, 27 May 2026 17:08:51 +0000
Subject: [PATCH 021/366] feat: Fix API key request model SDK metadata
---
.stats.yml | 4 ++--
api.md | 2 +-
2 files changed, 3 insertions(+), 3 deletions(-)
diff --git a/.stats.yml b/.stats.yml
index e86e3342..3cfc09d4 100644
--- a/.stats.yml
+++ b/.stats.yml
@@ -1,4 +1,4 @@
configured_endpoints: 117
-openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel/kernel-54d9016df9730e65881df9f694df4f33620b0c032f177849d36ccc426cd42fa8.yml
+openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel/kernel-3b34d85c005a4058ac1faaea092615af577d12cee6e420f102de57339251672d.yml
openapi_spec_hash: fad386b8e8712e6639ed9689e9dfc070
-config_hash: 58396d6c1fafaf72b26596b9117edf48
+config_hash: 0f222358f24700d1811c5d27078a3849
diff --git a/api.md b/api.md
index 1dc6fece..c9e1b290 100644
--- a/api.md
+++ b/api.md
@@ -419,7 +419,7 @@ Methods:
Types:
```python
-from kernel.types import APIKey, CreateAPIKeyRequest, CreatedAPIKey, UpdateAPIKeyRequest
+from kernel.types import APIKey, CreatedAPIKey
```
Methods:
From 57ae50c41ef33593fd2428b68c4f8931c04695a8 Mon Sep 17 00:00:00 2001
From: "stainless-app[bot]"
<142633134+stainless-app[bot]@users.noreply.github.com>
Date: Wed, 27 May 2026 17:09:23 +0000
Subject: [PATCH 022/366] release: 0.58.0
---
.release-please-manifest.json | 2 +-
CHANGELOG.md | 10 ++++++++++
pyproject.toml | 2 +-
src/kernel/_version.py | 2 +-
4 files changed, 13 insertions(+), 3 deletions(-)
diff --git a/.release-please-manifest.json b/.release-please-manifest.json
index 2afb750c..2fbefb94 100644
--- a/.release-please-manifest.json
+++ b/.release-please-manifest.json
@@ -1,3 +1,3 @@
{
- ".": "0.57.0"
+ ".": "0.58.0"
}
\ No newline at end of file
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 4df91e67..ba1969b1 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,15 @@
# Changelog
+## 0.58.0 (2026-05-27)
+
+Full Changelog: [v0.57.0...v0.58.0](https://github.com/kernel/kernel-python-sdk/compare/v0.57.0...v0.58.0)
+
+### Features
+
+* [codex] Expose API keys in SDK config ([41b3aa2](https://github.com/kernel/kernel-python-sdk/commit/41b3aa243c39af3614154af0059b0299c4e41664))
+* Fix API key request model SDK metadata ([3ced474](https://github.com/kernel/kernel-python-sdk/commit/3ced474265441099b9fd26f26050ec799e804fac))
+* Support telemetry enabled request config and fix SDK metadata ([a1054a6](https://github.com/kernel/kernel-python-sdk/commit/a1054a6b72e79e0fffc88b1b46661683a8a953b8))
+
## 0.57.0 (2026-05-26)
Full Changelog: [v0.57.0...v0.57.0](https://github.com/kernel/kernel-python-sdk/compare/v0.57.0...v0.57.0)
diff --git a/pyproject.toml b/pyproject.toml
index 6956b281..3ff0ba88 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -1,6 +1,6 @@
[project]
name = "kernel"
-version = "0.57.0"
+version = "0.58.0"
description = "The official Python library for the kernel API"
dynamic = ["readme"]
license = "Apache-2.0"
diff --git a/src/kernel/_version.py b/src/kernel/_version.py
index fb252c57..9422de86 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.57.0" # x-release-please-version
+__version__ = "0.58.0" # x-release-please-version
From 40ce01b93fc414802f2efc2e86a6b3abce88c356 Mon Sep 17 00:00:00 2001
From: "stainless-app[bot]"
<142633134+stainless-app[bot]@users.noreply.github.com>
Date: Wed, 27 May 2026 20:48:05 +0000
Subject: [PATCH 023/366] feat: Support Byteful mobile proxies
---
.stats.yml | 4 +-
src/kernel/types/proxy_check_response.py | 77 +--------------------
src/kernel/types/proxy_create_params.py | 75 +-------------------
src/kernel/types/proxy_create_response.py | 77 +--------------------
src/kernel/types/proxy_list_response.py | 77 +--------------------
src/kernel/types/proxy_retrieve_response.py | 77 +--------------------
6 files changed, 12 insertions(+), 375 deletions(-)
diff --git a/.stats.yml b/.stats.yml
index 3cfc09d4..834590a5 100644
--- a/.stats.yml
+++ b/.stats.yml
@@ -1,4 +1,4 @@
configured_endpoints: 117
-openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel/kernel-3b34d85c005a4058ac1faaea092615af577d12cee6e420f102de57339251672d.yml
-openapi_spec_hash: fad386b8e8712e6639ed9689e9dfc070
+openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel/kernel-a32ac633a8f67f3844b6ccb7b97687aec2cf2e2c611df4157c223dfac16db806.yml
+openapi_spec_hash: f8c9aabe60372f28ad9cceed42009274
config_hash: 0f222358f24700d1811c5d27078a3849
diff --git a/src/kernel/types/proxy_check_response.py b/src/kernel/types/proxy_check_response.py
index c26d665d..f3cdab80 100644
--- a/src/kernel/types/proxy_check_response.py
+++ b/src/kernel/types/proxy_check_response.py
@@ -59,87 +59,14 @@ class ConfigResidentialProxyConfig(BaseModel):
class ConfigMobileProxyConfig(BaseModel):
"""Configuration for mobile proxies."""
- asn: Optional[str] = None
- """Autonomous system number. See https://bgp.potaroo.net/cidr/autnums.html"""
-
- carrier: Optional[
- Literal[
- "a1",
- "aircel",
- "airtel",
- "att",
- "celcom",
- "chinamobile",
- "claro",
- "comcast",
- "cox",
- "digi",
- "dt",
- "docomo",
- "dtac",
- "etisalat",
- "idea",
- "kyivstar",
- "meo",
- "megafon",
- "mtn",
- "mtnza",
- "mts",
- "optus",
- "orange",
- "qwest",
- "reliance_jio",
- "robi",
- "sprint",
- "telefonica",
- "telstra",
- "tmobile",
- "tigo",
- "tim",
- "verizon",
- "vimpelcom",
- "vodacomza",
- "vodafone",
- "vivo",
- "zain",
- "vivabo",
- "telenormyanmar",
- "kcelljsc",
- "swisscom",
- "singtel",
- "asiacell",
- "windit",
- "cellc",
- "ooredoo",
- "drei",
- "umobile",
- "cableone",
- "proximus",
- "tele2",
- "mobitel",
- "o2",
- "bouygues",
- "free",
- "sfr",
- "digicel",
- ]
- ] = None
- """Mobile carrier."""
-
city: Optional[str] = None
- """City name (no spaces, e.g.
-
- `sanfrancisco`). If provided, `country` must also be provided.
- """
+ """Provider city alias. Mobile carrier routing can make observed geo vary."""
country: Optional[str] = None
"""ISO 3166 country code"""
state: Optional[str] = None
- """Two-letter state code."""
-
- zip: Optional[str] = None
- """US ZIP code."""
+ """US-only state code. Mobile carrier routing can make observed geo vary."""
class ConfigCustomProxyConfig(BaseModel):
diff --git a/src/kernel/types/proxy_create_params.py b/src/kernel/types/proxy_create_params.py
index 175b95ff..331bd776 100644
--- a/src/kernel/types/proxy_create_params.py
+++ b/src/kernel/types/proxy_create_params.py
@@ -81,85 +81,14 @@ class ConfigResidentialProxyConfig(TypedDict, total=False):
class ConfigMobileProxyConfig(TypedDict, total=False):
"""Configuration for mobile proxies."""
- asn: str
- """Autonomous system number. See https://bgp.potaroo.net/cidr/autnums.html"""
-
- carrier: Literal[
- "a1",
- "aircel",
- "airtel",
- "att",
- "celcom",
- "chinamobile",
- "claro",
- "comcast",
- "cox",
- "digi",
- "dt",
- "docomo",
- "dtac",
- "etisalat",
- "idea",
- "kyivstar",
- "meo",
- "megafon",
- "mtn",
- "mtnza",
- "mts",
- "optus",
- "orange",
- "qwest",
- "reliance_jio",
- "robi",
- "sprint",
- "telefonica",
- "telstra",
- "tmobile",
- "tigo",
- "tim",
- "verizon",
- "vimpelcom",
- "vodacomza",
- "vodafone",
- "vivo",
- "zain",
- "vivabo",
- "telenormyanmar",
- "kcelljsc",
- "swisscom",
- "singtel",
- "asiacell",
- "windit",
- "cellc",
- "ooredoo",
- "drei",
- "umobile",
- "cableone",
- "proximus",
- "tele2",
- "mobitel",
- "o2",
- "bouygues",
- "free",
- "sfr",
- "digicel",
- ]
- """Mobile carrier."""
-
city: str
- """City name (no spaces, e.g.
-
- `sanfrancisco`). If provided, `country` must also be provided.
- """
+ """Provider city alias. Mobile carrier routing can make observed geo vary."""
country: str
"""ISO 3166 country code"""
state: str
- """Two-letter state code."""
-
- zip: str
- """US ZIP code."""
+ """US-only state code. Mobile carrier routing can make observed geo vary."""
class ConfigCreateCustomProxyConfig(TypedDict, total=False):
diff --git a/src/kernel/types/proxy_create_response.py b/src/kernel/types/proxy_create_response.py
index d317662f..d71656c8 100644
--- a/src/kernel/types/proxy_create_response.py
+++ b/src/kernel/types/proxy_create_response.py
@@ -59,87 +59,14 @@ class ConfigResidentialProxyConfig(BaseModel):
class ConfigMobileProxyConfig(BaseModel):
"""Configuration for mobile proxies."""
- asn: Optional[str] = None
- """Autonomous system number. See https://bgp.potaroo.net/cidr/autnums.html"""
-
- carrier: Optional[
- Literal[
- "a1",
- "aircel",
- "airtel",
- "att",
- "celcom",
- "chinamobile",
- "claro",
- "comcast",
- "cox",
- "digi",
- "dt",
- "docomo",
- "dtac",
- "etisalat",
- "idea",
- "kyivstar",
- "meo",
- "megafon",
- "mtn",
- "mtnza",
- "mts",
- "optus",
- "orange",
- "qwest",
- "reliance_jio",
- "robi",
- "sprint",
- "telefonica",
- "telstra",
- "tmobile",
- "tigo",
- "tim",
- "verizon",
- "vimpelcom",
- "vodacomza",
- "vodafone",
- "vivo",
- "zain",
- "vivabo",
- "telenormyanmar",
- "kcelljsc",
- "swisscom",
- "singtel",
- "asiacell",
- "windit",
- "cellc",
- "ooredoo",
- "drei",
- "umobile",
- "cableone",
- "proximus",
- "tele2",
- "mobitel",
- "o2",
- "bouygues",
- "free",
- "sfr",
- "digicel",
- ]
- ] = None
- """Mobile carrier."""
-
city: Optional[str] = None
- """City name (no spaces, e.g.
-
- `sanfrancisco`). If provided, `country` must also be provided.
- """
+ """Provider city alias. Mobile carrier routing can make observed geo vary."""
country: Optional[str] = None
"""ISO 3166 country code"""
state: Optional[str] = None
- """Two-letter state code."""
-
- zip: Optional[str] = None
- """US ZIP code."""
+ """US-only state code. Mobile carrier routing can make observed geo vary."""
class ConfigCustomProxyConfig(BaseModel):
diff --git a/src/kernel/types/proxy_list_response.py b/src/kernel/types/proxy_list_response.py
index bbbe17c7..d1ddc079 100644
--- a/src/kernel/types/proxy_list_response.py
+++ b/src/kernel/types/proxy_list_response.py
@@ -60,87 +60,14 @@ class ProxyListResponseItemConfigResidentialProxyConfig(BaseModel):
class ProxyListResponseItemConfigMobileProxyConfig(BaseModel):
"""Configuration for mobile proxies."""
- asn: Optional[str] = None
- """Autonomous system number. See https://bgp.potaroo.net/cidr/autnums.html"""
-
- carrier: Optional[
- Literal[
- "a1",
- "aircel",
- "airtel",
- "att",
- "celcom",
- "chinamobile",
- "claro",
- "comcast",
- "cox",
- "digi",
- "dt",
- "docomo",
- "dtac",
- "etisalat",
- "idea",
- "kyivstar",
- "meo",
- "megafon",
- "mtn",
- "mtnza",
- "mts",
- "optus",
- "orange",
- "qwest",
- "reliance_jio",
- "robi",
- "sprint",
- "telefonica",
- "telstra",
- "tmobile",
- "tigo",
- "tim",
- "verizon",
- "vimpelcom",
- "vodacomza",
- "vodafone",
- "vivo",
- "zain",
- "vivabo",
- "telenormyanmar",
- "kcelljsc",
- "swisscom",
- "singtel",
- "asiacell",
- "windit",
- "cellc",
- "ooredoo",
- "drei",
- "umobile",
- "cableone",
- "proximus",
- "tele2",
- "mobitel",
- "o2",
- "bouygues",
- "free",
- "sfr",
- "digicel",
- ]
- ] = None
- """Mobile carrier."""
-
city: Optional[str] = None
- """City name (no spaces, e.g.
-
- `sanfrancisco`). If provided, `country` must also be provided.
- """
+ """Provider city alias. Mobile carrier routing can make observed geo vary."""
country: Optional[str] = None
"""ISO 3166 country code"""
state: Optional[str] = None
- """Two-letter state code."""
-
- zip: Optional[str] = None
- """US ZIP code."""
+ """US-only state code. Mobile carrier routing can make observed geo vary."""
class ProxyListResponseItemConfigCustomProxyConfig(BaseModel):
diff --git a/src/kernel/types/proxy_retrieve_response.py b/src/kernel/types/proxy_retrieve_response.py
index 6b0b1bbe..77d113e5 100644
--- a/src/kernel/types/proxy_retrieve_response.py
+++ b/src/kernel/types/proxy_retrieve_response.py
@@ -59,87 +59,14 @@ class ConfigResidentialProxyConfig(BaseModel):
class ConfigMobileProxyConfig(BaseModel):
"""Configuration for mobile proxies."""
- asn: Optional[str] = None
- """Autonomous system number. See https://bgp.potaroo.net/cidr/autnums.html"""
-
- carrier: Optional[
- Literal[
- "a1",
- "aircel",
- "airtel",
- "att",
- "celcom",
- "chinamobile",
- "claro",
- "comcast",
- "cox",
- "digi",
- "dt",
- "docomo",
- "dtac",
- "etisalat",
- "idea",
- "kyivstar",
- "meo",
- "megafon",
- "mtn",
- "mtnza",
- "mts",
- "optus",
- "orange",
- "qwest",
- "reliance_jio",
- "robi",
- "sprint",
- "telefonica",
- "telstra",
- "tmobile",
- "tigo",
- "tim",
- "verizon",
- "vimpelcom",
- "vodacomza",
- "vodafone",
- "vivo",
- "zain",
- "vivabo",
- "telenormyanmar",
- "kcelljsc",
- "swisscom",
- "singtel",
- "asiacell",
- "windit",
- "cellc",
- "ooredoo",
- "drei",
- "umobile",
- "cableone",
- "proximus",
- "tele2",
- "mobitel",
- "o2",
- "bouygues",
- "free",
- "sfr",
- "digicel",
- ]
- ] = None
- """Mobile carrier."""
-
city: Optional[str] = None
- """City name (no spaces, e.g.
-
- `sanfrancisco`). If provided, `country` must also be provided.
- """
+ """Provider city alias. Mobile carrier routing can make observed geo vary."""
country: Optional[str] = None
"""ISO 3166 country code"""
state: Optional[str] = None
- """Two-letter state code."""
-
- zip: Optional[str] = None
- """US ZIP code."""
+ """US-only state code. Mobile carrier routing can make observed geo vary."""
class ConfigCustomProxyConfig(BaseModel):
From e50ec81a3a0b791dfc0fc6e0824755a8a05fed69 Mon Sep 17 00:00:00 2001
From: "stainless-app[bot]"
<142633134+stainless-app[bot]@users.noreply.github.com>
Date: Fri, 29 May 2026 16:18:51 +0000
Subject: [PATCH 024/366] codegen metadata
---
.stats.yml | 6 +++---
1 file changed, 3 insertions(+), 3 deletions(-)
diff --git a/.stats.yml b/.stats.yml
index 834590a5..190a6ab6 100644
--- a/.stats.yml
+++ b/.stats.yml
@@ -1,4 +1,4 @@
configured_endpoints: 117
-openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel/kernel-a32ac633a8f67f3844b6ccb7b97687aec2cf2e2c611df4157c223dfac16db806.yml
-openapi_spec_hash: f8c9aabe60372f28ad9cceed42009274
-config_hash: 0f222358f24700d1811c5d27078a3849
+openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel/kernel-04da2c2ed5f83c54f59f6c148abcf013cc37282fde2b3b5b263dffab927d5ba2.yml
+openapi_spec_hash: 9b05d6877797e55051a83222fa7652d0
+config_hash: e0741f8035aea13f71e54e0fdb88eaa4
From b4561ed0168519afc9cee5f5b062586d46368dc8 Mon Sep 17 00:00:00 2001
From: "stainless-app[bot]"
<142633134+stainless-app[bot]@users.noreply.github.com>
Date: Fri, 29 May 2026 17:40:24 +0000
Subject: [PATCH 025/366] feat: api: surface category field on browser
telemetry events
---
.stats.yml | 4 ++--
src/kernel/types/browsers/browser_console_error_event.py | 2 ++
src/kernel/types/browsers/browser_console_log_event.py | 2 ++
src/kernel/types/browsers/browser_interaction_click_event.py | 2 ++
src/kernel/types/browsers/browser_interaction_key_event.py | 2 ++
.../browsers/browser_interaction_scroll_settled_event.py | 2 ++
.../types/browsers/browser_monitor_disconnected_event.py | 2 ++
.../types/browsers/browser_monitor_init_failed_event.py | 2 ++
.../types/browsers/browser_monitor_reconnect_failed_event.py | 2 ++
.../types/browsers/browser_monitor_reconnected_event.py | 2 ++
src/kernel/types/browsers/browser_monitor_screenshot_event.py | 2 ++
src/kernel/types/browsers/browser_network_idle_event.py | 2 ++
.../types/browsers/browser_network_loading_failed_event.py | 2 ++
src/kernel/types/browsers/browser_network_request_event.py | 2 ++
src/kernel/types/browsers/browser_network_response_event.py | 2 ++
.../types/browsers/browser_page_dom_content_loaded_event.py | 2 ++
.../types/browsers/browser_page_layout_settled_event.py | 2 ++
src/kernel/types/browsers/browser_page_layout_shift_event.py | 2 ++
src/kernel/types/browsers/browser_page_lcp_event.py | 2 ++
src/kernel/types/browsers/browser_page_load_event.py | 2 ++
src/kernel/types/browsers/browser_page_navigation_event.py | 2 ++
.../types/browsers/browser_page_navigation_settled_event.py | 2 ++
src/kernel/types/browsers/browser_page_tab_opened_event.py | 2 ++
23 files changed, 46 insertions(+), 2 deletions(-)
diff --git a/.stats.yml b/.stats.yml
index 190a6ab6..f41061ca 100644
--- a/.stats.yml
+++ b/.stats.yml
@@ -1,4 +1,4 @@
configured_endpoints: 117
-openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel/kernel-04da2c2ed5f83c54f59f6c148abcf013cc37282fde2b3b5b263dffab927d5ba2.yml
-openapi_spec_hash: 9b05d6877797e55051a83222fa7652d0
+openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel/kernel-4bada2a5bdbde93018e5a1b1e80e134acbc5509cfdea94db6e4c5b799eba7b82.yml
+openapi_spec_hash: e0d541d480f5663b1e6bd3bb19a0fe61
config_hash: e0741f8035aea13f71e54e0fdb88eaa4
diff --git a/src/kernel/types/browsers/browser_console_error_event.py b/src/kernel/types/browsers/browser_console_error_event.py
index a152f944..72a93ede 100644
--- a/src/kernel/types/browsers/browser_console_error_event.py
+++ b/src/kernel/types/browsers/browser_console_error_event.py
@@ -65,6 +65,8 @@ class BrowserConsoleErrorEvent(BaseModel):
Emitted from two distinct CDP sources with different data shapes. Runtime.consoleAPICalled (console.error calls) produces level, text, args, and stack_trace. Runtime.exceptionThrown (uncaught exceptions) produces text, line, column, source_url, and stack_trace. Fields not applicable to the source are absent.
"""
+ category: Literal["console"]
+
source: BrowserEventSource
"""Provenance metadata identifying which producer emitted the event."""
diff --git a/src/kernel/types/browsers/browser_console_log_event.py b/src/kernel/types/browsers/browser_console_log_event.py
index 5994c0be..338f6c85 100644
--- a/src/kernel/types/browsers/browser_console_log_event.py
+++ b/src/kernel/types/browsers/browser_console_log_event.py
@@ -42,6 +42,8 @@ class Data(BrowserEventContext):
class BrowserConsoleLogEvent(BaseModel):
"""A browser console log event (console.log, console.info, console.warn, etc.)."""
+ category: Literal["console"]
+
source: BrowserEventSource
"""Provenance metadata identifying which producer emitted the event."""
diff --git a/src/kernel/types/browsers/browser_interaction_click_event.py b/src/kernel/types/browsers/browser_interaction_click_event.py
index efc76b34..7c4f6119 100644
--- a/src/kernel/types/browsers/browser_interaction_click_event.py
+++ b/src/kernel/types/browsers/browser_interaction_click_event.py
@@ -35,6 +35,8 @@ class Data(BrowserEventContext):
class BrowserInteractionClickEvent(BaseModel):
"""A browser user click event captured via injected page script."""
+ category: Literal["interaction"]
+
source: BrowserEventSource
"""Provenance metadata identifying which producer emitted the event."""
diff --git a/src/kernel/types/browsers/browser_interaction_key_event.py b/src/kernel/types/browsers/browser_interaction_key_event.py
index e8860330..93f2c43e 100644
--- a/src/kernel/types/browsers/browser_interaction_key_event.py
+++ b/src/kernel/types/browsers/browser_interaction_key_event.py
@@ -29,6 +29,8 @@ class Data(BrowserEventContext):
class BrowserInteractionKeyEvent(BaseModel):
"""A browser keyboard event captured via injected page script."""
+ category: Literal["interaction"]
+
source: BrowserEventSource
"""Provenance metadata identifying which producer emitted the event."""
diff --git a/src/kernel/types/browsers/browser_interaction_scroll_settled_event.py b/src/kernel/types/browsers/browser_interaction_scroll_settled_event.py
index 16b7536b..2c7ee24c 100644
--- a/src/kernel/types/browsers/browser_interaction_scroll_settled_event.py
+++ b/src/kernel/types/browsers/browser_interaction_scroll_settled_event.py
@@ -37,6 +37,8 @@ class BrowserInteractionScrollSettledEvent(BaseModel):
A browser scroll settled event emitted after scroll position stops changing, captured via injected page script.
"""
+ category: Literal["interaction"]
+
source: BrowserEventSource
"""Provenance metadata identifying which producer emitted the event."""
diff --git a/src/kernel/types/browsers/browser_monitor_disconnected_event.py b/src/kernel/types/browsers/browser_monitor_disconnected_event.py
index b329ab13..ac37ec6a 100644
--- a/src/kernel/types/browsers/browser_monitor_disconnected_event.py
+++ b/src/kernel/types/browsers/browser_monitor_disconnected_event.py
@@ -20,6 +20,8 @@ class BrowserMonitorDisconnectedEvent(BaseModel):
Telemetry events may be dropped until monitor_reconnected arrives. Treat any in-progress computed state (network_idle, page_layout_settled) as unreliable until then.
"""
+ category: Literal["system"]
+
source: BrowserEventSource
"""Provenance metadata identifying which producer emitted the event."""
diff --git a/src/kernel/types/browsers/browser_monitor_init_failed_event.py b/src/kernel/types/browsers/browser_monitor_init_failed_event.py
index a745fb8d..dba0e5da 100644
--- a/src/kernel/types/browsers/browser_monitor_init_failed_event.py
+++ b/src/kernel/types/browsers/browser_monitor_init_failed_event.py
@@ -17,6 +17,8 @@ class Data(BaseModel):
class BrowserMonitorInitFailedEvent(BaseModel):
"""The CDP session could not be initialized."""
+ category: Literal["system"]
+
source: BrowserEventSource
"""Provenance metadata identifying which producer emitted the event."""
diff --git a/src/kernel/types/browsers/browser_monitor_reconnect_failed_event.py b/src/kernel/types/browsers/browser_monitor_reconnect_failed_event.py
index 79308d2e..57eec69b 100644
--- a/src/kernel/types/browsers/browser_monitor_reconnect_failed_event.py
+++ b/src/kernel/types/browsers/browser_monitor_reconnect_failed_event.py
@@ -23,6 +23,8 @@ class BrowserMonitorReconnectFailedEvent(BaseModel):
The CDP connection to Chrome could not be re-established after exhausting all reconnection attempts. No further telemetry events will arrive on this session.
"""
+ category: Literal["system"]
+
source: BrowserEventSource
"""Provenance metadata identifying which producer emitted the event."""
diff --git a/src/kernel/types/browsers/browser_monitor_reconnected_event.py b/src/kernel/types/browsers/browser_monitor_reconnected_event.py
index a49ad351..39363367 100644
--- a/src/kernel/types/browsers/browser_monitor_reconnected_event.py
+++ b/src/kernel/types/browsers/browser_monitor_reconnected_event.py
@@ -19,6 +19,8 @@ class BrowserMonitorReconnectedEvent(BaseModel):
The CDP connection to Chrome was successfully re-established after a disconnection. Events emitted during the gap are lost. Computed state is reset, so navigation and network tracking restart fresh from this point.
"""
+ category: Literal["system"]
+
source: BrowserEventSource
"""Provenance metadata identifying which producer emitted the event."""
diff --git a/src/kernel/types/browsers/browser_monitor_screenshot_event.py b/src/kernel/types/browsers/browser_monitor_screenshot_event.py
index 9b446fa9..bd2dc65b 100644
--- a/src/kernel/types/browsers/browser_monitor_screenshot_event.py
+++ b/src/kernel/types/browsers/browser_monitor_screenshot_event.py
@@ -17,6 +17,8 @@ class Data(BaseModel):
class BrowserMonitorScreenshotEvent(BaseModel):
"""A periodic screenshot of the browser viewport."""
+ category: Literal["system"]
+
source: BrowserEventSource
"""Provenance metadata identifying which producer emitted the event."""
diff --git a/src/kernel/types/browsers/browser_network_idle_event.py b/src/kernel/types/browsers/browser_network_idle_event.py
index 4b4fae6b..b1877115 100644
--- a/src/kernel/types/browsers/browser_network_idle_event.py
+++ b/src/kernel/types/browsers/browser_network_idle_event.py
@@ -15,6 +15,8 @@ class BrowserNetworkIdleEvent(BaseModel):
A browser network idle event emitted after a 500ms quiet period with no in-flight HTTP requests.
"""
+ category: Literal["network"]
+
source: BrowserEventSource
"""Provenance metadata identifying which producer emitted the event."""
diff --git a/src/kernel/types/browsers/browser_network_loading_failed_event.py b/src/kernel/types/browsers/browser_network_loading_failed_event.py
index bf566a6b..ac6b7961 100644
--- a/src/kernel/types/browsers/browser_network_loading_failed_event.py
+++ b/src/kernel/types/browsers/browser_network_loading_failed_event.py
@@ -39,6 +39,8 @@ class BrowserNetworkLoadingFailedEvent(BaseModel):
If the request was already in flight when CDP attached (no prior network_request was emitted for it), url, frame_id, loader_id, and resource_type are absent; BrowserEventContext is partially populated in that case.
"""
+ category: Literal["network"]
+
source: BrowserEventSource
"""Provenance metadata identifying which producer emitted the event."""
diff --git a/src/kernel/types/browsers/browser_network_request_event.py b/src/kernel/types/browsers/browser_network_request_event.py
index e1bd9428..efea950b 100644
--- a/src/kernel/types/browsers/browser_network_request_event.py
+++ b/src/kernel/types/browsers/browser_network_request_event.py
@@ -55,6 +55,8 @@ class Data(BrowserEventContext):
class BrowserNetworkRequestEvent(BaseModel):
"""A browser network request sent event."""
+ category: Literal["network"]
+
source: BrowserEventSource
"""Provenance metadata identifying which producer emitted the event."""
diff --git a/src/kernel/types/browsers/browser_network_response_event.py b/src/kernel/types/browsers/browser_network_response_event.py
index 8a71d24a..b39fefb8 100644
--- a/src/kernel/types/browsers/browser_network_response_event.py
+++ b/src/kernel/types/browsers/browser_network_response_event.py
@@ -52,6 +52,8 @@ class BrowserNetworkResponseEvent(BaseModel):
Fired after the response body is fully received, not when headers arrive.
"""
+ category: Literal["network"]
+
source: BrowserEventSource
"""Provenance metadata identifying which producer emitted the event."""
diff --git a/src/kernel/types/browsers/browser_page_dom_content_loaded_event.py b/src/kernel/types/browsers/browser_page_dom_content_loaded_event.py
index e917ca32..99393cce 100644
--- a/src/kernel/types/browsers/browser_page_dom_content_loaded_event.py
+++ b/src/kernel/types/browsers/browser_page_dom_content_loaded_event.py
@@ -26,6 +26,8 @@ class Data(BrowserEventContext):
class BrowserPageDomContentLoadedEvent(BaseModel):
"""A browser DOMContentLoaded event (CDP Page.domContentEventFired)."""
+ category: Literal["page"]
+
source: BrowserEventSource
"""Provenance metadata identifying which producer emitted the event."""
diff --git a/src/kernel/types/browsers/browser_page_layout_settled_event.py b/src/kernel/types/browsers/browser_page_layout_settled_event.py
index 289b53a5..f0558f12 100644
--- a/src/kernel/types/browsers/browser_page_layout_settled_event.py
+++ b/src/kernel/types/browsers/browser_page_layout_settled_event.py
@@ -15,6 +15,8 @@ class BrowserPageLayoutSettledEvent(BaseModel):
A browser layout settled event emitted 1 second after page load with no intervening layout shifts, indicating visual stability. Each layout shift resets the 1-second timer.
"""
+ category: Literal["page"]
+
source: BrowserEventSource
"""Provenance metadata identifying which producer emitted the event."""
diff --git a/src/kernel/types/browsers/browser_page_layout_shift_event.py b/src/kernel/types/browsers/browser_page_layout_shift_event.py
index 336bfce2..fb578ac8 100644
--- a/src/kernel/types/browsers/browser_page_layout_shift_event.py
+++ b/src/kernel/types/browsers/browser_page_layout_shift_event.py
@@ -50,6 +50,8 @@ class BrowserPageLayoutShiftEvent(BaseModel):
A browser cumulative layout shift (CLS) event from the Performance Timeline API.
"""
+ category: Literal["page"]
+
source: BrowserEventSource
"""Provenance metadata identifying which producer emitted the event."""
diff --git a/src/kernel/types/browsers/browser_page_lcp_event.py b/src/kernel/types/browsers/browser_page_lcp_event.py
index 13b9c270..0bcbddaa 100644
--- a/src/kernel/types/browsers/browser_page_lcp_event.py
+++ b/src/kernel/types/browsers/browser_page_lcp_event.py
@@ -56,6 +56,8 @@ class BrowserPageLcpEvent(BaseModel):
A browser Largest Contentful Paint (LCP) event from the Performance Timeline API.
"""
+ category: Literal["page"]
+
source: BrowserEventSource
"""Provenance metadata identifying which producer emitted the event."""
diff --git a/src/kernel/types/browsers/browser_page_load_event.py b/src/kernel/types/browsers/browser_page_load_event.py
index b4d85c8f..f27658f1 100644
--- a/src/kernel/types/browsers/browser_page_load_event.py
+++ b/src/kernel/types/browsers/browser_page_load_event.py
@@ -26,6 +26,8 @@ class Data(BrowserEventContext):
class BrowserPageLoadEvent(BaseModel):
"""A browser page load event (CDP Page.loadEventFired)."""
+ category: Literal["page"]
+
source: BrowserEventSource
"""Provenance metadata identifying which producer emitted the event."""
diff --git a/src/kernel/types/browsers/browser_page_navigation_event.py b/src/kernel/types/browsers/browser_page_navigation_event.py
index f857398c..5835d6d4 100644
--- a/src/kernel/types/browsers/browser_page_navigation_event.py
+++ b/src/kernel/types/browsers/browser_page_navigation_event.py
@@ -41,6 +41,8 @@ class BrowserPageNavigationEvent(BaseModel):
Carries nav context fields inline but not nav_seq, as this event resets the navigation epoch.
"""
+ category: Literal["page"]
+
source: BrowserEventSource
"""Provenance metadata identifying which producer emitted the event."""
diff --git a/src/kernel/types/browsers/browser_page_navigation_settled_event.py b/src/kernel/types/browsers/browser_page_navigation_settled_event.py
index b5227ff4..6e5d31c2 100644
--- a/src/kernel/types/browsers/browser_page_navigation_settled_event.py
+++ b/src/kernel/types/browsers/browser_page_navigation_settled_event.py
@@ -15,6 +15,8 @@ class BrowserPageNavigationSettledEvent(BaseModel):
Emitted when page_dom_content_loaded and page_layout_settled have both fired for the same navigation, indicating the page is loaded and visually stable. Independent of network_idle; a single pending request does not block it.
"""
+ category: Literal["page"]
+
source: BrowserEventSource
"""Provenance metadata identifying which producer emitted the event."""
diff --git a/src/kernel/types/browsers/browser_page_tab_opened_event.py b/src/kernel/types/browsers/browser_page_tab_opened_event.py
index 9831aee1..144be476 100644
--- a/src/kernel/types/browsers/browser_page_tab_opened_event.py
+++ b/src/kernel/types/browsers/browser_page_tab_opened_event.py
@@ -31,6 +31,8 @@ class BrowserPageTabOpenedEvent(BaseModel):
A new browser tab or target was opened (CDP Target.attachedToTarget for page targets). Fires before a CDP session is attached to the new target, so session_id, frame_id, loader_id, and nav_seq are absent; this event does not compose BrowserEventContext. Consumers reading context fields generically should treat it as a special case.
"""
+ category: Literal["page"]
+
source: BrowserEventSource
"""Provenance metadata identifying which producer emitted the event."""
From 2129543df84156a6a42320e3fb310693aca3f960 Mon Sep 17 00:00:00 2001
From: "stainless-app[bot]"
<142633134+stainless-app[bot]@users.noreply.github.com>
Date: Mon, 1 Jun 2026 14:20:33 +0000
Subject: [PATCH 026/366] fix(api): move batch + get_mouse_position into
Browser Computer Controls tag
---
.stats.yml | 4 ++--
src/kernel/resources/browsers/browsers.py | 6 ++++++
src/kernel/resources/browsers/computer.py | 4 ++++
3 files changed, 12 insertions(+), 2 deletions(-)
diff --git a/.stats.yml b/.stats.yml
index f41061ca..8ba534ce 100644
--- a/.stats.yml
+++ b/.stats.yml
@@ -1,4 +1,4 @@
configured_endpoints: 117
-openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel/kernel-4bada2a5bdbde93018e5a1b1e80e134acbc5509cfdea94db6e4c5b799eba7b82.yml
-openapi_spec_hash: e0d541d480f5663b1e6bd3bb19a0fe61
+openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel/kernel-7548abe9fea7553e12a128b31f7dc7fc33f2558a9f20d2eb33464db5f61ab3ea.yml
+openapi_spec_hash: 4b701cc4b33a2c944a5a9b63e9f364a2
config_hash: e0741f8035aea13f71e54e0fdb88eaa4
diff --git a/src/kernel/resources/browsers/browsers.py b/src/kernel/resources/browsers/browsers.py
index c020c288..801cd152 100644
--- a/src/kernel/resources/browsers/browsers.py
+++ b/src/kernel/resources/browsers/browsers.py
@@ -133,6 +133,7 @@ def logs(self) -> LogsResource:
@cached_property
def computer(self) -> ComputerResource:
+ """Control mouse, keyboard, and screen on the browser instance."""
return ComputerResource(self._client)
@cached_property
@@ -680,6 +681,7 @@ def logs(self) -> AsyncLogsResource:
@cached_property
def computer(self) -> AsyncComputerResource:
+ """Control mouse, keyboard, and screen on the browser instance."""
return AsyncComputerResource(self._client)
@cached_property
@@ -1250,6 +1252,7 @@ def logs(self) -> LogsResourceWithRawResponse:
@cached_property
def computer(self) -> ComputerResourceWithRawResponse:
+ """Control mouse, keyboard, and screen on the browser instance."""
return ComputerResourceWithRawResponse(self._browsers.computer)
@cached_property
@@ -1311,6 +1314,7 @@ def logs(self) -> AsyncLogsResourceWithRawResponse:
@cached_property
def computer(self) -> AsyncComputerResourceWithRawResponse:
+ """Control mouse, keyboard, and screen on the browser instance."""
return AsyncComputerResourceWithRawResponse(self._browsers.computer)
@cached_property
@@ -1372,6 +1376,7 @@ def logs(self) -> LogsResourceWithStreamingResponse:
@cached_property
def computer(self) -> ComputerResourceWithStreamingResponse:
+ """Control mouse, keyboard, and screen on the browser instance."""
return ComputerResourceWithStreamingResponse(self._browsers.computer)
@cached_property
@@ -1433,6 +1438,7 @@ def logs(self) -> AsyncLogsResourceWithStreamingResponse:
@cached_property
def computer(self) -> AsyncComputerResourceWithStreamingResponse:
+ """Control mouse, keyboard, and screen on the browser instance."""
return AsyncComputerResourceWithStreamingResponse(self._browsers.computer)
@cached_property
diff --git a/src/kernel/resources/browsers/computer.py b/src/kernel/resources/browsers/computer.py
index 54b638e5..d2b57304 100644
--- a/src/kernel/resources/browsers/computer.py
+++ b/src/kernel/resources/browsers/computer.py
@@ -46,6 +46,8 @@
class ComputerResource(SyncAPIResource):
+ """Control mouse, keyboard, and screen on the browser instance."""
+
@cached_property
def with_raw_response(self) -> ComputerResourceWithRawResponse:
"""
@@ -649,6 +651,8 @@ def write_clipboard(
class AsyncComputerResource(AsyncAPIResource):
+ """Control mouse, keyboard, and screen on the browser instance."""
+
@cached_property
def with_raw_response(self) -> AsyncComputerResourceWithRawResponse:
"""
From 0dfbd204bd2d4abdbee5c986d40facc18146c695 Mon Sep 17 00:00:00 2001
From: "stainless-app[bot]"
<142633134+stainless-app[bot]@users.noreply.github.com>
Date: Mon, 1 Jun 2026 15:53:42 +0000
Subject: [PATCH 027/366] codegen metadata
---
.stats.yml | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/.stats.yml b/.stats.yml
index 8ba534ce..e31b9566 100644
--- a/.stats.yml
+++ b/.stats.yml
@@ -1,4 +1,4 @@
configured_endpoints: 117
-openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel/kernel-7548abe9fea7553e12a128b31f7dc7fc33f2558a9f20d2eb33464db5f61ab3ea.yml
-openapi_spec_hash: 4b701cc4b33a2c944a5a9b63e9f364a2
+openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel/kernel-501a71a04e6a68427e378df10c7327025e3cb177f4550459b5a6eb752a177a25.yml
+openapi_spec_hash: d38661081dd50fded2208424bb19157b
config_hash: e0741f8035aea13f71e54e0fdb88eaa4
From 01345e1d68dc95c339b820f6f1c2c829f050d012 Mon Sep 17 00:00:00 2001
From: "stainless-app[bot]"
<142633134+stainless-app[bot]@users.noreply.github.com>
Date: Tue, 2 Jun 2026 15:22:06 +0000
Subject: [PATCH 028/366] codegen metadata
---
.stats.yml | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/.stats.yml b/.stats.yml
index e31b9566..8fee23fa 100644
--- a/.stats.yml
+++ b/.stats.yml
@@ -1,4 +1,4 @@
configured_endpoints: 117
-openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel/kernel-501a71a04e6a68427e378df10c7327025e3cb177f4550459b5a6eb752a177a25.yml
-openapi_spec_hash: d38661081dd50fded2208424bb19157b
+openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel/kernel-7988523e72ff96a93d2913e63ba27c5bb94af9893b94fceeeda64e0b43fb0663.yml
+openapi_spec_hash: ce2ae9f9137d4c2bf947d446d00ebec8
config_hash: e0741f8035aea13f71e54e0fdb88eaa4
From 5205d45ab670621c1f7bed9eec66f12e8ad9c0df Mon Sep 17 00:00:00 2001
From: "stainless-app[bot]"
<142633134+stainless-app[bot]@users.noreply.github.com>
Date: Tue, 2 Jun 2026 21:01:42 +0000
Subject: [PATCH 029/366] codegen metadata
---
.stats.yml | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/.stats.yml b/.stats.yml
index 8fee23fa..0dd6d527 100644
--- a/.stats.yml
+++ b/.stats.yml
@@ -1,4 +1,4 @@
configured_endpoints: 117
-openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel/kernel-7988523e72ff96a93d2913e63ba27c5bb94af9893b94fceeeda64e0b43fb0663.yml
-openapi_spec_hash: ce2ae9f9137d4c2bf947d446d00ebec8
+openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel/kernel-c8f49d6ea6355b703fa0e6cce4277df8954b9730ce4cdd3af5900f37769b40d9.yml
+openapi_spec_hash: ca03a797fb8e4cc79b6be4513f2fc6e7
config_hash: e0741f8035aea13f71e54e0fdb88eaa4
From 0804643d211f69b06036690e51a950b5d015418f Mon Sep 17 00:00:00 2001
From: "stainless-app[bot]"
<142633134+stainless-app[bot]@users.noreply.github.com>
Date: Wed, 3 Jun 2026 13:40:09 +0000
Subject: [PATCH 030/366] codegen metadata
---
.stats.yml | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/.stats.yml b/.stats.yml
index 0dd6d527..a84f620b 100644
--- a/.stats.yml
+++ b/.stats.yml
@@ -1,4 +1,4 @@
configured_endpoints: 117
-openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel/kernel-c8f49d6ea6355b703fa0e6cce4277df8954b9730ce4cdd3af5900f37769b40d9.yml
-openapi_spec_hash: ca03a797fb8e4cc79b6be4513f2fc6e7
+openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel/kernel-a42305a37f0b2f42afb552acc9a5294c210a5278eb6067df803abe36da45c5b9.yml
+openapi_spec_hash: 4140db7dd999fee4b02e76dfb7d003b5
config_hash: e0741f8035aea13f71e54e0fdb88eaa4
From a6942566069308b0def374a1c0a64ec07e52d9a3 Mon Sep 17 00:00:00 2001
From: "stainless-app[bot]"
<142633134+stainless-app[bot]@users.noreply.github.com>
Date: Wed, 3 Jun 2026 15:29:18 +0000
Subject: [PATCH 031/366] feat(api): move browser telemetry SSE stream to
/browsers/{id}/telemetry/stream
---
.stats.yml | 6 +++---
api.md | 2 +-
src/kernel/resources/browsers/telemetry.py | 4 ++--
3 files changed, 6 insertions(+), 6 deletions(-)
diff --git a/.stats.yml b/.stats.yml
index a84f620b..5a5e5632 100644
--- a/.stats.yml
+++ b/.stats.yml
@@ -1,4 +1,4 @@
configured_endpoints: 117
-openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel/kernel-a42305a37f0b2f42afb552acc9a5294c210a5278eb6067df803abe36da45c5b9.yml
-openapi_spec_hash: 4140db7dd999fee4b02e76dfb7d003b5
-config_hash: e0741f8035aea13f71e54e0fdb88eaa4
+openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel/kernel-1acd8f0b76ab00e36b53cc3ca90b72b2199f3388b3e307890adb464b87f9a2d8.yml
+openapi_spec_hash: 82003125c1c2c5d82d19270bafb4a6ca
+config_hash: ede72e4ae65cc5a6d6927938b3455c46
diff --git a/api.md b/api.md
index c9e1b290..a7fda470 100644
--- a/api.md
+++ b/api.md
@@ -143,7 +143,7 @@ from kernel.types.browsers import (
Methods:
-- client.browsers.telemetry.stream(id) -> TelemetryStreamResponse
+- client.browsers.telemetry.stream(id) -> TelemetryStreamResponse
## Replays
diff --git a/src/kernel/resources/browsers/telemetry.py b/src/kernel/resources/browsers/telemetry.py
index cb95b2d1..008b1d29 100644
--- a/src/kernel/resources/browsers/telemetry.py
+++ b/src/kernel/resources/browsers/telemetry.py
@@ -80,7 +80,7 @@ def stream(
extra_headers = {"Accept": "text/event-stream", **(extra_headers or {})}
extra_headers = {**strip_not_given({"Last-Event-ID": last_event_id}), **(extra_headers or {})}
return self._get(
- path_template("/browsers/{id}/telemetry", id=id),
+ path_template("/browsers/{id}/telemetry/stream", id=id),
options=make_request_options(
extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
),
@@ -149,7 +149,7 @@ async def stream(
extra_headers = {"Accept": "text/event-stream", **(extra_headers or {})}
extra_headers = {**strip_not_given({"Last-Event-ID": last_event_id}), **(extra_headers or {})}
return await self._get(
- path_template("/browsers/{id}/telemetry", id=id),
+ path_template("/browsers/{id}/telemetry/stream", id=id),
options=make_request_options(
extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
),
From 4c5dde5d65e16ab7e17ed2f8f16e3ffaa87ea901 Mon Sep 17 00:00:00 2001
From: "stainless-app[bot]"
<142633134+stainless-app[bot]@users.noreply.github.com>
Date: Wed, 3 Jun 2026 15:29:50 +0000
Subject: [PATCH 032/366] release: 0.59.0
---
.release-please-manifest.json | 2 +-
CHANGELOG.md | 15 +++++++++++++++
pyproject.toml | 2 +-
src/kernel/_version.py | 2 +-
4 files changed, 18 insertions(+), 3 deletions(-)
diff --git a/.release-please-manifest.json b/.release-please-manifest.json
index 2fbefb94..85c31182 100644
--- a/.release-please-manifest.json
+++ b/.release-please-manifest.json
@@ -1,3 +1,3 @@
{
- ".": "0.58.0"
+ ".": "0.59.0"
}
\ No newline at end of file
diff --git a/CHANGELOG.md b/CHANGELOG.md
index ba1969b1..1da0039a 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,20 @@
# Changelog
+## 0.59.0 (2026-06-03)
+
+Full Changelog: [v0.58.0...v0.59.0](https://github.com/kernel/kernel-python-sdk/compare/v0.58.0...v0.59.0)
+
+### Features
+
+* api: surface category field on browser telemetry events ([06a2f7b](https://github.com/kernel/kernel-python-sdk/commit/06a2f7babbc8f6ea31e46f3dfeb8d09dbaae88f0))
+* **api:** move browser telemetry SSE stream to /browsers/{id}/telemetry/stream ([5de851a](https://github.com/kernel/kernel-python-sdk/commit/5de851a5ff708a3e42e276e3cf9e2fe7ef0f2ed6))
+* Support Byteful mobile proxies ([8c0ce1c](https://github.com/kernel/kernel-python-sdk/commit/8c0ce1c1533a538483419f4da0937a6153eb5273))
+
+
+### Bug Fixes
+
+* **api:** move batch + get_mouse_position into Browser Computer Controls tag ([7cbc848](https://github.com/kernel/kernel-python-sdk/commit/7cbc848fe33394ee05f5eb96d62cdc8b5b9b9d9c))
+
## 0.58.0 (2026-05-27)
Full Changelog: [v0.57.0...v0.58.0](https://github.com/kernel/kernel-python-sdk/compare/v0.57.0...v0.58.0)
diff --git a/pyproject.toml b/pyproject.toml
index 3ff0ba88..24264fbc 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -1,6 +1,6 @@
[project]
name = "kernel"
-version = "0.58.0"
+version = "0.59.0"
description = "The official Python library for the kernel API"
dynamic = ["readme"]
license = "Apache-2.0"
diff --git a/src/kernel/_version.py b/src/kernel/_version.py
index 9422de86..86fdb652 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.58.0" # x-release-please-version
+__version__ = "0.59.0" # x-release-please-version
From cb5072516416627ffde7198900484c46dda5b9dc Mon Sep 17 00:00:00 2001
From: Rafael Garcia
Date: Wed, 3 Jun 2026 10:12:01 -0400
Subject: [PATCH 033/366] feat: route browser telemetry directly to the VM by
default
Telemetry is now a default direct-to-VM routing subresource. The
telemetry stream method path is changed from /browsers/{id}/telemetry to
/browsers/{id}/telemetry/stream so it mirrors the browser VM endpoint:
when the request is rewritten for direct routing it yields
{base_url}/telemetry/stream, which is the SSE stream on the VM (the VM's
/telemetry is a different, non-stream JSON endpoint).
"telemetry" is added to the default KERNEL_BROWSER_ROUTING_SUBRESOURCES
allowlist alongside "curl".
Co-Authored-By: Claude Opus 4.8 (1M context)
---
src/kernel/lib/browser_routing/routing.py | 2 +-
tests/test_browser_routing.py | 24 ++++++++++++++++++++++-
2 files changed, 24 insertions(+), 2 deletions(-)
diff --git a/src/kernel/lib/browser_routing/routing.py b/src/kernel/lib/browser_routing/routing.py
index aa84cc1b..f0f5e34a 100644
--- a/src/kernel/lib/browser_routing/routing.py
+++ b/src/kernel/lib/browser_routing/routing.py
@@ -41,7 +41,7 @@ class BrowserRoutingConfig:
def browser_routing_config_from_env() -> BrowserRoutingConfig:
raw = os.environ.get("KERNEL_BROWSER_ROUTING_SUBRESOURCES")
if raw is None:
- return BrowserRoutingConfig(subresources=("curl",))
+ return BrowserRoutingConfig(subresources=("curl", "telemetry"))
if raw.strip() == "":
return BrowserRoutingConfig()
diff --git a/tests/test_browser_routing.py b/tests/test_browser_routing.py
index ac841521..6b0c12a9 100644
--- a/tests/test_browser_routing.py
+++ b/tests/test_browser_routing.py
@@ -97,6 +97,28 @@ def test_browser_request_uses_curl_raw() -> None:
assert request.url.params.get("jwt") == "token-abc"
+@respx.mock
+def test_telemetry_stream_routes_directly_to_vm(monkeypatch: pytest.MonkeyPatch) -> None:
+ monkeypatch.setenv("KERNEL_BROWSER_ROUTING_SUBRESOURCES", "telemetry")
+ route = respx.get("http://browser-session.test/browser/kernel/telemetry/stream").mock(
+ return_value=httpx.Response(
+ 200,
+ headers={"content-type": "text/event-stream"},
+ content=b'id: 1\ndata: {"category":"api"}\n\n',
+ )
+ )
+ with Kernel(base_url=base_url, api_key=api_key, _strict_response_validation=True) as client:
+ _cache_browser(client)
+ stream = client.browsers.telemetry.stream("sess-1")
+ stream.close()
+
+ assert route.called
+ request = cast(httpx.Request, cast(Any, route.calls[0]).request)
+ assert request.url.path == "/browser/kernel/telemetry/stream"
+ assert request.url.params.get("jwt") == "token-abc"
+ assert request.headers.get("Authorization") is None
+
+
@respx.mock
def test_browser_request_params_cannot_override_target_url_or_jwt() -> None:
route = respx.get("http://browser-session.test/browser/kernel/curl/raw").mock(
@@ -315,7 +337,7 @@ def test_browser_route_from_browser_requires_base_url_and_jwt() -> None:
def test_browser_routing_config_from_env_defaults_to_curl(monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.delenv("KERNEL_BROWSER_ROUTING_SUBRESOURCES", raising=False)
- assert browser_routing_config_from_env().subresources == ("curl",)
+ assert browser_routing_config_from_env().subresources == ("curl", "telemetry")
def test_browser_routing_config_from_env_empty_string_disables_routing(monkeypatch: pytest.MonkeyPatch) -> None:
From 4c29993cdda50fcde19ee7bf696a268cd8b0eb0b Mon Sep 17 00:00:00 2001
From: Rafael Garcia
Date: Wed, 3 Jun 2026 10:34:56 -0400
Subject: [PATCH 034/366] feat(examples): add browser-telemetry example
Co-Authored-By: Claude Opus 4.8 (1M context)
---
examples/browser_telemetry.py | 32 ++++++++++++++++++++++++++++++++
1 file changed, 32 insertions(+)
create mode 100644 examples/browser_telemetry.py
diff --git a/examples/browser_telemetry.py b/examples/browser_telemetry.py
new file mode 100644
index 00000000..8d1795c4
--- /dev/null
+++ b/examples/browser_telemetry.py
@@ -0,0 +1,32 @@
+"""Example: stream live browser telemetry events from a session."""
+
+from kernel import Kernel
+
+
+def main() -> None:
+ client = Kernel()
+
+ # Enable telemetry capture when creating the browser.
+ browser = client.browsers.create(telemetry={"enabled": True})
+
+ try:
+ # Telemetry is a default direct-to-VM routing subresource, so the stream
+ # connects straight to the browser VM automatically.
+ stream = client.browsers.telemetry.stream(browser.session_id)
+
+ # Make a few browser activity calls to generate events. The "api" telemetry
+ # category emits an event per VM API call, so events arrive within ~1s.
+ for _ in range(3):
+ client.browsers.curl(browser.session_id, url="https://example.com", method="GET")
+
+ # Print a few events, then stop so we don't wait on the 15s keepalive.
+ for count, message in enumerate(stream, start=1):
+ print(message.seq, message.event.type)
+ if count >= 3:
+ break
+ finally:
+ client.browsers.delete_by_id(browser.session_id)
+
+
+if __name__ == "__main__":
+ main()
From a0ee2b2653c581839be11bb29be5b68166e80658 Mon Sep 17 00:00:00 2001
From: Rafael Garcia
Date: Wed, 3 Jun 2026 11:00:24 -0400
Subject: [PATCH 035/366] fix(streaming): don't dispatch empty SSE keepalive
comment frames
The SSE decoder's empty-block guard included last_event_id, which is sticky
across events per the SSE spec. Once any event carried an id, every subsequent
comment-only block (e.g. the server's ":\n\n" keepalive, sent every 15s on an
idle stream) fell through the guard and dispatched an empty-data event. The
typed Stream wrapper then calls .json() on it unconditionally, raising
JSONDecodeError and killing the stream.
This made idle browser telemetry streams crash after ~15s. Drop last_event_id
from the guard so dispatch depends only on the current block's event/data/retry
fields. Event-typed empty-data frames still dispatch (unchanged). Adds a
regression test for a keepalive comment following an id-bearing event.
Co-Authored-By: Claude Opus 4.8 (1M context)
---
src/kernel/_streaming.py | 7 ++++++-
tests/test_streaming.py | 26 ++++++++++++++++++++++++++
2 files changed, 32 insertions(+), 1 deletion(-)
diff --git a/src/kernel/_streaming.py b/src/kernel/_streaming.py
index 5520edb5..ddca7eb6 100644
--- a/src/kernel/_streaming.py
+++ b/src/kernel/_streaming.py
@@ -251,7 +251,12 @@ def decode(self, line: str) -> ServerSentEvent | None:
# See: https://html.spec.whatwg.org/multipage/server-sent-events.html#event-stream-interpretation # noqa: E501
if not line:
- if not self._event and not self._data and not self._last_event_id and self._retry is None:
+ # Whether to dispatch depends only on what was set in the *current* block. last_event_id
+ # is sticky across events (per the SSE spec, it is intentionally not reset below), so it
+ # must not be part of this check -- otherwise, once any event carries an id, every
+ # subsequent comment-only block (e.g. a ``:\n\n`` keepalive) would dispatch an empty
+ # event, which then fails to JSON-decode in the typed Stream wrapper.
+ if not self._event and not self._data and self._retry is None:
return None
sse = ServerSentEvent(
diff --git a/tests/test_streaming.py b/tests/test_streaming.py
index 4b8e4e48..c2ebd43d 100644
--- a/tests/test_streaming.py
+++ b/tests/test_streaming.py
@@ -26,6 +26,32 @@ def body() -> Iterator[bytes]:
await assert_empty_iter(iterator)
+@pytest.mark.asyncio
+@pytest.mark.parametrize("sync", [True, False], ids=["sync", "async"])
+async def test_keepalive_comment_after_event_with_id(sync: bool, client: Kernel, async_client: AsyncKernel) -> None:
+ # A ``:`` comment frame (the server's SSE keepalive) that arrives after an event which set an
+ # id must be ignored, not dispatched as an empty event. last_event_id is sticky, so this is a
+ # regression guard against it leaking an undecodable empty frame into the typed stream.
+ def body() -> Iterator[bytes]:
+ yield b"id: 1\n"
+ yield b'data: {"foo":true}\n'
+ yield b"\n"
+ yield b":\n"
+ yield b"\n"
+ yield b'data: {"bar":false}\n'
+ yield b"\n"
+
+ iterator = make_event_iterator(content=body(), sync=sync, client=client, async_client=async_client)
+
+ sse = await iter_next(iterator)
+ assert sse.json() == {"foo": True}
+
+ sse = await iter_next(iterator)
+ assert sse.json() == {"bar": False}
+
+ await assert_empty_iter(iterator)
+
+
@pytest.mark.asyncio
@pytest.mark.parametrize("sync", [True, False], ids=["sync", "async"])
async def test_data_missing_event(sync: bool, client: Kernel, async_client: AsyncKernel) -> None:
From 903fe13b84242acea9c27bc451c16fb5cd58e40c Mon Sep 17 00:00:00 2001
From: "stainless-app[bot]"
<142633134+stainless-app[bot]@users.noreply.github.com>
Date: Wed, 3 Jun 2026 17:45:26 +0000
Subject: [PATCH 036/366] feat: Fix browser pool update schema
---
.stats.yml | 4 ++--
src/kernel/resources/browser_pools.py | 24 +++++++++----------
.../types/browser_pool_update_params.py | 16 ++++++-------
tests/api_resources/test_browser_pools.py | 12 ++--------
4 files changed, 24 insertions(+), 32 deletions(-)
diff --git a/.stats.yml b/.stats.yml
index 5a5e5632..57627cd0 100644
--- a/.stats.yml
+++ b/.stats.yml
@@ -1,4 +1,4 @@
configured_endpoints: 117
-openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel/kernel-1acd8f0b76ab00e36b53cc3ca90b72b2199f3388b3e307890adb464b87f9a2d8.yml
-openapi_spec_hash: 82003125c1c2c5d82d19270bafb4a6ca
+openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel/kernel-2d1337eec44e036b9c896b7db4691f0a12edfa79d3f28b611818bcedf62d44ee.yml
+openapi_spec_hash: 30110dbbe733b16e40a6d0aa41d0c8c4
config_hash: ede72e4ae65cc5a6d6927938b3455c46
diff --git a/src/kernel/resources/browser_pools.py b/src/kernel/resources/browser_pools.py
index db8ebbeb..64751271 100644
--- a/src/kernel/resources/browser_pools.py
+++ b/src/kernel/resources/browser_pools.py
@@ -206,7 +206,6 @@ def update(
self,
id_or_name: str,
*,
- size: int,
chrome_policy: Dict[str, object] | Omit = omit,
discard_all_idle: bool | Omit = omit,
extensions: Iterable[BrowserExtension] | Omit = omit,
@@ -216,6 +215,7 @@ def update(
name: str | Omit = omit,
profile: BrowserProfile | Omit = omit,
proxy_id: str | Omit = omit,
+ size: int | Omit = omit,
start_url: str | Omit = omit,
stealth: bool | Omit = omit,
timeout_seconds: int | Omit = omit,
@@ -231,10 +231,6 @@ def update(
Updates the configuration used to create browsers in the pool.
Args:
- size: Number of browsers to maintain in the pool. The maximum size is determined by
- your organization's pooled sessions limit (the sum of all pool sizes cannot
- exceed your limit).
-
chrome_policy: Custom Chrome enterprise policy overrides applied to all browsers in this pool.
Keys are Chrome enterprise policy names; values must match their expected types.
Blocked: kernel-managed policies (extensions, proxy, CDP/automation). See
@@ -261,6 +257,10 @@ def update(
proxy_id: Optional proxy to associate to the browser session. Must reference a proxy
belonging to the caller's org.
+ size: Number of browsers to maintain in the pool. The maximum size is determined by
+ your organization's pooled sessions limit (the sum of all pool sizes cannot
+ exceed your limit).
+
start_url: Optional URL to navigate to when a new browser is warmed into the pool.
Best-effort: failures to navigate do not fail pool fill. Only applied to
newly-warmed browsers; browsers reused via release/acquire keep whatever URL the
@@ -300,7 +300,6 @@ def update(
path_template("/browser_pools/{id_or_name}", id_or_name=id_or_name),
body=maybe_transform(
{
- "size": size,
"chrome_policy": chrome_policy,
"discard_all_idle": discard_all_idle,
"extensions": extensions,
@@ -310,6 +309,7 @@ def update(
"name": name,
"profile": profile,
"proxy_id": proxy_id,
+ "size": size,
"start_url": start_url,
"stealth": stealth,
"timeout_seconds": timeout_seconds,
@@ -684,7 +684,6 @@ async def update(
self,
id_or_name: str,
*,
- size: int,
chrome_policy: Dict[str, object] | Omit = omit,
discard_all_idle: bool | Omit = omit,
extensions: Iterable[BrowserExtension] | Omit = omit,
@@ -694,6 +693,7 @@ async def update(
name: str | Omit = omit,
profile: BrowserProfile | Omit = omit,
proxy_id: str | Omit = omit,
+ size: int | Omit = omit,
start_url: str | Omit = omit,
stealth: bool | Omit = omit,
timeout_seconds: int | Omit = omit,
@@ -709,10 +709,6 @@ async def update(
Updates the configuration used to create browsers in the pool.
Args:
- size: Number of browsers to maintain in the pool. The maximum size is determined by
- your organization's pooled sessions limit (the sum of all pool sizes cannot
- exceed your limit).
-
chrome_policy: Custom Chrome enterprise policy overrides applied to all browsers in this pool.
Keys are Chrome enterprise policy names; values must match their expected types.
Blocked: kernel-managed policies (extensions, proxy, CDP/automation). See
@@ -739,6 +735,10 @@ async def update(
proxy_id: Optional proxy to associate to the browser session. Must reference a proxy
belonging to the caller's org.
+ size: Number of browsers to maintain in the pool. The maximum size is determined by
+ your organization's pooled sessions limit (the sum of all pool sizes cannot
+ exceed your limit).
+
start_url: Optional URL to navigate to when a new browser is warmed into the pool.
Best-effort: failures to navigate do not fail pool fill. Only applied to
newly-warmed browsers; browsers reused via release/acquire keep whatever URL the
@@ -778,7 +778,6 @@ async def update(
path_template("/browser_pools/{id_or_name}", id_or_name=id_or_name),
body=await async_maybe_transform(
{
- "size": size,
"chrome_policy": chrome_policy,
"discard_all_idle": discard_all_idle,
"extensions": extensions,
@@ -788,6 +787,7 @@ async def update(
"name": name,
"profile": profile,
"proxy_id": proxy_id,
+ "size": size,
"start_url": start_url,
"stealth": stealth,
"timeout_seconds": timeout_seconds,
diff --git a/src/kernel/types/browser_pool_update_params.py b/src/kernel/types/browser_pool_update_params.py
index 4efc9ced..07501695 100644
--- a/src/kernel/types/browser_pool_update_params.py
+++ b/src/kernel/types/browser_pool_update_params.py
@@ -3,7 +3,7 @@
from __future__ import annotations
from typing import Dict, Iterable
-from typing_extensions import Required, TypedDict
+from typing_extensions import TypedDict
from .shared_params.browser_profile import BrowserProfile
from .shared_params.browser_viewport import BrowserViewport
@@ -13,13 +13,6 @@
class BrowserPoolUpdateParams(TypedDict, total=False):
- size: Required[int]
- """Number of browsers to maintain in the pool.
-
- The maximum size is determined by your organization's pooled sessions limit (the
- sum of all pool sizes cannot exceed your limit).
- """
-
chrome_policy: Dict[str, object]
"""Custom Chrome enterprise policy overrides applied to all browsers in this pool.
@@ -68,6 +61,13 @@ class BrowserPoolUpdateParams(TypedDict, total=False):
Must reference a proxy belonging to the caller's org.
"""
+ size: int
+ """Number of browsers to maintain in the pool.
+
+ The maximum size is determined by your organization's pooled sessions limit (the
+ sum of all pool sizes cannot exceed your limit).
+ """
+
start_url: str
"""Optional URL to navigate to when a new browser is warmed into the pool.
diff --git a/tests/api_resources/test_browser_pools.py b/tests/api_resources/test_browser_pools.py
index 959eeef2..42f47e63 100644
--- a/tests/api_resources/test_browser_pools.py
+++ b/tests/api_resources/test_browser_pools.py
@@ -135,7 +135,6 @@ def test_path_params_retrieve(self, client: Kernel) -> None:
def test_method_update(self, client: Kernel) -> None:
browser_pool = client.browser_pools.update(
id_or_name="id_or_name",
- size=10,
)
assert_matches_type(BrowserPool, browser_pool, path=["response"])
@@ -144,7 +143,6 @@ def test_method_update(self, client: Kernel) -> None:
def test_method_update_with_all_params(self, client: Kernel) -> None:
browser_pool = client.browser_pools.update(
id_or_name="id_or_name",
- size=10,
chrome_policy={"foo": "bar"},
discard_all_idle=False,
extensions=[
@@ -163,6 +161,7 @@ def test_method_update_with_all_params(self, client: Kernel) -> None:
"save_changes": True,
},
proxy_id="proxy_id",
+ size=10,
start_url="https://example.com",
stealth=True,
timeout_seconds=60,
@@ -179,7 +178,6 @@ def test_method_update_with_all_params(self, client: Kernel) -> None:
def test_raw_response_update(self, client: Kernel) -> None:
response = client.browser_pools.with_raw_response.update(
id_or_name="id_or_name",
- size=10,
)
assert response.is_closed is True
@@ -192,7 +190,6 @@ def test_raw_response_update(self, client: Kernel) -> None:
def test_streaming_response_update(self, client: Kernel) -> None:
with client.browser_pools.with_streaming_response.update(
id_or_name="id_or_name",
- size=10,
) as response:
assert not response.is_closed
assert response.http_request.headers.get("X-Stainless-Lang") == "python"
@@ -208,7 +205,6 @@ def test_path_params_update(self, client: Kernel) -> None:
with pytest.raises(ValueError, match=r"Expected a non-empty value for `id_or_name` but received ''"):
client.browser_pools.with_raw_response.update(
id_or_name="",
- size=10,
)
@pytest.mark.skip(reason="Mock server tests are disabled")
@@ -559,7 +555,6 @@ async def test_path_params_retrieve(self, async_client: AsyncKernel) -> None:
async def test_method_update(self, async_client: AsyncKernel) -> None:
browser_pool = await async_client.browser_pools.update(
id_or_name="id_or_name",
- size=10,
)
assert_matches_type(BrowserPool, browser_pool, path=["response"])
@@ -568,7 +563,6 @@ async def test_method_update(self, async_client: AsyncKernel) -> None:
async def test_method_update_with_all_params(self, async_client: AsyncKernel) -> None:
browser_pool = await async_client.browser_pools.update(
id_or_name="id_or_name",
- size=10,
chrome_policy={"foo": "bar"},
discard_all_idle=False,
extensions=[
@@ -587,6 +581,7 @@ async def test_method_update_with_all_params(self, async_client: AsyncKernel) ->
"save_changes": True,
},
proxy_id="proxy_id",
+ size=10,
start_url="https://example.com",
stealth=True,
timeout_seconds=60,
@@ -603,7 +598,6 @@ async def test_method_update_with_all_params(self, async_client: AsyncKernel) ->
async def test_raw_response_update(self, async_client: AsyncKernel) -> None:
response = await async_client.browser_pools.with_raw_response.update(
id_or_name="id_or_name",
- size=10,
)
assert response.is_closed is True
@@ -616,7 +610,6 @@ async def test_raw_response_update(self, async_client: AsyncKernel) -> None:
async def test_streaming_response_update(self, async_client: AsyncKernel) -> None:
async with async_client.browser_pools.with_streaming_response.update(
id_or_name="id_or_name",
- size=10,
) as response:
assert not response.is_closed
assert response.http_request.headers.get("X-Stainless-Lang") == "python"
@@ -632,7 +625,6 @@ async def test_path_params_update(self, async_client: AsyncKernel) -> None:
with pytest.raises(ValueError, match=r"Expected a non-empty value for `id_or_name` but received ''"):
await async_client.browser_pools.with_raw_response.update(
id_or_name="",
- size=10,
)
@pytest.mark.skip(reason="Mock server tests are disabled")
From 55bd31edfed74f3bc4b08f83e07448e2e53378b0 Mon Sep 17 00:00:00 2001
From: "stainless-app[bot]"
<142633134+stainless-app[bot]@users.noreply.github.com>
Date: Wed, 3 Jun 2026 19:40:35 +0000
Subject: [PATCH 037/366] feat: Add API-backed API key management endpoints
---
.stats.yml | 4 ++--
src/kernel/resources/api_keys.py | 27 +++++++++++++++++++++++++
src/kernel/types/api_key_list_params.py | 14 ++++++++++++-
tests/api_resources/test_api_keys.py | 6 ++++++
4 files changed, 48 insertions(+), 3 deletions(-)
diff --git a/.stats.yml b/.stats.yml
index 57627cd0..9f173dc5 100644
--- a/.stats.yml
+++ b/.stats.yml
@@ -1,4 +1,4 @@
configured_endpoints: 117
-openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel/kernel-2d1337eec44e036b9c896b7db4691f0a12edfa79d3f28b611818bcedf62d44ee.yml
-openapi_spec_hash: 30110dbbe733b16e40a6d0aa41d0c8c4
+openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel/kernel-3a1db7f11a92b28681929255ada59d2317ee4db98b5ff5aa6ce142a0664058b3.yml
+openapi_spec_hash: b3064eaa589ae2a84993686ad1a3ee43
config_hash: ede72e4ae65cc5a6d6927938b3455c46
diff --git a/src/kernel/resources/api_keys.py b/src/kernel/resources/api_keys.py
index aae96794..93e80158 100644
--- a/src/kernel/resources/api_keys.py
+++ b/src/kernel/resources/api_keys.py
@@ -3,6 +3,7 @@
from __future__ import annotations
from typing import Optional
+from typing_extensions import Literal
import httpx
@@ -171,6 +172,9 @@ def list(
*,
limit: int | Omit = omit,
offset: int | Omit = omit,
+ query: str | Omit = omit,
+ sort_by: Literal["created_at", "name", "expires_at"] | Omit = omit,
+ sort_direction: Literal["asc", "desc"] | Omit = omit,
# Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
# The extra values given here take precedence over values defined on the client or passed to this method.
extra_headers: Headers | None = None,
@@ -187,6 +191,13 @@ def list(
offset: Number of results to skip
+ query: Case-insensitive substring match against API key name, creator, and project. API
+ key identifiers and masked keys match by exact value or prefix.
+
+ sort_by: Field to sort API keys by.
+
+ sort_direction: Sort direction for API keys.
+
extra_headers: Send extra headers
extra_query: Add additional query parameters to the request
@@ -207,6 +218,9 @@ def list(
{
"limit": limit,
"offset": offset,
+ "query": query,
+ "sort_by": sort_by,
+ "sort_direction": sort_direction,
},
api_key_list_params.APIKeyListParams,
),
@@ -395,6 +409,9 @@ def list(
*,
limit: int | Omit = omit,
offset: int | Omit = omit,
+ query: str | Omit = omit,
+ sort_by: Literal["created_at", "name", "expires_at"] | Omit = omit,
+ sort_direction: Literal["asc", "desc"] | Omit = omit,
# Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
# The extra values given here take precedence over values defined on the client or passed to this method.
extra_headers: Headers | None = None,
@@ -411,6 +428,13 @@ def list(
offset: Number of results to skip
+ query: Case-insensitive substring match against API key name, creator, and project. API
+ key identifiers and masked keys match by exact value or prefix.
+
+ sort_by: Field to sort API keys by.
+
+ sort_direction: Sort direction for API keys.
+
extra_headers: Send extra headers
extra_query: Add additional query parameters to the request
@@ -431,6 +455,9 @@ def list(
{
"limit": limit,
"offset": offset,
+ "query": query,
+ "sort_by": sort_by,
+ "sort_direction": sort_direction,
},
api_key_list_params.APIKeyListParams,
),
diff --git a/src/kernel/types/api_key_list_params.py b/src/kernel/types/api_key_list_params.py
index e7d807f2..79a9c41c 100644
--- a/src/kernel/types/api_key_list_params.py
+++ b/src/kernel/types/api_key_list_params.py
@@ -2,7 +2,7 @@
from __future__ import annotations
-from typing_extensions import TypedDict
+from typing_extensions import Literal, TypedDict
__all__ = ["APIKeyListParams"]
@@ -13,3 +13,15 @@ class APIKeyListParams(TypedDict, total=False):
offset: int
"""Number of results to skip"""
+
+ query: str
+ """Case-insensitive substring match against API key name, creator, and project.
+
+ API key identifiers and masked keys match by exact value or prefix.
+ """
+
+ sort_by: Literal["created_at", "name", "expires_at"]
+ """Field to sort API keys by."""
+
+ sort_direction: Literal["asc", "desc"]
+ """Sort direction for API keys."""
diff --git a/tests/api_resources/test_api_keys.py b/tests/api_resources/test_api_keys.py
index da854b23..9c3abcae 100644
--- a/tests/api_resources/test_api_keys.py
+++ b/tests/api_resources/test_api_keys.py
@@ -162,6 +162,9 @@ def test_method_list_with_all_params(self, client: Kernel) -> None:
api_key = client.api_keys.list(
limit=100,
offset=0,
+ query="query",
+ sort_by="created_at",
+ sort_direction="asc",
)
assert_matches_type(SyncOffsetPagination[APIKey], api_key, path=["response"])
@@ -379,6 +382,9 @@ async def test_method_list_with_all_params(self, async_client: AsyncKernel) -> N
api_key = await async_client.api_keys.list(
limit=100,
offset=0,
+ query="query",
+ sort_by="created_at",
+ sort_direction="asc",
)
assert_matches_type(AsyncOffsetPagination[APIKey], api_key, path=["response"])
From cecaff25a57356d751c477c86fbc3c09d91d8033 Mon Sep 17 00:00:00 2001
From: "stainless-app[bot]"
<142633134+stainless-app[bot]@users.noreply.github.com>
Date: Wed, 3 Jun 2026 19:41:02 +0000
Subject: [PATCH 038/366] release: 0.60.0
---
.release-please-manifest.json | 2 +-
CHANGELOG.md | 16 ++++++++++++++++
pyproject.toml | 2 +-
src/kernel/_version.py | 2 +-
4 files changed, 19 insertions(+), 3 deletions(-)
diff --git a/.release-please-manifest.json b/.release-please-manifest.json
index 85c31182..284ce936 100644
--- a/.release-please-manifest.json
+++ b/.release-please-manifest.json
@@ -1,3 +1,3 @@
{
- ".": "0.59.0"
+ ".": "0.60.0"
}
\ No newline at end of file
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 1da0039a..1d7d38d4 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,21 @@
# Changelog
+## 0.60.0 (2026-06-03)
+
+Full Changelog: [v0.59.0...v0.60.0](https://github.com/kernel/kernel-python-sdk/compare/v0.59.0...v0.60.0)
+
+### Features
+
+* Add API-backed API key management endpoints ([55bd31e](https://github.com/kernel/kernel-python-sdk/commit/55bd31edfed74f3bc4b08f83e07448e2e53378b0))
+* **examples:** add browser-telemetry example ([4c29993](https://github.com/kernel/kernel-python-sdk/commit/4c29993cdda50fcde19ee7bf696a268cd8b0eb0b))
+* Fix browser pool update schema ([903fe13](https://github.com/kernel/kernel-python-sdk/commit/903fe13b84242acea9c27bc451c16fb5cd58e40c))
+* route browser telemetry directly to the VM by default ([cb50725](https://github.com/kernel/kernel-python-sdk/commit/cb5072516416627ffde7198900484c46dda5b9dc))
+
+
+### Bug Fixes
+
+* **streaming:** don't dispatch empty SSE keepalive comment frames ([a0ee2b2](https://github.com/kernel/kernel-python-sdk/commit/a0ee2b2653c581839be11bb29be5b68166e80658))
+
## 0.59.0 (2026-06-03)
Full Changelog: [v0.58.0...v0.59.0](https://github.com/kernel/kernel-python-sdk/compare/v0.58.0...v0.59.0)
diff --git a/pyproject.toml b/pyproject.toml
index 24264fbc..ad5fcaee 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -1,6 +1,6 @@
[project]
name = "kernel"
-version = "0.59.0"
+version = "0.60.0"
description = "The official Python library for the kernel API"
dynamic = ["readme"]
license = "Apache-2.0"
diff --git a/src/kernel/_version.py b/src/kernel/_version.py
index 86fdb652..fdecb536 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.59.0" # x-release-please-version
+__version__ = "0.60.0" # x-release-please-version
From 53cffc4b688e4966d49eaf0ce574a26f360e701d Mon Sep 17 00:00:00 2001
From: "stainless-app[bot]"
<142633134+stainless-app[bot]@users.noreply.github.com>
Date: Wed, 3 Jun 2026 20:39:29 +0000
Subject: [PATCH 039/366] feat: Add record_audio option to browser replay
recording API
---
.stats.yml | 4 ++--
src/kernel/resources/browsers/replays.py | 10 ++++++++++
src/kernel/types/browsers/replay_start_params.py | 6 ++++++
tests/api_resources/browsers/test_replays.py | 2 ++
4 files changed, 20 insertions(+), 2 deletions(-)
diff --git a/.stats.yml b/.stats.yml
index 9f173dc5..27228661 100644
--- a/.stats.yml
+++ b/.stats.yml
@@ -1,4 +1,4 @@
configured_endpoints: 117
-openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel/kernel-3a1db7f11a92b28681929255ada59d2317ee4db98b5ff5aa6ce142a0664058b3.yml
-openapi_spec_hash: b3064eaa589ae2a84993686ad1a3ee43
+openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel/kernel-33e46e6a0095c2ec39a51860ee4e133c5a21a80a90cbe9e52953c07e5e0295de.yml
+openapi_spec_hash: 4aa466b9af39768b65a44b68ae0d1f6e
config_hash: ede72e4ae65cc5a6d6927938b3455c46
diff --git a/src/kernel/resources/browsers/replays.py b/src/kernel/resources/browsers/replays.py
index 8f56ea5f..e58f8333 100644
--- a/src/kernel/resources/browsers/replays.py
+++ b/src/kernel/resources/browsers/replays.py
@@ -128,6 +128,7 @@ def start(
*,
framerate: int | Omit = omit,
max_duration_in_seconds: int | Omit = omit,
+ record_audio: 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,
@@ -144,6 +145,9 @@ def start(
max_duration_in_seconds: Maximum recording duration in seconds.
+ record_audio: Record audio in addition to video. When false (the default), the recording is
+ video-only.
+
extra_headers: Send extra headers
extra_query: Add additional query parameters to the request
@@ -160,6 +164,7 @@ def start(
{
"framerate": framerate,
"max_duration_in_seconds": max_duration_in_seconds,
+ "record_audio": record_audio,
},
replay_start_params.ReplayStartParams,
),
@@ -305,6 +310,7 @@ async def start(
*,
framerate: int | Omit = omit,
max_duration_in_seconds: int | Omit = omit,
+ record_audio: 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,
@@ -321,6 +327,9 @@ async def start(
max_duration_in_seconds: Maximum recording duration in seconds.
+ record_audio: Record audio in addition to video. When false (the default), the recording is
+ video-only.
+
extra_headers: Send extra headers
extra_query: Add additional query parameters to the request
@@ -337,6 +346,7 @@ async def start(
{
"framerate": framerate,
"max_duration_in_seconds": max_duration_in_seconds,
+ "record_audio": record_audio,
},
replay_start_params.ReplayStartParams,
),
diff --git a/src/kernel/types/browsers/replay_start_params.py b/src/kernel/types/browsers/replay_start_params.py
index d77b6c49..c37c554d 100644
--- a/src/kernel/types/browsers/replay_start_params.py
+++ b/src/kernel/types/browsers/replay_start_params.py
@@ -16,3 +16,9 @@ class ReplayStartParams(TypedDict, total=False):
max_duration_in_seconds: int
"""Maximum recording duration in seconds."""
+
+ record_audio: bool
+ """Record audio in addition to video.
+
+ When false (the default), the recording is video-only.
+ """
diff --git a/tests/api_resources/browsers/test_replays.py b/tests/api_resources/browsers/test_replays.py
index 72e986c1..0b450ff8 100644
--- a/tests/api_resources/browsers/test_replays.py
+++ b/tests/api_resources/browsers/test_replays.py
@@ -142,6 +142,7 @@ def test_method_start_with_all_params(self, client: Kernel) -> None:
id="id",
framerate=1,
max_duration_in_seconds=1,
+ record_audio=True,
)
assert_matches_type(ReplayStartResponse, replay, path=["response"])
@@ -354,6 +355,7 @@ async def test_method_start_with_all_params(self, async_client: AsyncKernel) ->
id="id",
framerate=1,
max_duration_in_seconds=1,
+ record_audio=True,
)
assert_matches_type(ReplayStartResponse, replay, path=["response"])
From 44c4dd6f0faf5e921f6822c824cc6a96b35a92de Mon Sep 17 00:00:00 2001
From: "stainless-app[bot]"
<142633134+stainless-app[bot]@users.noreply.github.com>
Date: Wed, 3 Jun 2026 20:39:55 +0000
Subject: [PATCH 040/366] release: 0.61.0
---
.release-please-manifest.json | 2 +-
CHANGELOG.md | 8 ++++++++
pyproject.toml | 2 +-
src/kernel/_version.py | 2 +-
4 files changed, 11 insertions(+), 3 deletions(-)
diff --git a/.release-please-manifest.json b/.release-please-manifest.json
index 284ce936..f7e971ea 100644
--- a/.release-please-manifest.json
+++ b/.release-please-manifest.json
@@ -1,3 +1,3 @@
{
- ".": "0.60.0"
+ ".": "0.61.0"
}
\ No newline at end of file
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 1d7d38d4..fbffa57e 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,13 @@
# Changelog
+## 0.61.0 (2026-06-03)
+
+Full Changelog: [v0.60.0...v0.61.0](https://github.com/kernel/kernel-python-sdk/compare/v0.60.0...v0.61.0)
+
+### Features
+
+* Add record_audio option to browser replay recording API ([7d357c2](https://github.com/kernel/kernel-python-sdk/commit/7d357c27c32315ba3a7fcaddac0f6563aab91849))
+
## 0.60.0 (2026-06-03)
Full Changelog: [v0.59.0...v0.60.0](https://github.com/kernel/kernel-python-sdk/compare/v0.59.0...v0.60.0)
diff --git a/pyproject.toml b/pyproject.toml
index ad5fcaee..2b9342ba 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -1,6 +1,6 @@
[project]
name = "kernel"
-version = "0.60.0"
+version = "0.61.0"
description = "The official Python library for the kernel API"
dynamic = ["readme"]
license = "Apache-2.0"
diff --git a/src/kernel/_version.py b/src/kernel/_version.py
index fdecb536..fc502359 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.60.0" # x-release-please-version
+__version__ = "0.61.0" # x-release-please-version
From c9362944196fc5f690b31702a14dc710e1d55337 Mon Sep 17 00:00:00 2001
From: "stainless-app[bot]"
<142633134+stainless-app[bot]@users.noreply.github.com>
Date: Wed, 3 Jun 2026 21:04:44 +0000
Subject: [PATCH 041/366] feat: api: paginate GET /proxies
---
.stats.yml | 6 +-
api.md | 2 +-
src/kernel/resources/proxies.py | 83 +++++++++++++++++++++----
src/kernel/types/__init__.py | 1 +
src/kernel/types/proxy_list_params.py | 15 +++++
src/kernel/types/proxy_list_response.py | 42 ++++++-------
tests/api_resources/test_proxies.py | 31 +++++++--
7 files changed, 134 insertions(+), 46 deletions(-)
create mode 100644 src/kernel/types/proxy_list_params.py
diff --git a/.stats.yml b/.stats.yml
index 27228661..032ac8ce 100644
--- a/.stats.yml
+++ b/.stats.yml
@@ -1,4 +1,4 @@
configured_endpoints: 117
-openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel/kernel-33e46e6a0095c2ec39a51860ee4e133c5a21a80a90cbe9e52953c07e5e0295de.yml
-openapi_spec_hash: 4aa466b9af39768b65a44b68ae0d1f6e
-config_hash: ede72e4ae65cc5a6d6927938b3455c46
+openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel/kernel-f3e52a8a9d9e37f8b527bdcce63b39cf8807d138cf3570c9954ba7f42ef02375.yml
+openapi_spec_hash: 2b787691eba1bb7bafb553528c09d5c0
+config_hash: e12afec516ced9520590c7a05af7a6f7
diff --git a/api.md b/api.md
index a7fda470..1cc2b82f 100644
--- a/api.md
+++ b/api.md
@@ -324,7 +324,7 @@ Methods:
- client.proxies.create(\*\*params) -> ProxyCreateResponse
- client.proxies.retrieve(id) -> ProxyRetrieveResponse
-- client.proxies.list() -> ProxyListResponse
+- client.proxies.list(\*\*params) -> SyncOffsetPagination[ProxyListResponse]
- client.proxies.delete(id) -> None
- client.proxies.check(id, \*\*params) -> ProxyCheckResponse
diff --git a/src/kernel/resources/proxies.py b/src/kernel/resources/proxies.py
index 501a2e88..2239a854 100644
--- a/src/kernel/resources/proxies.py
+++ b/src/kernel/resources/proxies.py
@@ -6,7 +6,7 @@
import httpx
-from ..types import proxy_check_params, proxy_create_params
+from ..types import proxy_list_params, proxy_check_params, proxy_create_params
from .._types import Body, Omit, Query, Headers, NoneType, NotGiven, SequenceNotStr, omit, not_given
from .._utils import path_template, maybe_transform, async_maybe_transform
from .._compat import cached_property
@@ -17,7 +17,8 @@
async_to_raw_response_wrapper,
async_to_streamed_response_wrapper,
)
-from .._base_client import make_request_options
+from ..pagination import SyncOffsetPagination, AsyncOffsetPagination
+from .._base_client import AsyncPaginator, make_request_options
from ..types.proxy_list_response import ProxyListResponse
from ..types.proxy_check_response import ProxyCheckResponse
from ..types.proxy_create_response import ProxyCreateResponse
@@ -140,20 +141,48 @@ def retrieve(
def list(
self,
*,
+ limit: int | Omit = omit,
+ offset: int | Omit = omit,
# Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
# The extra values given here take precedence over values defined on the client or passed to this method.
extra_headers: Headers | None = None,
extra_query: Query | None = None,
extra_body: Body | None = None,
timeout: float | httpx.Timeout | None | NotGiven = not_given,
- ) -> ProxyListResponse:
- """List proxies owned by the caller's organization."""
- return self._get(
+ ) -> SyncOffsetPagination[ProxyListResponse]:
+ """
+ List proxies owned by the caller's organization.
+
+ Args:
+ limit: Limit the number of proxies to return.
+
+ offset: Offset the number of proxies to return.
+
+ 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
+ """
+ return self._get_api_list(
"/proxies",
+ page=SyncOffsetPagination[ProxyListResponse],
options=make_request_options(
- extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
+ extra_headers=extra_headers,
+ extra_query=extra_query,
+ extra_body=extra_body,
+ timeout=timeout,
+ query=maybe_transform(
+ {
+ "limit": limit,
+ "offset": offset,
+ },
+ proxy_list_params.ProxyListParams,
+ ),
),
- cast_to=ProxyListResponse,
+ model=ProxyListResponse,
)
def delete(
@@ -357,23 +386,51 @@ async def retrieve(
cast_to=ProxyRetrieveResponse,
)
- async def list(
+ def list(
self,
*,
+ limit: int | Omit = omit,
+ offset: int | Omit = omit,
# Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
# The extra values given here take precedence over values defined on the client or passed to this method.
extra_headers: Headers | None = None,
extra_query: Query | None = None,
extra_body: Body | None = None,
timeout: float | httpx.Timeout | None | NotGiven = not_given,
- ) -> ProxyListResponse:
- """List proxies owned by the caller's organization."""
- return await self._get(
+ ) -> AsyncPaginator[ProxyListResponse, AsyncOffsetPagination[ProxyListResponse]]:
+ """
+ List proxies owned by the caller's organization.
+
+ Args:
+ limit: Limit the number of proxies to return.
+
+ offset: Offset the number of proxies to return.
+
+ 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
+ """
+ return self._get_api_list(
"/proxies",
+ page=AsyncOffsetPagination[ProxyListResponse],
options=make_request_options(
- extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
+ extra_headers=extra_headers,
+ extra_query=extra_query,
+ extra_body=extra_body,
+ timeout=timeout,
+ query=maybe_transform(
+ {
+ "limit": limit,
+ "offset": offset,
+ },
+ proxy_list_params.ProxyListParams,
+ ),
),
- cast_to=ProxyListResponse,
+ model=ProxyListResponse,
)
async def delete(
diff --git a/src/kernel/types/__init__.py b/src/kernel/types/__init__.py
index fb6fd589..bdc65fdf 100644
--- a/src/kernel/types/__init__.py
+++ b/src/kernel/types/__init__.py
@@ -25,6 +25,7 @@
from .created_api_key import CreatedAPIKey as CreatedAPIKey
from .browser_pool_ref import BrowserPoolRef as BrowserPoolRef
from .app_list_response import AppListResponse as AppListResponse
+from .proxy_list_params import ProxyListParams as ProxyListParams
from .proxy_check_params import ProxyCheckParams as ProxyCheckParams
from .api_key_list_params import APIKeyListParams as APIKeyListParams
from .browser_curl_params import BrowserCurlParams as BrowserCurlParams
diff --git a/src/kernel/types/proxy_list_params.py b/src/kernel/types/proxy_list_params.py
new file mode 100644
index 00000000..78972bcb
--- /dev/null
+++ b/src/kernel/types/proxy_list_params.py
@@ -0,0 +1,15 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from __future__ import annotations
+
+from typing_extensions import TypedDict
+
+__all__ = ["ProxyListParams"]
+
+
+class ProxyListParams(TypedDict, total=False):
+ limit: int
+ """Limit the number of proxies to return."""
+
+ offset: int
+ """Offset the number of proxies to return."""
diff --git a/src/kernel/types/proxy_list_response.py b/src/kernel/types/proxy_list_response.py
index d1ddc079..6d40a8a4 100644
--- a/src/kernel/types/proxy_list_response.py
+++ b/src/kernel/types/proxy_list_response.py
@@ -8,31 +8,30 @@
__all__ = [
"ProxyListResponse",
- "ProxyListResponseItem",
- "ProxyListResponseItemConfig",
- "ProxyListResponseItemConfigDatacenterProxyConfig",
- "ProxyListResponseItemConfigIspProxyConfig",
- "ProxyListResponseItemConfigResidentialProxyConfig",
- "ProxyListResponseItemConfigMobileProxyConfig",
- "ProxyListResponseItemConfigCustomProxyConfig",
+ "Config",
+ "ConfigDatacenterProxyConfig",
+ "ConfigIspProxyConfig",
+ "ConfigResidentialProxyConfig",
+ "ConfigMobileProxyConfig",
+ "ConfigCustomProxyConfig",
]
-class ProxyListResponseItemConfigDatacenterProxyConfig(BaseModel):
+class ConfigDatacenterProxyConfig(BaseModel):
"""Configuration for a datacenter proxy."""
country: Optional[str] = None
"""ISO 3166 country code. Defaults to US if not provided."""
-class ProxyListResponseItemConfigIspProxyConfig(BaseModel):
+class ConfigIspProxyConfig(BaseModel):
"""Configuration for an ISP proxy."""
country: Optional[str] = None
"""ISO 3166 country code. Defaults to US if not provided."""
-class ProxyListResponseItemConfigResidentialProxyConfig(BaseModel):
+class ConfigResidentialProxyConfig(BaseModel):
"""Configuration for residential proxies."""
asn: Optional[str] = None
@@ -57,7 +56,7 @@ class ProxyListResponseItemConfigResidentialProxyConfig(BaseModel):
"""US ZIP code."""
-class ProxyListResponseItemConfigMobileProxyConfig(BaseModel):
+class ConfigMobileProxyConfig(BaseModel):
"""Configuration for mobile proxies."""
city: Optional[str] = None
@@ -70,7 +69,7 @@ class ProxyListResponseItemConfigMobileProxyConfig(BaseModel):
"""US-only state code. Mobile carrier routing can make observed geo vary."""
-class ProxyListResponseItemConfigCustomProxyConfig(BaseModel):
+class ConfigCustomProxyConfig(BaseModel):
"""Configuration for a custom proxy (e.g., private proxy server)."""
host: str
@@ -86,16 +85,16 @@ class ProxyListResponseItemConfigCustomProxyConfig(BaseModel):
"""Username for proxy authentication."""
-ProxyListResponseItemConfig: TypeAlias = Union[
- ProxyListResponseItemConfigDatacenterProxyConfig,
- ProxyListResponseItemConfigIspProxyConfig,
- ProxyListResponseItemConfigResidentialProxyConfig,
- ProxyListResponseItemConfigMobileProxyConfig,
- ProxyListResponseItemConfigCustomProxyConfig,
+Config: TypeAlias = Union[
+ ConfigDatacenterProxyConfig,
+ ConfigIspProxyConfig,
+ ConfigResidentialProxyConfig,
+ ConfigMobileProxyConfig,
+ ConfigCustomProxyConfig,
]
-class ProxyListResponseItem(BaseModel):
+class ProxyListResponse(BaseModel):
"""Configuration for routing traffic through a proxy."""
type: Literal["datacenter", "isp", "residential", "mobile", "custom"]
@@ -110,7 +109,7 @@ class ProxyListResponseItem(BaseModel):
bypass_hosts: Optional[List[str]] = None
"""Hostnames that should bypass the parent proxy and connect directly."""
- config: Optional[ProxyListResponseItemConfig] = None
+ config: Optional[Config] = None
"""Configuration specific to the selected proxy `type`."""
ip_address: Optional[str] = None
@@ -127,6 +126,3 @@ class ProxyListResponseItem(BaseModel):
status: Optional[Literal["available", "unavailable"]] = None
"""Current health status of the proxy."""
-
-
-ProxyListResponse: TypeAlias = List[ProxyListResponseItem]
diff --git a/tests/api_resources/test_proxies.py b/tests/api_resources/test_proxies.py
index fd8080ee..64cb90b9 100644
--- a/tests/api_resources/test_proxies.py
+++ b/tests/api_resources/test_proxies.py
@@ -15,6 +15,7 @@
ProxyCreateResponse,
ProxyRetrieveResponse,
)
+from kernel.pagination import SyncOffsetPagination, AsyncOffsetPagination
base_url = os.environ.get("TEST_API_BASE_URL", "http://127.0.0.1:4010")
@@ -114,7 +115,16 @@ def test_path_params_retrieve(self, client: Kernel) -> None:
@parametrize
def test_method_list(self, client: Kernel) -> None:
proxy = client.proxies.list()
- assert_matches_type(ProxyListResponse, proxy, path=["response"])
+ assert_matches_type(SyncOffsetPagination[ProxyListResponse], proxy, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ def test_method_list_with_all_params(self, client: Kernel) -> None:
+ proxy = client.proxies.list(
+ limit=1,
+ offset=0,
+ )
+ assert_matches_type(SyncOffsetPagination[ProxyListResponse], proxy, path=["response"])
@pytest.mark.skip(reason="Mock server tests are disabled")
@parametrize
@@ -124,7 +134,7 @@ def test_raw_response_list(self, client: Kernel) -> None:
assert response.is_closed is True
assert response.http_request.headers.get("X-Stainless-Lang") == "python"
proxy = response.parse()
- assert_matches_type(ProxyListResponse, proxy, path=["response"])
+ assert_matches_type(SyncOffsetPagination[ProxyListResponse], proxy, path=["response"])
@pytest.mark.skip(reason="Mock server tests are disabled")
@parametrize
@@ -134,7 +144,7 @@ def test_streaming_response_list(self, client: Kernel) -> None:
assert response.http_request.headers.get("X-Stainless-Lang") == "python"
proxy = response.parse()
- assert_matches_type(ProxyListResponse, proxy, path=["response"])
+ assert_matches_type(SyncOffsetPagination[ProxyListResponse], proxy, path=["response"])
assert cast(Any, response.is_closed) is True
@@ -329,7 +339,16 @@ async def test_path_params_retrieve(self, async_client: AsyncKernel) -> None:
@parametrize
async def test_method_list(self, async_client: AsyncKernel) -> None:
proxy = await async_client.proxies.list()
- assert_matches_type(ProxyListResponse, proxy, path=["response"])
+ assert_matches_type(AsyncOffsetPagination[ProxyListResponse], proxy, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ async def test_method_list_with_all_params(self, async_client: AsyncKernel) -> None:
+ proxy = await async_client.proxies.list(
+ limit=1,
+ offset=0,
+ )
+ assert_matches_type(AsyncOffsetPagination[ProxyListResponse], proxy, path=["response"])
@pytest.mark.skip(reason="Mock server tests are disabled")
@parametrize
@@ -339,7 +358,7 @@ async def test_raw_response_list(self, async_client: AsyncKernel) -> None:
assert response.is_closed is True
assert response.http_request.headers.get("X-Stainless-Lang") == "python"
proxy = await response.parse()
- assert_matches_type(ProxyListResponse, proxy, path=["response"])
+ assert_matches_type(AsyncOffsetPagination[ProxyListResponse], proxy, path=["response"])
@pytest.mark.skip(reason="Mock server tests are disabled")
@parametrize
@@ -349,7 +368,7 @@ async def test_streaming_response_list(self, async_client: AsyncKernel) -> None:
assert response.http_request.headers.get("X-Stainless-Lang") == "python"
proxy = await response.parse()
- assert_matches_type(ProxyListResponse, proxy, path=["response"])
+ assert_matches_type(AsyncOffsetPagination[ProxyListResponse], proxy, path=["response"])
assert cast(Any, response.is_closed) is True
From 8491ad9eb53ae5484aa6ace22192f359e261cad9 Mon Sep 17 00:00:00 2001
From: "stainless-app[bot]"
<142633134+stainless-app[bot]@users.noreply.github.com>
Date: Wed, 3 Jun 2026 21:54:03 +0000
Subject: [PATCH 042/366] feat: api: paginate GET /browser_pools
---
.stats.yml | 6 +-
api.md | 4 +-
src/kernel/resources/browser_pools.py | 83 ++++++++++++++++---
src/kernel/types/__init__.py | 2 +-
src/kernel/types/browser_pool_list_params.py | 15 ++++
.../types/browser_pool_list_response.py | 10 ---
tests/api_resources/test_browser_pools.py | 32 +++++--
7 files changed, 116 insertions(+), 36 deletions(-)
create mode 100644 src/kernel/types/browser_pool_list_params.py
delete mode 100644 src/kernel/types/browser_pool_list_response.py
diff --git a/.stats.yml b/.stats.yml
index 032ac8ce..d6e38f27 100644
--- a/.stats.yml
+++ b/.stats.yml
@@ -1,4 +1,4 @@
configured_endpoints: 117
-openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel/kernel-f3e52a8a9d9e37f8b527bdcce63b39cf8807d138cf3570c9954ba7f42ef02375.yml
-openapi_spec_hash: 2b787691eba1bb7bafb553528c09d5c0
-config_hash: e12afec516ced9520590c7a05af7a6f7
+openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel/kernel-e5524e6eeeedc614c05bf19896f1ce6c4f8a113de85c02952d6874d579651f70.yml
+openapi_spec_hash: 7815a189a50e6aa9e283213cc78d041c
+config_hash: 453b25a35834fd59a55a2315d1d50746
diff --git a/api.md b/api.md
index 1cc2b82f..8a6b8c6e 100644
--- a/api.md
+++ b/api.md
@@ -349,7 +349,7 @@ Methods:
Types:
```python
-from kernel.types import BrowserPool, BrowserPoolListResponse, BrowserPoolAcquireResponse
+from kernel.types import BrowserPool, BrowserPoolAcquireResponse
```
Methods:
@@ -357,7 +357,7 @@ Methods:
- client.browser_pools.create(\*\*params) -> BrowserPool
- client.browser_pools.retrieve(id_or_name) -> BrowserPool
- client.browser_pools.update(id_or_name, \*\*params) -> BrowserPool
-- client.browser_pools.list() -> BrowserPoolListResponse
+- client.browser_pools.list(\*\*params) -> SyncOffsetPagination[BrowserPool]
- client.browser_pools.delete(id_or_name, \*\*params) -> None
- client.browser_pools.acquire(id_or_name, \*\*params) -> BrowserPoolAcquireResponse
- client.browser_pools.flush(id_or_name) -> None
diff --git a/src/kernel/resources/browser_pools.py b/src/kernel/resources/browser_pools.py
index 64751271..203e22ad 100644
--- a/src/kernel/resources/browser_pools.py
+++ b/src/kernel/resources/browser_pools.py
@@ -7,6 +7,7 @@
import httpx
from ..types import (
+ browser_pool_list_params,
browser_pool_create_params,
browser_pool_delete_params,
browser_pool_update_params,
@@ -23,9 +24,9 @@
async_to_raw_response_wrapper,
async_to_streamed_response_wrapper,
)
-from .._base_client import make_request_options
+from ..pagination import SyncOffsetPagination, AsyncOffsetPagination
+from .._base_client import AsyncPaginator, make_request_options
from ..types.browser_pool import BrowserPool
-from ..types.browser_pool_list_response import BrowserPoolListResponse
from ..types.browser_pool_acquire_response import BrowserPoolAcquireResponse
from ..types.shared_params.browser_profile import BrowserProfile
from ..types.shared_params.browser_viewport import BrowserViewport
@@ -326,20 +327,48 @@ def update(
def list(
self,
*,
+ limit: int | Omit = omit,
+ offset: int | Omit = omit,
# Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
# The extra values given here take precedence over values defined on the client or passed to this method.
extra_headers: Headers | None = None,
extra_query: Query | None = None,
extra_body: Body | None = None,
timeout: float | httpx.Timeout | None | NotGiven = not_given,
- ) -> BrowserPoolListResponse:
- """List browser pools owned by the caller's organization."""
- return self._get(
+ ) -> SyncOffsetPagination[BrowserPool]:
+ """
+ List browser pools owned by the caller's organization.
+
+ Args:
+ limit: Limit the number of browser pools to return.
+
+ offset: Offset the number of browser pools to return.
+
+ 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
+ """
+ return self._get_api_list(
"/browser_pools",
+ page=SyncOffsetPagination[BrowserPool],
options=make_request_options(
- extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
+ extra_headers=extra_headers,
+ extra_query=extra_query,
+ extra_body=extra_body,
+ timeout=timeout,
+ query=maybe_transform(
+ {
+ "limit": limit,
+ "offset": offset,
+ },
+ browser_pool_list_params.BrowserPoolListParams,
+ ),
),
- cast_to=BrowserPoolListResponse,
+ model=BrowserPool,
)
def delete(
@@ -801,23 +830,51 @@ async def update(
cast_to=BrowserPool,
)
- async def list(
+ def list(
self,
*,
+ limit: int | Omit = omit,
+ offset: int | Omit = omit,
# Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
# The extra values given here take precedence over values defined on the client or passed to this method.
extra_headers: Headers | None = None,
extra_query: Query | None = None,
extra_body: Body | None = None,
timeout: float | httpx.Timeout | None | NotGiven = not_given,
- ) -> BrowserPoolListResponse:
- """List browser pools owned by the caller's organization."""
- return await self._get(
+ ) -> AsyncPaginator[BrowserPool, AsyncOffsetPagination[BrowserPool]]:
+ """
+ List browser pools owned by the caller's organization.
+
+ Args:
+ limit: Limit the number of browser pools to return.
+
+ offset: Offset the number of browser pools to return.
+
+ 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
+ """
+ return self._get_api_list(
"/browser_pools",
+ page=AsyncOffsetPagination[BrowserPool],
options=make_request_options(
- extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
+ extra_headers=extra_headers,
+ extra_query=extra_query,
+ extra_body=extra_body,
+ timeout=timeout,
+ query=maybe_transform(
+ {
+ "limit": limit,
+ "offset": offset,
+ },
+ browser_pool_list_params.BrowserPoolListParams,
+ ),
),
- cast_to=BrowserPoolListResponse,
+ model=BrowserPool,
)
async def delete(
diff --git a/src/kernel/types/__init__.py b/src/kernel/types/__init__.py
index bdc65fdf..ae16742d 100644
--- a/src/kernel/types/__init__.py
+++ b/src/kernel/types/__init__.py
@@ -57,6 +57,7 @@
from .extension_list_response import ExtensionListResponse as ExtensionListResponse
from .extension_upload_params import ExtensionUploadParams as ExtensionUploadParams
from .proxy_retrieve_response import ProxyRetrieveResponse as ProxyRetrieveResponse
+from .browser_pool_list_params import BrowserPoolListParams as BrowserPoolListParams
from .credential_create_params import CredentialCreateParams as CredentialCreateParams
from .credential_provider_item import CredentialProviderItem as CredentialProviderItem
from .credential_update_params import CredentialUpdateParams as CredentialUpdateParams
@@ -71,7 +72,6 @@
from .extension_upload_response import ExtensionUploadResponse as ExtensionUploadResponse
from .browser_pool_create_params import BrowserPoolCreateParams as BrowserPoolCreateParams
from .browser_pool_delete_params import BrowserPoolDeleteParams as BrowserPoolDeleteParams
-from .browser_pool_list_response import BrowserPoolListResponse as BrowserPoolListResponse
from .browser_pool_update_params import BrowserPoolUpdateParams as BrowserPoolUpdateParams
from .deployment_create_response import DeploymentCreateResponse as DeploymentCreateResponse
from .deployment_follow_response import DeploymentFollowResponse as DeploymentFollowResponse
diff --git a/src/kernel/types/browser_pool_list_params.py b/src/kernel/types/browser_pool_list_params.py
new file mode 100644
index 00000000..f8111e3e
--- /dev/null
+++ b/src/kernel/types/browser_pool_list_params.py
@@ -0,0 +1,15 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from __future__ import annotations
+
+from typing_extensions import TypedDict
+
+__all__ = ["BrowserPoolListParams"]
+
+
+class BrowserPoolListParams(TypedDict, total=False):
+ limit: int
+ """Limit the number of browser pools to return."""
+
+ offset: int
+ """Offset the number of browser pools to return."""
diff --git a/src/kernel/types/browser_pool_list_response.py b/src/kernel/types/browser_pool_list_response.py
deleted file mode 100644
index a11c4de2..00000000
--- a/src/kernel/types/browser_pool_list_response.py
+++ /dev/null
@@ -1,10 +0,0 @@
-# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
-
-from typing import List
-from typing_extensions import TypeAlias
-
-from .browser_pool import BrowserPool
-
-__all__ = ["BrowserPoolListResponse"]
-
-BrowserPoolListResponse: TypeAlias = List[BrowserPool]
diff --git a/tests/api_resources/test_browser_pools.py b/tests/api_resources/test_browser_pools.py
index 42f47e63..c71e6c27 100644
--- a/tests/api_resources/test_browser_pools.py
+++ b/tests/api_resources/test_browser_pools.py
@@ -11,9 +11,9 @@
from tests.utils import assert_matches_type
from kernel.types import (
BrowserPool,
- BrowserPoolListResponse,
BrowserPoolAcquireResponse,
)
+from kernel.pagination import SyncOffsetPagination, AsyncOffsetPagination
base_url = os.environ.get("TEST_API_BASE_URL", "http://127.0.0.1:4010")
@@ -211,7 +211,16 @@ def test_path_params_update(self, client: Kernel) -> None:
@parametrize
def test_method_list(self, client: Kernel) -> None:
browser_pool = client.browser_pools.list()
- assert_matches_type(BrowserPoolListResponse, browser_pool, path=["response"])
+ assert_matches_type(SyncOffsetPagination[BrowserPool], browser_pool, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ def test_method_list_with_all_params(self, client: Kernel) -> None:
+ browser_pool = client.browser_pools.list(
+ limit=1,
+ offset=0,
+ )
+ assert_matches_type(SyncOffsetPagination[BrowserPool], browser_pool, path=["response"])
@pytest.mark.skip(reason="Mock server tests are disabled")
@parametrize
@@ -221,7 +230,7 @@ def test_raw_response_list(self, client: Kernel) -> None:
assert response.is_closed is True
assert response.http_request.headers.get("X-Stainless-Lang") == "python"
browser_pool = response.parse()
- assert_matches_type(BrowserPoolListResponse, browser_pool, path=["response"])
+ assert_matches_type(SyncOffsetPagination[BrowserPool], browser_pool, path=["response"])
@pytest.mark.skip(reason="Mock server tests are disabled")
@parametrize
@@ -231,7 +240,7 @@ def test_streaming_response_list(self, client: Kernel) -> None:
assert response.http_request.headers.get("X-Stainless-Lang") == "python"
browser_pool = response.parse()
- assert_matches_type(BrowserPoolListResponse, browser_pool, path=["response"])
+ assert_matches_type(SyncOffsetPagination[BrowserPool], browser_pool, path=["response"])
assert cast(Any, response.is_closed) is True
@@ -631,7 +640,16 @@ async def test_path_params_update(self, async_client: AsyncKernel) -> None:
@parametrize
async def test_method_list(self, async_client: AsyncKernel) -> None:
browser_pool = await async_client.browser_pools.list()
- assert_matches_type(BrowserPoolListResponse, browser_pool, path=["response"])
+ assert_matches_type(AsyncOffsetPagination[BrowserPool], browser_pool, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ async def test_method_list_with_all_params(self, async_client: AsyncKernel) -> None:
+ browser_pool = await async_client.browser_pools.list(
+ limit=1,
+ offset=0,
+ )
+ assert_matches_type(AsyncOffsetPagination[BrowserPool], browser_pool, path=["response"])
@pytest.mark.skip(reason="Mock server tests are disabled")
@parametrize
@@ -641,7 +659,7 @@ async def test_raw_response_list(self, async_client: AsyncKernel) -> None:
assert response.is_closed is True
assert response.http_request.headers.get("X-Stainless-Lang") == "python"
browser_pool = await response.parse()
- assert_matches_type(BrowserPoolListResponse, browser_pool, path=["response"])
+ assert_matches_type(AsyncOffsetPagination[BrowserPool], browser_pool, path=["response"])
@pytest.mark.skip(reason="Mock server tests are disabled")
@parametrize
@@ -651,7 +669,7 @@ async def test_streaming_response_list(self, async_client: AsyncKernel) -> None:
assert response.http_request.headers.get("X-Stainless-Lang") == "python"
browser_pool = await response.parse()
- assert_matches_type(BrowserPoolListResponse, browser_pool, path=["response"])
+ assert_matches_type(AsyncOffsetPagination[BrowserPool], browser_pool, path=["response"])
assert cast(Any, response.is_closed) is True
From a0f7966d9eb2e70d5cecc38e0d79a334ddaf4227 Mon Sep 17 00:00:00 2001
From: "stainless-app[bot]"
<142633134+stainless-app[bot]@users.noreply.github.com>
Date: Thu, 4 Jun 2026 14:29:10 +0000
Subject: [PATCH 043/366] feat: api: paginate GET /extensions
---
.stats.yml | 6 +-
api.md | 2 +-
src/kernel/resources/extensions.py | 83 +++++++++++++++++----
src/kernel/types/__init__.py | 1 +
src/kernel/types/extension_list_params.py | 15 ++++
src/kernel/types/extension_list_response.py | 10 +--
tests/api_resources/test_extensions.py | 31 ++++++--
7 files changed, 118 insertions(+), 30 deletions(-)
create mode 100644 src/kernel/types/extension_list_params.py
diff --git a/.stats.yml b/.stats.yml
index d6e38f27..582e6b44 100644
--- a/.stats.yml
+++ b/.stats.yml
@@ -1,4 +1,4 @@
configured_endpoints: 117
-openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel/kernel-e5524e6eeeedc614c05bf19896f1ce6c4f8a113de85c02952d6874d579651f70.yml
-openapi_spec_hash: 7815a189a50e6aa9e283213cc78d041c
-config_hash: 453b25a35834fd59a55a2315d1d50746
+openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel/kernel-9a76e7a837ac9ffbe0e9557d618ce9b676e052249e364e516c28ddb33b912019.yml
+openapi_spec_hash: c0e5412cc95139f11a8b8635559bb985
+config_hash: 098a7f3fd134c8e4cbd3a44750892a18
diff --git a/api.md b/api.md
index 8a6b8c6e..6001d530 100644
--- a/api.md
+++ b/api.md
@@ -338,7 +338,7 @@ from kernel.types import ExtensionListResponse, ExtensionUploadResponse
Methods:
-- client.extensions.list() -> ExtensionListResponse
+- client.extensions.list(\*\*params) -> SyncOffsetPagination[ExtensionListResponse]
- client.extensions.delete(id_or_name) -> None
- client.extensions.download(id_or_name) -> BinaryAPIResponse
- client.extensions.download_from_chrome_store(\*\*params) -> BinaryAPIResponse
diff --git a/src/kernel/resources/extensions.py b/src/kernel/resources/extensions.py
index d5cf0eb7..5b7215be 100644
--- a/src/kernel/resources/extensions.py
+++ b/src/kernel/resources/extensions.py
@@ -7,7 +7,7 @@
import httpx
-from ..types import extension_upload_params, extension_download_from_chrome_store_params
+from ..types import extension_list_params, extension_upload_params, extension_download_from_chrome_store_params
from .._files import deepcopy_with_paths
from .._types import Body, Omit, Query, Headers, NoneType, NotGiven, FileTypes, omit, not_given
from .._utils import extract_files, path_template, maybe_transform, async_maybe_transform
@@ -27,7 +27,8 @@
async_to_custom_raw_response_wrapper,
async_to_custom_streamed_response_wrapper,
)
-from .._base_client import make_request_options
+from ..pagination import SyncOffsetPagination, AsyncOffsetPagination
+from .._base_client import AsyncPaginator, make_request_options
from ..types.extension_list_response import ExtensionListResponse
from ..types.extension_upload_response import ExtensionUploadResponse
@@ -59,20 +60,48 @@ def with_streaming_response(self) -> ExtensionsResourceWithStreamingResponse:
def list(
self,
*,
+ limit: int | Omit = omit,
+ offset: int | Omit = omit,
# Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
# The extra values given here take precedence over values defined on the client or passed to this method.
extra_headers: Headers | None = None,
extra_query: Query | None = None,
extra_body: Body | None = None,
timeout: float | httpx.Timeout | None | NotGiven = not_given,
- ) -> ExtensionListResponse:
- """List extensions owned by the caller's organization."""
- return self._get(
+ ) -> SyncOffsetPagination[ExtensionListResponse]:
+ """
+ List extensions owned by the caller's organization.
+
+ Args:
+ limit: Limit the number of extensions to return.
+
+ offset: Offset the number of extensions to return.
+
+ 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
+ """
+ return self._get_api_list(
"/extensions",
+ page=SyncOffsetPagination[ExtensionListResponse],
options=make_request_options(
- extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
+ extra_headers=extra_headers,
+ extra_query=extra_query,
+ extra_body=extra_body,
+ timeout=timeout,
+ query=maybe_transform(
+ {
+ "limit": limit,
+ "offset": offset,
+ },
+ extension_list_params.ExtensionListParams,
+ ),
),
- cast_to=ExtensionListResponse,
+ model=ExtensionListResponse,
)
def delete(
@@ -266,23 +295,51 @@ def with_streaming_response(self) -> AsyncExtensionsResourceWithStreamingRespons
"""
return AsyncExtensionsResourceWithStreamingResponse(self)
- async def list(
+ def list(
self,
*,
+ limit: int | Omit = omit,
+ offset: int | Omit = omit,
# Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
# The extra values given here take precedence over values defined on the client or passed to this method.
extra_headers: Headers | None = None,
extra_query: Query | None = None,
extra_body: Body | None = None,
timeout: float | httpx.Timeout | None | NotGiven = not_given,
- ) -> ExtensionListResponse:
- """List extensions owned by the caller's organization."""
- return await self._get(
+ ) -> AsyncPaginator[ExtensionListResponse, AsyncOffsetPagination[ExtensionListResponse]]:
+ """
+ List extensions owned by the caller's organization.
+
+ Args:
+ limit: Limit the number of extensions to return.
+
+ offset: Offset the number of extensions to return.
+
+ 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
+ """
+ return self._get_api_list(
"/extensions",
+ page=AsyncOffsetPagination[ExtensionListResponse],
options=make_request_options(
- extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
+ extra_headers=extra_headers,
+ extra_query=extra_query,
+ extra_body=extra_body,
+ timeout=timeout,
+ query=maybe_transform(
+ {
+ "limit": limit,
+ "offset": offset,
+ },
+ extension_list_params.ExtensionListParams,
+ ),
),
- cast_to=ExtensionListResponse,
+ model=ExtensionListResponse,
)
async def delete(
diff --git a/src/kernel/types/__init__.py b/src/kernel/types/__init__.py
index ae16742d..ef27f493 100644
--- a/src/kernel/types/__init__.py
+++ b/src/kernel/types/__init__.py
@@ -42,6 +42,7 @@
from .browser_curl_response import BrowserCurlResponse as BrowserCurlResponse
from .browser_list_response import BrowserListResponse as BrowserListResponse
from .browser_update_params import BrowserUpdateParams as BrowserUpdateParams
+from .extension_list_params import ExtensionListParams as ExtensionListParams
from .profile_create_params import ProfileCreateParams as ProfileCreateParams
from .project_create_params import ProjectCreateParams as ProjectCreateParams
from .project_update_params import ProjectUpdateParams as ProjectUpdateParams
diff --git a/src/kernel/types/extension_list_params.py b/src/kernel/types/extension_list_params.py
new file mode 100644
index 00000000..bccd61eb
--- /dev/null
+++ b/src/kernel/types/extension_list_params.py
@@ -0,0 +1,15 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from __future__ import annotations
+
+from typing_extensions import TypedDict
+
+__all__ = ["ExtensionListParams"]
+
+
+class ExtensionListParams(TypedDict, total=False):
+ limit: int
+ """Limit the number of extensions to return."""
+
+ offset: int
+ """Offset the number of extensions to return."""
diff --git a/src/kernel/types/extension_list_response.py b/src/kernel/types/extension_list_response.py
index bf9e544d..6bb33561 100644
--- a/src/kernel/types/extension_list_response.py
+++ b/src/kernel/types/extension_list_response.py
@@ -1,15 +1,14 @@
# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
-from typing import List, Optional
+from typing import Optional
from datetime import datetime
-from typing_extensions import TypeAlias
from .._models import BaseModel
-__all__ = ["ExtensionListResponse", "ExtensionListResponseItem"]
+__all__ = ["ExtensionListResponse"]
-class ExtensionListResponseItem(BaseModel):
+class ExtensionListResponse(BaseModel):
"""A browser extension uploaded to Kernel."""
id: str
@@ -29,6 +28,3 @@ class ExtensionListResponseItem(BaseModel):
Must be unique within the project.
"""
-
-
-ExtensionListResponse: TypeAlias = List[ExtensionListResponseItem]
diff --git a/tests/api_resources/test_extensions.py b/tests/api_resources/test_extensions.py
index 4a28fb5e..302ba958 100644
--- a/tests/api_resources/test_extensions.py
+++ b/tests/api_resources/test_extensions.py
@@ -21,6 +21,7 @@
StreamedBinaryAPIResponse,
AsyncStreamedBinaryAPIResponse,
)
+from kernel.pagination import SyncOffsetPagination, AsyncOffsetPagination
base_url = os.environ.get("TEST_API_BASE_URL", "http://127.0.0.1:4010")
@@ -32,7 +33,16 @@ class TestExtensions:
@parametrize
def test_method_list(self, client: Kernel) -> None:
extension = client.extensions.list()
- assert_matches_type(ExtensionListResponse, extension, path=["response"])
+ assert_matches_type(SyncOffsetPagination[ExtensionListResponse], extension, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ def test_method_list_with_all_params(self, client: Kernel) -> None:
+ extension = client.extensions.list(
+ limit=1,
+ offset=0,
+ )
+ assert_matches_type(SyncOffsetPagination[ExtensionListResponse], extension, path=["response"])
@pytest.mark.skip(reason="Mock server tests are disabled")
@parametrize
@@ -42,7 +52,7 @@ def test_raw_response_list(self, client: Kernel) -> None:
assert response.is_closed is True
assert response.http_request.headers.get("X-Stainless-Lang") == "python"
extension = response.parse()
- assert_matches_type(ExtensionListResponse, extension, path=["response"])
+ assert_matches_type(SyncOffsetPagination[ExtensionListResponse], extension, path=["response"])
@pytest.mark.skip(reason="Mock server tests are disabled")
@parametrize
@@ -52,7 +62,7 @@ def test_streaming_response_list(self, client: Kernel) -> None:
assert response.http_request.headers.get("X-Stainless-Lang") == "python"
extension = response.parse()
- assert_matches_type(ExtensionListResponse, extension, path=["response"])
+ assert_matches_type(SyncOffsetPagination[ExtensionListResponse], extension, path=["response"])
assert cast(Any, response.is_closed) is True
@@ -256,7 +266,16 @@ class TestAsyncExtensions:
@parametrize
async def test_method_list(self, async_client: AsyncKernel) -> None:
extension = await async_client.extensions.list()
- assert_matches_type(ExtensionListResponse, extension, path=["response"])
+ assert_matches_type(AsyncOffsetPagination[ExtensionListResponse], extension, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ async def test_method_list_with_all_params(self, async_client: AsyncKernel) -> None:
+ extension = await async_client.extensions.list(
+ limit=1,
+ offset=0,
+ )
+ assert_matches_type(AsyncOffsetPagination[ExtensionListResponse], extension, path=["response"])
@pytest.mark.skip(reason="Mock server tests are disabled")
@parametrize
@@ -266,7 +285,7 @@ async def test_raw_response_list(self, async_client: AsyncKernel) -> None:
assert response.is_closed is True
assert response.http_request.headers.get("X-Stainless-Lang") == "python"
extension = await response.parse()
- assert_matches_type(ExtensionListResponse, extension, path=["response"])
+ assert_matches_type(AsyncOffsetPagination[ExtensionListResponse], extension, path=["response"])
@pytest.mark.skip(reason="Mock server tests are disabled")
@parametrize
@@ -276,7 +295,7 @@ async def test_streaming_response_list(self, async_client: AsyncKernel) -> None:
assert response.http_request.headers.get("X-Stainless-Lang") == "python"
extension = await response.parse()
- assert_matches_type(ExtensionListResponse, extension, path=["response"])
+ assert_matches_type(AsyncOffsetPagination[ExtensionListResponse], extension, path=["response"])
assert cast(Any, response.is_closed) is True
From 8db458d8ac205e8e373c6f4557554b0a6c8f7860 Mon Sep 17 00:00:00 2001
From: "stainless-app[bot]"
<142633134+stainless-app[bot]@users.noreply.github.com>
Date: Thu, 4 Jun 2026 15:04:06 +0000
Subject: [PATCH 044/366] feat: api: paginate GET /org/credential_providers
---
.stats.yml | 6 +-
api.md | 3 +-
src/kernel/resources/credential_providers.py | 88 ++++++++++++++++---
src/kernel/types/__init__.py | 2 +-
.../types/credential_provider_list_params.py | 15 ++++
.../credential_provider_list_response.py | 10 ---
.../test_credential_providers.py | 32 +++++--
7 files changed, 119 insertions(+), 37 deletions(-)
create mode 100644 src/kernel/types/credential_provider_list_params.py
delete mode 100644 src/kernel/types/credential_provider_list_response.py
diff --git a/.stats.yml b/.stats.yml
index 582e6b44..5ea72073 100644
--- a/.stats.yml
+++ b/.stats.yml
@@ -1,4 +1,4 @@
configured_endpoints: 117
-openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel/kernel-9a76e7a837ac9ffbe0e9557d618ce9b676e052249e364e516c28ddb33b912019.yml
-openapi_spec_hash: c0e5412cc95139f11a8b8635559bb985
-config_hash: 098a7f3fd134c8e4cbd3a44750892a18
+openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel/kernel-e6c711f0d29a7d956cc8ca621440da966c1f1575b1205d01328b1099edf1c517.yml
+openapi_spec_hash: c06e7e36de1c6f9b29b54a6e3dc08ee5
+config_hash: 36159c262d293fbeacf513ab600a1729
diff --git a/api.md b/api.md
index 6001d530..8f9df345 100644
--- a/api.md
+++ b/api.md
@@ -441,7 +441,6 @@ from kernel.types import (
CredentialProviderItem,
CredentialProviderTestResult,
UpdateCredentialProviderRequest,
- CredentialProviderListResponse,
CredentialProviderListItemsResponse,
)
```
@@ -451,7 +450,7 @@ Methods:
- client.credential_providers.create(\*\*params) -> CredentialProvider
- client.credential_providers.retrieve(id) -> CredentialProvider
- client.credential_providers.update(id, \*\*params) -> CredentialProvider
-- client.credential_providers.list() -> CredentialProviderListResponse
+- client.credential_providers.list(\*\*params) -> SyncOffsetPagination[CredentialProvider]
- client.credential_providers.delete(id) -> None
- client.credential_providers.list_items(id) -> CredentialProviderListItemsResponse
- client.credential_providers.test(id) -> CredentialProviderTestResult
diff --git a/src/kernel/resources/credential_providers.py b/src/kernel/resources/credential_providers.py
index 2dede2c4..06bf2aee 100644
--- a/src/kernel/resources/credential_providers.py
+++ b/src/kernel/resources/credential_providers.py
@@ -6,7 +6,11 @@
import httpx
-from ..types import credential_provider_create_params, credential_provider_update_params
+from ..types import (
+ credential_provider_list_params,
+ credential_provider_create_params,
+ credential_provider_update_params,
+)
from .._types import Body, Omit, Query, Headers, NoneType, NotGiven, omit, not_given
from .._utils import path_template, maybe_transform, async_maybe_transform
from .._compat import cached_property
@@ -17,10 +21,10 @@
async_to_raw_response_wrapper,
async_to_streamed_response_wrapper,
)
-from .._base_client import make_request_options
+from ..pagination import SyncOffsetPagination, AsyncOffsetPagination
+from .._base_client import AsyncPaginator, make_request_options
from ..types.credential_provider import CredentialProvider
from ..types.credential_provider_test_result import CredentialProviderTestResult
-from ..types.credential_provider_list_response import CredentialProviderListResponse
from ..types.credential_provider_list_items_response import CredentialProviderListItemsResponse
__all__ = ["CredentialProvidersResource", "AsyncCredentialProvidersResource"]
@@ -194,20 +198,48 @@ def update(
def list(
self,
*,
+ limit: int | Omit = omit,
+ offset: int | Omit = omit,
# Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
# The extra values given here take precedence over values defined on the client or passed to this method.
extra_headers: Headers | None = None,
extra_query: Query | None = None,
extra_body: Body | None = None,
timeout: float | httpx.Timeout | None | NotGiven = not_given,
- ) -> CredentialProviderListResponse:
- """List external credential providers configured for the organization."""
- return self._get(
+ ) -> SyncOffsetPagination[CredentialProvider]:
+ """
+ List external credential providers configured for the organization.
+
+ Args:
+ limit: Limit the number of credential providers to return.
+
+ offset: Offset the number of credential providers to return.
+
+ 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
+ """
+ return self._get_api_list(
"/org/credential_providers",
+ page=SyncOffsetPagination[CredentialProvider],
options=make_request_options(
- extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
+ extra_headers=extra_headers,
+ extra_query=extra_query,
+ extra_body=extra_body,
+ timeout=timeout,
+ query=maybe_transform(
+ {
+ "limit": limit,
+ "offset": offset,
+ },
+ credential_provider_list_params.CredentialProviderListParams,
+ ),
),
- cast_to=CredentialProviderListResponse,
+ model=CredentialProvider,
)
def delete(
@@ -477,23 +509,51 @@ async def update(
cast_to=CredentialProvider,
)
- async def list(
+ def list(
self,
*,
+ limit: int | Omit = omit,
+ offset: int | Omit = omit,
# Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
# The extra values given here take precedence over values defined on the client or passed to this method.
extra_headers: Headers | None = None,
extra_query: Query | None = None,
extra_body: Body | None = None,
timeout: float | httpx.Timeout | None | NotGiven = not_given,
- ) -> CredentialProviderListResponse:
- """List external credential providers configured for the organization."""
- return await self._get(
+ ) -> AsyncPaginator[CredentialProvider, AsyncOffsetPagination[CredentialProvider]]:
+ """
+ List external credential providers configured for the organization.
+
+ Args:
+ limit: Limit the number of credential providers to return.
+
+ offset: Offset the number of credential providers to return.
+
+ 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
+ """
+ return self._get_api_list(
"/org/credential_providers",
+ page=AsyncOffsetPagination[CredentialProvider],
options=make_request_options(
- extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
+ extra_headers=extra_headers,
+ extra_query=extra_query,
+ extra_body=extra_body,
+ timeout=timeout,
+ query=maybe_transform(
+ {
+ "limit": limit,
+ "offset": offset,
+ },
+ credential_provider_list_params.CredentialProviderListParams,
+ ),
),
- cast_to=CredentialProviderListResponse,
+ model=CredentialProvider,
)
async def delete(
diff --git a/src/kernel/types/__init__.py b/src/kernel/types/__init__.py
index ef27f493..7932162d 100644
--- a/src/kernel/types/__init__.py
+++ b/src/kernel/types/__init__.py
@@ -86,9 +86,9 @@
from .browser_pool_acquire_response import BrowserPoolAcquireResponse as BrowserPoolAcquireResponse
from .credential_totp_code_response import CredentialTotpCodeResponse as CredentialTotpCodeResponse
from .browser_load_extensions_params import BrowserLoadExtensionsParams as BrowserLoadExtensionsParams
+from .credential_provider_list_params import CredentialProviderListParams as CredentialProviderListParams
from .credential_provider_test_result import CredentialProviderTestResult as CredentialProviderTestResult
from .credential_provider_create_params import CredentialProviderCreateParams as CredentialProviderCreateParams
-from .credential_provider_list_response import CredentialProviderListResponse as CredentialProviderListResponse
from .credential_provider_update_params import CredentialProviderUpdateParams as CredentialProviderUpdateParams
from .invocation_list_browsers_response import InvocationListBrowsersResponse as InvocationListBrowsersResponse
from .credential_provider_list_items_response import (
diff --git a/src/kernel/types/credential_provider_list_params.py b/src/kernel/types/credential_provider_list_params.py
new file mode 100644
index 00000000..87dbfd69
--- /dev/null
+++ b/src/kernel/types/credential_provider_list_params.py
@@ -0,0 +1,15 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from __future__ import annotations
+
+from typing_extensions import TypedDict
+
+__all__ = ["CredentialProviderListParams"]
+
+
+class CredentialProviderListParams(TypedDict, total=False):
+ limit: int
+ """Limit the number of credential providers to return."""
+
+ offset: int
+ """Offset the number of credential providers to return."""
diff --git a/src/kernel/types/credential_provider_list_response.py b/src/kernel/types/credential_provider_list_response.py
deleted file mode 100644
index 59814e03..00000000
--- a/src/kernel/types/credential_provider_list_response.py
+++ /dev/null
@@ -1,10 +0,0 @@
-# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
-
-from typing import List
-from typing_extensions import TypeAlias
-
-from .credential_provider import CredentialProvider
-
-__all__ = ["CredentialProviderListResponse"]
-
-CredentialProviderListResponse: TypeAlias = List[CredentialProvider]
diff --git a/tests/api_resources/test_credential_providers.py b/tests/api_resources/test_credential_providers.py
index 93f44145..d9795332 100644
--- a/tests/api_resources/test_credential_providers.py
+++ b/tests/api_resources/test_credential_providers.py
@@ -12,9 +12,9 @@
from kernel.types import (
CredentialProvider,
CredentialProviderTestResult,
- CredentialProviderListResponse,
CredentialProviderListItemsResponse,
)
+from kernel.pagination import SyncOffsetPagination, AsyncOffsetPagination
base_url = os.environ.get("TEST_API_BASE_URL", "http://127.0.0.1:4010")
@@ -174,7 +174,16 @@ def test_path_params_update(self, client: Kernel) -> None:
@parametrize
def test_method_list(self, client: Kernel) -> None:
credential_provider = client.credential_providers.list()
- assert_matches_type(CredentialProviderListResponse, credential_provider, path=["response"])
+ assert_matches_type(SyncOffsetPagination[CredentialProvider], credential_provider, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ def test_method_list_with_all_params(self, client: Kernel) -> None:
+ credential_provider = client.credential_providers.list(
+ limit=1,
+ offset=0,
+ )
+ assert_matches_type(SyncOffsetPagination[CredentialProvider], credential_provider, path=["response"])
@pytest.mark.skip(reason="Mock server tests are disabled")
@parametrize
@@ -184,7 +193,7 @@ def test_raw_response_list(self, client: Kernel) -> None:
assert response.is_closed is True
assert response.http_request.headers.get("X-Stainless-Lang") == "python"
credential_provider = response.parse()
- assert_matches_type(CredentialProviderListResponse, credential_provider, path=["response"])
+ assert_matches_type(SyncOffsetPagination[CredentialProvider], credential_provider, path=["response"])
@pytest.mark.skip(reason="Mock server tests are disabled")
@parametrize
@@ -194,7 +203,7 @@ def test_streaming_response_list(self, client: Kernel) -> None:
assert response.http_request.headers.get("X-Stainless-Lang") == "python"
credential_provider = response.parse()
- assert_matches_type(CredentialProviderListResponse, credential_provider, path=["response"])
+ assert_matches_type(SyncOffsetPagination[CredentialProvider], credential_provider, path=["response"])
assert cast(Any, response.is_closed) is True
@@ -482,7 +491,16 @@ async def test_path_params_update(self, async_client: AsyncKernel) -> None:
@parametrize
async def test_method_list(self, async_client: AsyncKernel) -> None:
credential_provider = await async_client.credential_providers.list()
- assert_matches_type(CredentialProviderListResponse, credential_provider, path=["response"])
+ assert_matches_type(AsyncOffsetPagination[CredentialProvider], credential_provider, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ async def test_method_list_with_all_params(self, async_client: AsyncKernel) -> None:
+ credential_provider = await async_client.credential_providers.list(
+ limit=1,
+ offset=0,
+ )
+ assert_matches_type(AsyncOffsetPagination[CredentialProvider], credential_provider, path=["response"])
@pytest.mark.skip(reason="Mock server tests are disabled")
@parametrize
@@ -492,7 +510,7 @@ async def test_raw_response_list(self, async_client: AsyncKernel) -> None:
assert response.is_closed is True
assert response.http_request.headers.get("X-Stainless-Lang") == "python"
credential_provider = await response.parse()
- assert_matches_type(CredentialProviderListResponse, credential_provider, path=["response"])
+ assert_matches_type(AsyncOffsetPagination[CredentialProvider], credential_provider, path=["response"])
@pytest.mark.skip(reason="Mock server tests are disabled")
@parametrize
@@ -502,7 +520,7 @@ async def test_streaming_response_list(self, async_client: AsyncKernel) -> None:
assert response.http_request.headers.get("X-Stainless-Lang") == "python"
credential_provider = await response.parse()
- assert_matches_type(CredentialProviderListResponse, credential_provider, path=["response"])
+ assert_matches_type(AsyncOffsetPagination[CredentialProvider], credential_provider, path=["response"])
assert cast(Any, response.is_closed) is True
From 3e647a8fe845e78724d6d4d4306728d81e665bff Mon Sep 17 00:00:00 2001
From: "stainless-app[bot]"
<142633134+stainless-app[bot]@users.noreply.github.com>
Date: Thu, 4 Jun 2026 15:04:39 +0000
Subject: [PATCH 045/366] release: 0.62.0
---
.release-please-manifest.json | 2 +-
CHANGELOG.md | 11 +++++++++++
pyproject.toml | 2 +-
src/kernel/_version.py | 2 +-
4 files changed, 14 insertions(+), 3 deletions(-)
diff --git a/.release-please-manifest.json b/.release-please-manifest.json
index f7e971ea..fdce8724 100644
--- a/.release-please-manifest.json
+++ b/.release-please-manifest.json
@@ -1,3 +1,3 @@
{
- ".": "0.61.0"
+ ".": "0.62.0"
}
\ No newline at end of file
diff --git a/CHANGELOG.md b/CHANGELOG.md
index fbffa57e..bbd58123 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,16 @@
# Changelog
+## 0.62.0 (2026-06-04)
+
+Full Changelog: [v0.61.0...v0.62.0](https://github.com/kernel/kernel-python-sdk/compare/v0.61.0...v0.62.0)
+
+### Features
+
+* api: paginate GET /browser_pools ([c25d693](https://github.com/kernel/kernel-python-sdk/commit/c25d693e310a1035d74654018f68b311f37545d0))
+* api: paginate GET /extensions ([55c3324](https://github.com/kernel/kernel-python-sdk/commit/55c332493f9c7efd21ef8a2fdae40c8318e704cb))
+* api: paginate GET /org/credential_providers ([01897bc](https://github.com/kernel/kernel-python-sdk/commit/01897bc9f7215580aa2f2ca41bfcf1b4d29e9005))
+* api: paginate GET /proxies ([7ff0bdf](https://github.com/kernel/kernel-python-sdk/commit/7ff0bdf181029f0711b694fb6498fbc75183b0f3))
+
## 0.61.0 (2026-06-03)
Full Changelog: [v0.60.0...v0.61.0](https://github.com/kernel/kernel-python-sdk/compare/v0.60.0...v0.61.0)
diff --git a/pyproject.toml b/pyproject.toml
index 2b9342ba..f246f34a 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -1,6 +1,6 @@
[project]
name = "kernel"
-version = "0.61.0"
+version = "0.62.0"
description = "The official Python library for the kernel API"
dynamic = ["readme"]
license = "Apache-2.0"
diff --git a/src/kernel/_version.py b/src/kernel/_version.py
index fc502359..785c4450 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.61.0" # x-release-please-version
+__version__ = "0.62.0" # x-release-please-version
From 86c57b88919ccd1e642bd9012062be8ab6e1865b Mon Sep 17 00:00:00 2001
From: "stainless-app[bot]"
<142633134+stainless-app[bot]@users.noreply.github.com>
Date: Thu, 4 Jun 2026 19:39:12 +0000
Subject: [PATCH 046/366] feat(api): allow setting a custom name on a browser
session at create time
---
.stats.yml | 4 ++--
src/kernel/resources/browsers/browsers.py | 16 ++++++++++++++--
src/kernel/types/browser_create_params.py | 7 +++++++
src/kernel/types/browser_create_response.py | 3 +++
src/kernel/types/browser_list_params.py | 2 +-
src/kernel/types/browser_list_response.py | 3 +++
.../types/browser_pool_acquire_response.py | 3 +++
src/kernel/types/browser_retrieve_response.py | 3 +++
src/kernel/types/browser_update_response.py | 3 +++
.../types/invocation_list_browsers_response.py | 3 +++
tests/api_resources/test_browsers.py | 2 ++
11 files changed, 44 insertions(+), 5 deletions(-)
diff --git a/.stats.yml b/.stats.yml
index 5ea72073..b8e42864 100644
--- a/.stats.yml
+++ b/.stats.yml
@@ -1,4 +1,4 @@
configured_endpoints: 117
-openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel/kernel-e6c711f0d29a7d956cc8ca621440da966c1f1575b1205d01328b1099edf1c517.yml
-openapi_spec_hash: c06e7e36de1c6f9b29b54a6e3dc08ee5
+openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel/kernel-e7925f5cb9626a47fd24470851ff4149c27bc89795f75e8641c618fff93a076c.yml
+openapi_spec_hash: 955d4bf81e9838b0732c0e52716a2030
config_hash: 36159c262d293fbeacf513ab600a1729
diff --git a/src/kernel/resources/browsers/browsers.py b/src/kernel/resources/browsers/browsers.py
index 801cd152..b439fdb3 100644
--- a/src/kernel/resources/browsers/browsers.py
+++ b/src/kernel/resources/browsers/browsers.py
@@ -169,6 +169,7 @@ def create(
headless: bool | Omit = omit,
invocation_id: str | Omit = omit,
kiosk_mode: bool | Omit = omit,
+ name: str | Omit = omit,
profile: BrowserProfile | Omit = omit,
proxy_id: str | Omit = omit,
start_url: str | Omit = omit,
@@ -205,6 +206,10 @@ def create(
kiosk_mode: If true, launches the browser in kiosk mode to hide address bar and tabs in live
view.
+ name: Optional human-readable name for the browser session, used to find it later in
+ the dashboard. Must be unique among active sessions within the project. Set at
+ creation time only.
+
profile: Profile selection for the browser session. Provide either id or name. If
specified, the matching profile will be loaded into the browser session.
Profiles must be created beforehand.
@@ -262,6 +267,7 @@ def create(
"headless": headless,
"invocation_id": invocation_id,
"kiosk_mode": kiosk_mode,
+ "name": name,
"profile": profile,
"proxy_id": proxy_id,
"start_url": start_url,
@@ -413,7 +419,7 @@ def list(
offset: Number of results to skip. Defaults to 0.
- query: Search browsers by session ID, profile ID, proxy ID, or pool name.
+ query: Search browsers by name, session ID, profile ID, proxy ID, or pool name.
status: Filter sessions by status. "active" returns only active sessions (default),
"deleted" returns only soft-deleted sessions, "all" returns both.
@@ -717,6 +723,7 @@ async def create(
headless: bool | Omit = omit,
invocation_id: str | Omit = omit,
kiosk_mode: bool | Omit = omit,
+ name: str | Omit = omit,
profile: BrowserProfile | Omit = omit,
proxy_id: str | Omit = omit,
start_url: str | Omit = omit,
@@ -753,6 +760,10 @@ async def create(
kiosk_mode: If true, launches the browser in kiosk mode to hide address bar and tabs in live
view.
+ name: Optional human-readable name for the browser session, used to find it later in
+ the dashboard. Must be unique among active sessions within the project. Set at
+ creation time only.
+
profile: Profile selection for the browser session. Provide either id or name. If
specified, the matching profile will be loaded into the browser session.
Profiles must be created beforehand.
@@ -810,6 +821,7 @@ async def create(
"headless": headless,
"invocation_id": invocation_id,
"kiosk_mode": kiosk_mode,
+ "name": name,
"profile": profile,
"proxy_id": proxy_id,
"start_url": start_url,
@@ -961,7 +973,7 @@ def list(
offset: Number of results to skip. Defaults to 0.
- query: Search browsers by session ID, profile ID, proxy ID, or pool name.
+ query: Search browsers by name, session ID, profile ID, proxy ID, or pool name.
status: Filter sessions by status. "active" returns only active sessions (default),
"deleted" returns only soft-deleted sessions, "all" returns both.
diff --git a/src/kernel/types/browser_create_params.py b/src/kernel/types/browser_create_params.py
index c94071cf..de6ba5dd 100644
--- a/src/kernel/types/browser_create_params.py
+++ b/src/kernel/types/browser_create_params.py
@@ -49,6 +49,13 @@ class BrowserCreateParams(TypedDict, total=False):
view.
"""
+ name: str
+ """
+ Optional human-readable name for the browser session, used to find it later in
+ the dashboard. Must be unique among active sessions within the project. Set at
+ creation time only.
+ """
+
profile: BrowserProfile
"""Profile selection for the browser session.
diff --git a/src/kernel/types/browser_create_response.py b/src/kernel/types/browser_create_response.py
index 651216ab..27da0664 100644
--- a/src/kernel/types/browser_create_response.py
+++ b/src/kernel/types/browser_create_response.py
@@ -63,6 +63,9 @@ class BrowserCreateResponse(BaseModel):
kiosk_mode: Optional[bool] = None
"""Whether the browser session is running in kiosk mode."""
+ name: Optional[str] = None
+ """Human-readable name of the browser session, if one was set at creation."""
+
pool: Optional[BrowserPoolRef] = None
"""Browser pool this session was acquired from, if any."""
diff --git a/src/kernel/types/browser_list_params.py b/src/kernel/types/browser_list_params.py
index d659b8cb..2761e83b 100644
--- a/src/kernel/types/browser_list_params.py
+++ b/src/kernel/types/browser_list_params.py
@@ -22,7 +22,7 @@ class BrowserListParams(TypedDict, total=False):
"""Number of results to skip. Defaults to 0."""
query: str
- """Search browsers by session ID, profile ID, proxy ID, or pool name."""
+ """Search browsers by name, session ID, profile ID, proxy ID, or pool name."""
status: Literal["active", "deleted", "all"]
"""Filter sessions by status.
diff --git a/src/kernel/types/browser_list_response.py b/src/kernel/types/browser_list_response.py
index 79db0cfc..83154a94 100644
--- a/src/kernel/types/browser_list_response.py
+++ b/src/kernel/types/browser_list_response.py
@@ -63,6 +63,9 @@ class BrowserListResponse(BaseModel):
kiosk_mode: Optional[bool] = None
"""Whether the browser session is running in kiosk mode."""
+ name: Optional[str] = None
+ """Human-readable name of the browser session, if one was set at creation."""
+
pool: Optional[BrowserPoolRef] = None
"""Browser pool this session was acquired from, if any."""
diff --git a/src/kernel/types/browser_pool_acquire_response.py b/src/kernel/types/browser_pool_acquire_response.py
index b3cbabfa..bcda928d 100644
--- a/src/kernel/types/browser_pool_acquire_response.py
+++ b/src/kernel/types/browser_pool_acquire_response.py
@@ -63,6 +63,9 @@ class BrowserPoolAcquireResponse(BaseModel):
kiosk_mode: Optional[bool] = None
"""Whether the browser session is running in kiosk mode."""
+ name: Optional[str] = None
+ """Human-readable name of the browser session, if one was set at creation."""
+
pool: Optional[BrowserPoolRef] = None
"""Browser pool this session was acquired from, if any."""
diff --git a/src/kernel/types/browser_retrieve_response.py b/src/kernel/types/browser_retrieve_response.py
index 86b5fbef..9042d010 100644
--- a/src/kernel/types/browser_retrieve_response.py
+++ b/src/kernel/types/browser_retrieve_response.py
@@ -63,6 +63,9 @@ class BrowserRetrieveResponse(BaseModel):
kiosk_mode: Optional[bool] = None
"""Whether the browser session is running in kiosk mode."""
+ name: Optional[str] = None
+ """Human-readable name of the browser session, if one was set at creation."""
+
pool: Optional[BrowserPoolRef] = None
"""Browser pool this session was acquired from, if any."""
diff --git a/src/kernel/types/browser_update_response.py b/src/kernel/types/browser_update_response.py
index 60e95176..89c79f48 100644
--- a/src/kernel/types/browser_update_response.py
+++ b/src/kernel/types/browser_update_response.py
@@ -63,6 +63,9 @@ class BrowserUpdateResponse(BaseModel):
kiosk_mode: Optional[bool] = None
"""Whether the browser session is running in kiosk mode."""
+ name: Optional[str] = None
+ """Human-readable name of the browser session, if one was set at creation."""
+
pool: Optional[BrowserPoolRef] = None
"""Browser pool this session was acquired from, if any."""
diff --git a/src/kernel/types/invocation_list_browsers_response.py b/src/kernel/types/invocation_list_browsers_response.py
index 0a1700c2..d42a3abf 100644
--- a/src/kernel/types/invocation_list_browsers_response.py
+++ b/src/kernel/types/invocation_list_browsers_response.py
@@ -63,6 +63,9 @@ class Browser(BaseModel):
kiosk_mode: Optional[bool] = None
"""Whether the browser session is running in kiosk mode."""
+ name: Optional[str] = None
+ """Human-readable name of the browser session, if one was set at creation."""
+
pool: Optional[BrowserPoolRef] = None
"""Browser pool this session was acquired from, if any."""
diff --git a/tests/api_resources/test_browsers.py b/tests/api_resources/test_browsers.py
index c845e52b..69b14ac4 100644
--- a/tests/api_resources/test_browsers.py
+++ b/tests/api_resources/test_browsers.py
@@ -45,6 +45,7 @@ def test_method_create_with_all_params(self, client: Kernel) -> None:
headless=False,
invocation_id="rr33xuugxj9h0bkf1rdt2bet",
kiosk_mode=True,
+ name="amazon-scrape-1",
profile={
"id": "id",
"name": "name",
@@ -451,6 +452,7 @@ async def test_method_create_with_all_params(self, async_client: AsyncKernel) ->
headless=False,
invocation_id="rr33xuugxj9h0bkf1rdt2bet",
kiosk_mode=True,
+ name="amazon-scrape-1",
profile={
"id": "id",
"name": "name",
From a25ccaa1022219a7e1ca1d2de59715d640efb00a Mon Sep 17 00:00:00 2001
From: "stainless-app[bot]"
<142633134+stainless-app[bot]@users.noreply.github.com>
Date: Thu, 4 Jun 2026 22:07:37 +0000
Subject: [PATCH 047/366] docs(api): use neutral example for browser session
name field
---
.stats.yml | 4 ++--
tests/api_resources/test_browsers.py | 4 ++--
2 files changed, 4 insertions(+), 4 deletions(-)
diff --git a/.stats.yml b/.stats.yml
index b8e42864..afee3b79 100644
--- a/.stats.yml
+++ b/.stats.yml
@@ -1,4 +1,4 @@
configured_endpoints: 117
-openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel/kernel-e7925f5cb9626a47fd24470851ff4149c27bc89795f75e8641c618fff93a076c.yml
-openapi_spec_hash: 955d4bf81e9838b0732c0e52716a2030
+openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel/kernel-152175b971564fc089733419adff5f79aa5556cb264bea7be5ca7582fda82b61.yml
+openapi_spec_hash: 054e48b4743d014f0ec199757990e2c3
config_hash: 36159c262d293fbeacf513ab600a1729
diff --git a/tests/api_resources/test_browsers.py b/tests/api_resources/test_browsers.py
index 69b14ac4..06d997c6 100644
--- a/tests/api_resources/test_browsers.py
+++ b/tests/api_resources/test_browsers.py
@@ -45,7 +45,7 @@ def test_method_create_with_all_params(self, client: Kernel) -> None:
headless=False,
invocation_id="rr33xuugxj9h0bkf1rdt2bet",
kiosk_mode=True,
- name="amazon-scrape-1",
+ name="checkout-flow-1",
profile={
"id": "id",
"name": "name",
@@ -452,7 +452,7 @@ async def test_method_create_with_all_params(self, async_client: AsyncKernel) ->
headless=False,
invocation_id="rr33xuugxj9h0bkf1rdt2bet",
kiosk_mode=True,
- name="amazon-scrape-1",
+ name="checkout-flow-1",
profile={
"id": "id",
"name": "name",
From 762eaa35d9bb53acdf9ac31df119640459fb24df Mon Sep 17 00:00:00 2001
From: "stainless-app[bot]"
<142633134+stainless-app[bot]@users.noreply.github.com>
Date: Thu, 4 Jun 2026 22:18:56 +0000
Subject: [PATCH 048/366] codegen metadata
---
.stats.yml | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/.stats.yml b/.stats.yml
index afee3b79..c2258b3d 100644
--- a/.stats.yml
+++ b/.stats.yml
@@ -1,4 +1,4 @@
configured_endpoints: 117
-openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel/kernel-152175b971564fc089733419adff5f79aa5556cb264bea7be5ca7582fda82b61.yml
-openapi_spec_hash: 054e48b4743d014f0ec199757990e2c3
+openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel/kernel-d0f7d8820ee342b6b8e32d5c08620d7a31135a6aabbe8266118b54269fbabb9d.yml
+openapi_spec_hash: 371eb419c25eb160fbc0135e45c133d1
config_hash: 36159c262d293fbeacf513ab600a1729
From e9fb0b07cc8258f4d7512ffd6f215374f1c7ddce Mon Sep 17 00:00:00 2001
From: "stainless-app[bot]"
<142633134+stainless-app[bot]@users.noreply.github.com>
Date: Fri, 5 Jun 2026 15:36:40 +0000
Subject: [PATCH 049/366] feat(api): allow setting key-value tags on a browser
session at create time
---
.stats.yml | 6 ++---
api.md | 1 +
src/kernel/resources/browsers/browsers.py | 23 +++++++++++++++++++
src/kernel/types/__init__.py | 2 ++
src/kernel/types/browser_create_params.py | 7 ++++++
src/kernel/types/browser_create_response.py | 7 ++++++
src/kernel/types/browser_list_params.py | 8 +++++++
src/kernel/types/browser_list_response.py | 7 ++++++
.../types/browser_pool_acquire_response.py | 7 ++++++
src/kernel/types/browser_retrieve_response.py | 7 ++++++
src/kernel/types/browser_update_response.py | 7 ++++++
.../invocation_list_browsers_response.py | 7 ++++++
src/kernel/types/tags.py | 8 +++++++
src/kernel/types/tags_param.py | 10 ++++++++
tests/api_resources/test_browsers.py | 10 ++++++++
15 files changed, 114 insertions(+), 3 deletions(-)
create mode 100644 src/kernel/types/tags.py
create mode 100644 src/kernel/types/tags_param.py
diff --git a/.stats.yml b/.stats.yml
index c2258b3d..b53121ed 100644
--- a/.stats.yml
+++ b/.stats.yml
@@ -1,4 +1,4 @@
configured_endpoints: 117
-openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel/kernel-d0f7d8820ee342b6b8e32d5c08620d7a31135a6aabbe8266118b54269fbabb9d.yml
-openapi_spec_hash: 371eb419c25eb160fbc0135e45c133d1
-config_hash: 36159c262d293fbeacf513ab600a1729
+openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel/kernel-8f84f4214a8024d8ea62ee639eeaf2fa26900fabe23c8b87accb41d529a0bf4f.yml
+openapi_spec_hash: db14f415438b3d338d9376bddc83a5cf
+config_hash: 590bf8cb85948cf1e63b7b5ef60686c8
diff --git a/api.md b/api.md
index 8f9df345..fe9135b5 100644
--- a/api.md
+++ b/api.md
@@ -83,6 +83,7 @@ from kernel.types import (
BrowserPoolRef,
BrowserUsage,
Profile,
+ Tags,
BrowserCreateResponse,
BrowserRetrieveResponse,
BrowserUpdateResponse,
diff --git a/src/kernel/resources/browsers/browsers.py b/src/kernel/resources/browsers/browsers.py
index b439fdb3..516eca31 100644
--- a/src/kernel/resources/browsers/browsers.py
+++ b/src/kernel/resources/browsers/browsers.py
@@ -85,6 +85,7 @@
)
from ...pagination import SyncOffsetPagination, AsyncOffsetPagination
from ..._base_client import AsyncPaginator, make_request_options
+from ...types.tags_param import TagsParam
from ...types.browser_curl_response import BrowserCurlResponse
from ...types.browser_list_response import BrowserListResponse
from ...lib.browser_routing.raw_http import (
@@ -174,6 +175,7 @@ def create(
proxy_id: str | Omit = omit,
start_url: str | Omit = omit,
stealth: bool | Omit = omit,
+ tags: TagsParam | Omit = omit,
telemetry: Optional[browser_create_params.Telemetry] | Omit = omit,
timeout_seconds: int | Omit = omit,
viewport: BrowserViewport | Omit = omit,
@@ -224,6 +226,9 @@ def create(
stealth: If true, launches the browser in stealth mode to reduce detection by anti-bot
mechanisms.
+ tags: Optional user-defined key-value tags for the browser session, used to find and
+ group sessions later. Set at creation time only. Up to 50 pairs.
+
telemetry: Telemetry configuration for the browser session. Set enabled to true to start
capture using VM defaults, or provide browser category settings. If omitted,
null, set to an empty object ({}), set to enabled: false without browser
@@ -272,6 +277,7 @@ def create(
"proxy_id": proxy_id,
"start_url": start_url,
"stealth": stealth,
+ "tags": tags,
"telemetry": telemetry,
"timeout_seconds": timeout_seconds,
"viewport": viewport,
@@ -399,6 +405,7 @@ def list(
offset: int | Omit = omit,
query: str | Omit = omit,
status: Literal["active", "deleted", "all"] | Omit = omit,
+ tags: Dict[str, 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,
@@ -424,6 +431,10 @@ def list(
status: Filter sessions by status. "active" returns only active sessions (default),
"deleted" returns only soft-deleted sessions, "all" returns both.
+ tags: Filter sessions by tag key-value pairs using deepObject style, e.g.
+ ?tags[team]=backend&tags[env]=staging. Multiple pairs are ANDed: a session must
+ match every supplied pair exactly.
+
extra_headers: Send extra headers
extra_query: Add additional query parameters to the request
@@ -447,6 +458,7 @@ def list(
"offset": offset,
"query": query,
"status": status,
+ "tags": tags,
},
browser_list_params.BrowserListParams,
),
@@ -728,6 +740,7 @@ async def create(
proxy_id: str | Omit = omit,
start_url: str | Omit = omit,
stealth: bool | Omit = omit,
+ tags: TagsParam | Omit = omit,
telemetry: Optional[browser_create_params.Telemetry] | Omit = omit,
timeout_seconds: int | Omit = omit,
viewport: BrowserViewport | Omit = omit,
@@ -778,6 +791,9 @@ async def create(
stealth: If true, launches the browser in stealth mode to reduce detection by anti-bot
mechanisms.
+ tags: Optional user-defined key-value tags for the browser session, used to find and
+ group sessions later. Set at creation time only. Up to 50 pairs.
+
telemetry: Telemetry configuration for the browser session. Set enabled to true to start
capture using VM defaults, or provide browser category settings. If omitted,
null, set to an empty object ({}), set to enabled: false without browser
@@ -826,6 +842,7 @@ async def create(
"proxy_id": proxy_id,
"start_url": start_url,
"stealth": stealth,
+ "tags": tags,
"telemetry": telemetry,
"timeout_seconds": timeout_seconds,
"viewport": viewport,
@@ -953,6 +970,7 @@ def list(
offset: int | Omit = omit,
query: str | Omit = omit,
status: Literal["active", "deleted", "all"] | Omit = omit,
+ tags: Dict[str, 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,
@@ -978,6 +996,10 @@ def list(
status: Filter sessions by status. "active" returns only active sessions (default),
"deleted" returns only soft-deleted sessions, "all" returns both.
+ tags: Filter sessions by tag key-value pairs using deepObject style, e.g.
+ ?tags[team]=backend&tags[env]=staging. Multiple pairs are ANDed: a session must
+ match every supplied pair exactly.
+
extra_headers: Send extra headers
extra_query: Add additional query parameters to the request
@@ -1001,6 +1023,7 @@ def list(
"offset": offset,
"query": query,
"status": status,
+ "tags": tags,
},
browser_list_params.BrowserListParams,
),
diff --git a/src/kernel/types/__init__.py b/src/kernel/types/__init__.py
index 7932162d..1e9b39a4 100644
--- a/src/kernel/types/__init__.py
+++ b/src/kernel/types/__init__.py
@@ -4,6 +4,7 @@
from . import browsers
from .. import _compat
+from .tags import Tags as Tags
from .shared import (
LogEvent as LogEvent,
AppAction as AppAction,
@@ -19,6 +20,7 @@
from .profile import Profile as Profile
from .project import Project as Project
from .credential import Credential as Credential
+from .tags_param import TagsParam as TagsParam
from .browser_pool import BrowserPool as BrowserPool
from .browser_usage import BrowserUsage as BrowserUsage
from .app_list_params import AppListParams as AppListParams
diff --git a/src/kernel/types/browser_create_params.py b/src/kernel/types/browser_create_params.py
index de6ba5dd..fe62f31a 100644
--- a/src/kernel/types/browser_create_params.py
+++ b/src/kernel/types/browser_create_params.py
@@ -5,6 +5,7 @@
from typing import Dict, Iterable, Optional
from typing_extensions import TypedDict
+from .tags_param import TagsParam
from .shared_params.browser_profile import BrowserProfile
from .shared_params.browser_viewport import BrowserViewport
from .shared_params.browser_extension import BrowserExtension
@@ -82,6 +83,12 @@ class BrowserCreateParams(TypedDict, total=False):
mechanisms.
"""
+ tags: TagsParam
+ """
+ Optional user-defined key-value tags for the browser session, used to find and
+ group sessions later. Set at creation time only. Up to 50 pairs.
+ """
+
telemetry: Optional[Telemetry]
"""Telemetry configuration for the browser session.
diff --git a/src/kernel/types/browser_create_response.py b/src/kernel/types/browser_create_response.py
index 27da0664..ec54e598 100644
--- a/src/kernel/types/browser_create_response.py
+++ b/src/kernel/types/browser_create_response.py
@@ -3,6 +3,7 @@
from typing import Dict, Optional
from datetime import datetime
+from .tags import Tags
from .profile import Profile
from .._models import BaseModel
from .browser_usage import BrowserUsage
@@ -84,6 +85,12 @@ class BrowserCreateResponse(BaseModel):
browser actually loaded.
"""
+ tags: Optional[Tags] = None
+ """User-defined key-value tags that were set on this browser session, if any.
+
+ Echoed back when present.
+ """
+
telemetry: Optional[BrowserTelemetryConfig] = None
"""Active telemetry configuration for the session, if any."""
diff --git a/src/kernel/types/browser_list_params.py b/src/kernel/types/browser_list_params.py
index 2761e83b..4006dfbe 100644
--- a/src/kernel/types/browser_list_params.py
+++ b/src/kernel/types/browser_list_params.py
@@ -2,6 +2,7 @@
from __future__ import annotations
+from typing import Dict
from typing_extensions import Literal, TypedDict
__all__ = ["BrowserListParams"]
@@ -30,3 +31,10 @@ class BrowserListParams(TypedDict, total=False):
"active" returns only active sessions (default), "deleted" returns only
soft-deleted sessions, "all" returns both.
"""
+
+ tags: Dict[str, str]
+ """Filter sessions by tag key-value pairs using deepObject style, e.g.
+
+ ?tags[team]=backend&tags[env]=staging. Multiple pairs are ANDed: a session must
+ match every supplied pair exactly.
+ """
diff --git a/src/kernel/types/browser_list_response.py b/src/kernel/types/browser_list_response.py
index 83154a94..4de6bb00 100644
--- a/src/kernel/types/browser_list_response.py
+++ b/src/kernel/types/browser_list_response.py
@@ -3,6 +3,7 @@
from typing import Dict, Optional
from datetime import datetime
+from .tags import Tags
from .profile import Profile
from .._models import BaseModel
from .browser_usage import BrowserUsage
@@ -84,6 +85,12 @@ class BrowserListResponse(BaseModel):
browser actually loaded.
"""
+ tags: Optional[Tags] = None
+ """User-defined key-value tags that were set on this browser session, if any.
+
+ Echoed back when present.
+ """
+
telemetry: Optional[BrowserTelemetryConfig] = None
"""Active telemetry configuration for the session, if any."""
diff --git a/src/kernel/types/browser_pool_acquire_response.py b/src/kernel/types/browser_pool_acquire_response.py
index bcda928d..6d3b0b68 100644
--- a/src/kernel/types/browser_pool_acquire_response.py
+++ b/src/kernel/types/browser_pool_acquire_response.py
@@ -3,6 +3,7 @@
from typing import Dict, Optional
from datetime import datetime
+from .tags import Tags
from .profile import Profile
from .._models import BaseModel
from .browser_usage import BrowserUsage
@@ -84,6 +85,12 @@ class BrowserPoolAcquireResponse(BaseModel):
browser actually loaded.
"""
+ tags: Optional[Tags] = None
+ """User-defined key-value tags that were set on this browser session, if any.
+
+ Echoed back when present.
+ """
+
telemetry: Optional[BrowserTelemetryConfig] = None
"""Active telemetry configuration for the session, if any."""
diff --git a/src/kernel/types/browser_retrieve_response.py b/src/kernel/types/browser_retrieve_response.py
index 9042d010..48dfa362 100644
--- a/src/kernel/types/browser_retrieve_response.py
+++ b/src/kernel/types/browser_retrieve_response.py
@@ -3,6 +3,7 @@
from typing import Dict, Optional
from datetime import datetime
+from .tags import Tags
from .profile import Profile
from .._models import BaseModel
from .browser_usage import BrowserUsage
@@ -84,6 +85,12 @@ class BrowserRetrieveResponse(BaseModel):
browser actually loaded.
"""
+ tags: Optional[Tags] = None
+ """User-defined key-value tags that were set on this browser session, if any.
+
+ Echoed back when present.
+ """
+
telemetry: Optional[BrowserTelemetryConfig] = None
"""Active telemetry configuration for the session, if any."""
diff --git a/src/kernel/types/browser_update_response.py b/src/kernel/types/browser_update_response.py
index 89c79f48..15c043ea 100644
--- a/src/kernel/types/browser_update_response.py
+++ b/src/kernel/types/browser_update_response.py
@@ -3,6 +3,7 @@
from typing import Dict, Optional
from datetime import datetime
+from .tags import Tags
from .profile import Profile
from .._models import BaseModel
from .browser_usage import BrowserUsage
@@ -84,6 +85,12 @@ class BrowserUpdateResponse(BaseModel):
browser actually loaded.
"""
+ tags: Optional[Tags] = None
+ """User-defined key-value tags that were set on this browser session, if any.
+
+ Echoed back when present.
+ """
+
telemetry: Optional[BrowserTelemetryConfig] = None
"""Active telemetry configuration for the session, if any."""
diff --git a/src/kernel/types/invocation_list_browsers_response.py b/src/kernel/types/invocation_list_browsers_response.py
index d42a3abf..b060f8bb 100644
--- a/src/kernel/types/invocation_list_browsers_response.py
+++ b/src/kernel/types/invocation_list_browsers_response.py
@@ -3,6 +3,7 @@
from typing import Dict, List, Optional
from datetime import datetime
+from .tags import Tags
from .profile import Profile
from .._models import BaseModel
from .browser_usage import BrowserUsage
@@ -84,6 +85,12 @@ class Browser(BaseModel):
browser actually loaded.
"""
+ tags: Optional[Tags] = None
+ """User-defined key-value tags that were set on this browser session, if any.
+
+ Echoed back when present.
+ """
+
telemetry: Optional[BrowserTelemetryConfig] = None
"""Active telemetry configuration for the session, if any."""
diff --git a/src/kernel/types/tags.py b/src/kernel/types/tags.py
new file mode 100644
index 00000000..988894da
--- /dev/null
+++ b/src/kernel/types/tags.py
@@ -0,0 +1,8 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from typing import Dict
+from typing_extensions import TypeAlias
+
+__all__ = ["Tags"]
+
+Tags: TypeAlias = Dict[str, str]
diff --git a/src/kernel/types/tags_param.py b/src/kernel/types/tags_param.py
new file mode 100644
index 00000000..9e9e5cba
--- /dev/null
+++ b/src/kernel/types/tags_param.py
@@ -0,0 +1,10 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from __future__ import annotations
+
+from typing import Dict
+from typing_extensions import TypeAlias
+
+__all__ = ["TagsParam"]
+
+TagsParam: TypeAlias = Dict[str, str]
diff --git a/tests/api_resources/test_browsers.py b/tests/api_resources/test_browsers.py
index 06d997c6..590640c9 100644
--- a/tests/api_resources/test_browsers.py
+++ b/tests/api_resources/test_browsers.py
@@ -54,6 +54,10 @@ def test_method_create_with_all_params(self, client: Kernel) -> None:
proxy_id="proxy_id",
start_url="https://example.com",
stealth=True,
+ tags={
+ "team": "backend",
+ "env": "staging",
+ },
telemetry={
"browser": {
"console": {"enabled": True},
@@ -232,6 +236,7 @@ def test_method_list_with_all_params(self, client: Kernel) -> None:
offset=0,
query="query",
status="active",
+ tags={"foo": "string"},
)
assert_matches_type(SyncOffsetPagination[BrowserListResponse], browser, path=["response"])
@@ -461,6 +466,10 @@ async def test_method_create_with_all_params(self, async_client: AsyncKernel) ->
proxy_id="proxy_id",
start_url="https://example.com",
stealth=True,
+ tags={
+ "team": "backend",
+ "env": "staging",
+ },
telemetry={
"browser": {
"console": {"enabled": True},
@@ -639,6 +648,7 @@ async def test_method_list_with_all_params(self, async_client: AsyncKernel) -> N
offset=0,
query="query",
status="active",
+ tags={"foo": "string"},
)
assert_matches_type(AsyncOffsetPagination[BrowserListResponse], browser, path=["response"])
From 40fa8f5936935a0b89b1ff5ca8d1e6da265328f4 Mon Sep 17 00:00:00 2001
From: "stainless-app[bot]"
<142633134+stainless-app[bot]@users.noreply.github.com>
Date: Fri, 5 Jun 2026 15:37:07 +0000
Subject: [PATCH 050/366] release: 0.63.0
---
.release-please-manifest.json | 2 +-
CHANGELOG.md | 14 ++++++++++++++
pyproject.toml | 2 +-
src/kernel/_version.py | 2 +-
4 files changed, 17 insertions(+), 3 deletions(-)
diff --git a/.release-please-manifest.json b/.release-please-manifest.json
index fdce8724..3cb6257c 100644
--- a/.release-please-manifest.json
+++ b/.release-please-manifest.json
@@ -1,3 +1,3 @@
{
- ".": "0.62.0"
+ ".": "0.63.0"
}
\ No newline at end of file
diff --git a/CHANGELOG.md b/CHANGELOG.md
index bbd58123..04be67f2 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,19 @@
# Changelog
+## 0.63.0 (2026-06-05)
+
+Full Changelog: [v0.62.0...v0.63.0](https://github.com/kernel/kernel-python-sdk/compare/v0.62.0...v0.63.0)
+
+### Features
+
+* **api:** allow setting a custom name on a browser session at create time ([0dea358](https://github.com/kernel/kernel-python-sdk/commit/0dea35847a9559c86038774966bae28911c0a044))
+* **api:** allow setting key-value tags on a browser session at create time ([f9ca7c1](https://github.com/kernel/kernel-python-sdk/commit/f9ca7c19aa71326c97b14d723dfd79840e3e8dd7))
+
+
+### Documentation
+
+* **api:** use neutral example for browser session name field ([01b15f6](https://github.com/kernel/kernel-python-sdk/commit/01b15f602a4c21ab4e757bf46ed2d4c1481c14ce))
+
## 0.62.0 (2026-06-04)
Full Changelog: [v0.61.0...v0.62.0](https://github.com/kernel/kernel-python-sdk/compare/v0.61.0...v0.62.0)
diff --git a/pyproject.toml b/pyproject.toml
index f246f34a..710e7a06 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -1,6 +1,6 @@
[project]
name = "kernel"
-version = "0.62.0"
+version = "0.63.0"
description = "The official Python library for the kernel API"
dynamic = ["readme"]
license = "Apache-2.0"
diff --git a/src/kernel/_version.py b/src/kernel/_version.py
index 785c4450..52d2f619 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.62.0" # x-release-please-version
+__version__ = "0.63.0" # x-release-please-version
From bef228eda49aba8fcb00372ab459793e728435cb Mon Sep 17 00:00:00 2001
From: "stainless-app[bot]"
<142633134+stainless-app[bot]@users.noreply.github.com>
Date: Fri, 5 Jun 2026 18:06:21 +0000
Subject: [PATCH 051/366] codegen metadata
---
.stats.yml | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/.stats.yml b/.stats.yml
index b53121ed..8991baf8 100644
--- a/.stats.yml
+++ b/.stats.yml
@@ -1,4 +1,4 @@
configured_endpoints: 117
-openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel/kernel-8f84f4214a8024d8ea62ee639eeaf2fa26900fabe23c8b87accb41d529a0bf4f.yml
-openapi_spec_hash: db14f415438b3d338d9376bddc83a5cf
+openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel/kernel-53de38ec04ee49846c89946b2fe72641c5ee005956d1fff4e8a50de9ba22e874.yml
+openapi_spec_hash: 96162b6a1e4a2521be1b6c4fb9b0e5ce
config_hash: 590bf8cb85948cf1e63b7b5ef60686c8
From 0b4c116f779cd882d705760290d5d385645333c9 Mon Sep 17 00:00:00 2001
From: "stainless-app[bot]"
<142633134+stainless-app[bot]@users.noreply.github.com>
Date: Fri, 5 Jun 2026 21:37:42 +0000
Subject: [PATCH 052/366] feat: Telemetry: expose opt-in categories + full
event taxonomy (public API)
---
.stats.yml | 6 +-
api.md | 8 ++
src/kernel/types/browser_create_params.py | 15 +--
src/kernel/types/browser_update_params.py | 15 +--
src/kernel/types/browsers/__init__.py | 8 ++
.../types/browsers/browser_api_call_event.py | 42 ++++++++
.../browser_captcha_solve_result_event.py | 63 ++++++++++++
.../browsers/browser_cdp_connect_event.py | 29 ++++++
.../browsers/browser_cdp_disconnect_event.py | 49 +++++++++
.../browser_live_view_connect_event.py | 40 ++++++++
.../browser_live_view_disconnect_event.py | 41 ++++++++
.../browser_monitor_disconnected_event.py | 2 +-
.../browser_monitor_init_failed_event.py | 2 +-
.../browser_monitor_reconnect_failed_event.py | 2 +-
.../browser_monitor_reconnected_event.py | 2 +-
.../browser_monitor_screenshot_event.py | 2 +-
.../browsers/browser_service_crashed_event.py | 50 ++++++++++
.../browsers/browser_system_oom_kill_event.py | 99 +++++++++++++++++++
.../browser_telemetry_categories_config.py | 45 ++++++++-
...owser_telemetry_categories_config_param.py | 45 ++++++++-
.../browser_telemetry_category_config.py | 5 +-
...browser_telemetry_category_config_param.py | 5 +-
.../types/browsers/browser_telemetry_event.py | 16 +++
.../browsers/telemetry_stream_response.py | 12 ++-
tests/api_resources/test_browsers.py | 20 ++++
25 files changed, 584 insertions(+), 39 deletions(-)
create mode 100644 src/kernel/types/browsers/browser_api_call_event.py
create mode 100644 src/kernel/types/browsers/browser_captcha_solve_result_event.py
create mode 100644 src/kernel/types/browsers/browser_cdp_connect_event.py
create mode 100644 src/kernel/types/browsers/browser_cdp_disconnect_event.py
create mode 100644 src/kernel/types/browsers/browser_live_view_connect_event.py
create mode 100644 src/kernel/types/browsers/browser_live_view_disconnect_event.py
create mode 100644 src/kernel/types/browsers/browser_service_crashed_event.py
create mode 100644 src/kernel/types/browsers/browser_system_oom_kill_event.py
diff --git a/.stats.yml b/.stats.yml
index 8991baf8..d9e97532 100644
--- a/.stats.yml
+++ b/.stats.yml
@@ -1,4 +1,4 @@
configured_endpoints: 117
-openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel/kernel-53de38ec04ee49846c89946b2fe72641c5ee005956d1fff4e8a50de9ba22e874.yml
-openapi_spec_hash: 96162b6a1e4a2521be1b6c4fb9b0e5ce
-config_hash: 590bf8cb85948cf1e63b7b5ef60686c8
+openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel/kernel-9d489e3e43edfa64a754d4281241718e01c85d9a82ef3687df12bbd3c4ff5b42.yml
+openapi_spec_hash: a953cafb7f40ec8495dbd7df8bab8bad
+config_hash: bb7acce8576a50dd449b0c8f58ef0f1d
diff --git a/api.md b/api.md
index fe9135b5..c5019550 100644
--- a/api.md
+++ b/api.md
@@ -108,7 +108,11 @@ Types:
```python
from kernel.types.browsers import (
+ BrowserAPICallEvent,
BrowserCallStack,
+ BrowserCaptchaSolveResultEvent,
+ BrowserCdpConnectEvent,
+ BrowserCdpDisconnectEvent,
BrowserConsoleErrorEvent,
BrowserConsoleLogEvent,
BrowserEventContext,
@@ -117,6 +121,8 @@ from kernel.types.browsers import (
BrowserInteractionClickEvent,
BrowserInteractionKeyEvent,
BrowserInteractionScrollSettledEvent,
+ BrowserLiveViewConnectEvent,
+ BrowserLiveViewDisconnectEvent,
BrowserMonitorDisconnectedEvent,
BrowserMonitorInitFailedEvent,
BrowserMonitorReconnectFailedEvent,
@@ -134,6 +140,8 @@ from kernel.types.browsers import (
BrowserPageNavigationEvent,
BrowserPageNavigationSettledEvent,
BrowserPageTabOpenedEvent,
+ BrowserServiceCrashedEvent,
+ BrowserSystemOomKillEvent,
BrowserTelemetryCategoriesConfig,
BrowserTelemetryCategoryConfig,
BrowserTelemetryConfig,
diff --git a/src/kernel/types/browser_create_params.py b/src/kernel/types/browser_create_params.py
index fe62f31a..6d7b80b3 100644
--- a/src/kernel/types/browser_create_params.py
+++ b/src/kernel/types/browser_create_params.py
@@ -131,17 +131,18 @@ class Telemetry(TypedDict, total=False):
"""
browser: BrowserTelemetryCategoriesConfigParam
- """Per-category enable/disable flags.
+ """Per-category capture flags.
- If enabled is true and browser is omitted or empty, the VM default category set
- is used. Explicitly disabling all four categories stops capture on update and
- starts no capture on create.
+ Selection is opt-in: only the categories set to enabled=true are captured;
+ anything omitted is off. If enabled is true and browser is omitted or empty, the
+ default category set is used. A browser config that enables nothing stops
+ capture on update and starts no capture on create.
"""
enabled: bool
"""Request shortcut for browser telemetry capture.
- True enables capture using VM defaults unless browser category settings are
- provided. False stops capture on update and starts no capture on create.
- enabled=false cannot be combined with browser category settings.
+ True enables capture using the default category set unless browser category
+ settings are provided. False stops capture on update and starts no capture on
+ create. enabled=false cannot be combined with browser category settings.
"""
diff --git a/src/kernel/types/browser_update_params.py b/src/kernel/types/browser_update_params.py
index 837c1784..2626917c 100644
--- a/src/kernel/types/browser_update_params.py
+++ b/src/kernel/types/browser_update_params.py
@@ -52,19 +52,20 @@ class Telemetry(TypedDict, total=False):
"""
browser: BrowserTelemetryCategoriesConfigParam
- """Per-category enable/disable flags.
+ """Per-category capture flags.
- If enabled is true and browser is omitted or empty, the VM default category set
- is used. Explicitly disabling all four categories stops capture on update and
- starts no capture on create.
+ Selection is opt-in: only the categories set to enabled=true are captured;
+ anything omitted is off. If enabled is true and browser is omitted or empty, the
+ default category set is used. A browser config that enables nothing stops
+ capture on update and starts no capture on create.
"""
enabled: bool
"""Request shortcut for browser telemetry capture.
- True enables capture using VM defaults unless browser category settings are
- provided. False stops capture on update and starts no capture on create.
- enabled=false cannot be combined with browser category settings.
+ True enables capture using the default category set unless browser category
+ settings are provided. False stops capture on update and starts no capture on
+ create. enabled=false cannot be combined with browser category settings.
"""
diff --git a/src/kernel/types/browsers/__init__.py b/src/kernel/types/browsers/__init__.py
index 34f4fd64..5f14ecd1 100644
--- a/src/kernel/types/browsers/__init__.py
+++ b/src/kernel/types/browsers/__init__.py
@@ -28,6 +28,7 @@
from .process_kill_response import ProcessKillResponse as ProcessKillResponse
from .process_resize_params import ProcessResizeParams as ProcessResizeParams
from .replay_start_response import ReplayStartResponse as ReplayStartResponse
+from .browser_api_call_event import BrowserAPICallEvent as BrowserAPICallEvent
from .browser_page_lcp_event import BrowserPageLcpEvent as BrowserPageLcpEvent
from .computer_scroll_params import ComputerScrollParams as ComputerScrollParams
from .process_spawn_response import ProcessSpawnResponse as ProcessSpawnResponse
@@ -37,6 +38,7 @@
from .process_resize_response import ProcessResizeResponse as ProcessResizeResponse
from .process_status_response import ProcessStatusResponse as ProcessStatusResponse
from .browser_telemetry_config import BrowserTelemetryConfig as BrowserTelemetryConfig
+from .browser_cdp_connect_event import BrowserCdpConnectEvent as BrowserCdpConnectEvent
from .browser_console_log_event import BrowserConsoleLogEvent as BrowserConsoleLogEvent
from .computer_press_key_params import ComputerPressKeyParams as ComputerPressKeyParams
from .computer_type_text_params import ComputerTypeTextParams as ComputerTypeTextParams
@@ -51,14 +53,18 @@
from .browser_console_error_event import BrowserConsoleErrorEvent as BrowserConsoleErrorEvent
from .computer_click_mouse_params import ComputerClickMouseParams as ComputerClickMouseParams
from .playwright_execute_response import PlaywrightExecuteResponse as PlaywrightExecuteResponse
+from .browser_cdp_disconnect_event import BrowserCdpDisconnectEvent as BrowserCdpDisconnectEvent
from .browser_interaction_key_event import BrowserInteractionKeyEvent as BrowserInteractionKeyEvent
from .browser_network_request_event import BrowserNetworkRequestEvent as BrowserNetworkRequestEvent
from .browser_page_navigation_event import BrowserPageNavigationEvent as BrowserPageNavigationEvent
from .browser_page_tab_opened_event import BrowserPageTabOpenedEvent as BrowserPageTabOpenedEvent
+from .browser_service_crashed_event import BrowserServiceCrashedEvent as BrowserServiceCrashedEvent
+from .browser_system_oom_kill_event import BrowserSystemOomKillEvent as BrowserSystemOomKillEvent
from .f_set_file_permissions_params import FSetFilePermissionsParams as FSetFilePermissionsParams
from .browser_network_response_event import BrowserNetworkResponseEvent as BrowserNetworkResponseEvent
from .process_stdout_stream_response import ProcessStdoutStreamResponse as ProcessStdoutStreamResponse
from .browser_interaction_click_event import BrowserInteractionClickEvent as BrowserInteractionClickEvent
+from .browser_live_view_connect_event import BrowserLiveViewConnectEvent as BrowserLiveViewConnectEvent
from .browser_page_layout_shift_event import BrowserPageLayoutShiftEvent as BrowserPageLayoutShiftEvent
from .computer_write_clipboard_params import ComputerWriteClipboardParams as ComputerWriteClipboardParams
from .browser_monitor_screenshot_event import BrowserMonitorScreenshotEvent as BrowserMonitorScreenshotEvent
@@ -67,6 +73,8 @@
from .browser_monitor_reconnected_event import BrowserMonitorReconnectedEvent as BrowserMonitorReconnectedEvent
from .browser_page_layout_settled_event import BrowserPageLayoutSettledEvent as BrowserPageLayoutSettledEvent
from .browser_telemetry_category_config import BrowserTelemetryCategoryConfig as BrowserTelemetryCategoryConfig
+from .browser_captcha_solve_result_event import BrowserCaptchaSolveResultEvent as BrowserCaptchaSolveResultEvent
+from .browser_live_view_disconnect_event import BrowserLiveViewDisconnectEvent as BrowserLiveViewDisconnectEvent
from .browser_monitor_disconnected_event import BrowserMonitorDisconnectedEvent as BrowserMonitorDisconnectedEvent
from .computer_capture_screenshot_params import ComputerCaptureScreenshotParams as ComputerCaptureScreenshotParams
from .browser_telemetry_categories_config import BrowserTelemetryCategoriesConfig as BrowserTelemetryCategoriesConfig
diff --git a/src/kernel/types/browsers/browser_api_call_event.py b/src/kernel/types/browsers/browser_api_call_event.py
new file mode 100644
index 00000000..702596d8
--- /dev/null
+++ b/src/kernel/types/browsers/browser_api_call_event.py
@@ -0,0 +1,42 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from typing import Optional
+from typing_extensions import Literal
+
+from ..._models import BaseModel
+from .browser_event_source import BrowserEventSource
+
+__all__ = ["BrowserAPICallEvent", "Data"]
+
+
+class Data(BaseModel):
+ duration_ms: float
+ """Wall-clock duration of the handler in milliseconds."""
+
+ operation_id: str
+ """OpenAPI operationId of the matched route (e.g. processExec, takeScreenshot)."""
+
+ request_id: str
+ """Per-request identifier from the in-VM API request middleware."""
+
+ status: int
+ """HTTP response status code."""
+
+
+class BrowserAPICallEvent(BaseModel):
+ """An agent-driven HTTP call handled by the in-VM API server."""
+
+ category: Literal["control"]
+
+ source: BrowserEventSource
+ """Provenance metadata identifying which producer emitted the event."""
+
+ ts: int
+ """Event timestamp in Unix microseconds."""
+
+ type: Literal["api_call"]
+
+ data: Optional[Data] = None
+
+ truncated: Optional[bool] = None
+ """True if the data field was truncated due to size limits."""
diff --git a/src/kernel/types/browsers/browser_captcha_solve_result_event.py b/src/kernel/types/browsers/browser_captcha_solve_result_event.py
new file mode 100644
index 00000000..fb8476d4
--- /dev/null
+++ b/src/kernel/types/browsers/browser_captcha_solve_result_event.py
@@ -0,0 +1,63 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from typing import Optional
+from typing_extensions import Literal
+
+from ..._models import BaseModel
+from .browser_event_source import BrowserEventSource
+
+__all__ = ["BrowserCaptchaSolveResultEvent", "Data"]
+
+
+class Data(BaseModel):
+ captcha_type: Literal["hcaptcha", "recaptcha_v2", "recaptcha_v3", "turnstile", "geetest", "other"]
+ """Captcha vendor family.
+
+ Provider-specific task names are normalized into this set; anything not covered
+ is reported as other.
+ """
+
+ duration_ms: float
+ """Wall-clock duration from solve start to terminal outcome."""
+
+ status: Literal["success", "failure", "timeout", "abandoned"]
+ """Terminal outcome.
+
+ success: solver returned a usable solution. failure: solver returned an error
+ (see error_code). timeout: solver did not return within the caller's wait
+ budget. abandoned: caller cancelled or the page navigated away mid-solve.
+ """
+
+ error_code: Optional[str] = None
+ """Solver-specific error code on failure (e.g.
+
+ ERROR_CAPTCHA_UNSOLVABLE). Absent on success.
+ """
+
+ task_id: Optional[str] = None
+ """Solver-assigned identifier. Opaque, useful for support cross-references."""
+
+ website_host: Optional[str] = None
+ """Host of the page where the captcha was solved."""
+
+ website_path: Optional[str] = None
+ """Path of the page where the captcha was solved. Query string excluded."""
+
+
+class BrowserCaptchaSolveResultEvent(BaseModel):
+ """A captcha solve attempt reached a terminal outcome."""
+
+ category: Literal["captcha"]
+
+ source: BrowserEventSource
+ """Provenance metadata identifying which producer emitted the event."""
+
+ ts: int
+ """Event timestamp in Unix microseconds."""
+
+ type: Literal["captcha_solve_result"]
+
+ data: Optional[Data] = None
+
+ truncated: Optional[bool] = None
+ """True if the data field was truncated due to size limits."""
diff --git a/src/kernel/types/browsers/browser_cdp_connect_event.py b/src/kernel/types/browsers/browser_cdp_connect_event.py
new file mode 100644
index 00000000..ebbc7fa3
--- /dev/null
+++ b/src/kernel/types/browsers/browser_cdp_connect_event.py
@@ -0,0 +1,29 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from typing import Optional
+from typing_extensions import Literal
+
+from ..._models import BaseModel
+from .browser_event_source import BrowserEventSource
+
+__all__ = ["BrowserCdpConnectEvent"]
+
+
+class BrowserCdpConnectEvent(BaseModel):
+ """An external client (e.g.
+
+ customer SDK, Playwright, Puppeteer) connected to the CDP WebSocket proxy on this VM.
+ """
+
+ category: Literal["connection"]
+
+ source: BrowserEventSource
+ """Provenance metadata identifying which producer emitted the event."""
+
+ ts: int
+ """Event timestamp in Unix microseconds."""
+
+ type: Literal["cdp_connect"]
+
+ truncated: Optional[bool] = None
+ """True if the data field was truncated due to size limits."""
diff --git a/src/kernel/types/browsers/browser_cdp_disconnect_event.py b/src/kernel/types/browsers/browser_cdp_disconnect_event.py
new file mode 100644
index 00000000..d9260ebc
--- /dev/null
+++ b/src/kernel/types/browsers/browser_cdp_disconnect_event.py
@@ -0,0 +1,49 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from typing import Optional
+from typing_extensions import Literal
+
+from ..._models import BaseModel
+from .browser_event_source import BrowserEventSource
+
+__all__ = ["BrowserCdpDisconnectEvent", "Data"]
+
+
+class Data(BaseModel):
+ duration_ms: float
+ """Wall-clock duration of the connection in milliseconds."""
+
+ message_count: int
+ """Number of CDP messages relayed across the connection in either direction."""
+
+ reason: Literal["client_close", "upstream_changed", "upstream_error", "context_cancelled"]
+ """Why the connection ended.
+
+ client_close: the client initiated the close. upstream_changed: Chromium
+ restarted mid-session and the proxy tore down so the client could reconnect
+ against the new upstream. upstream_error: upstream dial or message pump errored.
+ context_cancelled: the request context was cancelled (typically server
+ shutdown).
+ """
+
+
+class BrowserCdpDisconnectEvent(BaseModel):
+ """An external client disconnected from the CDP WebSocket proxy on this VM.
+
+ Pair with the immediately preceding cdp_connect on the same stream.
+ """
+
+ category: Literal["connection"]
+
+ source: BrowserEventSource
+ """Provenance metadata identifying which producer emitted the event."""
+
+ ts: int
+ """Event timestamp in Unix microseconds."""
+
+ type: Literal["cdp_disconnect"]
+
+ data: Optional[Data] = None
+
+ truncated: Optional[bool] = None
+ """True if the data field was truncated due to size limits."""
diff --git a/src/kernel/types/browsers/browser_live_view_connect_event.py b/src/kernel/types/browsers/browser_live_view_connect_event.py
new file mode 100644
index 00000000..620e3d36
--- /dev/null
+++ b/src/kernel/types/browsers/browser_live_view_connect_event.py
@@ -0,0 +1,40 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from typing import Optional
+from typing_extensions import Literal
+
+from ..._models import BaseModel
+from .browser_event_source import BrowserEventSource
+
+__all__ = ["BrowserLiveViewConnectEvent", "Data"]
+
+
+class Data(BaseModel):
+ session_id: str
+ """Live view session identifier.
+
+ Stable across reconnects, so a transient network blip can emit two events with
+ the same session_id.
+ """
+
+
+class BrowserLiveViewConnectEvent(BaseModel):
+ """A live view client connected to the headful browser's WebRTC server.
+
+ Headful only; not emitted for headless images.
+ """
+
+ category: Literal["connection"]
+
+ source: BrowserEventSource
+ """Provenance metadata identifying which producer emitted the event."""
+
+ ts: int
+ """Event timestamp in Unix microseconds."""
+
+ type: Literal["live_view_connect"]
+
+ data: Optional[Data] = None
+
+ truncated: Optional[bool] = None
+ """True if the data field was truncated due to size limits."""
diff --git a/src/kernel/types/browsers/browser_live_view_disconnect_event.py b/src/kernel/types/browsers/browser_live_view_disconnect_event.py
new file mode 100644
index 00000000..a847d34c
--- /dev/null
+++ b/src/kernel/types/browsers/browser_live_view_disconnect_event.py
@@ -0,0 +1,41 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from typing import Optional
+from typing_extensions import Literal
+
+from ..._models import BaseModel
+from .browser_event_source import BrowserEventSource
+
+__all__ = ["BrowserLiveViewDisconnectEvent", "Data"]
+
+
+class Data(BaseModel):
+ duration_ms: float
+ """Wall-clock duration of the connection in milliseconds."""
+
+ session_id: str
+ """
+ Live view session identifier; matches the corresponding live_view_connect event.
+ """
+
+
+class BrowserLiveViewDisconnectEvent(BaseModel):
+ """A live view client disconnected from the headful browser's WebRTC server.
+
+ Pair with live_view_connect by session_id.
+ """
+
+ category: Literal["connection"]
+
+ source: BrowserEventSource
+ """Provenance metadata identifying which producer emitted the event."""
+
+ ts: int
+ """Event timestamp in Unix microseconds."""
+
+ type: Literal["live_view_disconnect"]
+
+ data: Optional[Data] = None
+
+ truncated: Optional[bool] = None
+ """True if the data field was truncated due to size limits."""
diff --git a/src/kernel/types/browsers/browser_monitor_disconnected_event.py b/src/kernel/types/browsers/browser_monitor_disconnected_event.py
index ac37ec6a..2ea3afeb 100644
--- a/src/kernel/types/browsers/browser_monitor_disconnected_event.py
+++ b/src/kernel/types/browsers/browser_monitor_disconnected_event.py
@@ -20,7 +20,7 @@ class BrowserMonitorDisconnectedEvent(BaseModel):
Telemetry events may be dropped until monitor_reconnected arrives. Treat any in-progress computed state (network_idle, page_layout_settled) as unreliable until then.
"""
- category: Literal["system"]
+ category: Literal["monitor"]
source: BrowserEventSource
"""Provenance metadata identifying which producer emitted the event."""
diff --git a/src/kernel/types/browsers/browser_monitor_init_failed_event.py b/src/kernel/types/browsers/browser_monitor_init_failed_event.py
index dba0e5da..c3a7cfdb 100644
--- a/src/kernel/types/browsers/browser_monitor_init_failed_event.py
+++ b/src/kernel/types/browsers/browser_monitor_init_failed_event.py
@@ -17,7 +17,7 @@ class Data(BaseModel):
class BrowserMonitorInitFailedEvent(BaseModel):
"""The CDP session could not be initialized."""
- category: Literal["system"]
+ category: Literal["monitor"]
source: BrowserEventSource
"""Provenance metadata identifying which producer emitted the event."""
diff --git a/src/kernel/types/browsers/browser_monitor_reconnect_failed_event.py b/src/kernel/types/browsers/browser_monitor_reconnect_failed_event.py
index 57eec69b..c71bf530 100644
--- a/src/kernel/types/browsers/browser_monitor_reconnect_failed_event.py
+++ b/src/kernel/types/browsers/browser_monitor_reconnect_failed_event.py
@@ -23,7 +23,7 @@ class BrowserMonitorReconnectFailedEvent(BaseModel):
The CDP connection to Chrome could not be re-established after exhausting all reconnection attempts. No further telemetry events will arrive on this session.
"""
- category: Literal["system"]
+ category: Literal["monitor"]
source: BrowserEventSource
"""Provenance metadata identifying which producer emitted the event."""
diff --git a/src/kernel/types/browsers/browser_monitor_reconnected_event.py b/src/kernel/types/browsers/browser_monitor_reconnected_event.py
index 39363367..c27340c6 100644
--- a/src/kernel/types/browsers/browser_monitor_reconnected_event.py
+++ b/src/kernel/types/browsers/browser_monitor_reconnected_event.py
@@ -19,7 +19,7 @@ class BrowserMonitorReconnectedEvent(BaseModel):
The CDP connection to Chrome was successfully re-established after a disconnection. Events emitted during the gap are lost. Computed state is reset, so navigation and network tracking restart fresh from this point.
"""
- category: Literal["system"]
+ category: Literal["monitor"]
source: BrowserEventSource
"""Provenance metadata identifying which producer emitted the event."""
diff --git a/src/kernel/types/browsers/browser_monitor_screenshot_event.py b/src/kernel/types/browsers/browser_monitor_screenshot_event.py
index bd2dc65b..02135b47 100644
--- a/src/kernel/types/browsers/browser_monitor_screenshot_event.py
+++ b/src/kernel/types/browsers/browser_monitor_screenshot_event.py
@@ -17,7 +17,7 @@ class Data(BaseModel):
class BrowserMonitorScreenshotEvent(BaseModel):
"""A periodic screenshot of the browser viewport."""
- category: Literal["system"]
+ category: Literal["screenshot"]
source: BrowserEventSource
"""Provenance metadata identifying which producer emitted the event."""
diff --git a/src/kernel/types/browsers/browser_service_crashed_event.py b/src/kernel/types/browsers/browser_service_crashed_event.py
new file mode 100644
index 00000000..7bc6f1cd
--- /dev/null
+++ b/src/kernel/types/browsers/browser_service_crashed_event.py
@@ -0,0 +1,50 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from typing import Optional
+from typing_extensions import Literal
+
+from ..._models import BaseModel
+from .browser_event_source import BrowserEventSource
+
+__all__ = ["BrowserServiceCrashedEvent", "Data"]
+
+
+class Data(BaseModel):
+ phase: Literal["startup", "running", "gave_up"]
+ """Lifecycle phase the crash occurred in.
+
+ startup: the process died before reaching a healthy running state. running: a
+ previously healthy process died unexpectedly. gave_up: the process manager
+ exhausted its restart attempts and stopped trying.
+ """
+
+ service_name: str
+ """Program name of the crashed service (e.g. chromium, mutter, kernel-images-api)."""
+
+ pid: Optional[int] = None
+ """PID of the crashed process.
+
+ Absent when the process manager gave up after exhausting restart attempts.
+ """
+
+
+class BrowserServiceCrashedEvent(BaseModel):
+ """A managed service exited unexpectedly.
+
+ Intentional stops do not produce this event; only unexpected exits and terminal restart-give-up transitions do.
+ """
+
+ category: Literal["system"]
+
+ source: BrowserEventSource
+ """Provenance metadata identifying which producer emitted the event."""
+
+ ts: int
+ """Event timestamp in Unix microseconds."""
+
+ type: Literal["service_crashed"]
+
+ data: Optional[Data] = None
+
+ truncated: Optional[bool] = None
+ """True if the data field was truncated due to size limits."""
diff --git a/src/kernel/types/browsers/browser_system_oom_kill_event.py b/src/kernel/types/browsers/browser_system_oom_kill_event.py
new file mode 100644
index 00000000..2cafb817
--- /dev/null
+++ b/src/kernel/types/browsers/browser_system_oom_kill_event.py
@@ -0,0 +1,99 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from typing import List, Optional
+from typing_extensions import Literal
+
+from ..._models import BaseModel
+from .browser_event_source import BrowserEventSource
+
+__all__ = ["BrowserSystemOomKillEvent", "Data", "DataTopTask"]
+
+
+class DataTopTask(BaseModel):
+ name: str
+ """Comm of the process (max 15 chars, truncated by the kernel)."""
+
+ pid: int
+ """PID of the process."""
+
+ rss_kb: int
+ """Resident set size in KiB at the moment of the kill."""
+
+
+class Data(BaseModel):
+ pid: int
+ """PID of the killed process."""
+
+ process_name: str
+ """
+ Comm of the killed process as reported by the kernel (max 15 chars, truncated by
+ the kernel).
+ """
+
+ rss_kb: int
+ """
+ Resident set size of the killed process in KiB (sum of anon-rss, file-rss, and
+ shmem-rss).
+ """
+
+ constraint: Optional[Literal["none", "memcg", "cpuset", "memory_policy"]] = None
+ """Why the kernel decided to OOM-kill.
+
+ none means global memory exhaustion; memcg means a cgroup memory limit was hit;
+ cpuset / memory_policy are NUMA/policy-driven kills. Absent on kernels older
+ than 5.0.
+ """
+
+ mem_free_kb: Optional[int] = None
+ """Free system memory in KiB at the time of the kill.
+
+ Assumes a 4 KiB page size. Does not include reclaimable caches. Absent if the
+ kernel did not emit a parseable Mem-Info section.
+ """
+
+ mem_total_kb: Optional[int] = None
+ """Total system memory in KiB at the time of the kill.
+
+ Assumes a 4 KiB page size. Absent if the kernel did not emit a parseable
+ Mem-Info section.
+ """
+
+ top_tasks: Optional[List[DataTopTask]] = None
+ """Top processes by resident-set-size at the moment of the kill, sorted descending.
+
+ Empty if the kernel did not emit the Tasks state table. Capped at 5 entries.
+ """
+
+ trigger_pid: Optional[int] = None
+ """PID of the triggering process.
+
+ Absent if the kernel did not emit the standard header line.
+ """
+
+ trigger_process_name: Optional[str] = None
+ """
+ Comm of the process whose allocation request caused the kernel to invoke the
+ OOM-killer. Often the same as process_name but can differ. Max 15 chars.
+ """
+
+
+class BrowserSystemOomKillEvent(BaseModel):
+ """The Linux kernel OOM-killer terminated a process inside the VM.
+
+ Fires for any process killed by the kernel due to memory exhaustion, including Chrome renderer subprocesses that are not supervised.
+ """
+
+ category: Literal["system"]
+
+ source: BrowserEventSource
+ """Provenance metadata identifying which producer emitted the event."""
+
+ ts: int
+ """Event timestamp in Unix microseconds."""
+
+ type: Literal["system_oom_kill"]
+
+ data: Optional[Data] = None
+
+ truncated: Optional[bool] = None
+ """True if the data field was truncated due to size limits."""
diff --git a/src/kernel/types/browsers/browser_telemetry_categories_config.py b/src/kernel/types/browsers/browser_telemetry_categories_config.py
index 82cdb1db..0013fee3 100644
--- a/src/kernel/types/browsers/browser_telemetry_categories_config.py
+++ b/src/kernel/types/browsers/browser_telemetry_categories_config.py
@@ -9,24 +9,59 @@
class BrowserTelemetryCategoriesConfig(BaseModel):
- """Per-category telemetry capture settings."""
+ """Per-category telemetry capture settings.
+
+ Selection is opt-in: set a category to enabled=true to capture it; anything omitted is off. The default set (used by enabled=true with no per-category settings) is the lightweight operational signals: control, connection, system, captcha. The CDP categories (console, network, page, interaction) and screenshot are off by default and must be opted into.
+ """
+
+ captcha: Optional[BrowserTelemetryCategoryConfig] = None
+ """Captcha solve attempt outcomes. On by default."""
+
+ connection: Optional[BrowserTelemetryCategoryConfig] = None
+ """Client attach/detach lifecycle for the CDP proxy and live view. On by default."""
console: Optional[BrowserTelemetryCategoryConfig] = None
- """Console output (log, warn, error) and uncaught exceptions."""
+ """Console output (log, warn, error) and uncaught exceptions.
+
+ CDP category; off by default.
+ """
+
+ control: Optional[BrowserTelemetryCategoryConfig] = None
+ """Agent-driven actions against the browser, such as inbound calls to the in-VM
+ API.
+
+ On by default.
+ """
interaction: Optional[BrowserTelemetryCategoryConfig] = None
- """User interaction events including clicks, keydowns, and scroll-settled events."""
+ """User interaction events including clicks, keydowns, and scroll-settled events.
+
+ CDP category; off by default.
+ """
network: Optional[BrowserTelemetryCategoryConfig] = None
"""
HTTP request and response metadata including URL, method, status code, and
timing. Request post data is forwarded as-is from CDP. Text response bodies are
truncated at 8 KB for structured types (JSON, XML, form data) and 4 KB for other
- text types. Binary responses (images, fonts, media) are excluded.
+ text types. Binary responses (images, fonts, media) are excluded. CDP category;
+ off by default.
"""
page: Optional[BrowserTelemetryCategoryConfig] = None
"""
Page lifecycle events including navigation, DOMContentLoaded, load, layout
- shifts, and LCP.
+ shifts, and LCP. CDP category; off by default.
+ """
+
+ screenshot: Optional[BrowserTelemetryCategoryConfig] = None
+ """Periodic base64-encoded viewport screenshots.
+
+ High volume; off by default and must be opted into.
+ """
+
+ system: Optional[BrowserTelemetryCategoryConfig] = None
+ """Browser VM health, such as out-of-memory kills and managed-service crashes.
+
+ On by default.
"""
diff --git a/src/kernel/types/browsers/browser_telemetry_categories_config_param.py b/src/kernel/types/browsers/browser_telemetry_categories_config_param.py
index 75a3a797..a1520d7c 100644
--- a/src/kernel/types/browsers/browser_telemetry_categories_config_param.py
+++ b/src/kernel/types/browsers/browser_telemetry_categories_config_param.py
@@ -10,24 +10,59 @@
class BrowserTelemetryCategoriesConfigParam(TypedDict, total=False):
- """Per-category telemetry capture settings."""
+ """Per-category telemetry capture settings.
+
+ Selection is opt-in: set a category to enabled=true to capture it; anything omitted is off. The default set (used by enabled=true with no per-category settings) is the lightweight operational signals: control, connection, system, captcha. The CDP categories (console, network, page, interaction) and screenshot are off by default and must be opted into.
+ """
+
+ captcha: BrowserTelemetryCategoryConfigParam
+ """Captcha solve attempt outcomes. On by default."""
+
+ connection: BrowserTelemetryCategoryConfigParam
+ """Client attach/detach lifecycle for the CDP proxy and live view. On by default."""
console: BrowserTelemetryCategoryConfigParam
- """Console output (log, warn, error) and uncaught exceptions."""
+ """Console output (log, warn, error) and uncaught exceptions.
+
+ CDP category; off by default.
+ """
+
+ control: BrowserTelemetryCategoryConfigParam
+ """Agent-driven actions against the browser, such as inbound calls to the in-VM
+ API.
+
+ On by default.
+ """
interaction: BrowserTelemetryCategoryConfigParam
- """User interaction events including clicks, keydowns, and scroll-settled events."""
+ """User interaction events including clicks, keydowns, and scroll-settled events.
+
+ CDP category; off by default.
+ """
network: BrowserTelemetryCategoryConfigParam
"""
HTTP request and response metadata including URL, method, status code, and
timing. Request post data is forwarded as-is from CDP. Text response bodies are
truncated at 8 KB for structured types (JSON, XML, form data) and 4 KB for other
- text types. Binary responses (images, fonts, media) are excluded.
+ text types. Binary responses (images, fonts, media) are excluded. CDP category;
+ off by default.
"""
page: BrowserTelemetryCategoryConfigParam
"""
Page lifecycle events including navigation, DOMContentLoaded, load, layout
- shifts, and LCP.
+ shifts, and LCP. CDP category; off by default.
+ """
+
+ screenshot: BrowserTelemetryCategoryConfigParam
+ """Periodic base64-encoded viewport screenshots.
+
+ High volume; off by default and must be opted into.
+ """
+
+ system: BrowserTelemetryCategoryConfigParam
+ """Browser VM health, such as out-of-memory kills and managed-service crashes.
+
+ On by default.
"""
diff --git a/src/kernel/types/browsers/browser_telemetry_category_config.py b/src/kernel/types/browsers/browser_telemetry_category_config.py
index 4a2da2cb..f75616c9 100644
--- a/src/kernel/types/browsers/browser_telemetry_category_config.py
+++ b/src/kernel/types/browsers/browser_telemetry_category_config.py
@@ -11,4 +11,7 @@ class BrowserTelemetryCategoryConfig(BaseModel):
"""Per-category telemetry configuration."""
enabled: Optional[bool] = None
- """Whether this category is captured. Defaults to true if omitted."""
+ """Whether this category is captured.
+
+ Selection is opt-in, so an omitted category is not captured.
+ """
diff --git a/src/kernel/types/browsers/browser_telemetry_category_config_param.py b/src/kernel/types/browsers/browser_telemetry_category_config_param.py
index 3824b4c8..3958f7d8 100644
--- a/src/kernel/types/browsers/browser_telemetry_category_config_param.py
+++ b/src/kernel/types/browsers/browser_telemetry_category_config_param.py
@@ -11,4 +11,7 @@ class BrowserTelemetryCategoryConfigParam(TypedDict, total=False):
"""Per-category telemetry configuration."""
enabled: bool
- """Whether this category is captured. Defaults to true if omitted."""
+ """Whether this category is captured.
+
+ Selection is opt-in, so an omitted category is not captured.
+ """
diff --git a/src/kernel/types/browsers/browser_telemetry_event.py b/src/kernel/types/browsers/browser_telemetry_event.py
index d8506bf7..26750b02 100644
--- a/src/kernel/types/browsers/browser_telemetry_event.py
+++ b/src/kernel/types/browsers/browser_telemetry_event.py
@@ -6,22 +6,30 @@
from typing_extensions import Annotated, TypeAlias
from ..._utils import PropertyInfo
+from .browser_api_call_event import BrowserAPICallEvent
from .browser_page_lcp_event import BrowserPageLcpEvent
from .browser_page_load_event import BrowserPageLoadEvent
from .browser_console_log_event import BrowserConsoleLogEvent
+from .browser_cdp_connect_event import BrowserCdpConnectEvent
from .browser_network_idle_event import BrowserNetworkIdleEvent
from .browser_console_error_event import BrowserConsoleErrorEvent
+from .browser_cdp_disconnect_event import BrowserCdpDisconnectEvent
from .browser_interaction_key_event import BrowserInteractionKeyEvent
from .browser_network_request_event import BrowserNetworkRequestEvent
from .browser_page_navigation_event import BrowserPageNavigationEvent
from .browser_page_tab_opened_event import BrowserPageTabOpenedEvent
+from .browser_service_crashed_event import BrowserServiceCrashedEvent
+from .browser_system_oom_kill_event import BrowserSystemOomKillEvent
from .browser_network_response_event import BrowserNetworkResponseEvent
from .browser_interaction_click_event import BrowserInteractionClickEvent
+from .browser_live_view_connect_event import BrowserLiveViewConnectEvent
from .browser_page_layout_shift_event import BrowserPageLayoutShiftEvent
from .browser_monitor_screenshot_event import BrowserMonitorScreenshotEvent
from .browser_monitor_init_failed_event import BrowserMonitorInitFailedEvent
from .browser_monitor_reconnected_event import BrowserMonitorReconnectedEvent
from .browser_page_layout_settled_event import BrowserPageLayoutSettledEvent
+from .browser_captcha_solve_result_event import BrowserCaptchaSolveResultEvent
+from .browser_live_view_disconnect_event import BrowserLiveViewDisconnectEvent
from .browser_monitor_disconnected_event import BrowserMonitorDisconnectedEvent
from .browser_network_loading_failed_event import BrowserNetworkLoadingFailedEvent
from .browser_page_dom_content_loaded_event import BrowserPageDomContentLoadedEvent
@@ -55,6 +63,14 @@
BrowserMonitorReconnectedEvent,
BrowserMonitorReconnectFailedEvent,
BrowserMonitorInitFailedEvent,
+ BrowserAPICallEvent,
+ BrowserCdpConnectEvent,
+ BrowserCdpDisconnectEvent,
+ BrowserLiveViewConnectEvent,
+ BrowserLiveViewDisconnectEvent,
+ BrowserCaptchaSolveResultEvent,
+ BrowserSystemOomKillEvent,
+ BrowserServiceCrashedEvent,
],
PropertyInfo(discriminator="type"),
]
diff --git a/src/kernel/types/browsers/telemetry_stream_response.py b/src/kernel/types/browsers/telemetry_stream_response.py
index fef04b3f..25dd6650 100644
--- a/src/kernel/types/browsers/telemetry_stream_response.py
+++ b/src/kernel/types/browsers/telemetry_stream_response.py
@@ -16,11 +16,13 @@ class TelemetryStreamResponse(BaseModel):
event: "BrowserTelemetryEvent"
"""Union type representing any browser telemetry event.
- Discriminated on `type`. Events with a `monitor_` prefix (monitor_screenshot,
- monitor_disconnected, monitor_reconnected, monitor_reconnect_failed,
- monitor_init_failed) are always emitted regardless of the category configuration
- in BrowserTelemetryConfig. All other event types are controlled by the
- per-category enable/disable flags.
+ Discriminated on `type`. Each event's `category` determines when it is captured.
+ The CDP collector-health events (monitor_disconnected, monitor_reconnected,
+ monitor_reconnect_failed, monitor_init_failed) use the `monitor` category, which
+ is not user-configurable: it flows automatically whenever any CDP category
+ (console, network, page, interaction) is captured, and is silent otherwise.
+ monitor_screenshot uses the opt-in `screenshot` category. All other event types
+ are controlled by their per-category enable/disable flags.
"""
seq: int
diff --git a/tests/api_resources/test_browsers.py b/tests/api_resources/test_browsers.py
index 590640c9..5284214a 100644
--- a/tests/api_resources/test_browsers.py
+++ b/tests/api_resources/test_browsers.py
@@ -60,10 +60,15 @@ def test_method_create_with_all_params(self, client: Kernel) -> None:
},
telemetry={
"browser": {
+ "captcha": {"enabled": True},
+ "connection": {"enabled": True},
"console": {"enabled": True},
+ "control": {"enabled": True},
"interaction": {"enabled": True},
"network": {"enabled": True},
"page": {"enabled": True},
+ "screenshot": {"enabled": True},
+ "system": {"enabled": True},
},
"enabled": True,
},
@@ -171,10 +176,15 @@ def test_method_update_with_all_params(self, client: Kernel) -> None:
proxy_id="proxy_id",
telemetry={
"browser": {
+ "captcha": {"enabled": True},
+ "connection": {"enabled": True},
"console": {"enabled": True},
+ "control": {"enabled": True},
"interaction": {"enabled": True},
"network": {"enabled": True},
"page": {"enabled": True},
+ "screenshot": {"enabled": True},
+ "system": {"enabled": True},
},
"enabled": True,
},
@@ -472,10 +482,15 @@ async def test_method_create_with_all_params(self, async_client: AsyncKernel) ->
},
telemetry={
"browser": {
+ "captcha": {"enabled": True},
+ "connection": {"enabled": True},
"console": {"enabled": True},
+ "control": {"enabled": True},
"interaction": {"enabled": True},
"network": {"enabled": True},
"page": {"enabled": True},
+ "screenshot": {"enabled": True},
+ "system": {"enabled": True},
},
"enabled": True,
},
@@ -583,10 +598,15 @@ async def test_method_update_with_all_params(self, async_client: AsyncKernel) ->
proxy_id="proxy_id",
telemetry={
"browser": {
+ "captcha": {"enabled": True},
+ "connection": {"enabled": True},
"console": {"enabled": True},
+ "control": {"enabled": True},
"interaction": {"enabled": True},
"network": {"enabled": True},
"page": {"enabled": True},
+ "screenshot": {"enabled": True},
+ "system": {"enabled": True},
},
"enabled": True,
},
From f1d75826280c610d60d31ab8b4fcea51cf57e466 Mon Sep 17 00:00:00 2001
From: "stainless-app[bot]"
<142633134+stainless-app[bot]@users.noreply.github.com>
Date: Fri, 5 Jun 2026 21:38:06 +0000
Subject: [PATCH 053/366] release: 0.64.0
---
.release-please-manifest.json | 2 +-
CHANGELOG.md | 8 ++++++++
pyproject.toml | 2 +-
src/kernel/_version.py | 2 +-
4 files changed, 11 insertions(+), 3 deletions(-)
diff --git a/.release-please-manifest.json b/.release-please-manifest.json
index 3cb6257c..4c56f2a4 100644
--- a/.release-please-manifest.json
+++ b/.release-please-manifest.json
@@ -1,3 +1,3 @@
{
- ".": "0.63.0"
+ ".": "0.64.0"
}
\ No newline at end of file
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 04be67f2..7f4edf1e 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,13 @@
# Changelog
+## 0.64.0 (2026-06-05)
+
+Full Changelog: [v0.63.0...v0.64.0](https://github.com/kernel/kernel-python-sdk/compare/v0.63.0...v0.64.0)
+
+### Features
+
+* Telemetry: expose opt-in categories + full event taxonomy (public API) ([a1502dd](https://github.com/kernel/kernel-python-sdk/commit/a1502dd2639b97fb14ec57bfef5e37668b379db7))
+
## 0.63.0 (2026-06-05)
Full Changelog: [v0.62.0...v0.63.0](https://github.com/kernel/kernel-python-sdk/compare/v0.62.0...v0.63.0)
diff --git a/pyproject.toml b/pyproject.toml
index 710e7a06..a312a055 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -1,6 +1,6 @@
[project]
name = "kernel"
-version = "0.63.0"
+version = "0.64.0"
description = "The official Python library for the kernel API"
dynamic = ["readme"]
license = "Apache-2.0"
diff --git a/src/kernel/_version.py b/src/kernel/_version.py
index 52d2f619..b55ca852 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.63.0" # x-release-please-version
+__version__ = "0.64.0" # x-release-please-version
From 681211642cf3a20949f0994f6b9d44ce9036fd4b Mon Sep 17 00:00:00 2001
From: Sayan- <1415138+Sayan-@users.noreply.github.com>
Date: Mon, 8 Jun 2026 16:24:27 +0000
Subject: [PATCH 054/366] fix: sort imports in browser_telemetry_event
---
src/kernel/types/browsers/browser_telemetry_event.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/kernel/types/browsers/browser_telemetry_event.py b/src/kernel/types/browsers/browser_telemetry_event.py
index 26750b02..d27537e8 100644
--- a/src/kernel/types/browsers/browser_telemetry_event.py
+++ b/src/kernel/types/browsers/browser_telemetry_event.py
@@ -9,8 +9,8 @@
from .browser_api_call_event import BrowserAPICallEvent
from .browser_page_lcp_event import BrowserPageLcpEvent
from .browser_page_load_event import BrowserPageLoadEvent
-from .browser_console_log_event import BrowserConsoleLogEvent
from .browser_cdp_connect_event import BrowserCdpConnectEvent
+from .browser_console_log_event import BrowserConsoleLogEvent
from .browser_network_idle_event import BrowserNetworkIdleEvent
from .browser_console_error_event import BrowserConsoleErrorEvent
from .browser_cdp_disconnect_event import BrowserCdpDisconnectEvent
From 0c17ce2efa54552d1457b82625ffbb118a6e7bf1 Mon Sep 17 00:00:00 2001
From: "stainless-app[bot]"
<142633134+stainless-app[bot]@users.noreply.github.com>
Date: Mon, 8 Jun 2026 19:03:34 +0000
Subject: [PATCH 055/366] feat(api): allow setting a name and tags on a
pool-acquired browser session
---
.stats.yml | 4 +--
src/kernel/resources/browser_pools.py | 35 +++++++++++++++++--
.../types/browser_pool_acquire_params.py | 17 +++++++++
tests/api_resources/test_browser_pools.py | 10 ++++++
4 files changed, 62 insertions(+), 4 deletions(-)
diff --git a/.stats.yml b/.stats.yml
index d9e97532..cd599894 100644
--- a/.stats.yml
+++ b/.stats.yml
@@ -1,4 +1,4 @@
configured_endpoints: 117
-openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel/kernel-9d489e3e43edfa64a754d4281241718e01c85d9a82ef3687df12bbd3c4ff5b42.yml
-openapi_spec_hash: a953cafb7f40ec8495dbd7df8bab8bad
+openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel/kernel-662a9d6352d842f37e06e0197a61fe10850483302650713345d45780b3128343.yml
+openapi_spec_hash: e65977d16d95d48c75d02a1133131149
config_hash: bb7acce8576a50dd449b0c8f58ef0f1d
diff --git a/src/kernel/resources/browser_pools.py b/src/kernel/resources/browser_pools.py
index 203e22ad..5099f446 100644
--- a/src/kernel/resources/browser_pools.py
+++ b/src/kernel/resources/browser_pools.py
@@ -26,6 +26,7 @@
)
from ..pagination import SyncOffsetPagination, AsyncOffsetPagination
from .._base_client import AsyncPaginator, make_request_options
+from ..types.tags_param import TagsParam
from ..types.browser_pool import BrowserPool
from ..types.browser_pool_acquire_response import BrowserPoolAcquireResponse
from ..types.shared_params.browser_profile import BrowserProfile
@@ -417,6 +418,8 @@ def acquire(
id_or_name: str,
*,
acquire_timeout_seconds: int | Omit = omit,
+ name: str | Omit = omit,
+ tags: TagsParam | Omit = omit,
# Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
# The extra values given here take precedence over values defined on the client or passed to this method.
extra_headers: Headers | None = None,
@@ -436,6 +439,15 @@ def acquire(
calculated time it would take to fill the pool at the currently configured fill
rate.
+ name: Optional human-readable name for the acquired browser session, used to find it
+ later in the dashboard. Must be unique among active sessions within the pool's
+ project. Applies to this lease only and is cleared when the browser is released
+ back to the pool.
+
+ tags: Optional user-defined key-value tags for the acquired browser session, used to
+ find and group sessions later. Applies to this lease only and are cleared when
+ the browser is released back to the pool. Up to 50 pairs.
+
extra_headers: Send extra headers
extra_query: Add additional query parameters to the request
@@ -449,7 +461,11 @@ def acquire(
return self._post(
path_template("/browser_pools/{id_or_name}/acquire", id_or_name=id_or_name),
body=maybe_transform(
- {"acquire_timeout_seconds": acquire_timeout_seconds},
+ {
+ "acquire_timeout_seconds": acquire_timeout_seconds,
+ "name": name,
+ "tags": tags,
+ },
browser_pool_acquire_params.BrowserPoolAcquireParams,
),
options=make_request_options(
@@ -923,6 +939,8 @@ async def acquire(
id_or_name: str,
*,
acquire_timeout_seconds: int | Omit = omit,
+ name: str | Omit = omit,
+ tags: TagsParam | Omit = omit,
# Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
# The extra values given here take precedence over values defined on the client or passed to this method.
extra_headers: Headers | None = None,
@@ -942,6 +960,15 @@ async def acquire(
calculated time it would take to fill the pool at the currently configured fill
rate.
+ name: Optional human-readable name for the acquired browser session, used to find it
+ later in the dashboard. Must be unique among active sessions within the pool's
+ project. Applies to this lease only and is cleared when the browser is released
+ back to the pool.
+
+ tags: Optional user-defined key-value tags for the acquired browser session, used to
+ find and group sessions later. Applies to this lease only and are cleared when
+ the browser is released back to the pool. Up to 50 pairs.
+
extra_headers: Send extra headers
extra_query: Add additional query parameters to the request
@@ -955,7 +982,11 @@ async def acquire(
return await self._post(
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},
+ {
+ "acquire_timeout_seconds": acquire_timeout_seconds,
+ "name": name,
+ "tags": tags,
+ },
browser_pool_acquire_params.BrowserPoolAcquireParams,
),
options=make_request_options(
diff --git a/src/kernel/types/browser_pool_acquire_params.py b/src/kernel/types/browser_pool_acquire_params.py
index d0df921a..022d69bd 100644
--- a/src/kernel/types/browser_pool_acquire_params.py
+++ b/src/kernel/types/browser_pool_acquire_params.py
@@ -4,6 +4,8 @@
from typing_extensions import TypedDict
+from .tags_param import TagsParam
+
__all__ = ["BrowserPoolAcquireParams"]
@@ -14,3 +16,18 @@ class BrowserPoolAcquireParams(TypedDict, total=False):
Defaults to the calculated time it would take to fill the pool at the currently
configured fill rate.
"""
+
+ name: str
+ """
+ Optional human-readable name for the acquired browser session, used to find it
+ later in the dashboard. Must be unique among active sessions within the pool's
+ project. Applies to this lease only and is cleared when the browser is released
+ back to the pool.
+ """
+
+ tags: TagsParam
+ """
+ Optional user-defined key-value tags for the acquired browser session, used to
+ find and group sessions later. Applies to this lease only and are cleared when
+ the browser is released back to the pool. Up to 50 pairs.
+ """
diff --git a/tests/api_resources/test_browser_pools.py b/tests/api_resources/test_browser_pools.py
index c71e6c27..e6f8b853 100644
--- a/tests/api_resources/test_browser_pools.py
+++ b/tests/api_resources/test_browser_pools.py
@@ -309,6 +309,11 @@ def test_method_acquire_with_all_params(self, client: Kernel) -> None:
browser_pool = client.browser_pools.acquire(
id_or_name="id_or_name",
acquire_timeout_seconds=0,
+ name="checkout-flow-1",
+ tags={
+ "team": "backend",
+ "env": "staging",
+ },
)
assert_matches_type(BrowserPoolAcquireResponse, browser_pool, path=["response"])
@@ -738,6 +743,11 @@ async def test_method_acquire_with_all_params(self, async_client: AsyncKernel) -
browser_pool = await async_client.browser_pools.acquire(
id_or_name="id_or_name",
acquire_timeout_seconds=0,
+ name="checkout-flow-1",
+ tags={
+ "team": "backend",
+ "env": "staging",
+ },
)
assert_matches_type(BrowserPoolAcquireResponse, browser_pool, path=["response"])
From 4d072e8653b77056fdcc1b014066b4091dd38920 Mon Sep 17 00:00:00 2001
From: "stainless-app[bot]"
<142633134+stainless-app[bot]@users.noreply.github.com>
Date: Mon, 8 Jun 2026 20:24:53 +0000
Subject: [PATCH 056/366] feat(api): support id-or-name lookup on browser
session get/patch/delete
---
.stats.yml | 6 +--
api.md | 6 +--
src/kernel/resources/browsers/browsers.py | 52 +++++++++++------------
tests/api_resources/test_browsers.py | 52 +++++++++++------------
4 files changed, 58 insertions(+), 58 deletions(-)
diff --git a/.stats.yml b/.stats.yml
index cd599894..dcde96de 100644
--- a/.stats.yml
+++ b/.stats.yml
@@ -1,4 +1,4 @@
configured_endpoints: 117
-openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel/kernel-662a9d6352d842f37e06e0197a61fe10850483302650713345d45780b3128343.yml
-openapi_spec_hash: e65977d16d95d48c75d02a1133131149
-config_hash: bb7acce8576a50dd449b0c8f58ef0f1d
+openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel/kernel-08c2d6a44f4cdcbfb6803a3043fdc1a3e33911dec4652cb3a870f01bc584421f.yml
+openapi_spec_hash: c816491451347eb93b793cddf6a78648
+config_hash: 9e45c27425021d49b5391f5cc980b046
diff --git a/api.md b/api.md
index c5019550..2d311077 100644
--- a/api.md
+++ b/api.md
@@ -95,11 +95,11 @@ from kernel.types import (
Methods:
- client.browsers.create(\*\*params) -> BrowserCreateResponse
-- client.browsers.retrieve(id, \*\*params) -> BrowserRetrieveResponse
-- client.browsers.update(id, \*\*params) -> BrowserUpdateResponse
+- client.browsers.retrieve(id_or_name, \*\*params) -> BrowserRetrieveResponse
+- client.browsers.update(id_or_name, \*\*params) -> BrowserUpdateResponse
- client.browsers.list(\*\*params) -> SyncOffsetPagination[BrowserListResponse]
- client.browsers.curl(id, \*\*params) -> BrowserCurlResponse
-- client.browsers.delete_by_id(id) -> None
+- client.browsers.delete_by_id(id_or_name) -> None
- client.browsers.load_extensions(id, \*\*params) -> None
## Telemetry
diff --git a/src/kernel/resources/browsers/browsers.py b/src/kernel/resources/browsers/browsers.py
index 516eca31..61ada1c2 100644
--- a/src/kernel/resources/browsers/browsers.py
+++ b/src/kernel/resources/browsers/browsers.py
@@ -292,7 +292,7 @@ def create(
def retrieve(
self,
- id: str,
+ id_or_name: str,
*,
include_deleted: bool | Omit = omit,
# Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
@@ -316,10 +316,10 @@ def retrieve(
timeout: Override the client-level default timeout for this request, in seconds
"""
- if not id:
- raise ValueError(f"Expected a non-empty value for `id` but received {id!r}")
+ if not id_or_name:
+ raise ValueError(f"Expected a non-empty value for `id_or_name` but received {id_or_name!r}")
return self._get(
- path_template("/browsers/{id}", id=id),
+ path_template("/browsers/{id_or_name}", id_or_name=id_or_name),
options=make_request_options(
extra_headers=extra_headers,
extra_query=extra_query,
@@ -334,7 +334,7 @@ def retrieve(
def update(
self,
- id: str,
+ id_or_name: str,
*,
disable_default_proxy: bool | Omit = omit,
profile: BrowserProfile | Omit = omit,
@@ -377,10 +377,10 @@ def update(
timeout: Override the client-level default timeout for this request, in seconds
"""
- if not id:
- raise ValueError(f"Expected a non-empty value for `id` but received {id!r}")
+ if not id_or_name:
+ raise ValueError(f"Expected a non-empty value for `id_or_name` but received {id_or_name!r}")
return self._patch(
- path_template("/browsers/{id}", id=id),
+ path_template("/browsers/{id_or_name}", id_or_name=id_or_name),
body=maybe_transform(
{
"disable_default_proxy": disable_default_proxy,
@@ -590,7 +590,7 @@ def stream(
def delete_by_id(
self,
- id: str,
+ id_or_name: str,
*,
# Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
# The extra values given here take precedence over values defined on the client or passed to this method.
@@ -600,7 +600,7 @@ def delete_by_id(
timeout: float | httpx.Timeout | None | NotGiven = not_given,
) -> None:
"""
- Delete a browser session by ID
+ Delete a browser session by ID or name
Args:
extra_headers: Send extra headers
@@ -611,11 +611,11 @@ def delete_by_id(
timeout: Override the client-level default timeout for this request, in seconds
"""
- if not id:
- raise ValueError(f"Expected a non-empty value for `id` but received {id!r}")
+ if not id_or_name:
+ 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(
- path_template("/browsers/{id}", id=id),
+ path_template("/browsers/{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
),
@@ -857,7 +857,7 @@ async def create(
async def retrieve(
self,
- id: str,
+ id_or_name: str,
*,
include_deleted: bool | Omit = omit,
# Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
@@ -881,10 +881,10 @@ async def retrieve(
timeout: Override the client-level default timeout for this request, in seconds
"""
- if not id:
- raise ValueError(f"Expected a non-empty value for `id` but received {id!r}")
+ if not id_or_name:
+ raise ValueError(f"Expected a non-empty value for `id_or_name` but received {id_or_name!r}")
return await self._get(
- path_template("/browsers/{id}", id=id),
+ path_template("/browsers/{id_or_name}", id_or_name=id_or_name),
options=make_request_options(
extra_headers=extra_headers,
extra_query=extra_query,
@@ -899,7 +899,7 @@ async def retrieve(
async def update(
self,
- id: str,
+ id_or_name: str,
*,
disable_default_proxy: bool | Omit = omit,
profile: BrowserProfile | Omit = omit,
@@ -942,10 +942,10 @@ async def update(
timeout: Override the client-level default timeout for this request, in seconds
"""
- if not id:
- raise ValueError(f"Expected a non-empty value for `id` but received {id!r}")
+ if not 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(
- path_template("/browsers/{id}", id=id),
+ path_template("/browsers/{id_or_name}", id_or_name=id_or_name),
body=await async_maybe_transform(
{
"disable_default_proxy": disable_default_proxy,
@@ -1155,7 +1155,7 @@ async def stream(
async def delete_by_id(
self,
- id: str,
+ id_or_name: str,
*,
# Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
# The extra values given here take precedence over values defined on the client or passed to this method.
@@ -1165,7 +1165,7 @@ async def delete_by_id(
timeout: float | httpx.Timeout | None | NotGiven = not_given,
) -> None:
"""
- Delete a browser session by ID
+ Delete a browser session by ID or name
Args:
extra_headers: Send extra headers
@@ -1176,11 +1176,11 @@ async def delete_by_id(
timeout: Override the client-level default timeout for this request, in seconds
"""
- if not id:
- raise ValueError(f"Expected a non-empty value for `id` but received {id!r}")
+ if not id_or_name:
+ 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(
- path_template("/browsers/{id}", id=id),
+ path_template("/browsers/{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/tests/api_resources/test_browsers.py b/tests/api_resources/test_browsers.py
index 5284214a..e09d4705 100644
--- a/tests/api_resources/test_browsers.py
+++ b/tests/api_resources/test_browsers.py
@@ -107,7 +107,7 @@ def test_streaming_response_create(self, client: Kernel) -> None:
@parametrize
def test_method_retrieve(self, client: Kernel) -> None:
browser = client.browsers.retrieve(
- id="htzv5orfit78e1m2biiifpbv",
+ id_or_name="htzv5orfit78e1m2biiifpbv",
)
assert_matches_type(BrowserRetrieveResponse, browser, path=["response"])
@@ -115,7 +115,7 @@ def test_method_retrieve(self, client: Kernel) -> None:
@parametrize
def test_method_retrieve_with_all_params(self, client: Kernel) -> None:
browser = client.browsers.retrieve(
- id="htzv5orfit78e1m2biiifpbv",
+ id_or_name="htzv5orfit78e1m2biiifpbv",
include_deleted=True,
)
assert_matches_type(BrowserRetrieveResponse, browser, path=["response"])
@@ -124,7 +124,7 @@ def test_method_retrieve_with_all_params(self, client: Kernel) -> None:
@parametrize
def test_raw_response_retrieve(self, client: Kernel) -> None:
response = client.browsers.with_raw_response.retrieve(
- id="htzv5orfit78e1m2biiifpbv",
+ id_or_name="htzv5orfit78e1m2biiifpbv",
)
assert response.is_closed is True
@@ -136,7 +136,7 @@ def test_raw_response_retrieve(self, client: Kernel) -> None:
@parametrize
def test_streaming_response_retrieve(self, client: Kernel) -> None:
with client.browsers.with_streaming_response.retrieve(
- id="htzv5orfit78e1m2biiifpbv",
+ id_or_name="htzv5orfit78e1m2biiifpbv",
) as response:
assert not response.is_closed
assert response.http_request.headers.get("X-Stainless-Lang") == "python"
@@ -149,16 +149,16 @@ def test_streaming_response_retrieve(self, client: Kernel) -> None:
@pytest.mark.skip(reason="Mock server tests are disabled")
@parametrize
def test_path_params_retrieve(self, client: Kernel) -> None:
- with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"):
+ with pytest.raises(ValueError, match=r"Expected a non-empty value for `id_or_name` but received ''"):
client.browsers.with_raw_response.retrieve(
- id="",
+ id_or_name="",
)
@pytest.mark.skip(reason="Mock server tests are disabled")
@parametrize
def test_method_update(self, client: Kernel) -> None:
browser = client.browsers.update(
- id="htzv5orfit78e1m2biiifpbv",
+ id_or_name="htzv5orfit78e1m2biiifpbv",
)
assert_matches_type(BrowserUpdateResponse, browser, path=["response"])
@@ -166,7 +166,7 @@ def test_method_update(self, client: Kernel) -> None:
@parametrize
def test_method_update_with_all_params(self, client: Kernel) -> None:
browser = client.browsers.update(
- id="htzv5orfit78e1m2biiifpbv",
+ id_or_name="htzv5orfit78e1m2biiifpbv",
disable_default_proxy=True,
profile={
"id": "id",
@@ -201,7 +201,7 @@ def test_method_update_with_all_params(self, client: Kernel) -> None:
@parametrize
def test_raw_response_update(self, client: Kernel) -> None:
response = client.browsers.with_raw_response.update(
- id="htzv5orfit78e1m2biiifpbv",
+ id_or_name="htzv5orfit78e1m2biiifpbv",
)
assert response.is_closed is True
@@ -213,7 +213,7 @@ def test_raw_response_update(self, client: Kernel) -> None:
@parametrize
def test_streaming_response_update(self, client: Kernel) -> None:
with client.browsers.with_streaming_response.update(
- id="htzv5orfit78e1m2biiifpbv",
+ id_or_name="htzv5orfit78e1m2biiifpbv",
) as response:
assert not response.is_closed
assert response.http_request.headers.get("X-Stainless-Lang") == "python"
@@ -226,9 +226,9 @@ def test_streaming_response_update(self, client: Kernel) -> None:
@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 ''"):
+ with pytest.raises(ValueError, match=r"Expected a non-empty value for `id_or_name` but received ''"):
client.browsers.with_raw_response.update(
- id="",
+ id_or_name="",
)
@pytest.mark.skip(reason="Mock server tests are disabled")
@@ -369,7 +369,7 @@ def test_streaming_response_delete_by_id(self, client: Kernel) -> None:
@pytest.mark.skip(reason="Mock server tests are disabled")
@parametrize
def test_path_params_delete_by_id(self, client: Kernel) -> None:
- with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"):
+ with pytest.raises(ValueError, match=r"Expected a non-empty value for `id_or_name` but received ''"):
client.browsers.with_raw_response.delete_by_id(
"",
)
@@ -529,7 +529,7 @@ async def test_streaming_response_create(self, async_client: AsyncKernel) -> Non
@parametrize
async def test_method_retrieve(self, async_client: AsyncKernel) -> None:
browser = await async_client.browsers.retrieve(
- id="htzv5orfit78e1m2biiifpbv",
+ id_or_name="htzv5orfit78e1m2biiifpbv",
)
assert_matches_type(BrowserRetrieveResponse, browser, path=["response"])
@@ -537,7 +537,7 @@ async def test_method_retrieve(self, async_client: AsyncKernel) -> None:
@parametrize
async def test_method_retrieve_with_all_params(self, async_client: AsyncKernel) -> None:
browser = await async_client.browsers.retrieve(
- id="htzv5orfit78e1m2biiifpbv",
+ id_or_name="htzv5orfit78e1m2biiifpbv",
include_deleted=True,
)
assert_matches_type(BrowserRetrieveResponse, browser, path=["response"])
@@ -546,7 +546,7 @@ async def test_method_retrieve_with_all_params(self, async_client: AsyncKernel)
@parametrize
async def test_raw_response_retrieve(self, async_client: AsyncKernel) -> None:
response = await async_client.browsers.with_raw_response.retrieve(
- id="htzv5orfit78e1m2biiifpbv",
+ id_or_name="htzv5orfit78e1m2biiifpbv",
)
assert response.is_closed is True
@@ -558,7 +558,7 @@ async def test_raw_response_retrieve(self, async_client: AsyncKernel) -> None:
@parametrize
async def test_streaming_response_retrieve(self, async_client: AsyncKernel) -> None:
async with async_client.browsers.with_streaming_response.retrieve(
- id="htzv5orfit78e1m2biiifpbv",
+ id_or_name="htzv5orfit78e1m2biiifpbv",
) as response:
assert not response.is_closed
assert response.http_request.headers.get("X-Stainless-Lang") == "python"
@@ -571,16 +571,16 @@ async def test_streaming_response_retrieve(self, async_client: AsyncKernel) -> N
@pytest.mark.skip(reason="Mock server tests are disabled")
@parametrize
async def test_path_params_retrieve(self, async_client: AsyncKernel) -> None:
- with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"):
+ with pytest.raises(ValueError, match=r"Expected a non-empty value for `id_or_name` but received ''"):
await async_client.browsers.with_raw_response.retrieve(
- id="",
+ id_or_name="",
)
@pytest.mark.skip(reason="Mock server tests are disabled")
@parametrize
async def test_method_update(self, async_client: AsyncKernel) -> None:
browser = await async_client.browsers.update(
- id="htzv5orfit78e1m2biiifpbv",
+ id_or_name="htzv5orfit78e1m2biiifpbv",
)
assert_matches_type(BrowserUpdateResponse, browser, path=["response"])
@@ -588,7 +588,7 @@ async def test_method_update(self, async_client: AsyncKernel) -> None:
@parametrize
async def test_method_update_with_all_params(self, async_client: AsyncKernel) -> None:
browser = await async_client.browsers.update(
- id="htzv5orfit78e1m2biiifpbv",
+ id_or_name="htzv5orfit78e1m2biiifpbv",
disable_default_proxy=True,
profile={
"id": "id",
@@ -623,7 +623,7 @@ async def test_method_update_with_all_params(self, async_client: AsyncKernel) ->
@parametrize
async def test_raw_response_update(self, async_client: AsyncKernel) -> None:
response = await async_client.browsers.with_raw_response.update(
- id="htzv5orfit78e1m2biiifpbv",
+ id_or_name="htzv5orfit78e1m2biiifpbv",
)
assert response.is_closed is True
@@ -635,7 +635,7 @@ async def test_raw_response_update(self, async_client: AsyncKernel) -> None:
@parametrize
async def test_streaming_response_update(self, async_client: AsyncKernel) -> None:
async with async_client.browsers.with_streaming_response.update(
- id="htzv5orfit78e1m2biiifpbv",
+ id_or_name="htzv5orfit78e1m2biiifpbv",
) as response:
assert not response.is_closed
assert response.http_request.headers.get("X-Stainless-Lang") == "python"
@@ -648,9 +648,9 @@ async def test_streaming_response_update(self, async_client: AsyncKernel) -> Non
@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 ''"):
+ with pytest.raises(ValueError, match=r"Expected a non-empty value for `id_or_name` but received ''"):
await async_client.browsers.with_raw_response.update(
- id="",
+ id_or_name="",
)
@pytest.mark.skip(reason="Mock server tests are disabled")
@@ -791,7 +791,7 @@ async def test_streaming_response_delete_by_id(self, async_client: AsyncKernel)
@pytest.mark.skip(reason="Mock server tests are disabled")
@parametrize
async def test_path_params_delete_by_id(self, async_client: AsyncKernel) -> None:
- with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"):
+ with pytest.raises(ValueError, match=r"Expected a non-empty value for `id_or_name` but received ''"):
await async_client.browsers.with_raw_response.delete_by_id(
"",
)
From 686dcdd0ae8954d43d99006d25bd1f0068979c08 Mon Sep 17 00:00:00 2001
From: "stainless-app[bot]"
<142633134+stainless-app[bot]@users.noreply.github.com>
Date: Mon, 8 Jun 2026 20:25:20 +0000
Subject: [PATCH 057/366] release: 0.65.0
---
.release-please-manifest.json | 2 +-
CHANGELOG.md | 9 +++++++++
pyproject.toml | 2 +-
src/kernel/_version.py | 2 +-
4 files changed, 12 insertions(+), 3 deletions(-)
diff --git a/.release-please-manifest.json b/.release-please-manifest.json
index 4c56f2a4..57726a4f 100644
--- a/.release-please-manifest.json
+++ b/.release-please-manifest.json
@@ -1,3 +1,3 @@
{
- ".": "0.64.0"
+ ".": "0.65.0"
}
\ No newline at end of file
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 7f4edf1e..18792366 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,14 @@
# Changelog
+## 0.65.0 (2026-06-08)
+
+Full Changelog: [v0.64.0...v0.65.0](https://github.com/kernel/kernel-python-sdk/compare/v0.64.0...v0.65.0)
+
+### Features
+
+* **api:** allow setting a name and tags on a pool-acquired browser session ([2977911](https://github.com/kernel/kernel-python-sdk/commit/2977911151def0f4193c5e8063ab33800d167585))
+* **api:** support id-or-name lookup on browser session get/patch/delete ([7e6d287](https://github.com/kernel/kernel-python-sdk/commit/7e6d28795b4b2b9f9e14cf14711a72053530b576))
+
## 0.64.0 (2026-06-05)
Full Changelog: [v0.63.0...v0.64.0](https://github.com/kernel/kernel-python-sdk/compare/v0.63.0...v0.64.0)
diff --git a/pyproject.toml b/pyproject.toml
index a312a055..75d7d508 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -1,6 +1,6 @@
[project]
name = "kernel"
-version = "0.64.0"
+version = "0.65.0"
description = "The official Python library for the kernel API"
dynamic = ["readme"]
license = "Apache-2.0"
diff --git a/src/kernel/_version.py b/src/kernel/_version.py
index b55ca852..7a6f830a 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.64.0" # x-release-please-version
+__version__ = "0.65.0" # x-release-please-version
From afe6e270f06bad5b5d5e710a3de40fe735de7d1d Mon Sep 17 00:00:00 2001
From: "stainless-app[bot]"
<142633134+stainless-app[bot]@users.noreply.github.com>
Date: Tue, 9 Jun 2026 18:59:15 +0000
Subject: [PATCH 058/366] feat: Add org-level default per-project concurrency
cap
---
.stats.yml | 8 +-
api.md | 15 ++
src/kernel/_client.py | 38 +++
src/kernel/resources/__init__.py | 14 +
src/kernel/resources/organization/__init__.py | 33 +++
src/kernel/resources/organization/limits.py | 245 ++++++++++++++++++
.../resources/organization/organization.py | 108 ++++++++
src/kernel/types/organization/__init__.py | 6 +
.../types/organization/limit_update_params.py | 17 ++
src/kernel/types/organization/org_limits.py | 24 ++
tests/api_resources/organization/__init__.py | 1 +
.../api_resources/organization/test_limits.py | 152 +++++++++++
12 files changed, 657 insertions(+), 4 deletions(-)
create mode 100644 src/kernel/resources/organization/__init__.py
create mode 100644 src/kernel/resources/organization/limits.py
create mode 100644 src/kernel/resources/organization/organization.py
create mode 100644 src/kernel/types/organization/__init__.py
create mode 100644 src/kernel/types/organization/limit_update_params.py
create mode 100644 src/kernel/types/organization/org_limits.py
create mode 100644 tests/api_resources/organization/__init__.py
create mode 100644 tests/api_resources/organization/test_limits.py
diff --git a/.stats.yml b/.stats.yml
index dcde96de..fc334e13 100644
--- a/.stats.yml
+++ b/.stats.yml
@@ -1,4 +1,4 @@
-configured_endpoints: 117
-openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel/kernel-08c2d6a44f4cdcbfb6803a3043fdc1a3e33911dec4652cb3a870f01bc584421f.yml
-openapi_spec_hash: c816491451347eb93b793cddf6a78648
-config_hash: 9e45c27425021d49b5391f5cc980b046
+configured_endpoints: 119
+openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel/kernel-76f6c461ca9fd01958f3315a4f5d558ef80896c8aa0496e38caef55d4bd51dbd.yml
+openapi_spec_hash: bb8124b6016b73022c52e6ef5b7220a4
+config_hash: 80eef1b592110714ea55cd26c470fabb
diff --git a/api.md b/api.md
index 2d311077..4b24f102 100644
--- a/api.md
+++ b/api.md
@@ -423,6 +423,21 @@ Methods:
- client.projects.limits.retrieve(id) -> ProjectLimits
- client.projects.limits.update(id, \*\*params) -> ProjectLimits
+# Organization
+
+## Limits
+
+Types:
+
+```python
+from kernel.types.organization import OrgLimits, UpdateOrgLimitsRequest
+```
+
+Methods:
+
+- client.organization.limits.retrieve() -> OrgLimits
+- client.organization.limits.update(\*\*params) -> OrgLimits
+
# APIKeys
Types:
diff --git a/src/kernel/_client.py b/src/kernel/_client.py
index 537d1598..5e7e5e0d 100644
--- a/src/kernel/_client.py
+++ b/src/kernel/_client.py
@@ -58,6 +58,7 @@
credentials,
deployments,
invocations,
+ organization,
browser_pools,
credential_providers,
)
@@ -74,6 +75,7 @@
from .resources.browsers.browsers import BrowsersResource, AsyncBrowsersResource
from .resources.projects.projects import ProjectsResource, AsyncProjectsResource
from .resources.credential_providers import CredentialProvidersResource, AsyncCredentialProvidersResource
+ from .resources.organization.organization import OrganizationResource, AsyncOrganizationResource
__all__ = [
"ENVIRONMENTS",
@@ -262,6 +264,12 @@ def projects(self) -> ProjectsResource:
return ProjectsResource(self)
+ @cached_property
+ def organization(self) -> OrganizationResource:
+ from .resources.organization import OrganizationResource
+
+ return OrganizationResource(self)
+
@cached_property
def api_keys(self) -> APIKeysResource:
"""Create and manage API keys for organization and project-scoped access."""
@@ -593,6 +601,12 @@ def projects(self) -> AsyncProjectsResource:
return AsyncProjectsResource(self)
+ @cached_property
+ def organization(self) -> AsyncOrganizationResource:
+ from .resources.organization import AsyncOrganizationResource
+
+ return AsyncOrganizationResource(self)
+
@cached_property
def api_keys(self) -> AsyncAPIKeysResource:
"""Create and manage API keys for organization and project-scoped access."""
@@ -837,6 +851,12 @@ def projects(self) -> projects.ProjectsResourceWithRawResponse:
return ProjectsResourceWithRawResponse(self._client.projects)
+ @cached_property
+ def organization(self) -> organization.OrganizationResourceWithRawResponse:
+ from .resources.organization import OrganizationResourceWithRawResponse
+
+ return OrganizationResourceWithRawResponse(self._client.organization)
+
@cached_property
def api_keys(self) -> api_keys.APIKeysResourceWithRawResponse:
"""Create and manage API keys for organization and project-scoped access."""
@@ -934,6 +954,12 @@ def projects(self) -> projects.AsyncProjectsResourceWithRawResponse:
return AsyncProjectsResourceWithRawResponse(self._client.projects)
+ @cached_property
+ def organization(self) -> organization.AsyncOrganizationResourceWithRawResponse:
+ from .resources.organization import AsyncOrganizationResourceWithRawResponse
+
+ return AsyncOrganizationResourceWithRawResponse(self._client.organization)
+
@cached_property
def api_keys(self) -> api_keys.AsyncAPIKeysResourceWithRawResponse:
"""Create and manage API keys for organization and project-scoped access."""
@@ -1031,6 +1057,12 @@ def projects(self) -> projects.ProjectsResourceWithStreamingResponse:
return ProjectsResourceWithStreamingResponse(self._client.projects)
+ @cached_property
+ def organization(self) -> organization.OrganizationResourceWithStreamingResponse:
+ from .resources.organization import OrganizationResourceWithStreamingResponse
+
+ return OrganizationResourceWithStreamingResponse(self._client.organization)
+
@cached_property
def api_keys(self) -> api_keys.APIKeysResourceWithStreamingResponse:
"""Create and manage API keys for organization and project-scoped access."""
@@ -1128,6 +1160,12 @@ def projects(self) -> projects.AsyncProjectsResourceWithStreamingResponse:
return AsyncProjectsResourceWithStreamingResponse(self._client.projects)
+ @cached_property
+ def organization(self) -> organization.AsyncOrganizationResourceWithStreamingResponse:
+ from .resources.organization import AsyncOrganizationResourceWithStreamingResponse
+
+ return AsyncOrganizationResourceWithStreamingResponse(self._client.organization)
+
@cached_property
def api_keys(self) -> api_keys.AsyncAPIKeysResourceWithStreamingResponse:
"""Create and manage API keys for organization and project-scoped access."""
diff --git a/src/kernel/resources/__init__.py b/src/kernel/resources/__init__.py
index 64b98338..1497ad67 100644
--- a/src/kernel/resources/__init__.py
+++ b/src/kernel/resources/__init__.py
@@ -88,6 +88,14 @@
InvocationsResourceWithStreamingResponse,
AsyncInvocationsResourceWithStreamingResponse,
)
+from .organization import (
+ OrganizationResource,
+ AsyncOrganizationResource,
+ OrganizationResourceWithRawResponse,
+ AsyncOrganizationResourceWithRawResponse,
+ OrganizationResourceWithStreamingResponse,
+ AsyncOrganizationResourceWithStreamingResponse,
+)
from .browser_pools import (
BrowserPoolsResource,
AsyncBrowserPoolsResource,
@@ -172,6 +180,12 @@
"AsyncProjectsResourceWithRawResponse",
"ProjectsResourceWithStreamingResponse",
"AsyncProjectsResourceWithStreamingResponse",
+ "OrganizationResource",
+ "AsyncOrganizationResource",
+ "OrganizationResourceWithRawResponse",
+ "AsyncOrganizationResourceWithRawResponse",
+ "OrganizationResourceWithStreamingResponse",
+ "AsyncOrganizationResourceWithStreamingResponse",
"APIKeysResource",
"AsyncAPIKeysResource",
"APIKeysResourceWithRawResponse",
diff --git a/src/kernel/resources/organization/__init__.py b/src/kernel/resources/organization/__init__.py
new file mode 100644
index 00000000..68c28d60
--- /dev/null
+++ b/src/kernel/resources/organization/__init__.py
@@ -0,0 +1,33 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from .limits import (
+ LimitsResource,
+ AsyncLimitsResource,
+ LimitsResourceWithRawResponse,
+ AsyncLimitsResourceWithRawResponse,
+ LimitsResourceWithStreamingResponse,
+ AsyncLimitsResourceWithStreamingResponse,
+)
+from .organization import (
+ OrganizationResource,
+ AsyncOrganizationResource,
+ OrganizationResourceWithRawResponse,
+ AsyncOrganizationResourceWithRawResponse,
+ OrganizationResourceWithStreamingResponse,
+ AsyncOrganizationResourceWithStreamingResponse,
+)
+
+__all__ = [
+ "LimitsResource",
+ "AsyncLimitsResource",
+ "LimitsResourceWithRawResponse",
+ "AsyncLimitsResourceWithRawResponse",
+ "LimitsResourceWithStreamingResponse",
+ "AsyncLimitsResourceWithStreamingResponse",
+ "OrganizationResource",
+ "AsyncOrganizationResource",
+ "OrganizationResourceWithRawResponse",
+ "AsyncOrganizationResourceWithRawResponse",
+ "OrganizationResourceWithStreamingResponse",
+ "AsyncOrganizationResourceWithStreamingResponse",
+]
diff --git a/src/kernel/resources/organization/limits.py b/src/kernel/resources/organization/limits.py
new file mode 100644
index 00000000..4bf68845
--- /dev/null
+++ b/src/kernel/resources/organization/limits.py
@@ -0,0 +1,245 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from __future__ import annotations
+
+from typing import Optional
+
+import httpx
+
+from ..._types import Body, Omit, Query, Headers, NotGiven, omit, not_given
+from ..._utils import maybe_transform, async_maybe_transform
+from ..._compat import cached_property
+from ..._resource import SyncAPIResource, AsyncAPIResource
+from ..._response import (
+ to_raw_response_wrapper,
+ to_streamed_response_wrapper,
+ async_to_raw_response_wrapper,
+ async_to_streamed_response_wrapper,
+)
+from ..._base_client import make_request_options
+from ...types.organization import limit_update_params
+from ...types.organization.org_limits import OrgLimits
+
+__all__ = ["LimitsResource", "AsyncLimitsResource"]
+
+
+class LimitsResource(SyncAPIResource):
+ """Read and manage organization-level limits."""
+
+ @cached_property
+ def with_raw_response(self) -> LimitsResourceWithRawResponse:
+ """
+ This property can be used as a prefix for any HTTP method call to return
+ the raw response object instead of the parsed content.
+
+ For more information, see https://www.github.com/kernel/kernel-python-sdk#accessing-raw-response-data-eg-headers
+ """
+ return LimitsResourceWithRawResponse(self)
+
+ @cached_property
+ def with_streaming_response(self) -> LimitsResourceWithStreamingResponse:
+ """
+ An alternative to `.with_raw_response` that doesn't eagerly read the response body.
+
+ For more information, see https://www.github.com/kernel/kernel-python-sdk#with_streaming_response
+ """
+ return LimitsResourceWithStreamingResponse(self)
+
+ def retrieve(
+ self,
+ *,
+ # 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,
+ ) -> OrgLimits:
+ """
+ Get the organization's concurrent session ceiling and the default per-project
+ concurrency cap applied to projects without an explicit override.
+ """
+ return self._get(
+ "/org/limits",
+ options=make_request_options(
+ extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
+ ),
+ cast_to=OrgLimits,
+ )
+
+ def update(
+ self,
+ *,
+ default_project_max_concurrent_sessions: Optional[int] | Omit = omit,
+ # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
+ # The extra values given here take precedence over values defined on the client or passed to this method.
+ extra_headers: Headers | None = None,
+ extra_query: Query | None = None,
+ extra_body: Body | None = None,
+ timeout: float | httpx.Timeout | None | NotGiven = not_given,
+ ) -> OrgLimits:
+ """
+ Set the default per-project concurrency cap applied to projects without an
+ explicit override. Set the value to 0 to remove the default; omit to leave it
+ unchanged. The default cannot exceed the organization's concurrent session
+ ceiling.
+
+ Args:
+ default_project_max_concurrent_sessions: Default maximum concurrent browser sessions for projects without an explicit
+ override. Set to 0 to remove the default; omit to leave unchanged. Cannot exceed
+ the organization's concurrent session ceiling.
+
+ 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
+ """
+ return self._patch(
+ "/org/limits",
+ body=maybe_transform(
+ {"default_project_max_concurrent_sessions": default_project_max_concurrent_sessions},
+ limit_update_params.LimitUpdateParams,
+ ),
+ options=make_request_options(
+ extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
+ ),
+ cast_to=OrgLimits,
+ )
+
+
+class AsyncLimitsResource(AsyncAPIResource):
+ """Read and manage organization-level limits."""
+
+ @cached_property
+ def with_raw_response(self) -> AsyncLimitsResourceWithRawResponse:
+ """
+ This property can be used as a prefix for any HTTP method call to return
+ the raw response object instead of the parsed content.
+
+ For more information, see https://www.github.com/kernel/kernel-python-sdk#accessing-raw-response-data-eg-headers
+ """
+ return AsyncLimitsResourceWithRawResponse(self)
+
+ @cached_property
+ def with_streaming_response(self) -> AsyncLimitsResourceWithStreamingResponse:
+ """
+ An alternative to `.with_raw_response` that doesn't eagerly read the response body.
+
+ For more information, see https://www.github.com/kernel/kernel-python-sdk#with_streaming_response
+ """
+ return AsyncLimitsResourceWithStreamingResponse(self)
+
+ async def retrieve(
+ self,
+ *,
+ # 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,
+ ) -> OrgLimits:
+ """
+ Get the organization's concurrent session ceiling and the default per-project
+ concurrency cap applied to projects without an explicit override.
+ """
+ return await self._get(
+ "/org/limits",
+ options=make_request_options(
+ extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
+ ),
+ cast_to=OrgLimits,
+ )
+
+ async def update(
+ self,
+ *,
+ default_project_max_concurrent_sessions: Optional[int] | Omit = omit,
+ # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
+ # The extra values given here take precedence over values defined on the client or passed to this method.
+ extra_headers: Headers | None = None,
+ extra_query: Query | None = None,
+ extra_body: Body | None = None,
+ timeout: float | httpx.Timeout | None | NotGiven = not_given,
+ ) -> OrgLimits:
+ """
+ Set the default per-project concurrency cap applied to projects without an
+ explicit override. Set the value to 0 to remove the default; omit to leave it
+ unchanged. The default cannot exceed the organization's concurrent session
+ ceiling.
+
+ Args:
+ default_project_max_concurrent_sessions: Default maximum concurrent browser sessions for projects without an explicit
+ override. Set to 0 to remove the default; omit to leave unchanged. Cannot exceed
+ the organization's concurrent session ceiling.
+
+ 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
+ """
+ return await self._patch(
+ "/org/limits",
+ body=await async_maybe_transform(
+ {"default_project_max_concurrent_sessions": default_project_max_concurrent_sessions},
+ limit_update_params.LimitUpdateParams,
+ ),
+ options=make_request_options(
+ extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
+ ),
+ cast_to=OrgLimits,
+ )
+
+
+class LimitsResourceWithRawResponse:
+ def __init__(self, limits: LimitsResource) -> None:
+ self._limits = limits
+
+ self.retrieve = to_raw_response_wrapper(
+ limits.retrieve,
+ )
+ self.update = to_raw_response_wrapper(
+ limits.update,
+ )
+
+
+class AsyncLimitsResourceWithRawResponse:
+ def __init__(self, limits: AsyncLimitsResource) -> None:
+ self._limits = limits
+
+ self.retrieve = async_to_raw_response_wrapper(
+ limits.retrieve,
+ )
+ self.update = async_to_raw_response_wrapper(
+ limits.update,
+ )
+
+
+class LimitsResourceWithStreamingResponse:
+ def __init__(self, limits: LimitsResource) -> None:
+ self._limits = limits
+
+ self.retrieve = to_streamed_response_wrapper(
+ limits.retrieve,
+ )
+ self.update = to_streamed_response_wrapper(
+ limits.update,
+ )
+
+
+class AsyncLimitsResourceWithStreamingResponse:
+ def __init__(self, limits: AsyncLimitsResource) -> None:
+ self._limits = limits
+
+ self.retrieve = async_to_streamed_response_wrapper(
+ limits.retrieve,
+ )
+ self.update = async_to_streamed_response_wrapper(
+ limits.update,
+ )
diff --git a/src/kernel/resources/organization/organization.py b/src/kernel/resources/organization/organization.py
new file mode 100644
index 00000000..21163122
--- /dev/null
+++ b/src/kernel/resources/organization/organization.py
@@ -0,0 +1,108 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from __future__ import annotations
+
+from .limits import (
+ LimitsResource,
+ AsyncLimitsResource,
+ LimitsResourceWithRawResponse,
+ AsyncLimitsResourceWithRawResponse,
+ LimitsResourceWithStreamingResponse,
+ AsyncLimitsResourceWithStreamingResponse,
+)
+from ..._compat import cached_property
+from ..._resource import SyncAPIResource, AsyncAPIResource
+
+__all__ = ["OrganizationResource", "AsyncOrganizationResource"]
+
+
+class OrganizationResource(SyncAPIResource):
+ @cached_property
+ def limits(self) -> LimitsResource:
+ """Read and manage organization-level limits."""
+ return LimitsResource(self._client)
+
+ @cached_property
+ def with_raw_response(self) -> OrganizationResourceWithRawResponse:
+ """
+ This property can be used as a prefix for any HTTP method call to return
+ the raw response object instead of the parsed content.
+
+ For more information, see https://www.github.com/kernel/kernel-python-sdk#accessing-raw-response-data-eg-headers
+ """
+ return OrganizationResourceWithRawResponse(self)
+
+ @cached_property
+ def with_streaming_response(self) -> OrganizationResourceWithStreamingResponse:
+ """
+ An alternative to `.with_raw_response` that doesn't eagerly read the response body.
+
+ For more information, see https://www.github.com/kernel/kernel-python-sdk#with_streaming_response
+ """
+ return OrganizationResourceWithStreamingResponse(self)
+
+
+class AsyncOrganizationResource(AsyncAPIResource):
+ @cached_property
+ def limits(self) -> AsyncLimitsResource:
+ """Read and manage organization-level limits."""
+ return AsyncLimitsResource(self._client)
+
+ @cached_property
+ def with_raw_response(self) -> AsyncOrganizationResourceWithRawResponse:
+ """
+ This property can be used as a prefix for any HTTP method call to return
+ the raw response object instead of the parsed content.
+
+ For more information, see https://www.github.com/kernel/kernel-python-sdk#accessing-raw-response-data-eg-headers
+ """
+ return AsyncOrganizationResourceWithRawResponse(self)
+
+ @cached_property
+ def with_streaming_response(self) -> AsyncOrganizationResourceWithStreamingResponse:
+ """
+ An alternative to `.with_raw_response` that doesn't eagerly read the response body.
+
+ For more information, see https://www.github.com/kernel/kernel-python-sdk#with_streaming_response
+ """
+ return AsyncOrganizationResourceWithStreamingResponse(self)
+
+
+class OrganizationResourceWithRawResponse:
+ def __init__(self, organization: OrganizationResource) -> None:
+ self._organization = organization
+
+ @cached_property
+ def limits(self) -> LimitsResourceWithRawResponse:
+ """Read and manage organization-level limits."""
+ return LimitsResourceWithRawResponse(self._organization.limits)
+
+
+class AsyncOrganizationResourceWithRawResponse:
+ def __init__(self, organization: AsyncOrganizationResource) -> None:
+ self._organization = organization
+
+ @cached_property
+ def limits(self) -> AsyncLimitsResourceWithRawResponse:
+ """Read and manage organization-level limits."""
+ return AsyncLimitsResourceWithRawResponse(self._organization.limits)
+
+
+class OrganizationResourceWithStreamingResponse:
+ def __init__(self, organization: OrganizationResource) -> None:
+ self._organization = organization
+
+ @cached_property
+ def limits(self) -> LimitsResourceWithStreamingResponse:
+ """Read and manage organization-level limits."""
+ return LimitsResourceWithStreamingResponse(self._organization.limits)
+
+
+class AsyncOrganizationResourceWithStreamingResponse:
+ def __init__(self, organization: AsyncOrganizationResource) -> None:
+ self._organization = organization
+
+ @cached_property
+ def limits(self) -> AsyncLimitsResourceWithStreamingResponse:
+ """Read and manage organization-level limits."""
+ return AsyncLimitsResourceWithStreamingResponse(self._organization.limits)
diff --git a/src/kernel/types/organization/__init__.py b/src/kernel/types/organization/__init__.py
new file mode 100644
index 00000000..d2411f9a
--- /dev/null
+++ b/src/kernel/types/organization/__init__.py
@@ -0,0 +1,6 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from __future__ import annotations
+
+from .org_limits import OrgLimits as OrgLimits
+from .limit_update_params import LimitUpdateParams as LimitUpdateParams
diff --git a/src/kernel/types/organization/limit_update_params.py b/src/kernel/types/organization/limit_update_params.py
new file mode 100644
index 00000000..619eae91
--- /dev/null
+++ b/src/kernel/types/organization/limit_update_params.py
@@ -0,0 +1,17 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from __future__ import annotations
+
+from typing import Optional
+from typing_extensions import TypedDict
+
+__all__ = ["LimitUpdateParams"]
+
+
+class LimitUpdateParams(TypedDict, total=False):
+ default_project_max_concurrent_sessions: Optional[int]
+ """
+ Default maximum concurrent browser sessions for projects without an explicit
+ override. Set to 0 to remove the default; omit to leave unchanged. Cannot exceed
+ the organization's concurrent session ceiling.
+ """
diff --git a/src/kernel/types/organization/org_limits.py b/src/kernel/types/organization/org_limits.py
new file mode 100644
index 00000000..da9153c1
--- /dev/null
+++ b/src/kernel/types/organization/org_limits.py
@@ -0,0 +1,24 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from typing import Optional
+
+from ..._models import BaseModel
+
+__all__ = ["OrgLimits"]
+
+
+class OrgLimits(BaseModel):
+ default_project_max_concurrent_sessions: Optional[int] = None
+ """
+ Default maximum concurrent browser sessions applied to every project that has no
+ explicit per-project override. Null means no org-level default, so such projects
+ are uncapped (only the org-wide limit applies). Applies to existing and newly
+ created projects.
+ """
+
+ max_concurrent_sessions: Optional[int] = None
+ """
+ The organization's effective concurrent browser session ceiling, from its plan
+ or an override. Read-only and shared across all projects in the org; a
+ per-project default cannot exceed it.
+ """
diff --git a/tests/api_resources/organization/__init__.py b/tests/api_resources/organization/__init__.py
new file mode 100644
index 00000000..fd8019a9
--- /dev/null
+++ b/tests/api_resources/organization/__init__.py
@@ -0,0 +1 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
diff --git a/tests/api_resources/organization/test_limits.py b/tests/api_resources/organization/test_limits.py
new file mode 100644
index 00000000..a9dca6d0
--- /dev/null
+++ b/tests/api_resources/organization/test_limits.py
@@ -0,0 +1,152 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from __future__ import annotations
+
+import os
+from typing import Any, cast
+
+import pytest
+
+from kernel import Kernel, AsyncKernel
+from tests.utils import assert_matches_type
+from kernel.types.organization import OrgLimits
+
+base_url = os.environ.get("TEST_API_BASE_URL", "http://127.0.0.1:4010")
+
+
+class TestLimits:
+ parametrize = pytest.mark.parametrize("client", [False, True], indirect=True, ids=["loose", "strict"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ def test_method_retrieve(self, client: Kernel) -> None:
+ limit = client.organization.limits.retrieve()
+ assert_matches_type(OrgLimits, limit, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ def test_raw_response_retrieve(self, client: Kernel) -> None:
+ response = client.organization.limits.with_raw_response.retrieve()
+
+ assert response.is_closed is True
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+ limit = response.parse()
+ assert_matches_type(OrgLimits, limit, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ def test_streaming_response_retrieve(self, client: Kernel) -> None:
+ with client.organization.limits.with_streaming_response.retrieve() as response:
+ assert not response.is_closed
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+
+ limit = response.parse()
+ assert_matches_type(OrgLimits, limit, path=["response"])
+
+ assert cast(Any, response.is_closed) is True
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ def test_method_update(self, client: Kernel) -> None:
+ limit = client.organization.limits.update()
+ assert_matches_type(OrgLimits, limit, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ def test_method_update_with_all_params(self, client: Kernel) -> None:
+ limit = client.organization.limits.update(
+ default_project_max_concurrent_sessions=0,
+ )
+ assert_matches_type(OrgLimits, limit, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ def test_raw_response_update(self, client: Kernel) -> None:
+ response = client.organization.limits.with_raw_response.update()
+
+ assert response.is_closed is True
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+ limit = response.parse()
+ assert_matches_type(OrgLimits, limit, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ def test_streaming_response_update(self, client: Kernel) -> None:
+ with client.organization.limits.with_streaming_response.update() as response:
+ assert not response.is_closed
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+
+ limit = response.parse()
+ assert_matches_type(OrgLimits, limit, path=["response"])
+
+ assert cast(Any, response.is_closed) is True
+
+
+class TestAsyncLimits:
+ parametrize = pytest.mark.parametrize(
+ "async_client", [False, True, {"http_client": "aiohttp"}], indirect=True, ids=["loose", "strict", "aiohttp"]
+ )
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ async def test_method_retrieve(self, async_client: AsyncKernel) -> None:
+ limit = await async_client.organization.limits.retrieve()
+ assert_matches_type(OrgLimits, limit, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ async def test_raw_response_retrieve(self, async_client: AsyncKernel) -> None:
+ response = await async_client.organization.limits.with_raw_response.retrieve()
+
+ assert response.is_closed is True
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+ limit = await response.parse()
+ assert_matches_type(OrgLimits, limit, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ async def test_streaming_response_retrieve(self, async_client: AsyncKernel) -> None:
+ async with async_client.organization.limits.with_streaming_response.retrieve() as response:
+ assert not response.is_closed
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+
+ limit = await response.parse()
+ assert_matches_type(OrgLimits, limit, path=["response"])
+
+ assert cast(Any, response.is_closed) is True
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ async def test_method_update(self, async_client: AsyncKernel) -> None:
+ limit = await async_client.organization.limits.update()
+ assert_matches_type(OrgLimits, limit, 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:
+ limit = await async_client.organization.limits.update(
+ default_project_max_concurrent_sessions=0,
+ )
+ assert_matches_type(OrgLimits, limit, 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.organization.limits.with_raw_response.update()
+
+ assert response.is_closed is True
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+ limit = await response.parse()
+ assert_matches_type(OrgLimits, limit, 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.organization.limits.with_streaming_response.update() as response:
+ assert not response.is_closed
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+
+ limit = await response.parse()
+ assert_matches_type(OrgLimits, limit, path=["response"])
+
+ assert cast(Any, response.is_closed) is True
From b5ea7e5317e87bfb4d71e36a8e3d11932242f7ad Mon Sep 17 00:00:00 2001
From: "stainless-app[bot]"
<142633134+stainless-app[bot]@users.noreply.github.com>
Date: Tue, 9 Jun 2026 21:31:38 +0000
Subject: [PATCH 059/366] codegen metadata
---
.stats.yml | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/.stats.yml b/.stats.yml
index fc334e13..17293bfb 100644
--- a/.stats.yml
+++ b/.stats.yml
@@ -1,4 +1,4 @@
configured_endpoints: 119
-openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel/kernel-76f6c461ca9fd01958f3315a4f5d558ef80896c8aa0496e38caef55d4bd51dbd.yml
-openapi_spec_hash: bb8124b6016b73022c52e6ef5b7220a4
+openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel/kernel-f9a96fe14f0b3c93230a26f9b64827a35a19a28d4e7cd2719315c4d76cce78fc.yml
+openapi_spec_hash: 852e2a64b850f759ccbcf81b1579497a
config_hash: 80eef1b592110714ea55cd26c470fabb
From c1eee8d3908d37f9d6d2931a891109e008404c1b Mon Sep 17 00:00:00 2001
From: "stainless-app[bot]"
<142633134+stainless-app[bot]@users.noreply.github.com>
Date: Wed, 10 Jun 2026 20:41:45 +0000
Subject: [PATCH 060/366] feat: Support updating browser session name and tags
via PATCH
---
.stats.yml | 4 +--
src/kernel/resources/browsers/browsers.py | 38 +++++++++++++++++++----
src/kernel/types/browser_create_params.py | 7 +++--
src/kernel/types/browser_update_params.py | 16 ++++++++++
tests/api_resources/test_browsers.py | 10 ++++++
5 files changed, 64 insertions(+), 11 deletions(-)
diff --git a/.stats.yml b/.stats.yml
index 17293bfb..5ad69241 100644
--- a/.stats.yml
+++ b/.stats.yml
@@ -1,4 +1,4 @@
configured_endpoints: 119
-openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel/kernel-f9a96fe14f0b3c93230a26f9b64827a35a19a28d4e7cd2719315c4d76cce78fc.yml
-openapi_spec_hash: 852e2a64b850f759ccbcf81b1579497a
+openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel/kernel-e66c3f8aedccc39104386a3ec619f3fdcef7e8b00d9e5aa82e414a1b387351c2.yml
+openapi_spec_hash: afeddf18ebc3da1521b3e6f6739411fa
config_hash: 80eef1b592110714ea55cd26c470fabb
diff --git a/src/kernel/resources/browsers/browsers.py b/src/kernel/resources/browsers/browsers.py
index 61ada1c2..c5c365e6 100644
--- a/src/kernel/resources/browsers/browsers.py
+++ b/src/kernel/resources/browsers/browsers.py
@@ -209,8 +209,8 @@ def create(
view.
name: Optional human-readable name for the browser session, used to find it later in
- the dashboard. Must be unique among active sessions within the project. Set at
- creation time only.
+ the dashboard. Must be unique among active sessions within the project. Can be
+ changed later via PATCH /browsers/{id_or_name}.
profile: Profile selection for the browser session. Provide either id or name. If
specified, the matching profile will be loaded into the browser session.
@@ -227,7 +227,8 @@ def create(
mechanisms.
tags: Optional user-defined key-value tags for the browser session, used to find and
- group sessions later. Set at creation time only. Up to 50 pairs.
+ group sessions later. Can be changed later via PATCH /browsers/{id_or_name}. Up
+ to 50 pairs.
telemetry: Telemetry configuration for the browser session. Set enabled to true to start
capture using VM defaults, or provide browser category settings. If omitted,
@@ -337,8 +338,10 @@ def update(
id_or_name: str,
*,
disable_default_proxy: bool | Omit = omit,
+ name: Optional[str] | Omit = omit,
profile: BrowserProfile | Omit = omit,
proxy_id: Optional[str] | Omit = omit,
+ tags: Optional[TagsParam] | Omit = omit,
telemetry: Optional[browser_update_params.Telemetry] | Omit = omit,
viewport: browser_update_params.Viewport | Omit = omit,
# Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
@@ -355,12 +358,20 @@ def update(
disable_default_proxy: If true, stealth browsers connect directly instead of using the default stealth
proxy.
+ name: Human-readable name for the browser session. Omit to leave unchanged, set to an
+ empty string to clear the name. When set, must be unique among active sessions
+ within the project.
+
profile: Profile to load into the browser session. Only allowed if the session does not
already have a profile loaded.
proxy_id: ID of the proxy to use. Omit to leave unchanged, set to empty string to remove
proxy.
+ tags: User-defined key-value tags for the browser session. Omit to leave unchanged.
+ Provide a map to replace the entire tag set (full replace, not a merge). Set to
+ an empty object ({}) to clear all tags. Up to 50 pairs.
+
telemetry: Telemetry configuration. Omit, set to null, or set to an empty object ({}) to
leave the existing configuration unchanged. Set enabled to true to enable
capture using VM defaults. Set enabled to false to stop capture. Provide browser
@@ -384,8 +395,10 @@ def update(
body=maybe_transform(
{
"disable_default_proxy": disable_default_proxy,
+ "name": name,
"profile": profile,
"proxy_id": proxy_id,
+ "tags": tags,
"telemetry": telemetry,
"viewport": viewport,
},
@@ -774,8 +787,8 @@ async def create(
view.
name: Optional human-readable name for the browser session, used to find it later in
- the dashboard. Must be unique among active sessions within the project. Set at
- creation time only.
+ the dashboard. Must be unique among active sessions within the project. Can be
+ changed later via PATCH /browsers/{id_or_name}.
profile: Profile selection for the browser session. Provide either id or name. If
specified, the matching profile will be loaded into the browser session.
@@ -792,7 +805,8 @@ async def create(
mechanisms.
tags: Optional user-defined key-value tags for the browser session, used to find and
- group sessions later. Set at creation time only. Up to 50 pairs.
+ group sessions later. Can be changed later via PATCH /browsers/{id_or_name}. Up
+ to 50 pairs.
telemetry: Telemetry configuration for the browser session. Set enabled to true to start
capture using VM defaults, or provide browser category settings. If omitted,
@@ -902,8 +916,10 @@ async def update(
id_or_name: str,
*,
disable_default_proxy: bool | Omit = omit,
+ name: Optional[str] | Omit = omit,
profile: BrowserProfile | Omit = omit,
proxy_id: Optional[str] | Omit = omit,
+ tags: Optional[TagsParam] | Omit = omit,
telemetry: Optional[browser_update_params.Telemetry] | Omit = omit,
viewport: browser_update_params.Viewport | Omit = omit,
# Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
@@ -920,12 +936,20 @@ async def update(
disable_default_proxy: If true, stealth browsers connect directly instead of using the default stealth
proxy.
+ name: Human-readable name for the browser session. Omit to leave unchanged, set to an
+ empty string to clear the name. When set, must be unique among active sessions
+ within the project.
+
profile: Profile to load into the browser session. Only allowed if the session does not
already have a profile loaded.
proxy_id: ID of the proxy to use. Omit to leave unchanged, set to empty string to remove
proxy.
+ tags: User-defined key-value tags for the browser session. Omit to leave unchanged.
+ Provide a map to replace the entire tag set (full replace, not a merge). Set to
+ an empty object ({}) to clear all tags. Up to 50 pairs.
+
telemetry: Telemetry configuration. Omit, set to null, or set to an empty object ({}) to
leave the existing configuration unchanged. Set enabled to true to enable
capture using VM defaults. Set enabled to false to stop capture. Provide browser
@@ -949,8 +973,10 @@ async def update(
body=await async_maybe_transform(
{
"disable_default_proxy": disable_default_proxy,
+ "name": name,
"profile": profile,
"proxy_id": proxy_id,
+ "tags": tags,
"telemetry": telemetry,
"viewport": viewport,
},
diff --git a/src/kernel/types/browser_create_params.py b/src/kernel/types/browser_create_params.py
index 6d7b80b3..01308f38 100644
--- a/src/kernel/types/browser_create_params.py
+++ b/src/kernel/types/browser_create_params.py
@@ -53,8 +53,8 @@ class BrowserCreateParams(TypedDict, total=False):
name: str
"""
Optional human-readable name for the browser session, used to find it later in
- the dashboard. Must be unique among active sessions within the project. Set at
- creation time only.
+ the dashboard. Must be unique among active sessions within the project. Can be
+ changed later via PATCH /browsers/{id_or_name}.
"""
profile: BrowserProfile
@@ -86,7 +86,8 @@ class BrowserCreateParams(TypedDict, total=False):
tags: TagsParam
"""
Optional user-defined key-value tags for the browser session, used to find and
- group sessions later. Set at creation time only. Up to 50 pairs.
+ group sessions later. Can be changed later via PATCH /browsers/{id_or_name}. Up
+ to 50 pairs.
"""
telemetry: Optional[Telemetry]
diff --git a/src/kernel/types/browser_update_params.py b/src/kernel/types/browser_update_params.py
index 2626917c..7488be4f 100644
--- a/src/kernel/types/browser_update_params.py
+++ b/src/kernel/types/browser_update_params.py
@@ -5,6 +5,7 @@
from typing import Optional
from typing_extensions import TypedDict
+from .tags_param import TagsParam
from .shared_params.browser_profile import BrowserProfile
from .shared_params.browser_viewport import BrowserViewport
from .browsers.browser_telemetry_categories_config_param import BrowserTelemetryCategoriesConfigParam
@@ -19,6 +20,13 @@ class BrowserUpdateParams(TypedDict, total=False):
proxy.
"""
+ name: Optional[str]
+ """Human-readable name for the browser session.
+
+ Omit to leave unchanged, set to an empty string to clear the name. When set,
+ must be unique among active sessions within the project.
+ """
+
profile: BrowserProfile
"""Profile to load into the browser session.
@@ -31,6 +39,14 @@ class BrowserUpdateParams(TypedDict, total=False):
Omit to leave unchanged, set to empty string to remove proxy.
"""
+ tags: Optional[TagsParam]
+ """User-defined key-value tags for the browser session.
+
+ Omit to leave unchanged. Provide a map to replace the entire tag set (full
+ replace, not a merge). Set to an empty object ({}) to clear all tags. Up to 50
+ pairs.
+ """
+
telemetry: Optional[Telemetry]
"""Telemetry configuration.
diff --git a/tests/api_resources/test_browsers.py b/tests/api_resources/test_browsers.py
index e09d4705..322fffed 100644
--- a/tests/api_resources/test_browsers.py
+++ b/tests/api_resources/test_browsers.py
@@ -168,12 +168,17 @@ def test_method_update_with_all_params(self, client: Kernel) -> None:
browser = client.browsers.update(
id_or_name="htzv5orfit78e1m2biiifpbv",
disable_default_proxy=True,
+ name="checkout-flow-1",
profile={
"id": "id",
"name": "name",
"save_changes": True,
},
proxy_id="proxy_id",
+ tags={
+ "team": "backend",
+ "env": "staging",
+ },
telemetry={
"browser": {
"captcha": {"enabled": True},
@@ -590,12 +595,17 @@ async def test_method_update_with_all_params(self, async_client: AsyncKernel) ->
browser = await async_client.browsers.update(
id_or_name="htzv5orfit78e1m2biiifpbv",
disable_default_proxy=True,
+ name="checkout-flow-1",
profile={
"id": "id",
"name": "name",
"save_changes": True,
},
proxy_id="proxy_id",
+ tags={
+ "team": "backend",
+ "env": "staging",
+ },
telemetry={
"browser": {
"captcha": {"enabled": True},
From ea6b94db06c728c21e3a809cd18d8e42c5063988 Mon Sep 17 00:00:00 2001
From: "stainless-app[bot]"
<142633134+stainless-app[bot]@users.noreply.github.com>
Date: Wed, 10 Jun 2026 20:42:14 +0000
Subject: [PATCH 061/366] release: 0.66.0
---
.release-please-manifest.json | 2 +-
CHANGELOG.md | 9 +++++++++
pyproject.toml | 2 +-
src/kernel/_version.py | 2 +-
4 files changed, 12 insertions(+), 3 deletions(-)
diff --git a/.release-please-manifest.json b/.release-please-manifest.json
index 57726a4f..b429f966 100644
--- a/.release-please-manifest.json
+++ b/.release-please-manifest.json
@@ -1,3 +1,3 @@
{
- ".": "0.65.0"
+ ".": "0.66.0"
}
\ No newline at end of file
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 18792366..af41d8db 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,14 @@
# Changelog
+## 0.66.0 (2026-06-10)
+
+Full Changelog: [v0.65.0...v0.66.0](https://github.com/kernel/kernel-python-sdk/compare/v0.65.0...v0.66.0)
+
+### Features
+
+* Add org-level default per-project concurrency cap ([2de4525](https://github.com/kernel/kernel-python-sdk/commit/2de45255a1f5ed003ec0492461ee967bf961db2e))
+* Support updating browser session name and tags via PATCH ([e26e9c6](https://github.com/kernel/kernel-python-sdk/commit/e26e9c6b0513a1c7f9723554f4ba977988290d5e))
+
## 0.65.0 (2026-06-08)
Full Changelog: [v0.64.0...v0.65.0](https://github.com/kernel/kernel-python-sdk/compare/v0.64.0...v0.65.0)
diff --git a/pyproject.toml b/pyproject.toml
index 75d7d508..c9fef2a6 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -1,6 +1,6 @@
[project]
name = "kernel"
-version = "0.65.0"
+version = "0.66.0"
description = "The official Python library for the kernel API"
dynamic = ["readme"]
license = "Apache-2.0"
diff --git a/src/kernel/_version.py b/src/kernel/_version.py
index 7a6f830a..eda0bdfb 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.65.0" # x-release-please-version
+__version__ = "0.66.0" # x-release-please-version
From 222b534c9bcc3de72555cf75ac94f7b9b08bfaf7 Mon Sep 17 00:00:00 2001
From: "stainless-app[bot]"
<142633134+stainless-app[bot]@users.noreply.github.com>
Date: Thu, 11 Jun 2026 19:08:20 +0000
Subject: [PATCH 062/366] codegen metadata
---
.stats.yml | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/.stats.yml b/.stats.yml
index 5ad69241..f6e3d871 100644
--- a/.stats.yml
+++ b/.stats.yml
@@ -1,4 +1,4 @@
configured_endpoints: 119
-openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel/kernel-e66c3f8aedccc39104386a3ec619f3fdcef7e8b00d9e5aa82e414a1b387351c2.yml
-openapi_spec_hash: afeddf18ebc3da1521b3e6f6739411fa
+openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel/kernel-663f08b43eda6383b99ab0f3b1cd049d62f22c81daf149d8e84ff18f53c84c89.yml
+openapi_spec_hash: 29ea250251cc14d70e3f8f737ebc1466
config_hash: 80eef1b592110714ea55cd26c470fabb
From 592933283ff7bf0e6596ddbfbc4851e285a1d36e Mon Sep 17 00:00:00 2001
From: "stainless-app[bot]"
<142633134+stainless-app[bot]@users.noreply.github.com>
Date: Thu, 11 Jun 2026 19:42:18 +0000
Subject: [PATCH 063/366] docs(api): correct project-scoping descriptions in
OpenAPI spec
---
.stats.yml | 4 ++--
src/kernel/resources/auth/connections.py | 24 +++++++++----------
src/kernel/resources/browser_pools.py | 20 ++++++++--------
src/kernel/resources/browsers/browsers.py | 8 +++----
src/kernel/resources/credentials.py | 10 ++++----
src/kernel/resources/extensions.py | 4 ++--
src/kernel/resources/proxies.py | 12 +++++-----
.../types/auth/connection_create_params.py | 5 ++--
.../types/auth/connection_login_params.py | 5 ++--
.../types/auth/connection_update_params.py | 5 ++--
src/kernel/types/browser_create_params.py | 2 +-
src/kernel/types/browser_pool.py | 2 +-
.../types/browser_pool_create_params.py | 2 +-
.../types/browser_pool_update_params.py | 2 +-
14 files changed, 53 insertions(+), 52 deletions(-)
diff --git a/.stats.yml b/.stats.yml
index f6e3d871..1d17b2b3 100644
--- a/.stats.yml
+++ b/.stats.yml
@@ -1,4 +1,4 @@
configured_endpoints: 119
-openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel/kernel-663f08b43eda6383b99ab0f3b1cd049d62f22c81daf149d8e84ff18f53c84c89.yml
-openapi_spec_hash: 29ea250251cc14d70e3f8f737ebc1466
+openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel/kernel-c6458d9c6cb5d7e3d8309c79b69eba3a22269e0ecc0bdafbaee00fde4b302e99.yml
+openapi_spec_hash: ee77b293c4bda91c1a32cfdd12b8739e
config_hash: 80eef1b592110714ea55cd26c470fabb
diff --git a/src/kernel/resources/auth/connections.py b/src/kernel/resources/auth/connections.py
index dc912aba..362e6129 100644
--- a/src/kernel/resources/auth/connections.py
+++ b/src/kernel/resources/auth/connections.py
@@ -136,8 +136,8 @@ def create(
login_url: Optional login page URL to skip discovery
- proxy: Proxy selection. Provide either id or name. The proxy must belong to the
- caller's org.
+ proxy: Proxy selection. Provide either id or name. The proxy must be in the same
+ project as the resource referencing it.
record_session: Whether to record browser sessions for this connection by default. Useful for
debugging. Can be overridden per-login. Defaults to false.
@@ -264,8 +264,8 @@ def update(
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.
+ proxy: Proxy selection. Provide either id or name. The proxy must be in the same
+ project as the resource referencing it.
record_session: Whether to record browser sessions for this connection by default
@@ -457,8 +457,8 @@ def login(
credentials are stored.
Args:
- proxy: Proxy selection. Provide either id or name. The proxy must belong to the
- caller's org.
+ proxy: Proxy selection. Provide either id or name. The proxy must be in the same
+ project as the resource referencing it.
record_session: Override the connection's default for recording this login's browser session.
When omitted, the connection's record_session default is used.
@@ -653,8 +653,8 @@ async def create(
login_url: Optional login page URL to skip discovery
- proxy: Proxy selection. Provide either id or name. The proxy must belong to the
- caller's org.
+ proxy: Proxy selection. Provide either id or name. The proxy must be in the same
+ project as the resource referencing it.
record_session: Whether to record browser sessions for this connection by default. Useful for
debugging. Can be overridden per-login. Defaults to false.
@@ -781,8 +781,8 @@ async def update(
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.
+ proxy: Proxy selection. Provide either id or name. The proxy must be in the same
+ project as the resource referencing it.
record_session: Whether to record browser sessions for this connection by default
@@ -974,8 +974,8 @@ async def login(
credentials are stored.
Args:
- proxy: Proxy selection. Provide either id or name. The proxy must belong to the
- caller's org.
+ proxy: Proxy selection. Provide either id or name. The proxy must be in the same
+ project as the resource referencing it.
record_session: Override the connection's default for recording this login's browser session.
When omitted, the connection's record_session default is used.
diff --git a/src/kernel/resources/browser_pools.py b/src/kernel/resources/browser_pools.py
index 5099f446..fc32c951 100644
--- a/src/kernel/resources/browser_pools.py
+++ b/src/kernel/resources/browser_pools.py
@@ -109,8 +109,8 @@ def create(
specified, the matching profile will be loaded into the browser session.
Profiles must be created beforehand.
- proxy_id: Optional proxy to associate to the browser session. Must reference a proxy
- belonging to the caller's org.
+ proxy_id: Optional proxy to associate to the browser session. Must reference a proxy in
+ the same project as the browser session.
start_url: Optional URL to navigate to when a new browser is warmed into the pool.
Best-effort: failures to navigate do not fail pool fill. Only applied to
@@ -256,8 +256,8 @@ def update(
specified, the matching profile will be loaded into the browser session.
Profiles must be created beforehand.
- proxy_id: Optional proxy to associate to the browser session. Must reference a proxy
- belonging to the caller's org.
+ proxy_id: Optional proxy to associate to the browser session. Must reference a proxy in
+ the same project as the browser session.
size: Number of browsers to maintain in the pool. The maximum size is determined by
your organization's pooled sessions limit (the sum of all pool sizes cannot
@@ -338,7 +338,7 @@ def list(
timeout: float | httpx.Timeout | None | NotGiven = not_given,
) -> SyncOffsetPagination[BrowserPool]:
"""
- List browser pools owned by the caller's organization.
+ List browser pools in the resolved project.
Args:
limit: Limit the number of browser pools to return.
@@ -630,8 +630,8 @@ async def create(
specified, the matching profile will be loaded into the browser session.
Profiles must be created beforehand.
- proxy_id: Optional proxy to associate to the browser session. Must reference a proxy
- belonging to the caller's org.
+ proxy_id: Optional proxy to associate to the browser session. Must reference a proxy in
+ the same project as the browser session.
start_url: Optional URL to navigate to when a new browser is warmed into the pool.
Best-effort: failures to navigate do not fail pool fill. Only applied to
@@ -777,8 +777,8 @@ async def update(
specified, the matching profile will be loaded into the browser session.
Profiles must be created beforehand.
- proxy_id: Optional proxy to associate to the browser session. Must reference a proxy
- belonging to the caller's org.
+ proxy_id: Optional proxy to associate to the browser session. Must reference a proxy in
+ the same project as the browser session.
size: Number of browsers to maintain in the pool. The maximum size is determined by
your organization's pooled sessions limit (the sum of all pool sizes cannot
@@ -859,7 +859,7 @@ def list(
timeout: float | httpx.Timeout | None | NotGiven = not_given,
) -> AsyncPaginator[BrowserPool, AsyncOffsetPagination[BrowserPool]]:
"""
- List browser pools owned by the caller's organization.
+ List browser pools in the resolved project.
Args:
limit: Limit the number of browser pools to return.
diff --git a/src/kernel/resources/browsers/browsers.py b/src/kernel/resources/browsers/browsers.py
index c5c365e6..a5564b84 100644
--- a/src/kernel/resources/browsers/browsers.py
+++ b/src/kernel/resources/browsers/browsers.py
@@ -216,8 +216,8 @@ def create(
specified, the matching profile will be loaded into the browser session.
Profiles must be created beforehand.
- proxy_id: Optional proxy to associate to the browser session. Must reference a proxy
- belonging to the caller's org.
+ proxy_id: Optional proxy to associate to the browser session. Must reference a proxy in
+ the same project as the browser session.
start_url: Optional URL to open when the browser session is created. Navigation is
best-effort, so navigation failures do not prevent the session from being
@@ -794,8 +794,8 @@ async def create(
specified, the matching profile will be loaded into the browser session.
Profiles must be created beforehand.
- proxy_id: Optional proxy to associate to the browser session. Must reference a proxy
- belonging to the caller's org.
+ proxy_id: Optional proxy to associate to the browser session. Must reference a proxy in
+ the same project as the browser session.
start_url: Optional URL to open when the browser session is created. Navigation is
best-effort, so navigation failures do not prevent the session from being
diff --git a/src/kernel/resources/credentials.py b/src/kernel/resources/credentials.py
index 7d00faab..4c90ef85 100644
--- a/src/kernel/resources/credentials.py
+++ b/src/kernel/resources/credentials.py
@@ -212,10 +212,9 @@ def list(
extra_body: Body | None = None,
timeout: float | httpx.Timeout | None | NotGiven = not_given,
) -> SyncOffsetPagination[Credential]:
- """List credentials owned by the caller's organization.
+ """List credentials in the resolved project.
- Credential values are not
- returned.
+ Credential values are not returned.
Args:
domain: Filter by domain
@@ -509,10 +508,9 @@ def list(
extra_body: Body | None = None,
timeout: float | httpx.Timeout | None | NotGiven = not_given,
) -> AsyncPaginator[Credential, AsyncOffsetPagination[Credential]]:
- """List credentials owned by the caller's organization.
+ """List credentials in the resolved project.
- Credential values are not
- returned.
+ Credential values are not returned.
Args:
domain: Filter by domain
diff --git a/src/kernel/resources/extensions.py b/src/kernel/resources/extensions.py
index 5b7215be..8d7acb14 100644
--- a/src/kernel/resources/extensions.py
+++ b/src/kernel/resources/extensions.py
@@ -70,7 +70,7 @@ def list(
timeout: float | httpx.Timeout | None | NotGiven = not_given,
) -> SyncOffsetPagination[ExtensionListResponse]:
"""
- List extensions owned by the caller's organization.
+ List extensions in the resolved project.
Args:
limit: Limit the number of extensions to return.
@@ -308,7 +308,7 @@ def list(
timeout: float | httpx.Timeout | None | NotGiven = not_given,
) -> AsyncPaginator[ExtensionListResponse, AsyncOffsetPagination[ExtensionListResponse]]:
"""
- List extensions owned by the caller's organization.
+ List extensions in the resolved project.
Args:
limit: Limit the number of extensions to return.
diff --git a/src/kernel/resources/proxies.py b/src/kernel/resources/proxies.py
index 2239a854..831e7819 100644
--- a/src/kernel/resources/proxies.py
+++ b/src/kernel/resources/proxies.py
@@ -65,7 +65,7 @@ def create(
timeout: float | httpx.Timeout | None | NotGiven = not_given,
) -> ProxyCreateResponse:
"""
- Create a new proxy configuration for the caller's organization.
+ Create a new proxy configuration in the resolved project.
Args:
type: Proxy type to use. In terms of quality for avoiding bot-detection, from best to
@@ -117,7 +117,7 @@ def retrieve(
timeout: float | httpx.Timeout | None | NotGiven = not_given,
) -> ProxyRetrieveResponse:
"""
- Retrieve a proxy belonging to the caller's organization by ID.
+ Retrieve a proxy in the resolved project by ID.
Args:
extra_headers: Send extra headers
@@ -151,7 +151,7 @@ def list(
timeout: float | httpx.Timeout | None | NotGiven = not_given,
) -> SyncOffsetPagination[ProxyListResponse]:
"""
- List proxies owned by the caller's organization.
+ List proxies in the resolved project.
Args:
limit: Limit the number of proxies to return.
@@ -313,7 +313,7 @@ async def create(
timeout: float | httpx.Timeout | None | NotGiven = not_given,
) -> ProxyCreateResponse:
"""
- Create a new proxy configuration for the caller's organization.
+ Create a new proxy configuration in the resolved project.
Args:
type: Proxy type to use. In terms of quality for avoiding bot-detection, from best to
@@ -365,7 +365,7 @@ async def retrieve(
timeout: float | httpx.Timeout | None | NotGiven = not_given,
) -> ProxyRetrieveResponse:
"""
- Retrieve a proxy belonging to the caller's organization by ID.
+ Retrieve a proxy in the resolved project by ID.
Args:
extra_headers: Send extra headers
@@ -399,7 +399,7 @@ def list(
timeout: float | httpx.Timeout | None | NotGiven = not_given,
) -> AsyncPaginator[ProxyListResponse, AsyncOffsetPagination[ProxyListResponse]]:
"""
- List proxies owned by the caller's organization.
+ List proxies in the resolved project.
Args:
limit: Limit the number of proxies to return.
diff --git a/src/kernel/types/auth/connection_create_params.py b/src/kernel/types/auth/connection_create_params.py
index acde944c..e5a969ce 100644
--- a/src/kernel/types/auth/connection_create_params.py
+++ b/src/kernel/types/auth/connection_create_params.py
@@ -83,7 +83,8 @@ class ConnectionCreateParams(TypedDict, total=False):
proxy: Proxy
"""Proxy selection.
- Provide either id or name. The proxy must belong to the caller's org.
+ Provide either id or name. The proxy must be in the same project as the resource
+ referencing it.
"""
record_session: bool
@@ -124,7 +125,7 @@ class Credential(TypedDict, total=False):
class Proxy(TypedDict, total=False):
"""Proxy selection.
- Provide either id or name. The proxy must belong to the caller's org.
+ Provide either id or name. The proxy must be in the same project as the resource referencing it.
"""
id: str
diff --git a/src/kernel/types/auth/connection_login_params.py b/src/kernel/types/auth/connection_login_params.py
index 39756cb1..5e136cc3 100644
--- a/src/kernel/types/auth/connection_login_params.py
+++ b/src/kernel/types/auth/connection_login_params.py
@@ -11,7 +11,8 @@ class ConnectionLoginParams(TypedDict, total=False):
proxy: Proxy
"""Proxy selection.
- Provide either id or name. The proxy must belong to the caller's org.
+ Provide either id or name. The proxy must be in the same project as the resource
+ referencing it.
"""
record_session: bool
@@ -24,7 +25,7 @@ class ConnectionLoginParams(TypedDict, total=False):
class Proxy(TypedDict, total=False):
"""Proxy selection.
- Provide either id or name. The proxy must belong to the caller's org.
+ Provide either id or name. The proxy must be in the same project as the resource referencing it.
"""
id: str
diff --git a/src/kernel/types/auth/connection_update_params.py b/src/kernel/types/auth/connection_update_params.py
index 8875f609..e0dadf70 100644
--- a/src/kernel/types/auth/connection_update_params.py
+++ b/src/kernel/types/auth/connection_update_params.py
@@ -50,7 +50,8 @@ class ConnectionUpdateParams(TypedDict, total=False):
proxy: Proxy
"""Proxy selection.
- Provide either id or name. The proxy must belong to the caller's org.
+ Provide either id or name. The proxy must be in the same project as the resource
+ referencing it.
"""
record_session: bool
@@ -85,7 +86,7 @@ class Credential(TypedDict, total=False):
class Proxy(TypedDict, total=False):
"""Proxy selection.
- Provide either id or name. The proxy must belong to the caller's org.
+ Provide either id or name. The proxy must be in the same project as the resource referencing it.
"""
id: str
diff --git a/src/kernel/types/browser_create_params.py b/src/kernel/types/browser_create_params.py
index 01308f38..10d53936 100644
--- a/src/kernel/types/browser_create_params.py
+++ b/src/kernel/types/browser_create_params.py
@@ -67,7 +67,7 @@ class BrowserCreateParams(TypedDict, total=False):
proxy_id: str
"""Optional proxy to associate to the browser session.
- Must reference a proxy belonging to the caller's org.
+ Must reference a proxy in the same project as the browser session.
"""
start_url: str
diff --git a/src/kernel/types/browser_pool.py b/src/kernel/types/browser_pool.py
index 8ead6807..8ad50acf 100644
--- a/src/kernel/types/browser_pool.py
+++ b/src/kernel/types/browser_pool.py
@@ -60,7 +60,7 @@ class BrowserPoolConfig(BaseModel):
proxy_id: Optional[str] = None
"""Optional proxy to associate to the browser session.
- Must reference a proxy belonging to the caller's org.
+ Must reference a proxy in the same project as the browser session.
"""
start_url: Optional[str] = None
diff --git a/src/kernel/types/browser_pool_create_params.py b/src/kernel/types/browser_pool_create_params.py
index 1ffeeb75..33d04ce4 100644
--- a/src/kernel/types/browser_pool_create_params.py
+++ b/src/kernel/types/browser_pool_create_params.py
@@ -59,7 +59,7 @@ class BrowserPoolCreateParams(TypedDict, total=False):
proxy_id: str
"""Optional proxy to associate to the browser session.
- Must reference a proxy belonging to the caller's org.
+ Must reference a proxy in the same project as the browser session.
"""
start_url: str
diff --git a/src/kernel/types/browser_pool_update_params.py b/src/kernel/types/browser_pool_update_params.py
index 07501695..e80b5aa0 100644
--- a/src/kernel/types/browser_pool_update_params.py
+++ b/src/kernel/types/browser_pool_update_params.py
@@ -58,7 +58,7 @@ class BrowserPoolUpdateParams(TypedDict, total=False):
proxy_id: str
"""Optional proxy to associate to the browser session.
- Must reference a proxy belonging to the caller's org.
+ Must reference a proxy in the same project as the browser session.
"""
size: int
From 17997322232374c6c49d99333b564a0537f247c0 Mon Sep 17 00:00:00 2001
From: "stainless-app[bot]"
<142633134+stainless-app[bot]@users.noreply.github.com>
Date: Thu, 11 Jun 2026 21:01:00 +0000
Subject: [PATCH 064/366] feat: Add project_id SDK client option mapped to
X-Kernel-Project-Id
---
.stats.yml | 4 ++--
src/kernel/_client.py | 16 ++++++++++++++++
2 files changed, 18 insertions(+), 2 deletions(-)
diff --git a/.stats.yml b/.stats.yml
index 1d17b2b3..a1640098 100644
--- a/.stats.yml
+++ b/.stats.yml
@@ -1,4 +1,4 @@
configured_endpoints: 119
-openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel/kernel-c6458d9c6cb5d7e3d8309c79b69eba3a22269e0ecc0bdafbaee00fde4b302e99.yml
+openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel/kernel-51549f813f3002e18c6ca8d850cc0c7932828d511c151e0412c73b6798d19e30.yml
openapi_spec_hash: ee77b293c4bda91c1a32cfdd12b8739e
-config_hash: 80eef1b592110714ea55cd26c470fabb
+config_hash: 57567e00b41af47cef1b78e51b747aa0
diff --git a/src/kernel/_client.py b/src/kernel/_client.py
index 5e7e5e0d..977b83d1 100644
--- a/src/kernel/_client.py
+++ b/src/kernel/_client.py
@@ -100,6 +100,8 @@ class Kernel(SyncAPIClient):
api_key: str
browser_route_cache: BrowserRouteCache
+ project_id: str | None
+
_environment: Literal["production", "development"] | NotGiven
_browser_routing: BrowserRoutingConfig
@@ -107,6 +109,7 @@ def __init__(
self,
*,
api_key: str | None = None,
+ project_id: str | None = None,
environment: Literal["production", "development"] | NotGiven = not_given,
base_url: str | httpx.URL | None | NotGiven = not_given,
timeout: float | Timeout | None | NotGiven = not_given,
@@ -140,6 +143,8 @@ def __init__(
)
self.api_key = api_key
+ self.project_id = project_id
+
self._environment = environment
base_url_env = os.environ.get("KERNEL_BASE_URL")
@@ -309,6 +314,7 @@ def default_headers(self) -> dict[str, str | Omit]:
return {
**super().default_headers,
"X-Stainless-Async": "false",
+ "X-Kernel-Project-Id": self.project_id if self.project_id is not None else Omit(),
**self._custom_headers,
}
@@ -347,6 +353,7 @@ def copy(
self,
*,
api_key: str | None = None,
+ project_id: str | None = None,
environment: Literal["production", "development"] | None = None,
base_url: str | httpx.URL | None = None,
timeout: float | Timeout | None | NotGiven = not_given,
@@ -383,6 +390,7 @@ def copy(
http_client = http_client or self._client
return self.__class__(
api_key=api_key or self.api_key,
+ project_id=project_id or self.project_id,
base_url=base_url or self.base_url,
environment=environment or self._environment,
timeout=self.timeout if isinstance(timeout, NotGiven) else timeout,
@@ -437,6 +445,8 @@ class AsyncKernel(AsyncAPIClient):
api_key: str
browser_route_cache: BrowserRouteCache
+ project_id: str | None
+
_environment: Literal["production", "development"] | NotGiven
_browser_routing: BrowserRoutingConfig
@@ -444,6 +454,7 @@ def __init__(
self,
*,
api_key: str | None = None,
+ project_id: str | None = None,
environment: Literal["production", "development"] | NotGiven = not_given,
base_url: str | httpx.URL | None | NotGiven = not_given,
timeout: float | Timeout | None | NotGiven = not_given,
@@ -477,6 +488,8 @@ def __init__(
)
self.api_key = api_key
+ self.project_id = project_id
+
self._environment = environment
base_url_env = os.environ.get("KERNEL_BASE_URL")
@@ -646,6 +659,7 @@ def default_headers(self) -> dict[str, str | Omit]:
return {
**super().default_headers,
"X-Stainless-Async": f"async:{get_async_library()}",
+ "X-Kernel-Project-Id": self.project_id if self.project_id is not None else Omit(),
**self._custom_headers,
}
@@ -684,6 +698,7 @@ def copy(
self,
*,
api_key: str | None = None,
+ project_id: str | None = None,
environment: Literal["production", "development"] | None = None,
base_url: str | httpx.URL | None = None,
timeout: float | Timeout | None | NotGiven = not_given,
@@ -720,6 +735,7 @@ def copy(
http_client = http_client or self._client
return self.__class__(
api_key=api_key or self.api_key,
+ project_id=project_id or self.project_id,
base_url=base_url or self.base_url,
environment=environment or self._environment,
timeout=self.timeout if isinstance(timeout, NotGiven) else timeout,
From 516dcec7b1c7d053bd8396928426a121647104de Mon Sep 17 00:00:00 2001
From: "stainless-app[bot]"
<142633134+stainless-app[bot]@users.noreply.github.com>
Date: Thu, 11 Jun 2026 21:01:56 +0000
Subject: [PATCH 065/366] release: 0.67.0
---
.release-please-manifest.json | 2 +-
CHANGELOG.md | 13 +++++++++++++
pyproject.toml | 2 +-
src/kernel/_version.py | 2 +-
4 files changed, 16 insertions(+), 3 deletions(-)
diff --git a/.release-please-manifest.json b/.release-please-manifest.json
index b429f966..db00cae0 100644
--- a/.release-please-manifest.json
+++ b/.release-please-manifest.json
@@ -1,3 +1,3 @@
{
- ".": "0.66.0"
+ ".": "0.67.0"
}
\ No newline at end of file
diff --git a/CHANGELOG.md b/CHANGELOG.md
index af41d8db..342bd471 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,18 @@
# Changelog
+## 0.67.0 (2026-06-11)
+
+Full Changelog: [v0.66.0...v0.67.0](https://github.com/kernel/kernel-python-sdk/compare/v0.66.0...v0.67.0)
+
+### Features
+
+* Add project_id SDK client option mapped to X-Kernel-Project-Id ([1799732](https://github.com/kernel/kernel-python-sdk/commit/17997322232374c6c49d99333b564a0537f247c0))
+
+
+### Documentation
+
+* **api:** correct project-scoping descriptions in OpenAPI spec ([5929332](https://github.com/kernel/kernel-python-sdk/commit/592933283ff7bf0e6596ddbfbc4851e285a1d36e))
+
## 0.66.0 (2026-06-10)
Full Changelog: [v0.65.0...v0.66.0](https://github.com/kernel/kernel-python-sdk/compare/v0.65.0...v0.66.0)
diff --git a/pyproject.toml b/pyproject.toml
index c9fef2a6..035e174c 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -1,6 +1,6 @@
[project]
name = "kernel"
-version = "0.66.0"
+version = "0.67.0"
description = "The official Python library for the kernel API"
dynamic = ["readme"]
license = "Apache-2.0"
diff --git a/src/kernel/_version.py b/src/kernel/_version.py
index eda0bdfb..7c28de48 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.66.0" # x-release-please-version
+__version__ = "0.67.0" # x-release-please-version
From c0a3e14b51e39dfdba37bacf023b2f98d4a3be69 Mon Sep 17 00:00:00 2001
From: Ilyaas Kapadia <86218345+IlyaasK@users.noreply.github.com>
Date: Fri, 12 Jun 2026 15:32:57 -0400
Subject: [PATCH 066/366] fix(pagination): stop skipping a page per
auto-pagination iteration
Sync/AsyncOffsetPagination.next_page_info() requested the next page at
next_offset + len(items). The X-Next-Offset header already holds the
offset where the next page starts (the API computes offset + limit), so
adding the current page length skipped a full page per iteration: with
250 items at limit 100, iteration returned items 0-99 and 200-249 and
silently dropped 100-199. X-Has-More still terminated the loop, hiding
the data loss.
Use the header value directly. Absent header already terminated via the
None check, unchanged.
Hand-maintained fix: with Stainless's hosted generator winding down, the
template-level fix is not coming; this commit and its test are the
canonical behavior.
Co-Authored-By: Claude Fable 5
---
src/kernel/pagination.py | 14 ++++++--------
tests/test_pagination.py | 42 ++++++++++++++++++++++++++++++++++++++++
2 files changed, 48 insertions(+), 8 deletions(-)
create mode 100644 tests/test_pagination.py
diff --git a/src/kernel/pagination.py b/src/kernel/pagination.py
index cdf83c2f..2a7dde8e 100644
--- a/src/kernel/pagination.py
+++ b/src/kernel/pagination.py
@@ -42,10 +42,9 @@ def next_page_info(self) -> Optional[PageInfo]:
if next_offset is None:
return None # type: ignore[unreachable]
- length = len(self._get_page_items())
- current_count = next_offset + length
-
- return PageInfo(params={"offset": current_count})
+ # X-Next-Offset already holds the offset where the next page starts;
+ # adding the current page length on top skips a full page per iteration.
+ return PageInfo(params={"offset": next_offset})
@classmethod
def build(cls: Type[_BaseModelT], *, response: Response, data: object) -> _BaseModelT: # noqa: ARG003
@@ -85,10 +84,9 @@ def next_page_info(self) -> Optional[PageInfo]:
if next_offset is None:
return None # type: ignore[unreachable]
- length = len(self._get_page_items())
- current_count = next_offset + length
-
- return PageInfo(params={"offset": current_count})
+ # X-Next-Offset already holds the offset where the next page starts;
+ # adding the current page length on top skips a full page per iteration.
+ return PageInfo(params={"offset": next_offset})
@classmethod
def build(cls: Type[_BaseModelT], *, response: Response, data: object) -> _BaseModelT: # noqa: ARG003
diff --git a/tests/test_pagination.py b/tests/test_pagination.py
new file mode 100644
index 00000000..0c49517f
--- /dev/null
+++ b/tests/test_pagination.py
@@ -0,0 +1,42 @@
+from typing import Any, List, Optional
+
+import httpx
+
+from kernel.pagination import SyncOffsetPagination
+
+
+def _page(
+ *, items: List[Any], next_offset: Optional[int], has_more: Optional[bool]
+) -> SyncOffsetPagination[Any]:
+ headers: dict[str, str] = {}
+ if next_offset is not None:
+ headers["X-Next-Offset"] = str(next_offset)
+ if has_more is not None:
+ headers["X-Has-More"] = "true" if has_more else "false"
+ response = httpx.Response(200, headers=headers)
+ return SyncOffsetPagination.build(response=response, data=items)
+
+
+def test_next_page_starts_at_exactly_x_next_offset() -> None:
+ # X-Next-Offset already holds the next page's start. Adding the current
+ # page length on top (the old behavior) skipped a full page per iteration.
+ page = _page(items=[{}] * 100, next_offset=100, has_more=True)
+ info = page.next_page_info()
+ assert info is not None
+ assert info.params == {"offset": 100}
+
+
+def test_stops_when_x_next_offset_absent() -> None:
+ page = _page(items=[{}] * 100, next_offset=None, has_more=True)
+ assert page.next_page_info() is None
+ assert page.has_next_page() is False
+
+
+def test_stops_when_x_has_more_false() -> None:
+ page = _page(items=[{}] * 50, next_offset=200, has_more=False)
+ assert page.has_next_page() is False
+
+
+def test_stops_on_empty_page() -> None:
+ page = _page(items=[], next_offset=300, has_more=True)
+ assert page.has_next_page() is False
From 4554697a9d850848b602a2422199fb2ce72d8efc Mon Sep 17 00:00:00 2001
From: Ilyaas Kapadia <86218345+IlyaasK@users.noreply.github.com>
Date: Fri, 12 Jun 2026 16:07:37 -0400
Subject: [PATCH 067/366] fix(pagination): fail loudly on contradictory
headers; test both classes
Review follow-up: raise instead of silently truncating when X-Has-More
is true but X-Next-Offset is absent, mirror all tests across the Sync
and Async classes so the two generated variants cannot drift apart, pin
the 0 last-page sentinel, and fix the stale next_offset wording in the
README pagination example.
Co-Authored-By: Claude Fable 5
---
README.md | 4 ++--
src/kernel/pagination.py | 18 ++++++++++++----
tests/test_pagination.py | 46 ++++++++++++++++++++++++++++------------
3 files changed, 49 insertions(+), 19 deletions(-)
diff --git a/README.md b/README.md
index d7639da5..2e8ed560 100644
--- a/README.md
+++ b/README.md
@@ -185,8 +185,8 @@ first_page = await client.deployments.list(
)
print(
- f"the current start offset for this page: {first_page.next_offset}"
-) # => "the current start offset for this page: 1"
+ f"the offset where the next page starts: {first_page.next_offset}"
+) # => "the offset where the next page starts: 2"
for deployment in first_page.items:
print(deployment.id)
diff --git a/src/kernel/pagination.py b/src/kernel/pagination.py
index 2a7dde8e..6e3ebb54 100644
--- a/src/kernel/pagination.py
+++ b/src/kernel/pagination.py
@@ -39,8 +39,13 @@ def has_next_page(self) -> bool:
@override
def next_page_info(self) -> Optional[PageInfo]:
next_offset = self.next_offset
- if next_offset is None:
- return None # type: ignore[unreachable]
+ if next_offset is None: # type: ignore[unreachable]
+ if self.has_more:
+ raise RuntimeError(
+ "Server reported X-Has-More: true without an X-Next-Offset header; "
+ "refusing to silently truncate pagination"
+ )
+ return None
# X-Next-Offset already holds the offset where the next page starts;
# adding the current page length on top skips a full page per iteration.
@@ -81,8 +86,13 @@ def has_next_page(self) -> bool:
@override
def next_page_info(self) -> Optional[PageInfo]:
next_offset = self.next_offset
- if next_offset is None:
- return None # type: ignore[unreachable]
+ if next_offset is None: # type: ignore[unreachable]
+ if self.has_more:
+ raise RuntimeError(
+ "Server reported X-Has-More: true without an X-Next-Offset header; "
+ "refusing to silently truncate pagination"
+ )
+ return None
# X-Next-Offset already holds the offset where the next page starts;
# adding the current page length on top skips a full page per iteration.
diff --git a/tests/test_pagination.py b/tests/test_pagination.py
index 0c49517f..46723c97 100644
--- a/tests/test_pagination.py
+++ b/tests/test_pagination.py
@@ -1,42 +1,62 @@
-from typing import Any, List, Optional
+from typing import Any, List, Type, Union, Optional
import httpx
+import pytest
-from kernel.pagination import SyncOffsetPagination
+from kernel.pagination import AsyncOffsetPagination, SyncOffsetPagination
+
+PageClass = Union[Type[SyncOffsetPagination[Any]], Type[AsyncOffsetPagination[Any]]]
+
+# build() and next_page_info() are plain synchronous methods on both classes,
+# so both generated variants are pinned against drifting apart on regeneration.
+both_classes = pytest.mark.parametrize("cls", [SyncOffsetPagination, AsyncOffsetPagination])
def _page(
- *, items: List[Any], next_offset: Optional[int], has_more: Optional[bool]
-) -> SyncOffsetPagination[Any]:
+ cls: PageClass, *, items: List[Any], next_offset: Optional[int], has_more: Optional[bool]
+) -> Any:
headers: dict[str, str] = {}
if next_offset is not None:
headers["X-Next-Offset"] = str(next_offset)
if has_more is not None:
headers["X-Has-More"] = "true" if has_more else "false"
response = httpx.Response(200, headers=headers)
- return SyncOffsetPagination.build(response=response, data=items)
+ return cls.build(response=response, data=items)
-def test_next_page_starts_at_exactly_x_next_offset() -> None:
+@both_classes
+def test_next_page_starts_at_exactly_x_next_offset(cls: PageClass) -> None:
# X-Next-Offset already holds the next page's start. Adding the current
# page length on top (the old behavior) skipped a full page per iteration.
- page = _page(items=[{}] * 100, next_offset=100, has_more=True)
+ page = _page(cls, items=[{}] * 100, next_offset=100, has_more=True)
info = page.next_page_info()
assert info is not None
assert info.params == {"offset": 100}
-def test_stops_when_x_next_offset_absent() -> None:
- page = _page(items=[{}] * 100, next_offset=None, has_more=True)
+@both_classes
+def test_stops_cleanly_when_last_page_omits_x_next_offset(cls: PageClass) -> None:
+ page = _page(cls, items=[{}] * 50, next_offset=None, has_more=False)
assert page.next_page_info() is None
assert page.has_next_page() is False
-def test_stops_when_x_has_more_false() -> None:
- page = _page(items=[{}] * 50, next_offset=200, has_more=False)
+@both_classes
+def test_stops_when_x_has_more_false(cls: PageClass) -> None:
+ # Covers the 0 sentinel the API emits on last pages: has_more is false
+ # whenever next_offset is 0, and has_more gates first.
+ page = _page(cls, items=[{}] * 50, next_offset=0, has_more=False)
assert page.has_next_page() is False
-def test_stops_on_empty_page() -> None:
- page = _page(items=[], next_offset=300, has_more=True)
+@both_classes
+def test_stops_on_empty_page(cls: PageClass) -> None:
+ page = _page(cls, items=[], next_offset=300, has_more=True)
assert page.has_next_page() is False
+
+
+@both_classes
+def test_refuses_to_silently_truncate_on_contradictory_headers(cls: PageClass) -> None:
+ page = _page(cls, items=[{}] * 100, next_offset=None, has_more=True)
+ with pytest.raises(RuntimeError, match="refusing to silently truncate"):
+ page.has_next_page()
From 843fadeb07fb7cebd5d2689eb78511bb8ee0d293 Mon Sep 17 00:00:00 2001
From: Ilyaas Kapadia <86218345+IlyaasK@users.noreply.github.com>
Date: Mon, 15 Jun 2026 12:05:56 -0400
Subject: [PATCH 068/366] style: ruff import-sort and format the pagination
test
Co-Authored-By: Claude Fable 5
---
tests/test_pagination.py | 6 ++----
1 file changed, 2 insertions(+), 4 deletions(-)
diff --git a/tests/test_pagination.py b/tests/test_pagination.py
index 46723c97..d9628582 100644
--- a/tests/test_pagination.py
+++ b/tests/test_pagination.py
@@ -3,7 +3,7 @@
import httpx
import pytest
-from kernel.pagination import AsyncOffsetPagination, SyncOffsetPagination
+from kernel.pagination import SyncOffsetPagination, AsyncOffsetPagination
PageClass = Union[Type[SyncOffsetPagination[Any]], Type[AsyncOffsetPagination[Any]]]
@@ -12,9 +12,7 @@
both_classes = pytest.mark.parametrize("cls", [SyncOffsetPagination, AsyncOffsetPagination])
-def _page(
- cls: PageClass, *, items: List[Any], next_offset: Optional[int], has_more: Optional[bool]
-) -> Any:
+def _page(cls: PageClass, *, items: List[Any], next_offset: Optional[int], has_more: Optional[bool]) -> Any:
headers: dict[str, str] = {}
if next_offset is not None:
headers["X-Next-Offset"] = str(next_offset)
From 5d2afd7df0f2d21d754ca936286ac3a92762e9db Mon Sep 17 00:00:00 2001
From: Ilyaas Kapadia <86218345+IlyaasK@users.noreply.github.com>
Date: Mon, 15 Jun 2026 13:47:49 -0400
Subject: [PATCH 069/366] fix(pagination): correct type:ignore placement; stop
on 0 sentinel
Two review follow-ups, both classes:
- Move '# type: ignore[unreachable]' from the 'if next_offset is None:' line
onto the first body statement ('if self.has_more:'). Under warn_unreachable
mypy reports the unreachable diagnostic at the first body statement, so the
ignore must sit there or the lint fails. Verified with mypy==1.17.0.
- Treat next_offset == 0 as a stop, not an advancing offset (matching the Go
and Node SDKs). A real next-page offset is always offset+limit >= 1, so 0 is
unambiguously the last-page sentinel. New test asserts via next_page_info
directly under has_more=True so the has_more gate can't mask it.
Co-Authored-By: Claude Fable 5
---
src/kernel/pagination.py | 24 ++++++++++++++++--------
tests/test_pagination.py | 12 +++++++++---
2 files changed, 25 insertions(+), 11 deletions(-)
diff --git a/src/kernel/pagination.py b/src/kernel/pagination.py
index 6e3ebb54..39d5a085 100644
--- a/src/kernel/pagination.py
+++ b/src/kernel/pagination.py
@@ -39,16 +39,20 @@ def has_next_page(self) -> bool:
@override
def next_page_info(self) -> Optional[PageInfo]:
next_offset = self.next_offset
- if next_offset is None: # type: ignore[unreachable]
- if self.has_more:
+ if next_offset is None:
+ if self.has_more: # type: ignore[unreachable]
raise RuntimeError(
"Server reported X-Has-More: true without an X-Next-Offset header; "
"refusing to silently truncate pagination"
)
return None
- # X-Next-Offset already holds the offset where the next page starts;
- # adding the current page length on top skips a full page per iteration.
+ # X-Next-Offset is the next page's absolute start, or 0 on the last page
+ # (the API's stop sentinel). Only a positive offset advances; the old code
+ # added the current page length on top, skipping a page per iteration.
+ if next_offset == 0:
+ return None
+
return PageInfo(params={"offset": next_offset})
@classmethod
@@ -86,16 +90,20 @@ def has_next_page(self) -> bool:
@override
def next_page_info(self) -> Optional[PageInfo]:
next_offset = self.next_offset
- if next_offset is None: # type: ignore[unreachable]
- if self.has_more:
+ if next_offset is None:
+ if self.has_more: # type: ignore[unreachable]
raise RuntimeError(
"Server reported X-Has-More: true without an X-Next-Offset header; "
"refusing to silently truncate pagination"
)
return None
- # X-Next-Offset already holds the offset where the next page starts;
- # adding the current page length on top skips a full page per iteration.
+ # X-Next-Offset is the next page's absolute start, or 0 on the last page
+ # (the API's stop sentinel). Only a positive offset advances; the old code
+ # added the current page length on top, skipping a page per iteration.
+ if next_offset == 0:
+ return None
+
return PageInfo(params={"offset": next_offset})
@classmethod
diff --git a/tests/test_pagination.py b/tests/test_pagination.py
index d9628582..cd59b3c6 100644
--- a/tests/test_pagination.py
+++ b/tests/test_pagination.py
@@ -41,12 +41,18 @@ def test_stops_cleanly_when_last_page_omits_x_next_offset(cls: PageClass) -> Non
@both_classes
def test_stops_when_x_has_more_false(cls: PageClass) -> None:
- # Covers the 0 sentinel the API emits on last pages: has_more is false
- # whenever next_offset is 0, and has_more gates first.
- page = _page(cls, items=[{}] * 50, next_offset=0, has_more=False)
+ page = _page(cls, items=[{}] * 50, next_offset=200, has_more=False)
assert page.has_next_page() is False
+@both_classes
+def test_stops_on_next_offset_zero_sentinel(cls: PageClass) -> None:
+ # 0 is the API's last-page sentinel. Assert via next_page_info directly,
+ # with has_more=True, so the has_more gate cannot mask the 0 handling.
+ page = _page(cls, items=[{}] * 50, next_offset=0, has_more=True)
+ assert page.next_page_info() is None
+
+
@both_classes
def test_stops_on_empty_page(cls: PageClass) -> None:
page = _page(cls, items=[], next_offset=300, has_more=True)
From 979c30aee99dd850dedcc70f400dd412047e28e0 Mon Sep 17 00:00:00 2001
From: "stainless-app[bot]"
<142633134+stainless-app[bot]@users.noreply.github.com>
Date: Fri, 12 Jun 2026 16:08:40 +0000
Subject: [PATCH 070/366] feat(api): surface deleted/expired API keys for audit
trail (KERNEL-1350)
---
.stats.yml | 4 +-
api.md | 2 +-
src/kernel/resources/api_keys.py | 36 ++++++++++++++++--
src/kernel/types/__init__.py | 1 +
src/kernel/types/api_key.py | 15 ++++++++
src/kernel/types/api_key_list_params.py | 6 +++
src/kernel/types/api_key_retrieve_params.py | 15 ++++++++
tests/api_resources/test_api_keys.py | 41 ++++++++++++++++-----
8 files changed, 105 insertions(+), 15 deletions(-)
create mode 100644 src/kernel/types/api_key_retrieve_params.py
diff --git a/.stats.yml b/.stats.yml
index a1640098..3ff52d98 100644
--- a/.stats.yml
+++ b/.stats.yml
@@ -1,4 +1,4 @@
configured_endpoints: 119
-openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel/kernel-51549f813f3002e18c6ca8d850cc0c7932828d511c151e0412c73b6798d19e30.yml
-openapi_spec_hash: ee77b293c4bda91c1a32cfdd12b8739e
+openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel/kernel-42074f2b600b0dc805377d6793e4bb30c959738b0f9cc44c409d094517e5e0ab.yml
+openapi_spec_hash: 81c27a833d6d9637787634180dec2abd
config_hash: 57567e00b41af47cef1b78e51b747aa0
diff --git a/api.md b/api.md
index 4b24f102..d0bc3c26 100644
--- a/api.md
+++ b/api.md
@@ -449,7 +449,7 @@ from kernel.types import APIKey, CreatedAPIKey
Methods:
- client.api_keys.create(\*\*params) -> CreatedAPIKey
-- client.api_keys.retrieve(id) -> APIKey
+- client.api_keys.retrieve(id, \*\*params) -> APIKey
- client.api_keys.update(id, \*\*params) -> APIKey
- client.api_keys.list(\*\*params) -> SyncOffsetPagination[APIKey]
- client.api_keys.delete(id) -> None
diff --git a/src/kernel/resources/api_keys.py b/src/kernel/resources/api_keys.py
index 93e80158..3801c4f1 100644
--- a/src/kernel/resources/api_keys.py
+++ b/src/kernel/resources/api_keys.py
@@ -7,7 +7,7 @@
import httpx
-from ..types import api_key_list_params, api_key_create_params, api_key_update_params
+from ..types import api_key_list_params, api_key_create_params, api_key_update_params, api_key_retrieve_params
from .._types import Body, Omit, Query, Headers, NoneType, NotGiven, omit, not_given
from .._utils import path_template, maybe_transform, async_maybe_transform
from .._compat import cached_property
@@ -99,6 +99,7 @@ def retrieve(
self,
id: str,
*,
+ include_deleted: 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,
@@ -112,6 +113,9 @@ def retrieve(
masked.
Args:
+ include_deleted: When true, return the API key even if it has been deleted (soft-deleted), for
+ audit purposes. Defaults to false, which returns 404 for a deleted key.
+
extra_headers: Send extra headers
extra_query: Add additional query parameters to the request
@@ -125,7 +129,13 @@ def retrieve(
return self._get(
path_template("/org/api_keys/{id}", id=id),
options=make_request_options(
- extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
+ extra_headers=extra_headers,
+ extra_query=extra_query,
+ extra_body=extra_body,
+ timeout=timeout,
+ query=maybe_transform(
+ {"include_deleted": include_deleted}, api_key_retrieve_params.APIKeyRetrieveParams
+ ),
),
cast_to=APIKey,
)
@@ -170,6 +180,7 @@ def update(
def list(
self,
*,
+ include_deleted: bool | Omit = omit,
limit: int | Omit = omit,
offset: int | Omit = omit,
query: str | Omit = omit,
@@ -187,6 +198,9 @@ def list(
API keys are masked.
Args:
+ include_deleted: When true, include deleted (soft-deleted) API keys in the results for audit
+ purposes. Defaults to false, which returns only live keys.
+
limit: Maximum number of results to return
offset: Number of results to skip
@@ -216,6 +230,7 @@ def list(
timeout=timeout,
query=maybe_transform(
{
+ "include_deleted": include_deleted,
"limit": limit,
"offset": offset,
"query": query,
@@ -336,6 +351,7 @@ async def retrieve(
self,
id: str,
*,
+ include_deleted: 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,
@@ -349,6 +365,9 @@ async def retrieve(
masked.
Args:
+ include_deleted: When true, return the API key even if it has been deleted (soft-deleted), for
+ audit purposes. Defaults to false, which returns 404 for a deleted key.
+
extra_headers: Send extra headers
extra_query: Add additional query parameters to the request
@@ -362,7 +381,13 @@ async def retrieve(
return await self._get(
path_template("/org/api_keys/{id}", id=id),
options=make_request_options(
- extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
+ extra_headers=extra_headers,
+ extra_query=extra_query,
+ extra_body=extra_body,
+ timeout=timeout,
+ query=await async_maybe_transform(
+ {"include_deleted": include_deleted}, api_key_retrieve_params.APIKeyRetrieveParams
+ ),
),
cast_to=APIKey,
)
@@ -407,6 +432,7 @@ async def update(
def list(
self,
*,
+ include_deleted: bool | Omit = omit,
limit: int | Omit = omit,
offset: int | Omit = omit,
query: str | Omit = omit,
@@ -424,6 +450,9 @@ def list(
API keys are masked.
Args:
+ include_deleted: When true, include deleted (soft-deleted) API keys in the results for audit
+ purposes. Defaults to false, which returns only live keys.
+
limit: Maximum number of results to return
offset: Number of results to skip
@@ -453,6 +482,7 @@ def list(
timeout=timeout,
query=maybe_transform(
{
+ "include_deleted": include_deleted,
"limit": limit,
"offset": offset,
"query": query,
diff --git a/src/kernel/types/__init__.py b/src/kernel/types/__init__.py
index 1e9b39a4..d091f8de 100644
--- a/src/kernel/types/__init__.py
+++ b/src/kernel/types/__init__.py
@@ -54,6 +54,7 @@
from .deployment_state_event import DeploymentStateEvent as DeploymentStateEvent
from .invocation_list_params import InvocationListParams as InvocationListParams
from .invocation_state_event import InvocationStateEvent as InvocationStateEvent
+from .api_key_retrieve_params import APIKeyRetrieveParams as APIKeyRetrieveParams
from .browser_create_response import BrowserCreateResponse as BrowserCreateResponse
from .browser_retrieve_params import BrowserRetrieveParams as BrowserRetrieveParams
from .browser_update_response import BrowserUpdateResponse as BrowserUpdateResponse
diff --git a/src/kernel/types/api_key.py b/src/kernel/types/api_key.py
index 6df577f8..0f784c63 100644
--- a/src/kernel/types/api_key.py
+++ b/src/kernel/types/api_key.py
@@ -2,6 +2,7 @@
from typing import Optional
from datetime import datetime
+from typing_extensions import Literal
from .._models import BaseModel
@@ -28,6 +29,12 @@ class APIKey(BaseModel):
created_by: CreatedBy
+ deleted_at: Optional[datetime] = None
+ """When the API key was deleted (soft-deleted).
+
+ Null for keys that have not been deleted.
+ """
+
expires_at: Optional[datetime] = None
"""When the API key expires"""
@@ -45,3 +52,11 @@ class APIKey(BaseModel):
Null means the key is org-wide or the project name is unavailable.
"""
+
+ status: Literal["active", "expired", "deleted"]
+ """Derived lifecycle status of the API key.
+
+ `active` means usable. `expired` means past its expires_at. `deleted` means it
+ was deleted (soft-deleted) and can no longer authenticate. Deleted takes
+ precedence over expired.
+ """
diff --git a/src/kernel/types/api_key_list_params.py b/src/kernel/types/api_key_list_params.py
index 79a9c41c..673c5d6d 100644
--- a/src/kernel/types/api_key_list_params.py
+++ b/src/kernel/types/api_key_list_params.py
@@ -8,6 +8,12 @@
class APIKeyListParams(TypedDict, total=False):
+ include_deleted: bool
+ """
+ When true, include deleted (soft-deleted) API keys in the results for audit
+ purposes. Defaults to false, which returns only live keys.
+ """
+
limit: int
"""Maximum number of results to return"""
diff --git a/src/kernel/types/api_key_retrieve_params.py b/src/kernel/types/api_key_retrieve_params.py
new file mode 100644
index 00000000..e2b9ea3b
--- /dev/null
+++ b/src/kernel/types/api_key_retrieve_params.py
@@ -0,0 +1,15 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from __future__ import annotations
+
+from typing_extensions import TypedDict
+
+__all__ = ["APIKeyRetrieveParams"]
+
+
+class APIKeyRetrieveParams(TypedDict, total=False):
+ include_deleted: bool
+ """
+ When true, return the API key even if it has been deleted (soft-deleted), for
+ audit purposes. Defaults to false, which returns 404 for a deleted key.
+ """
diff --git a/tests/api_resources/test_api_keys.py b/tests/api_resources/test_api_keys.py
index 9c3abcae..34273831 100644
--- a/tests/api_resources/test_api_keys.py
+++ b/tests/api_resources/test_api_keys.py
@@ -9,7 +9,10 @@
from kernel import Kernel, AsyncKernel
from tests.utils import assert_matches_type
-from kernel.types import APIKey, CreatedAPIKey
+from kernel.types import (
+ APIKey,
+ CreatedAPIKey,
+)
from kernel.pagination import SyncOffsetPagination, AsyncOffsetPagination
base_url = os.environ.get("TEST_API_BASE_URL", "http://127.0.0.1:4010")
@@ -66,7 +69,16 @@ def test_streaming_response_create(self, client: Kernel) -> None:
@parametrize
def test_method_retrieve(self, client: Kernel) -> None:
api_key = client.api_keys.retrieve(
- "id",
+ id="id",
+ )
+ assert_matches_type(APIKey, api_key, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ def test_method_retrieve_with_all_params(self, client: Kernel) -> None:
+ api_key = client.api_keys.retrieve(
+ id="id",
+ include_deleted=True,
)
assert_matches_type(APIKey, api_key, path=["response"])
@@ -74,7 +86,7 @@ def test_method_retrieve(self, client: Kernel) -> None:
@parametrize
def test_raw_response_retrieve(self, client: Kernel) -> None:
response = client.api_keys.with_raw_response.retrieve(
- "id",
+ id="id",
)
assert response.is_closed is True
@@ -86,7 +98,7 @@ def test_raw_response_retrieve(self, client: Kernel) -> None:
@parametrize
def test_streaming_response_retrieve(self, client: Kernel) -> None:
with client.api_keys.with_streaming_response.retrieve(
- "id",
+ id="id",
) as response:
assert not response.is_closed
assert response.http_request.headers.get("X-Stainless-Lang") == "python"
@@ -101,7 +113,7 @@ def test_streaming_response_retrieve(self, client: Kernel) -> None:
def test_path_params_retrieve(self, client: Kernel) -> None:
with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"):
client.api_keys.with_raw_response.retrieve(
- "",
+ id="",
)
@pytest.mark.skip(reason="Mock server tests are disabled")
@@ -160,6 +172,7 @@ def test_method_list(self, client: Kernel) -> None:
@parametrize
def test_method_list_with_all_params(self, client: Kernel) -> None:
api_key = client.api_keys.list(
+ include_deleted=True,
limit=100,
offset=0,
query="query",
@@ -286,7 +299,16 @@ async def test_streaming_response_create(self, async_client: AsyncKernel) -> Non
@parametrize
async def test_method_retrieve(self, async_client: AsyncKernel) -> None:
api_key = await async_client.api_keys.retrieve(
- "id",
+ id="id",
+ )
+ assert_matches_type(APIKey, api_key, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ async def test_method_retrieve_with_all_params(self, async_client: AsyncKernel) -> None:
+ api_key = await async_client.api_keys.retrieve(
+ id="id",
+ include_deleted=True,
)
assert_matches_type(APIKey, api_key, path=["response"])
@@ -294,7 +316,7 @@ async def test_method_retrieve(self, async_client: AsyncKernel) -> None:
@parametrize
async def test_raw_response_retrieve(self, async_client: AsyncKernel) -> None:
response = await async_client.api_keys.with_raw_response.retrieve(
- "id",
+ id="id",
)
assert response.is_closed is True
@@ -306,7 +328,7 @@ async def test_raw_response_retrieve(self, async_client: AsyncKernel) -> None:
@parametrize
async def test_streaming_response_retrieve(self, async_client: AsyncKernel) -> None:
async with async_client.api_keys.with_streaming_response.retrieve(
- "id",
+ id="id",
) as response:
assert not response.is_closed
assert response.http_request.headers.get("X-Stainless-Lang") == "python"
@@ -321,7 +343,7 @@ async def test_streaming_response_retrieve(self, async_client: AsyncKernel) -> N
async def test_path_params_retrieve(self, async_client: AsyncKernel) -> None:
with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"):
await async_client.api_keys.with_raw_response.retrieve(
- "",
+ id="",
)
@pytest.mark.skip(reason="Mock server tests are disabled")
@@ -380,6 +402,7 @@ async def test_method_list(self, async_client: AsyncKernel) -> None:
@parametrize
async def test_method_list_with_all_params(self, async_client: AsyncKernel) -> None:
api_key = await async_client.api_keys.list(
+ include_deleted=True,
limit=100,
offset=0,
query="query",
From 6ae9965398cd8f0eeead935a1a4e2a7ed80d0f95 Mon Sep 17 00:00:00 2001
From: "stainless-app[bot]"
<142633134+stainless-app[bot]@users.noreply.github.com>
Date: Fri, 12 Jun 2026 19:04:28 +0000
Subject: [PATCH 071/366] codegen metadata
---
.stats.yml | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/.stats.yml b/.stats.yml
index 3ff52d98..ab902c2f 100644
--- a/.stats.yml
+++ b/.stats.yml
@@ -1,4 +1,4 @@
configured_endpoints: 119
-openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel/kernel-42074f2b600b0dc805377d6793e4bb30c959738b0f9cc44c409d094517e5e0ab.yml
-openapi_spec_hash: 81c27a833d6d9637787634180dec2abd
+openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel/kernel-c98841235b0ece0591f28f7dd424339b6ef2f3e8f539b95b670ae0da2ef43df4.yml
+openapi_spec_hash: c1e9456765f0743a333af297d135d5cf
config_hash: 57567e00b41af47cef1b78e51b747aa0
From b5cff488d52311a0f57e693546e0cd2d9f0a8349 Mon Sep 17 00:00:00 2001
From: "stainless-app[bot]"
<142633134+stainless-app[bot]@users.noreply.github.com>
Date: Sun, 14 Jun 2026 19:09:02 +0000
Subject: [PATCH 072/366] feat: Add API key rotate endpoint
---
.stats.yml | 8 +-
api.md | 1 +
src/kernel/resources/api_keys.py | 122 +++++++++++++++++++++-
src/kernel/types/__init__.py | 1 +
src/kernel/types/api_key_rotate_params.py | 23 ++++
tests/api_resources/test_api_keys.py | 104 ++++++++++++++++++
6 files changed, 254 insertions(+), 5 deletions(-)
create mode 100644 src/kernel/types/api_key_rotate_params.py
diff --git a/.stats.yml b/.stats.yml
index ab902c2f..8d8fe88a 100644
--- a/.stats.yml
+++ b/.stats.yml
@@ -1,4 +1,4 @@
-configured_endpoints: 119
-openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel/kernel-c98841235b0ece0591f28f7dd424339b6ef2f3e8f539b95b670ae0da2ef43df4.yml
-openapi_spec_hash: c1e9456765f0743a333af297d135d5cf
-config_hash: 57567e00b41af47cef1b78e51b747aa0
+configured_endpoints: 120
+openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel/kernel-1f10598afa01d76d22b0ed63685248f482f74c9353cffe1d3e4a3d38da7716cf.yml
+openapi_spec_hash: 8936f458bfa681b709e459ca1cc76fb5
+config_hash: 03c7e57f268c750e2415831662e95969
diff --git a/api.md b/api.md
index d0bc3c26..8ba0dd44 100644
--- a/api.md
+++ b/api.md
@@ -453,6 +453,7 @@ Methods:
- client.api_keys.update(id, \*\*params) -> APIKey
- client.api_keys.list(\*\*params) -> SyncOffsetPagination[APIKey]
- client.api_keys.delete(id) -> None
+- client.api_keys.rotate(id, \*\*params) -> CreatedAPIKey
# CredentialProviders
diff --git a/src/kernel/resources/api_keys.py b/src/kernel/resources/api_keys.py
index 3801c4f1..3c203dfd 100644
--- a/src/kernel/resources/api_keys.py
+++ b/src/kernel/resources/api_keys.py
@@ -7,7 +7,13 @@
import httpx
-from ..types import api_key_list_params, api_key_create_params, api_key_update_params, api_key_retrieve_params
+from ..types import (
+ api_key_list_params,
+ api_key_create_params,
+ api_key_rotate_params,
+ api_key_update_params,
+ api_key_retrieve_params,
+)
from .._types import Body, Omit, Query, Headers, NoneType, NotGiven, omit, not_given
from .._utils import path_template, maybe_transform, async_maybe_transform
from .._compat import cached_property
@@ -277,6 +283,57 @@ def delete(
cast_to=NoneType,
)
+ def rotate(
+ self,
+ id: str,
+ *,
+ days_to_expire: Optional[int] | Omit = omit,
+ expire_in_days: Optional[int] | Omit = omit,
+ # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
+ # The extra values given here take precedence over values defined on the client or passed to this method.
+ extra_headers: Headers | None = None,
+ extra_query: Query | None = None,
+ extra_body: Body | None = None,
+ timeout: float | httpx.Timeout | None | NotGiven = not_given,
+ ) -> CreatedAPIKey:
+ """Rotate an API key.
+
+ Issues a new key that copies the name and project of the
+ rotated key, and schedules the rotated key to expire after a grace period so
+ in-flight callers can swap over. The new plaintext key is returned once.
+
+ Args:
+ days_to_expire: Lifetime in days for the new key, up to 3650. Omit to reuse the rotated key's
+ original lifetime, or never-expires if it had none.
+
+ expire_in_days: Grace period in days before the rotated key expires. Use 0 to expire it
+ immediately. Omit for the default grace period of 7 days.
+
+ extra_headers: Send extra headers
+
+ extra_query: Add additional query parameters to the request
+
+ extra_body: Add additional JSON properties to the request
+
+ timeout: Override the client-level default timeout for this request, in seconds
+ """
+ if not id:
+ raise ValueError(f"Expected a non-empty value for `id` but received {id!r}")
+ return self._post(
+ path_template("/org/api_keys/{id}/rotate", id=id),
+ body=maybe_transform(
+ {
+ "days_to_expire": days_to_expire,
+ "expire_in_days": expire_in_days,
+ },
+ api_key_rotate_params.APIKeyRotateParams,
+ ),
+ options=make_request_options(
+ extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
+ ),
+ cast_to=CreatedAPIKey,
+ )
+
class AsyncAPIKeysResource(AsyncAPIResource):
"""Create and manage API keys for organization and project-scoped access."""
@@ -529,6 +586,57 @@ async def delete(
cast_to=NoneType,
)
+ async def rotate(
+ self,
+ id: str,
+ *,
+ days_to_expire: Optional[int] | Omit = omit,
+ expire_in_days: Optional[int] | Omit = omit,
+ # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
+ # The extra values given here take precedence over values defined on the client or passed to this method.
+ extra_headers: Headers | None = None,
+ extra_query: Query | None = None,
+ extra_body: Body | None = None,
+ timeout: float | httpx.Timeout | None | NotGiven = not_given,
+ ) -> CreatedAPIKey:
+ """Rotate an API key.
+
+ Issues a new key that copies the name and project of the
+ rotated key, and schedules the rotated key to expire after a grace period so
+ in-flight callers can swap over. The new plaintext key is returned once.
+
+ Args:
+ days_to_expire: Lifetime in days for the new key, up to 3650. Omit to reuse the rotated key's
+ original lifetime, or never-expires if it had none.
+
+ expire_in_days: Grace period in days before the rotated key expires. Use 0 to expire it
+ immediately. Omit for the default grace period of 7 days.
+
+ extra_headers: Send extra headers
+
+ extra_query: Add additional query parameters to the request
+
+ extra_body: Add additional JSON properties to the request
+
+ timeout: Override the client-level default timeout for this request, in seconds
+ """
+ if not id:
+ raise ValueError(f"Expected a non-empty value for `id` but received {id!r}")
+ return await self._post(
+ path_template("/org/api_keys/{id}/rotate", id=id),
+ body=await async_maybe_transform(
+ {
+ "days_to_expire": days_to_expire,
+ "expire_in_days": expire_in_days,
+ },
+ api_key_rotate_params.APIKeyRotateParams,
+ ),
+ options=make_request_options(
+ extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
+ ),
+ cast_to=CreatedAPIKey,
+ )
+
class APIKeysResourceWithRawResponse:
def __init__(self, api_keys: APIKeysResource) -> None:
@@ -549,6 +657,9 @@ def __init__(self, api_keys: APIKeysResource) -> None:
self.delete = to_raw_response_wrapper(
api_keys.delete,
)
+ self.rotate = to_raw_response_wrapper(
+ api_keys.rotate,
+ )
class AsyncAPIKeysResourceWithRawResponse:
@@ -570,6 +681,9 @@ def __init__(self, api_keys: AsyncAPIKeysResource) -> None:
self.delete = async_to_raw_response_wrapper(
api_keys.delete,
)
+ self.rotate = async_to_raw_response_wrapper(
+ api_keys.rotate,
+ )
class APIKeysResourceWithStreamingResponse:
@@ -591,6 +705,9 @@ def __init__(self, api_keys: APIKeysResource) -> None:
self.delete = to_streamed_response_wrapper(
api_keys.delete,
)
+ self.rotate = to_streamed_response_wrapper(
+ api_keys.rotate,
+ )
class AsyncAPIKeysResourceWithStreamingResponse:
@@ -612,3 +729,6 @@ def __init__(self, api_keys: AsyncAPIKeysResource) -> None:
self.delete = async_to_streamed_response_wrapper(
api_keys.delete,
)
+ self.rotate = async_to_streamed_response_wrapper(
+ api_keys.rotate,
+ )
diff --git a/src/kernel/types/__init__.py b/src/kernel/types/__init__.py
index d091f8de..e38ba407 100644
--- a/src/kernel/types/__init__.py
+++ b/src/kernel/types/__init__.py
@@ -39,6 +39,7 @@
from .proxy_list_response import ProxyListResponse as ProxyListResponse
from .proxy_check_response import ProxyCheckResponse as ProxyCheckResponse
from .api_key_create_params import APIKeyCreateParams as APIKeyCreateParams
+from .api_key_rotate_params import APIKeyRotateParams as APIKeyRotateParams
from .api_key_update_params import APIKeyUpdateParams as APIKeyUpdateParams
from .browser_create_params import BrowserCreateParams as BrowserCreateParams
from .browser_curl_response import BrowserCurlResponse as BrowserCurlResponse
diff --git a/src/kernel/types/api_key_rotate_params.py b/src/kernel/types/api_key_rotate_params.py
new file mode 100644
index 00000000..3810e589
--- /dev/null
+++ b/src/kernel/types/api_key_rotate_params.py
@@ -0,0 +1,23 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from __future__ import annotations
+
+from typing import Optional
+from typing_extensions import TypedDict
+
+__all__ = ["APIKeyRotateParams"]
+
+
+class APIKeyRotateParams(TypedDict, total=False):
+ days_to_expire: Optional[int]
+ """Lifetime in days for the new key, up to 3650.
+
+ Omit to reuse the rotated key's original lifetime, or never-expires if it had
+ none.
+ """
+
+ expire_in_days: Optional[int]
+ """Grace period in days before the rotated key expires.
+
+ Use 0 to expire it immediately. Omit for the default grace period of 7 days.
+ """
diff --git a/tests/api_resources/test_api_keys.py b/tests/api_resources/test_api_keys.py
index 34273831..9eb265fc 100644
--- a/tests/api_resources/test_api_keys.py
+++ b/tests/api_resources/test_api_keys.py
@@ -245,6 +245,58 @@ def test_path_params_delete(self, client: Kernel) -> None:
"",
)
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ def test_method_rotate(self, client: Kernel) -> None:
+ api_key = client.api_keys.rotate(
+ id="id",
+ )
+ assert_matches_type(CreatedAPIKey, api_key, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ def test_method_rotate_with_all_params(self, client: Kernel) -> None:
+ api_key = client.api_keys.rotate(
+ id="id",
+ days_to_expire=30,
+ expire_in_days=7,
+ )
+ assert_matches_type(CreatedAPIKey, api_key, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ def test_raw_response_rotate(self, client: Kernel) -> None:
+ response = client.api_keys.with_raw_response.rotate(
+ id="id",
+ )
+
+ assert response.is_closed is True
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+ api_key = response.parse()
+ assert_matches_type(CreatedAPIKey, api_key, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ def test_streaming_response_rotate(self, client: Kernel) -> None:
+ with client.api_keys.with_streaming_response.rotate(
+ id="id",
+ ) as response:
+ assert not response.is_closed
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+
+ api_key = response.parse()
+ assert_matches_type(CreatedAPIKey, api_key, path=["response"])
+
+ assert cast(Any, response.is_closed) is True
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ def test_path_params_rotate(self, client: Kernel) -> None:
+ with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"):
+ client.api_keys.with_raw_response.rotate(
+ id="",
+ )
+
class TestAsyncAPIKeys:
parametrize = pytest.mark.parametrize(
@@ -474,3 +526,55 @@ async def test_path_params_delete(self, async_client: AsyncKernel) -> None:
await async_client.api_keys.with_raw_response.delete(
"",
)
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ async def test_method_rotate(self, async_client: AsyncKernel) -> None:
+ api_key = await async_client.api_keys.rotate(
+ id="id",
+ )
+ assert_matches_type(CreatedAPIKey, api_key, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ async def test_method_rotate_with_all_params(self, async_client: AsyncKernel) -> None:
+ api_key = await async_client.api_keys.rotate(
+ id="id",
+ days_to_expire=30,
+ expire_in_days=7,
+ )
+ assert_matches_type(CreatedAPIKey, api_key, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ async def test_raw_response_rotate(self, async_client: AsyncKernel) -> None:
+ response = await async_client.api_keys.with_raw_response.rotate(
+ id="id",
+ )
+
+ assert response.is_closed is True
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+ api_key = await response.parse()
+ assert_matches_type(CreatedAPIKey, api_key, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ async def test_streaming_response_rotate(self, async_client: AsyncKernel) -> None:
+ async with async_client.api_keys.with_streaming_response.rotate(
+ id="id",
+ ) as response:
+ assert not response.is_closed
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+
+ api_key = await response.parse()
+ assert_matches_type(CreatedAPIKey, api_key, 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_rotate(self, async_client: AsyncKernel) -> None:
+ with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"):
+ await async_client.api_keys.with_raw_response.rotate(
+ id="",
+ )
From 1c9e0b9f63ea3e9369c6f757f477f0ab9c71e429 Mon Sep 17 00:00:00 2001
From: "stainless-app[bot]"
<142633134+stainless-app[bot]@users.noreply.github.com>
Date: Sun, 14 Jun 2026 20:21:20 +0000
Subject: [PATCH 073/366] refactor(api): align API key audit surface with
browser sibling (KERNEL-1350)
---
.stats.yml | 4 ++--
src/kernel/resources/api_keys.py | 20 ++++++++++++++++----
src/kernel/types/api_key.py | 9 ---------
src/kernel/types/api_key_list_params.py | 13 +++++++++++--
tests/api_resources/test_api_keys.py | 2 ++
5 files changed, 31 insertions(+), 17 deletions(-)
diff --git a/.stats.yml b/.stats.yml
index 8d8fe88a..4127b5f2 100644
--- a/.stats.yml
+++ b/.stats.yml
@@ -1,4 +1,4 @@
configured_endpoints: 120
-openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel/kernel-1f10598afa01d76d22b0ed63685248f482f74c9353cffe1d3e4a3d38da7716cf.yml
-openapi_spec_hash: 8936f458bfa681b709e459ca1cc76fb5
+openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel/kernel-e8afdbeac9332cf79200c2eb873e532104fd0a7472b08e63cde6c857a87cf0c3.yml
+openapi_spec_hash: 2525caf30dffbdd83c83948201f11a52
config_hash: 03c7e57f268c750e2415831662e95969
diff --git a/src/kernel/resources/api_keys.py b/src/kernel/resources/api_keys.py
index 3c203dfd..da28774d 100644
--- a/src/kernel/resources/api_keys.py
+++ b/src/kernel/resources/api_keys.py
@@ -192,6 +192,7 @@ def list(
query: str | Omit = omit,
sort_by: Literal["created_at", "name", "expires_at"] | Omit = omit,
sort_direction: Literal["asc", "desc"] | Omit = omit,
+ status: Literal["active", "deleted", "all"] | 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,
@@ -204,8 +205,8 @@ def list(
API keys are masked.
Args:
- include_deleted: When true, include deleted (soft-deleted) API keys in the results for audit
- purposes. Defaults to false, which returns only live keys.
+ include_deleted: Deprecated: use status=all instead. When true, include deleted (soft-deleted)
+ API keys in the results for audit purposes.
limit: Maximum number of results to return
@@ -218,6 +219,10 @@ def list(
sort_direction: Sort direction for API keys.
+ status: Filter API keys by status. "active" returns keys that are not deleted (default;
+ expired-but-not-deleted keys are still included), "deleted" returns only
+ soft-deleted keys, "all" returns both.
+
extra_headers: Send extra headers
extra_query: Add additional query parameters to the request
@@ -242,6 +247,7 @@ def list(
"query": query,
"sort_by": sort_by,
"sort_direction": sort_direction,
+ "status": status,
},
api_key_list_params.APIKeyListParams,
),
@@ -495,6 +501,7 @@ def list(
query: str | Omit = omit,
sort_by: Literal["created_at", "name", "expires_at"] | Omit = omit,
sort_direction: Literal["asc", "desc"] | Omit = omit,
+ status: Literal["active", "deleted", "all"] | 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,
@@ -507,8 +514,8 @@ def list(
API keys are masked.
Args:
- include_deleted: When true, include deleted (soft-deleted) API keys in the results for audit
- purposes. Defaults to false, which returns only live keys.
+ include_deleted: Deprecated: use status=all instead. When true, include deleted (soft-deleted)
+ API keys in the results for audit purposes.
limit: Maximum number of results to return
@@ -521,6 +528,10 @@ def list(
sort_direction: Sort direction for API keys.
+ status: Filter API keys by status. "active" returns keys that are not deleted (default;
+ expired-but-not-deleted keys are still included), "deleted" returns only
+ soft-deleted keys, "all" returns both.
+
extra_headers: Send extra headers
extra_query: Add additional query parameters to the request
@@ -545,6 +556,7 @@ def list(
"query": query,
"sort_by": sort_by,
"sort_direction": sort_direction,
+ "status": status,
},
api_key_list_params.APIKeyListParams,
),
diff --git a/src/kernel/types/api_key.py b/src/kernel/types/api_key.py
index 0f784c63..44519527 100644
--- a/src/kernel/types/api_key.py
+++ b/src/kernel/types/api_key.py
@@ -2,7 +2,6 @@
from typing import Optional
from datetime import datetime
-from typing_extensions import Literal
from .._models import BaseModel
@@ -52,11 +51,3 @@ class APIKey(BaseModel):
Null means the key is org-wide or the project name is unavailable.
"""
-
- status: Literal["active", "expired", "deleted"]
- """Derived lifecycle status of the API key.
-
- `active` means usable. `expired` means past its expires_at. `deleted` means it
- was deleted (soft-deleted) and can no longer authenticate. Deleted takes
- precedence over expired.
- """
diff --git a/src/kernel/types/api_key_list_params.py b/src/kernel/types/api_key_list_params.py
index 673c5d6d..3ddc2a38 100644
--- a/src/kernel/types/api_key_list_params.py
+++ b/src/kernel/types/api_key_list_params.py
@@ -9,9 +9,10 @@
class APIKeyListParams(TypedDict, total=False):
include_deleted: bool
- """
+ """Deprecated: use status=all instead.
+
When true, include deleted (soft-deleted) API keys in the results for audit
- purposes. Defaults to false, which returns only live keys.
+ purposes.
"""
limit: int
@@ -31,3 +32,11 @@ class APIKeyListParams(TypedDict, total=False):
sort_direction: Literal["asc", "desc"]
"""Sort direction for API keys."""
+
+ status: Literal["active", "deleted", "all"]
+ """Filter API keys by status.
+
+ "active" returns keys that are not deleted (default; expired-but-not-deleted
+ keys are still included), "deleted" returns only soft-deleted keys, "all"
+ returns both.
+ """
diff --git a/tests/api_resources/test_api_keys.py b/tests/api_resources/test_api_keys.py
index 9eb265fc..5fdb5dc6 100644
--- a/tests/api_resources/test_api_keys.py
+++ b/tests/api_resources/test_api_keys.py
@@ -178,6 +178,7 @@ def test_method_list_with_all_params(self, client: Kernel) -> None:
query="query",
sort_by="created_at",
sort_direction="asc",
+ status="active",
)
assert_matches_type(SyncOffsetPagination[APIKey], api_key, path=["response"])
@@ -460,6 +461,7 @@ async def test_method_list_with_all_params(self, async_client: AsyncKernel) -> N
query="query",
sort_by="created_at",
sort_direction="asc",
+ status="active",
)
assert_matches_type(AsyncOffsetPagination[APIKey], api_key, path=["response"])
From 977844fa7ec1d98ebbf0903264d9de9dcff4f616 Mon Sep 17 00:00:00 2001
From: "stainless-app[bot]"
<142633134+stainless-app[bot]@users.noreply.github.com>
Date: Mon, 15 Jun 2026 18:41:38 +0000
Subject: [PATCH 074/366] codegen metadata
---
.stats.yml | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/.stats.yml b/.stats.yml
index 4127b5f2..29192087 100644
--- a/.stats.yml
+++ b/.stats.yml
@@ -1,4 +1,4 @@
configured_endpoints: 120
-openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel/kernel-e8afdbeac9332cf79200c2eb873e532104fd0a7472b08e63cde6c857a87cf0c3.yml
-openapi_spec_hash: 2525caf30dffbdd83c83948201f11a52
+openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel/kernel-d26459bd3514237e8d757be3cbdc76ca62f6083504b85601e57db830888964f7.yml
+openapi_spec_hash: 5dd151a8099398819a97692c1c60c3c6
config_hash: 03c7e57f268c750e2415831662e95969
From f10cf197b13b57df014439d00402eb5eea6bac98 Mon Sep 17 00:00:00 2001
From: "stainless-app[bot]"
<142633134+stainless-app[bot]@users.noreply.github.com>
Date: Mon, 15 Jun 2026 18:42:05 +0000
Subject: [PATCH 075/366] release: 0.68.0
---
.release-please-manifest.json | 2 +-
CHANGELOG.md | 26 ++++++++++++++++++++++++++
pyproject.toml | 2 +-
src/kernel/_version.py | 2 +-
4 files changed, 29 insertions(+), 3 deletions(-)
diff --git a/.release-please-manifest.json b/.release-please-manifest.json
index db00cae0..0b8a7c68 100644
--- a/.release-please-manifest.json
+++ b/.release-please-manifest.json
@@ -1,3 +1,3 @@
{
- ".": "0.67.0"
+ ".": "0.68.0"
}
\ No newline at end of file
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 342bd471..899cf79c 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,31 @@
# Changelog
+## 0.68.0 (2026-06-15)
+
+Full Changelog: [v0.67.0...v0.68.0](https://github.com/kernel/kernel-python-sdk/compare/v0.67.0...v0.68.0)
+
+### Features
+
+* Add API key rotate endpoint ([e383f7d](https://github.com/kernel/kernel-python-sdk/commit/e383f7d4b6af4f2921200d4a0f6922e730f58e08))
+* **api:** surface deleted/expired API keys for audit trail (KERNEL-1350) ([55e32db](https://github.com/kernel/kernel-python-sdk/commit/55e32dbaf07275c63623a31ec6df88c6aa128f8f))
+
+
+### Bug Fixes
+
+* **pagination:** correct type:ignore placement; stop on 0 sentinel ([5d2afd7](https://github.com/kernel/kernel-python-sdk/commit/5d2afd7df0f2d21d754ca936286ac3a92762e9db))
+* **pagination:** fail loudly on contradictory headers; test both classes ([4554697](https://github.com/kernel/kernel-python-sdk/commit/4554697a9d850848b602a2422199fb2ce72d8efc))
+* **pagination:** stop skipping a page per auto-pagination iteration ([c0a3e14](https://github.com/kernel/kernel-python-sdk/commit/c0a3e14b51e39dfdba37bacf023b2f98d4a3be69))
+
+
+### Styles
+
+* ruff import-sort and format the pagination test ([843fade](https://github.com/kernel/kernel-python-sdk/commit/843fadeb07fb7cebd5d2689eb78511bb8ee0d293))
+
+
+### Refactors
+
+* **api:** align API key audit surface with browser sibling (KERNEL-1350) ([12868d1](https://github.com/kernel/kernel-python-sdk/commit/12868d18dbb081c0e998299b799ac693dd878d0f))
+
## 0.67.0 (2026-06-11)
Full Changelog: [v0.66.0...v0.67.0](https://github.com/kernel/kernel-python-sdk/compare/v0.66.0...v0.67.0)
diff --git a/pyproject.toml b/pyproject.toml
index 035e174c..4020062f 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -1,6 +1,6 @@
[project]
name = "kernel"
-version = "0.67.0"
+version = "0.68.0"
description = "The official Python library for the kernel API"
dynamic = ["readme"]
license = "Apache-2.0"
diff --git a/src/kernel/_version.py b/src/kernel/_version.py
index 7c28de48..b0e37130 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.67.0" # x-release-please-version
+__version__ = "0.68.0" # x-release-please-version
From fba59b842edefbb1c4779de686d41cc27ff4bf9f Mon Sep 17 00:00:00 2001
From: "stainless-app[bot]"
<142633134+stainless-app[bot]@users.noreply.github.com>
Date: Thu, 18 Jun 2026 18:55:39 +0000
Subject: [PATCH 076/366] feat: Add free-text search to remaining paginated
list endpoints
---
.stats.yml | 4 ++--
src/kernel/resources/auth/connections.py | 8 ++++++++
src/kernel/resources/browser_pools.py | 8 ++++++++
src/kernel/resources/credential_providers.py | 8 ++++++++
src/kernel/resources/credentials.py | 8 ++++++++
src/kernel/resources/deployments.py | 8 ++++++++
src/kernel/resources/extensions.py | 8 ++++++++
src/kernel/resources/invocations.py | 8 ++++++++
src/kernel/resources/proxies.py | 8 ++++++++
src/kernel/types/auth/connection_list_params.py | 3 +++
src/kernel/types/browser_pool_list_params.py | 3 +++
src/kernel/types/credential_list_params.py | 3 +++
src/kernel/types/credential_provider_list_params.py | 3 +++
src/kernel/types/deployment_list_params.py | 3 +++
src/kernel/types/extension_list_params.py | 3 +++
src/kernel/types/invocation_list_params.py | 3 +++
src/kernel/types/proxy_list_params.py | 3 +++
tests/api_resources/auth/test_connections.py | 2 ++
tests/api_resources/test_browser_pools.py | 2 ++
tests/api_resources/test_credential_providers.py | 2 ++
tests/api_resources/test_credentials.py | 2 ++
tests/api_resources/test_deployments.py | 2 ++
tests/api_resources/test_extensions.py | 2 ++
tests/api_resources/test_invocations.py | 2 ++
tests/api_resources/test_proxies.py | 2 ++
25 files changed, 106 insertions(+), 2 deletions(-)
diff --git a/.stats.yml b/.stats.yml
index 29192087..4424fe73 100644
--- a/.stats.yml
+++ b/.stats.yml
@@ -1,4 +1,4 @@
configured_endpoints: 120
-openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel/kernel-d26459bd3514237e8d757be3cbdc76ca62f6083504b85601e57db830888964f7.yml
-openapi_spec_hash: 5dd151a8099398819a97692c1c60c3c6
+openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel/kernel-cde481b2f320ce48f83db84ae96226b0e7568146c9387c4fefebf286ecb0dd0a.yml
+openapi_spec_hash: 6bd86d767290fcd7e2a6aae26dff5417
config_hash: 03c7e57f268c750e2415831662e95969
diff --git a/src/kernel/resources/auth/connections.py b/src/kernel/resources/auth/connections.py
index 362e6129..c901fdb8 100644
--- a/src/kernel/resources/auth/connections.py
+++ b/src/kernel/resources/auth/connections.py
@@ -310,6 +310,7 @@ def list(
limit: int | Omit = omit,
offset: int | Omit = omit,
profile_name: str | Omit = omit,
+ query: 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,
@@ -329,6 +330,8 @@ def list(
profile_name: Filter by profile name
+ query: Search auth connections by ID, domain, or profile name.
+
extra_headers: Send extra headers
extra_query: Add additional query parameters to the request
@@ -351,6 +354,7 @@ def list(
"limit": limit,
"offset": offset,
"profile_name": profile_name,
+ "query": query,
},
connection_list_params.ConnectionListParams,
),
@@ -827,6 +831,7 @@ def list(
limit: int | Omit = omit,
offset: int | Omit = omit,
profile_name: str | Omit = omit,
+ query: 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,
@@ -846,6 +851,8 @@ def list(
profile_name: Filter by profile name
+ query: Search auth connections by ID, domain, or profile name.
+
extra_headers: Send extra headers
extra_query: Add additional query parameters to the request
@@ -868,6 +875,7 @@ def list(
"limit": limit,
"offset": offset,
"profile_name": profile_name,
+ "query": query,
},
connection_list_params.ConnectionListParams,
),
diff --git a/src/kernel/resources/browser_pools.py b/src/kernel/resources/browser_pools.py
index fc32c951..7c3abda9 100644
--- a/src/kernel/resources/browser_pools.py
+++ b/src/kernel/resources/browser_pools.py
@@ -330,6 +330,7 @@ def list(
*,
limit: int | Omit = omit,
offset: int | Omit = omit,
+ query: 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,
@@ -345,6 +346,8 @@ def list(
offset: Offset the number of browser pools to return.
+ query: Search browser pools by name or ID.
+
extra_headers: Send extra headers
extra_query: Add additional query parameters to the request
@@ -365,6 +368,7 @@ def list(
{
"limit": limit,
"offset": offset,
+ "query": query,
},
browser_pool_list_params.BrowserPoolListParams,
),
@@ -851,6 +855,7 @@ def list(
*,
limit: int | Omit = omit,
offset: int | Omit = omit,
+ query: 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,
@@ -866,6 +871,8 @@ def list(
offset: Offset the number of browser pools to return.
+ query: Search browser pools by name or ID.
+
extra_headers: Send extra headers
extra_query: Add additional query parameters to the request
@@ -886,6 +893,7 @@ def list(
{
"limit": limit,
"offset": offset,
+ "query": query,
},
browser_pool_list_params.BrowserPoolListParams,
),
diff --git a/src/kernel/resources/credential_providers.py b/src/kernel/resources/credential_providers.py
index 06bf2aee..00db5c5e 100644
--- a/src/kernel/resources/credential_providers.py
+++ b/src/kernel/resources/credential_providers.py
@@ -200,6 +200,7 @@ def list(
*,
limit: int | Omit = omit,
offset: int | Omit = omit,
+ query: 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,
@@ -215,6 +216,8 @@ def list(
offset: Offset the number of credential providers to return.
+ query: Search credential providers by name or ID.
+
extra_headers: Send extra headers
extra_query: Add additional query parameters to the request
@@ -235,6 +238,7 @@ def list(
{
"limit": limit,
"offset": offset,
+ "query": query,
},
credential_provider_list_params.CredentialProviderListParams,
),
@@ -514,6 +518,7 @@ def list(
*,
limit: int | Omit = omit,
offset: int | Omit = omit,
+ query: 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,
@@ -529,6 +534,8 @@ def list(
offset: Offset the number of credential providers to return.
+ query: Search credential providers by name or ID.
+
extra_headers: Send extra headers
extra_query: Add additional query parameters to the request
@@ -549,6 +556,7 @@ def list(
{
"limit": limit,
"offset": offset,
+ "query": query,
},
credential_provider_list_params.CredentialProviderListParams,
),
diff --git a/src/kernel/resources/credentials.py b/src/kernel/resources/credentials.py
index 4c90ef85..3919ecff 100644
--- a/src/kernel/resources/credentials.py
+++ b/src/kernel/resources/credentials.py
@@ -205,6 +205,7 @@ def list(
domain: str | Omit = omit,
limit: int | Omit = omit,
offset: int | Omit = omit,
+ query: 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,
@@ -223,6 +224,8 @@ def list(
offset: Number of results to skip
+ query: Search credentials by name, domain, or ID.
+
extra_headers: Send extra headers
extra_query: Add additional query parameters to the request
@@ -244,6 +247,7 @@ def list(
"domain": domain,
"limit": limit,
"offset": offset,
+ "query": query,
},
credential_list_params.CredentialListParams,
),
@@ -501,6 +505,7 @@ def list(
domain: str | Omit = omit,
limit: int | Omit = omit,
offset: int | Omit = omit,
+ query: 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,
@@ -519,6 +524,8 @@ def list(
offset: Number of results to skip
+ query: Search credentials by name, domain, or ID.
+
extra_headers: Send extra headers
extra_query: Add additional query parameters to the request
@@ -540,6 +547,7 @@ def list(
"domain": domain,
"limit": limit,
"offset": offset,
+ "query": query,
},
credential_list_params.CredentialListParams,
),
diff --git a/src/kernel/resources/deployments.py b/src/kernel/resources/deployments.py
index 9b079eb9..c6de9212 100644
--- a/src/kernel/resources/deployments.py
+++ b/src/kernel/resources/deployments.py
@@ -163,6 +163,7 @@ def list(
app_version: str | Omit = omit,
limit: int | Omit = omit,
offset: int | Omit = omit,
+ query: 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,
@@ -183,6 +184,8 @@ def list(
offset: Offset the number of deployments to return.
+ query: Search deployments by ID or app name.
+
extra_headers: Send extra headers
extra_query: Add additional query parameters to the request
@@ -205,6 +208,7 @@ def list(
"app_version": app_version,
"limit": limit,
"offset": offset,
+ "query": query,
},
deployment_list_params.DeploymentListParams,
),
@@ -429,6 +433,7 @@ def list(
app_version: str | Omit = omit,
limit: int | Omit = omit,
offset: int | Omit = omit,
+ query: 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,
@@ -449,6 +454,8 @@ def list(
offset: Offset the number of deployments to return.
+ query: Search deployments by ID or app name.
+
extra_headers: Send extra headers
extra_query: Add additional query parameters to the request
@@ -471,6 +478,7 @@ def list(
"app_version": app_version,
"limit": limit,
"offset": offset,
+ "query": query,
},
deployment_list_params.DeploymentListParams,
),
diff --git a/src/kernel/resources/extensions.py b/src/kernel/resources/extensions.py
index 8d7acb14..f358d341 100644
--- a/src/kernel/resources/extensions.py
+++ b/src/kernel/resources/extensions.py
@@ -62,6 +62,7 @@ def list(
*,
limit: int | Omit = omit,
offset: int | Omit = omit,
+ query: 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,
@@ -77,6 +78,8 @@ def list(
offset: Offset the number of extensions to return.
+ query: Search extensions by name or ID.
+
extra_headers: Send extra headers
extra_query: Add additional query parameters to the request
@@ -97,6 +100,7 @@ def list(
{
"limit": limit,
"offset": offset,
+ "query": query,
},
extension_list_params.ExtensionListParams,
),
@@ -300,6 +304,7 @@ def list(
*,
limit: int | Omit = omit,
offset: int | Omit = omit,
+ query: 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,
@@ -315,6 +320,8 @@ def list(
offset: Offset the number of extensions to return.
+ query: Search extensions by name or ID.
+
extra_headers: Send extra headers
extra_query: Add additional query parameters to the request
@@ -335,6 +342,7 @@ def list(
{
"limit": limit,
"offset": offset,
+ "query": query,
},
extension_list_params.ExtensionListParams,
),
diff --git a/src/kernel/resources/invocations.py b/src/kernel/resources/invocations.py
index 6d367e9e..30db7dac 100644
--- a/src/kernel/resources/invocations.py
+++ b/src/kernel/resources/invocations.py
@@ -203,6 +203,7 @@ def list(
deployment_id: str | Omit = omit,
limit: int | Omit = omit,
offset: int | Omit = omit,
+ query: str | Omit = omit,
since: str | Omit = omit,
status: Literal["queued", "running", "succeeded", "failed"] | Omit = omit,
version: str | Omit = omit,
@@ -229,6 +230,8 @@ def list(
offset: Offset the number of invocations to return.
+ query: Search invocations by ID, app name, or action name.
+
since: Show invocations that have started since the given time (RFC timestamps or
durations like 5m).
@@ -259,6 +262,7 @@ def list(
"deployment_id": deployment_id,
"limit": limit,
"offset": offset,
+ "query": query,
"since": since,
"status": status,
"version": version,
@@ -556,6 +560,7 @@ def list(
deployment_id: str | Omit = omit,
limit: int | Omit = omit,
offset: int | Omit = omit,
+ query: str | Omit = omit,
since: str | Omit = omit,
status: Literal["queued", "running", "succeeded", "failed"] | Omit = omit,
version: str | Omit = omit,
@@ -582,6 +587,8 @@ def list(
offset: Offset the number of invocations to return.
+ query: Search invocations by ID, app name, or action name.
+
since: Show invocations that have started since the given time (RFC timestamps or
durations like 5m).
@@ -612,6 +619,7 @@ def list(
"deployment_id": deployment_id,
"limit": limit,
"offset": offset,
+ "query": query,
"since": since,
"status": status,
"version": version,
diff --git a/src/kernel/resources/proxies.py b/src/kernel/resources/proxies.py
index 831e7819..0b4902de 100644
--- a/src/kernel/resources/proxies.py
+++ b/src/kernel/resources/proxies.py
@@ -143,6 +143,7 @@ def list(
*,
limit: int | Omit = omit,
offset: int | Omit = omit,
+ query: 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,
@@ -158,6 +159,8 @@ def list(
offset: Offset the number of proxies to return.
+ query: Search proxies by name, host, IP address, or ID.
+
extra_headers: Send extra headers
extra_query: Add additional query parameters to the request
@@ -178,6 +181,7 @@ def list(
{
"limit": limit,
"offset": offset,
+ "query": query,
},
proxy_list_params.ProxyListParams,
),
@@ -391,6 +395,7 @@ def list(
*,
limit: int | Omit = omit,
offset: int | Omit = omit,
+ query: 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,
@@ -406,6 +411,8 @@ def list(
offset: Offset the number of proxies to return.
+ query: Search proxies by name, host, IP address, or ID.
+
extra_headers: Send extra headers
extra_query: Add additional query parameters to the request
@@ -426,6 +433,7 @@ def list(
{
"limit": limit,
"offset": offset,
+ "query": query,
},
proxy_list_params.ProxyListParams,
),
diff --git a/src/kernel/types/auth/connection_list_params.py b/src/kernel/types/auth/connection_list_params.py
index dd8d0de2..dd6ca9f2 100644
--- a/src/kernel/types/auth/connection_list_params.py
+++ b/src/kernel/types/auth/connection_list_params.py
@@ -19,3 +19,6 @@ class ConnectionListParams(TypedDict, total=False):
profile_name: str
"""Filter by profile name"""
+
+ query: str
+ """Search auth connections by ID, domain, or profile name."""
diff --git a/src/kernel/types/browser_pool_list_params.py b/src/kernel/types/browser_pool_list_params.py
index f8111e3e..26436a61 100644
--- a/src/kernel/types/browser_pool_list_params.py
+++ b/src/kernel/types/browser_pool_list_params.py
@@ -13,3 +13,6 @@ class BrowserPoolListParams(TypedDict, total=False):
offset: int
"""Offset the number of browser pools to return."""
+
+ query: str
+ """Search browser pools by name or ID."""
diff --git a/src/kernel/types/credential_list_params.py b/src/kernel/types/credential_list_params.py
index 945909e5..50e25895 100644
--- a/src/kernel/types/credential_list_params.py
+++ b/src/kernel/types/credential_list_params.py
@@ -16,3 +16,6 @@ class CredentialListParams(TypedDict, total=False):
offset: int
"""Number of results to skip"""
+
+ query: str
+ """Search credentials by name, domain, or ID."""
diff --git a/src/kernel/types/credential_provider_list_params.py b/src/kernel/types/credential_provider_list_params.py
index 87dbfd69..69005988 100644
--- a/src/kernel/types/credential_provider_list_params.py
+++ b/src/kernel/types/credential_provider_list_params.py
@@ -13,3 +13,6 @@ class CredentialProviderListParams(TypedDict, total=False):
offset: int
"""Offset the number of credential providers to return."""
+
+ query: str
+ """Search credential providers by name or ID."""
diff --git a/src/kernel/types/deployment_list_params.py b/src/kernel/types/deployment_list_params.py
index 4c1c0717..7ee96dac 100644
--- a/src/kernel/types/deployment_list_params.py
+++ b/src/kernel/types/deployment_list_params.py
@@ -19,3 +19,6 @@ class DeploymentListParams(TypedDict, total=False):
offset: int
"""Offset the number of deployments to return."""
+
+ query: str
+ """Search deployments by ID or app name."""
diff --git a/src/kernel/types/extension_list_params.py b/src/kernel/types/extension_list_params.py
index bccd61eb..bade1423 100644
--- a/src/kernel/types/extension_list_params.py
+++ b/src/kernel/types/extension_list_params.py
@@ -13,3 +13,6 @@ class ExtensionListParams(TypedDict, total=False):
offset: int
"""Offset the number of extensions to return."""
+
+ query: str
+ """Search extensions by name or ID."""
diff --git a/src/kernel/types/invocation_list_params.py b/src/kernel/types/invocation_list_params.py
index 9673f2d6..2bcfe9c7 100644
--- a/src/kernel/types/invocation_list_params.py
+++ b/src/kernel/types/invocation_list_params.py
@@ -23,6 +23,9 @@ class InvocationListParams(TypedDict, total=False):
offset: int
"""Offset the number of invocations to return."""
+ query: str
+ """Search invocations by ID, app name, or action name."""
+
since: str
"""
Show invocations that have started since the given time (RFC timestamps or
diff --git a/src/kernel/types/proxy_list_params.py b/src/kernel/types/proxy_list_params.py
index 78972bcb..75c5f133 100644
--- a/src/kernel/types/proxy_list_params.py
+++ b/src/kernel/types/proxy_list_params.py
@@ -13,3 +13,6 @@ class ProxyListParams(TypedDict, total=False):
offset: int
"""Offset the number of proxies to return."""
+
+ query: str
+ """Search proxies by name, host, IP address, or ID."""
diff --git a/tests/api_resources/auth/test_connections.py b/tests/api_resources/auth/test_connections.py
index 702207fe..2b9ce1c4 100644
--- a/tests/api_resources/auth/test_connections.py
+++ b/tests/api_resources/auth/test_connections.py
@@ -208,6 +208,7 @@ def test_method_list_with_all_params(self, client: Kernel) -> None:
limit=100,
offset=0,
profile_name="profile_name",
+ query="query",
)
assert_matches_type(SyncOffsetPagination[ManagedAuth], connection, path=["response"])
@@ -621,6 +622,7 @@ async def test_method_list_with_all_params(self, async_client: AsyncKernel) -> N
limit=100,
offset=0,
profile_name="profile_name",
+ query="query",
)
assert_matches_type(AsyncOffsetPagination[ManagedAuth], connection, path=["response"])
diff --git a/tests/api_resources/test_browser_pools.py b/tests/api_resources/test_browser_pools.py
index e6f8b853..c6910837 100644
--- a/tests/api_resources/test_browser_pools.py
+++ b/tests/api_resources/test_browser_pools.py
@@ -219,6 +219,7 @@ def test_method_list_with_all_params(self, client: Kernel) -> None:
browser_pool = client.browser_pools.list(
limit=1,
offset=0,
+ query="query",
)
assert_matches_type(SyncOffsetPagination[BrowserPool], browser_pool, path=["response"])
@@ -653,6 +654,7 @@ async def test_method_list_with_all_params(self, async_client: AsyncKernel) -> N
browser_pool = await async_client.browser_pools.list(
limit=1,
offset=0,
+ query="query",
)
assert_matches_type(AsyncOffsetPagination[BrowserPool], browser_pool, path=["response"])
diff --git a/tests/api_resources/test_credential_providers.py b/tests/api_resources/test_credential_providers.py
index d9795332..a47d76bb 100644
--- a/tests/api_resources/test_credential_providers.py
+++ b/tests/api_resources/test_credential_providers.py
@@ -182,6 +182,7 @@ def test_method_list_with_all_params(self, client: Kernel) -> None:
credential_provider = client.credential_providers.list(
limit=1,
offset=0,
+ query="query",
)
assert_matches_type(SyncOffsetPagination[CredentialProvider], credential_provider, path=["response"])
@@ -499,6 +500,7 @@ async def test_method_list_with_all_params(self, async_client: AsyncKernel) -> N
credential_provider = await async_client.credential_providers.list(
limit=1,
offset=0,
+ query="query",
)
assert_matches_type(AsyncOffsetPagination[CredentialProvider], credential_provider, path=["response"])
diff --git a/tests/api_resources/test_credentials.py b/tests/api_resources/test_credentials.py
index ff932bef..31704230 100644
--- a/tests/api_resources/test_credentials.py
+++ b/tests/api_resources/test_credentials.py
@@ -197,6 +197,7 @@ def test_method_list_with_all_params(self, client: Kernel) -> None:
domain="domain",
limit=100,
offset=0,
+ query="query",
)
assert_matches_type(SyncOffsetPagination[Credential], credential, path=["response"])
@@ -488,6 +489,7 @@ async def test_method_list_with_all_params(self, async_client: AsyncKernel) -> N
domain="domain",
limit=100,
offset=0,
+ query="query",
)
assert_matches_type(AsyncOffsetPagination[Credential], credential, path=["response"])
diff --git a/tests/api_resources/test_deployments.py b/tests/api_resources/test_deployments.py
index ed562864..4f619015 100644
--- a/tests/api_resources/test_deployments.py
+++ b/tests/api_resources/test_deployments.py
@@ -130,6 +130,7 @@ def test_method_list_with_all_params(self, client: Kernel) -> None:
app_version="app_version",
limit=1,
offset=0,
+ query="query",
)
assert_matches_type(SyncOffsetPagination[DeploymentListResponse], deployment, path=["response"])
@@ -361,6 +362,7 @@ async def test_method_list_with_all_params(self, async_client: AsyncKernel) -> N
app_version="app_version",
limit=1,
offset=0,
+ query="query",
)
assert_matches_type(AsyncOffsetPagination[DeploymentListResponse], deployment, path=["response"])
diff --git a/tests/api_resources/test_extensions.py b/tests/api_resources/test_extensions.py
index 302ba958..fe8a4cb6 100644
--- a/tests/api_resources/test_extensions.py
+++ b/tests/api_resources/test_extensions.py
@@ -41,6 +41,7 @@ def test_method_list_with_all_params(self, client: Kernel) -> None:
extension = client.extensions.list(
limit=1,
offset=0,
+ query="query",
)
assert_matches_type(SyncOffsetPagination[ExtensionListResponse], extension, path=["response"])
@@ -274,6 +275,7 @@ async def test_method_list_with_all_params(self, async_client: AsyncKernel) -> N
extension = await async_client.extensions.list(
limit=1,
offset=0,
+ query="query",
)
assert_matches_type(AsyncOffsetPagination[ExtensionListResponse], extension, path=["response"])
diff --git a/tests/api_resources/test_invocations.py b/tests/api_resources/test_invocations.py
index d2784c21..2617629e 100644
--- a/tests/api_resources/test_invocations.py
+++ b/tests/api_resources/test_invocations.py
@@ -190,6 +190,7 @@ def test_method_list_with_all_params(self, client: Kernel) -> None:
deployment_id="deployment_id",
limit=1,
offset=0,
+ query="query",
since="2025-06-20T12:00:00Z",
status="queued",
version="version",
@@ -524,6 +525,7 @@ async def test_method_list_with_all_params(self, async_client: AsyncKernel) -> N
deployment_id="deployment_id",
limit=1,
offset=0,
+ query="query",
since="2025-06-20T12:00:00Z",
status="queued",
version="version",
diff --git a/tests/api_resources/test_proxies.py b/tests/api_resources/test_proxies.py
index 64cb90b9..29df16e4 100644
--- a/tests/api_resources/test_proxies.py
+++ b/tests/api_resources/test_proxies.py
@@ -123,6 +123,7 @@ def test_method_list_with_all_params(self, client: Kernel) -> None:
proxy = client.proxies.list(
limit=1,
offset=0,
+ query="query",
)
assert_matches_type(SyncOffsetPagination[ProxyListResponse], proxy, path=["response"])
@@ -347,6 +348,7 @@ async def test_method_list_with_all_params(self, async_client: AsyncKernel) -> N
proxy = await async_client.proxies.list(
limit=1,
offset=0,
+ query="query",
)
assert_matches_type(AsyncOffsetPagination[ProxyListResponse], proxy, path=["response"])
From 4506e68cd579e0c402df5d783ffca2d15be117db Mon Sep 17 00:00:00 2001
From: "stainless-app[bot]"
<142633134+stainless-app[bot]@users.noreply.github.com>
Date: Thu, 18 Jun 2026 18:56:06 +0000
Subject: [PATCH 077/366] release: 0.69.0
---
.release-please-manifest.json | 2 +-
CHANGELOG.md | 8 ++++++++
pyproject.toml | 2 +-
src/kernel/_version.py | 2 +-
4 files changed, 11 insertions(+), 3 deletions(-)
diff --git a/.release-please-manifest.json b/.release-please-manifest.json
index 0b8a7c68..a9d0cc14 100644
--- a/.release-please-manifest.json
+++ b/.release-please-manifest.json
@@ -1,3 +1,3 @@
{
- ".": "0.68.0"
+ ".": "0.69.0"
}
\ No newline at end of file
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 899cf79c..a818acee 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,13 @@
# Changelog
+## 0.69.0 (2026-06-18)
+
+Full Changelog: [v0.68.0...v0.69.0](https://github.com/kernel/kernel-python-sdk/compare/v0.68.0...v0.69.0)
+
+### Features
+
+* Add free-text search to remaining paginated list endpoints ([b34f843](https://github.com/kernel/kernel-python-sdk/commit/b34f843f664307beeb23ec96a1162ee70fd3df1e))
+
## 0.68.0 (2026-06-15)
Full Changelog: [v0.67.0...v0.68.0](https://github.com/kernel/kernel-python-sdk/compare/v0.67.0...v0.68.0)
diff --git a/pyproject.toml b/pyproject.toml
index 4020062f..2f216eb5 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -1,6 +1,6 @@
[project]
name = "kernel"
-version = "0.68.0"
+version = "0.69.0"
description = "The official Python library for the kernel API"
dynamic = ["readme"]
license = "Apache-2.0"
diff --git a/src/kernel/_version.py b/src/kernel/_version.py
index b0e37130..1d6a78bf 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.68.0" # x-release-please-version
+__version__ = "0.69.0" # x-release-please-version
From 9bcc418b4294295c0d8377adfc921c3748d42019 Mon Sep 17 00:00:00 2001
From: Ilyaas Kapadia <86218345+IlyaasK@users.noreply.github.com>
Date: Wed, 24 Jun 2026 10:40:41 -0400
Subject: [PATCH 078/366] fix: don't misroute telemetry/events to the browser
VM
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
The direct-to-VM routing allowlist matched on the subresource SEGMENT
("telemetry"), so the new historical GET /browsers/{id}/telemetry/events
endpoint (served by the control plane from S2) was routed to the session VM —
which only serves the live telemetry/stream SSE — once a session was route-cached,
yielding failures / wrong data.
Fix the granularity:
- Allowlist entries are now full path-prefixes ("curl", "telemetry/stream").
- Matching is segment-boundary aware: "telemetry/stream" matches
"telemetry/stream[/...]" but NOT "telemetry/events" or "telemetry/streamfoo".
- Safe by default: any path not in the allowlist (including future browser
sub-endpoints) goes to the control plane — slower, never misrouted.
All in src/kernel/lib/ (Stainless-preserved), so durable across regens. Adds a
regression vector and updates the default-config assertion.
Co-Authored-By: Claude Opus 4.8 (1M context)
---
src/kernel/lib/browser_routing/routing.py | 22 ++++++++++++++++++++--
tests/test_browser_routing.py | 18 +++++++++++++++++-
2 files changed, 37 insertions(+), 3 deletions(-)
diff --git a/src/kernel/lib/browser_routing/routing.py b/src/kernel/lib/browser_routing/routing.py
index f0f5e34a..7f28d726 100644
--- a/src/kernel/lib/browser_routing/routing.py
+++ b/src/kernel/lib/browser_routing/routing.py
@@ -41,7 +41,10 @@ class BrowserRoutingConfig:
def browser_routing_config_from_env() -> BrowserRoutingConfig:
raw = os.environ.get("KERNEL_BROWSER_ROUTING_SUBRESOURCES")
if raw is None:
- return BrowserRoutingConfig(subresources=("curl", "telemetry"))
+ # Path prefixes eligible for direct-to-VM routing. "telemetry/stream" is
+ # the live SSE endpoint (VM); "telemetry/events" is a historical read
+ # served by the control plane (S2) and must NOT be here.
+ return BrowserRoutingConfig(subresources=("curl", "telemetry/stream"))
if raw.strip() == "":
return BrowserRoutingConfig()
@@ -188,6 +191,21 @@ def _session_id_from_browser_pool_release_request(request: httpx.Request, path:
return normalized or None
+def _matches_direct_vm_prefix(tail: str, prefixes: tuple[str, ...]) -> bool:
+ """Whether tail (the path after browsers/{id}/) is covered by an allow prefix,
+ matching on segment boundaries: "telemetry/stream" matches "telemetry/stream"
+ and "telemetry/stream/...", but not "telemetry/events" or "telemetry/streamfoo".
+ Keeps historical control-plane reads (e.g. telemetry/events, served from S2)
+ off the VM.
+ """
+ tail = tail.strip("/")
+ for prefix in prefixes:
+ prefix = prefix.strip("/")
+ if prefix and (tail == prefix or tail.startswith(prefix + "/")):
+ return True
+ return False
+
+
def rewrite_direct_vm_options(
options: FinalRequestOptions,
*,
@@ -199,7 +217,7 @@ def rewrite_direct_vm_options(
return options
session_id, subresource, suffix = match
- if subresource not in set(config.subresources):
+ if not _matches_direct_vm_prefix(f"{subresource}{suffix}", config.subresources):
return options
route = cache.get(session_id)
diff --git a/tests/test_browser_routing.py b/tests/test_browser_routing.py
index 6b0c12a9..54b7bd91 100644
--- a/tests/test_browser_routing.py
+++ b/tests/test_browser_routing.py
@@ -337,7 +337,23 @@ def test_browser_route_from_browser_requires_base_url_and_jwt() -> None:
def test_browser_routing_config_from_env_defaults_to_curl(monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.delenv("KERNEL_BROWSER_ROUTING_SUBRESOURCES", raising=False)
- assert browser_routing_config_from_env().subresources == ("curl", "telemetry")
+ assert browser_routing_config_from_env().subresources == ("curl", "telemetry/stream")
+
+
+def test_direct_vm_routing_allowlist_segment_boundary() -> None:
+ # Pins the fix: telemetry/stream (live SSE) routes to the VM; telemetry/events
+ # (historical, served by the control plane from S2) does NOT; and a
+ # stream-prefixed-but-different path is not matched.
+ from kernel.lib.browser_routing.routing import _matches_direct_vm_prefix
+
+ prefixes = ("curl", "telemetry/stream")
+ assert _matches_direct_vm_prefix("telemetry/stream", prefixes) is True
+ assert _matches_direct_vm_prefix("telemetry/stream/x", prefixes) is True
+ assert _matches_direct_vm_prefix("telemetry/events", prefixes) is False
+ assert _matches_direct_vm_prefix("telemetry/streaming-config", prefixes) is False
+ assert _matches_direct_vm_prefix("telemetry", prefixes) is False
+ assert _matches_direct_vm_prefix("curl/raw", prefixes) is True
+ assert _matches_direct_vm_prefix("fs/read", prefixes) is False
def test_browser_routing_config_from_env_empty_string_disables_routing(monkeypatch: pytest.MonkeyPatch) -> None:
From e31a9857d89597286e5d490a61657fe8a24df2a3 Mon Sep 17 00:00:00 2001
From: Ilyaas Kapadia <86218345+IlyaasK@users.noreply.github.com>
Date: Wed, 24 Jun 2026 11:22:04 -0400
Subject: [PATCH 079/366] test: cover telemetry/events on control plane +
segment-boundary matcher
Adds an integration test (rewrite_direct_vm_options) asserting telemetry/events
stays on the control plane while telemetry/stream routes to the VM, and switches
the stream test env to the full-path 'telemetry/stream'. Addresses review.
Co-Authored-By: Claude Opus 4.8 (1M context)
---
tests/test_browser_routing.py | 31 ++++++++++++++++++++++++++++++-
1 file changed, 30 insertions(+), 1 deletion(-)
diff --git a/tests/test_browser_routing.py b/tests/test_browser_routing.py
index 54b7bd91..ce9a726f 100644
--- a/tests/test_browser_routing.py
+++ b/tests/test_browser_routing.py
@@ -99,7 +99,7 @@ def test_browser_request_uses_curl_raw() -> None:
@respx.mock
def test_telemetry_stream_routes_directly_to_vm(monkeypatch: pytest.MonkeyPatch) -> None:
- monkeypatch.setenv("KERNEL_BROWSER_ROUTING_SUBRESOURCES", "telemetry")
+ monkeypatch.setenv("KERNEL_BROWSER_ROUTING_SUBRESOURCES", "telemetry/stream")
route = respx.get("http://browser-session.test/browser/kernel/telemetry/stream").mock(
return_value=httpx.Response(
200,
@@ -356,6 +356,35 @@ def test_direct_vm_routing_allowlist_segment_boundary() -> None:
assert _matches_direct_vm_prefix("fs/read", prefixes) is False
+def test_rewrite_direct_vm_options_keeps_telemetry_events_on_control_plane() -> None:
+ # Integration through the real routing hook: telemetry/events (historical,
+ # control-plane/S2) must NOT be rewritten to the VM, while telemetry/stream
+ # (live SSE) must be.
+ from kernel._models import FinalRequestOptions
+ from kernel.lib.browser_routing.routing import (
+ BrowserRoute,
+ BrowserRouteCache,
+ BrowserRoutingConfig,
+ rewrite_direct_vm_options,
+ )
+
+ cache = BrowserRouteCache()
+ cache.set(
+ BrowserRoute(session_id="sess-1", base_url="http://browser-session.test/browser/kernel", jwt="token-abc")
+ )
+ config = BrowserRoutingConfig(subresources=("curl", "telemetry/stream"))
+
+ events = rewrite_direct_vm_options(
+ FinalRequestOptions(method="get", url="/browsers/sess-1/telemetry/events"), cache=cache, config=config
+ )
+ assert events.url == "/browsers/sess-1/telemetry/events" # unchanged -> control plane
+
+ stream = rewrite_direct_vm_options(
+ FinalRequestOptions(method="get", url="/browsers/sess-1/telemetry/stream"), cache=cache, config=config
+ )
+ assert str(stream.url).startswith("http://browser-session.test/browser/kernel/telemetry/stream")
+
+
def test_browser_routing_config_from_env_empty_string_disables_routing(monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.setenv("KERNEL_BROWSER_ROUTING_SUBRESOURCES", "")
assert browser_routing_config_from_env().subresources == ()
From b3d537ad5b497c6af66e1d35890caf67f911474d Mon Sep 17 00:00:00 2001
From: "stainless-app[bot]"
<142633134+stainless-app[bot]@users.noreply.github.com>
Date: Sat, 20 Jun 2026 20:02:35 +0000
Subject: [PATCH 080/366] codegen metadata
---
.stats.yml | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/.stats.yml b/.stats.yml
index 4424fe73..ed276ab4 100644
--- a/.stats.yml
+++ b/.stats.yml
@@ -1,4 +1,4 @@
configured_endpoints: 120
-openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel/kernel-cde481b2f320ce48f83db84ae96226b0e7568146c9387c4fefebf286ecb0dd0a.yml
-openapi_spec_hash: 6bd86d767290fcd7e2a6aae26dff5417
+openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel/kernel-d70be5e703da22000f3f7e76b19d5fa3e3bf8c33be2e9570bfafb5cf9d956a17.yml
+openapi_spec_hash: deaa8926543116d1f81d34d1e01e0c0a
config_hash: 03c7e57f268c750e2415831662e95969
From 858dc7c9e6b9be7f26c12c2bb1f987951078a794 Mon Sep 17 00:00:00 2001
From: "stainless-app[bot]"
<142633134+stainless-app[bot]@users.noreply.github.com>
Date: Mon, 22 Jun 2026 17:41:37 +0000
Subject: [PATCH 081/366] feat: Align browser-pool timeout/viewport/fill-rate
contract with implementation; reject save_changes on update
---
.stats.yml | 4 ++--
src/kernel/resources/browser_pools.py | 24 ++++++++++++-------
src/kernel/types/browser_pool.py | 8 +++++--
.../types/browser_pool_create_params.py | 8 +++++--
.../types/browser_pool_update_params.py | 8 +++++--
src/kernel/types/shared/browser_viewport.py | 7 +++---
.../types/shared_params/browser_viewport.py | 7 +++---
tests/api_resources/test_browser_pools.py | 8 +++----
8 files changed, 48 insertions(+), 26 deletions(-)
diff --git a/.stats.yml b/.stats.yml
index ed276ab4..316f1e84 100644
--- a/.stats.yml
+++ b/.stats.yml
@@ -1,4 +1,4 @@
configured_endpoints: 120
-openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel/kernel-d70be5e703da22000f3f7e76b19d5fa3e3bf8c33be2e9570bfafb5cf9d956a17.yml
-openapi_spec_hash: deaa8926543116d1f81d34d1e01e0c0a
+openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel/kernel-ab4e7c50c41fefd891648fa84ba0258986f192ba1bde9f42cbdf487f64c4cc27.yml
+openapi_spec_hash: b4bcd6557f7045ecff30b01e02d28ac5
config_hash: 03c7e57f268c750e2415831662e95969
diff --git a/src/kernel/resources/browser_pools.py b/src/kernel/resources/browser_pools.py
index 7c3abda9..831b93e0 100644
--- a/src/kernel/resources/browser_pools.py
+++ b/src/kernel/resources/browser_pools.py
@@ -96,7 +96,9 @@ def create(
extensions: List of browser extensions to load into the session. Provide each by id or name.
- fill_rate_per_minute: Percentage of the pool to fill per minute. Defaults to 10%.
+ fill_rate_per_minute: Percentage of the pool to fill per minute. Defaults to 10. The cap is 25 for
+ most organizations but can be raised per-organization, so only the lower bound
+ is enforced here.
headless: If true, launches the browser using a headless image. Defaults to false.
@@ -122,7 +124,7 @@ def create(
mechanisms.
timeout_seconds: Default idle timeout in seconds for browsers acquired from this pool before they
- are destroyed. Defaults to 600 seconds if not specified
+ are destroyed. Defaults to 600 seconds. Minimum 10, maximum 259200 (72 hours).
viewport: Initial browser window size in pixels with optional refresh rate. If omitted,
image defaults apply (1920x1080@25). For GPU images, the default is
@@ -243,7 +245,9 @@ def update(
extensions: List of browser extensions to load into the session. Provide each by id or name.
- fill_rate_per_minute: Percentage of the pool to fill per minute. Defaults to 10%.
+ fill_rate_per_minute: Percentage of the pool to fill per minute. Defaults to 10. The cap is 25 for
+ most organizations but can be raised per-organization, so only the lower bound
+ is enforced here.
headless: If true, launches the browser using a headless image. Defaults to false.
@@ -273,7 +277,7 @@ def update(
mechanisms.
timeout_seconds: Default idle timeout in seconds for browsers acquired from this pool before they
- are destroyed. Defaults to 600 seconds if not specified
+ are destroyed. Defaults to 600 seconds. Minimum 10, maximum 259200 (72 hours).
viewport: Initial browser window size in pixels with optional refresh rate. If omitted,
image defaults apply (1920x1080@25). For GPU images, the default is
@@ -621,7 +625,9 @@ async def create(
extensions: List of browser extensions to load into the session. Provide each by id or name.
- fill_rate_per_minute: Percentage of the pool to fill per minute. Defaults to 10%.
+ fill_rate_per_minute: Percentage of the pool to fill per minute. Defaults to 10. The cap is 25 for
+ most organizations but can be raised per-organization, so only the lower bound
+ is enforced here.
headless: If true, launches the browser using a headless image. Defaults to false.
@@ -647,7 +653,7 @@ async def create(
mechanisms.
timeout_seconds: Default idle timeout in seconds for browsers acquired from this pool before they
- are destroyed. Defaults to 600 seconds if not specified
+ are destroyed. Defaults to 600 seconds. Minimum 10, maximum 259200 (72 hours).
viewport: Initial browser window size in pixels with optional refresh rate. If omitted,
image defaults apply (1920x1080@25). For GPU images, the default is
@@ -768,7 +774,9 @@ async def update(
extensions: List of browser extensions to load into the session. Provide each by id or name.
- fill_rate_per_minute: Percentage of the pool to fill per minute. Defaults to 10%.
+ fill_rate_per_minute: Percentage of the pool to fill per minute. Defaults to 10. The cap is 25 for
+ most organizations but can be raised per-organization, so only the lower bound
+ is enforced here.
headless: If true, launches the browser using a headless image. Defaults to false.
@@ -798,7 +806,7 @@ async def update(
mechanisms.
timeout_seconds: Default idle timeout in seconds for browsers acquired from this pool before they
- are destroyed. Defaults to 600 seconds if not specified
+ are destroyed. Defaults to 600 seconds. Minimum 10, maximum 259200 (72 hours).
viewport: Initial browser window size in pixels with optional refresh rate. If omitted,
image defaults apply (1920x1080@25). For GPU images, the default is
diff --git a/src/kernel/types/browser_pool.py b/src/kernel/types/browser_pool.py
index 8ad50acf..877fbcde 100644
--- a/src/kernel/types/browser_pool.py
+++ b/src/kernel/types/browser_pool.py
@@ -36,7 +36,11 @@ class BrowserPoolConfig(BaseModel):
"""
fill_rate_per_minute: Optional[int] = None
- """Percentage of the pool to fill per minute. Defaults to 10%."""
+ """Percentage of the pool to fill per minute.
+
+ Defaults to 10. The cap is 25 for most organizations but can be raised
+ per-organization, so only the lower bound is enforced here.
+ """
headless: Optional[bool] = None
"""If true, launches the browser using a headless image. Defaults to false."""
@@ -81,7 +85,7 @@ class BrowserPoolConfig(BaseModel):
timeout_seconds: Optional[int] = None
"""
Default idle timeout in seconds for browsers acquired from this pool before they
- are destroyed. Defaults to 600 seconds if not specified
+ are destroyed. Defaults to 600 seconds. Minimum 10, maximum 259200 (72 hours).
"""
viewport: Optional[BrowserViewport] = None
diff --git a/src/kernel/types/browser_pool_create_params.py b/src/kernel/types/browser_pool_create_params.py
index 33d04ce4..d404d4e8 100644
--- a/src/kernel/types/browser_pool_create_params.py
+++ b/src/kernel/types/browser_pool_create_params.py
@@ -35,7 +35,11 @@ class BrowserPoolCreateParams(TypedDict, total=False):
"""
fill_rate_per_minute: int
- """Percentage of the pool to fill per minute. Defaults to 10%."""
+ """Percentage of the pool to fill per minute.
+
+ Defaults to 10. The cap is 25 for most organizations but can be raised
+ per-organization, so only the lower bound is enforced here.
+ """
headless: bool
"""If true, launches the browser using a headless image. Defaults to false."""
@@ -80,7 +84,7 @@ class BrowserPoolCreateParams(TypedDict, total=False):
timeout_seconds: int
"""
Default idle timeout in seconds for browsers acquired from this pool before they
- are destroyed. Defaults to 600 seconds if not specified
+ are destroyed. Defaults to 600 seconds. Minimum 10, maximum 259200 (72 hours).
"""
viewport: BrowserViewport
diff --git a/src/kernel/types/browser_pool_update_params.py b/src/kernel/types/browser_pool_update_params.py
index e80b5aa0..79e8172b 100644
--- a/src/kernel/types/browser_pool_update_params.py
+++ b/src/kernel/types/browser_pool_update_params.py
@@ -34,7 +34,11 @@ class BrowserPoolUpdateParams(TypedDict, total=False):
"""
fill_rate_per_minute: int
- """Percentage of the pool to fill per minute. Defaults to 10%."""
+ """Percentage of the pool to fill per minute.
+
+ Defaults to 10. The cap is 25 for most organizations but can be raised
+ per-organization, so only the lower bound is enforced here.
+ """
headless: bool
"""If true, launches the browser using a headless image. Defaults to false."""
@@ -86,7 +90,7 @@ class BrowserPoolUpdateParams(TypedDict, total=False):
timeout_seconds: int
"""
Default idle timeout in seconds for browsers acquired from this pool before they
- are destroyed. Defaults to 600 seconds if not specified
+ are destroyed. Defaults to 600 seconds. Minimum 10, maximum 259200 (72 hours).
"""
viewport: BrowserViewport
diff --git a/src/kernel/types/shared/browser_viewport.py b/src/kernel/types/shared/browser_viewport.py
index c53505be..7314603e 100644
--- a/src/kernel/types/shared/browser_viewport.py
+++ b/src/kernel/types/shared/browser_viewport.py
@@ -23,13 +23,14 @@ class BrowserViewport(BaseModel):
"""
height: int
- """Browser window height in pixels."""
+ """Browser window height in pixels. Any positive integer is accepted."""
width: int
- """Browser window width in pixels."""
+ """Browser window width in pixels. Any positive integer is accepted."""
refresh_rate: Optional[int] = None
"""Display refresh rate in Hz.
- If omitted, automatically determined from width and height.
+ Any positive integer is accepted; if omitted, automatically determined from
+ width and height.
"""
diff --git a/src/kernel/types/shared_params/browser_viewport.py b/src/kernel/types/shared_params/browser_viewport.py
index 2290f930..1250eada 100644
--- a/src/kernel/types/shared_params/browser_viewport.py
+++ b/src/kernel/types/shared_params/browser_viewport.py
@@ -23,13 +23,14 @@ class BrowserViewport(TypedDict, total=False):
"""
height: Required[int]
- """Browser window height in pixels."""
+ """Browser window height in pixels. Any positive integer is accepted."""
width: Required[int]
- """Browser window width in pixels."""
+ """Browser window width in pixels. Any positive integer is accepted."""
refresh_rate: int
"""Display refresh rate in Hz.
- If omitted, automatically determined from width and height.
+ Any positive integer is accepted; if omitted, automatically determined from
+ width and height.
"""
diff --git a/tests/api_resources/test_browser_pools.py b/tests/api_resources/test_browser_pools.py
index c6910837..2c77704d 100644
--- a/tests/api_resources/test_browser_pools.py
+++ b/tests/api_resources/test_browser_pools.py
@@ -53,7 +53,7 @@ def test_method_create_with_all_params(self, client: Kernel) -> None:
proxy_id="proxy_id",
start_url="https://example.com",
stealth=True,
- timeout_seconds=60,
+ timeout_seconds=10,
viewport={
"height": 800,
"width": 1280,
@@ -164,7 +164,7 @@ def test_method_update_with_all_params(self, client: Kernel) -> None:
size=10,
start_url="https://example.com",
stealth=True,
- timeout_seconds=60,
+ timeout_seconds=10,
viewport={
"height": 800,
"width": 1280,
@@ -488,7 +488,7 @@ async def test_method_create_with_all_params(self, async_client: AsyncKernel) ->
proxy_id="proxy_id",
start_url="https://example.com",
stealth=True,
- timeout_seconds=60,
+ timeout_seconds=10,
viewport={
"height": 800,
"width": 1280,
@@ -599,7 +599,7 @@ async def test_method_update_with_all_params(self, async_client: AsyncKernel) ->
size=10,
start_url="https://example.com",
stealth=True,
- timeout_seconds=60,
+ timeout_seconds=10,
viewport={
"height": 800,
"width": 1280,
From 21416e8499e06dd059357d32353eea3314b799f2 Mon Sep 17 00:00:00 2001
From: "stainless-app[bot]"
<142633134+stainless-app[bot]@users.noreply.github.com>
Date: Tue, 23 Jun 2026 14:13:52 +0000
Subject: [PATCH 082/366] feat(api): add GET /extensions/{id_or_name}/metadata
---
.stats.yml | 8 +-
api.md | 3 +-
src/kernel/resources/extensions.py | 81 +++++++++++++++++++++
src/kernel/types/__init__.py | 1 +
src/kernel/types/extension_get_response.py | 30 ++++++++
tests/api_resources/test_extensions.py | 85 ++++++++++++++++++++++
6 files changed, 203 insertions(+), 5 deletions(-)
create mode 100644 src/kernel/types/extension_get_response.py
diff --git a/.stats.yml b/.stats.yml
index 316f1e84..db9991cb 100644
--- a/.stats.yml
+++ b/.stats.yml
@@ -1,4 +1,4 @@
-configured_endpoints: 120
-openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel/kernel-ab4e7c50c41fefd891648fa84ba0258986f192ba1bde9f42cbdf487f64c4cc27.yml
-openapi_spec_hash: b4bcd6557f7045ecff30b01e02d28ac5
-config_hash: 03c7e57f268c750e2415831662e95969
+configured_endpoints: 121
+openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel/kernel-bccc42bb10d1afe703f0374d11f33e775522069d69a13bb2345cc566a49ba4f3.yml
+openapi_spec_hash: 9f00975c0e741ed84011413674313be0
+config_hash: 3a50aee540dce69a53bb8942f5086f5e
diff --git a/api.md b/api.md
index 8ba0dd44..fd5d40cb 100644
--- a/api.md
+++ b/api.md
@@ -342,7 +342,7 @@ Methods:
Types:
```python
-from kernel.types import ExtensionListResponse, ExtensionUploadResponse
+from kernel.types import ExtensionListResponse, ExtensionGetResponse, ExtensionUploadResponse
```
Methods:
@@ -351,6 +351,7 @@ Methods:
- client.extensions.delete(id_or_name) -> None
- client.extensions.download(id_or_name) -> BinaryAPIResponse
- client.extensions.download_from_chrome_store(\*\*params) -> BinaryAPIResponse
+- client.extensions.get(id_or_name) -> ExtensionGetResponse
- client.extensions.upload(\*\*params) -> ExtensionUploadResponse
# BrowserPools
diff --git a/src/kernel/resources/extensions.py b/src/kernel/resources/extensions.py
index f358d341..5c023531 100644
--- a/src/kernel/resources/extensions.py
+++ b/src/kernel/resources/extensions.py
@@ -29,6 +29,7 @@
)
from ..pagination import SyncOffsetPagination, AsyncOffsetPagination
from .._base_client import AsyncPaginator, make_request_options
+from ..types.extension_get_response import ExtensionGetResponse
from ..types.extension_list_response import ExtensionListResponse
from ..types.extension_upload_response import ExtensionUploadResponse
@@ -224,6 +225,40 @@ def download_from_chrome_store(
cast_to=BinaryAPIResponse,
)
+ def get(
+ self,
+ id_or_name: str,
+ *,
+ # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
+ # The extra values given here take precedence over values defined on the client or passed to this method.
+ extra_headers: Headers | None = None,
+ extra_query: Query | None = None,
+ extra_body: Body | None = None,
+ timeout: float | httpx.Timeout | None | NotGiven = not_given,
+ ) -> ExtensionGetResponse:
+ """
+ Get an extension's metadata (name, size, timestamps) by ID or name, without
+ downloading the archive.
+
+ Args:
+ extra_headers: Send extra headers
+
+ extra_query: Add additional query parameters to the request
+
+ extra_body: Add additional JSON properties to the request
+
+ timeout: Override the client-level default timeout for this request, in seconds
+ """
+ if not id_or_name:
+ raise ValueError(f"Expected a non-empty value for `id_or_name` but received {id_or_name!r}")
+ return self._get(
+ path_template("/extensions/{id_or_name}/metadata", id_or_name=id_or_name),
+ options=make_request_options(
+ extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
+ ),
+ cast_to=ExtensionGetResponse,
+ )
+
def upload(
self,
*,
@@ -466,6 +501,40 @@ async def download_from_chrome_store(
cast_to=AsyncBinaryAPIResponse,
)
+ async def get(
+ self,
+ id_or_name: str,
+ *,
+ # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
+ # The extra values given here take precedence over values defined on the client or passed to this method.
+ extra_headers: Headers | None = None,
+ extra_query: Query | None = None,
+ extra_body: Body | None = None,
+ timeout: float | httpx.Timeout | None | NotGiven = not_given,
+ ) -> ExtensionGetResponse:
+ """
+ Get an extension's metadata (name, size, timestamps) by ID or name, without
+ downloading the archive.
+
+ Args:
+ extra_headers: Send extra headers
+
+ extra_query: Add additional query parameters to the request
+
+ extra_body: Add additional JSON properties to the request
+
+ timeout: Override the client-level default timeout for this request, in seconds
+ """
+ if not id_or_name:
+ raise ValueError(f"Expected a non-empty value for `id_or_name` but received {id_or_name!r}")
+ return await self._get(
+ path_template("/extensions/{id_or_name}/metadata", id_or_name=id_or_name),
+ options=make_request_options(
+ extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
+ ),
+ cast_to=ExtensionGetResponse,
+ )
+
async def upload(
self,
*,
@@ -537,6 +606,9 @@ def __init__(self, extensions: ExtensionsResource) -> None:
extensions.download_from_chrome_store,
BinaryAPIResponse,
)
+ self.get = to_raw_response_wrapper(
+ extensions.get,
+ )
self.upload = to_raw_response_wrapper(
extensions.upload,
)
@@ -560,6 +632,9 @@ def __init__(self, extensions: AsyncExtensionsResource) -> None:
extensions.download_from_chrome_store,
AsyncBinaryAPIResponse,
)
+ self.get = async_to_raw_response_wrapper(
+ extensions.get,
+ )
self.upload = async_to_raw_response_wrapper(
extensions.upload,
)
@@ -583,6 +658,9 @@ def __init__(self, extensions: ExtensionsResource) -> None:
extensions.download_from_chrome_store,
StreamedBinaryAPIResponse,
)
+ self.get = to_streamed_response_wrapper(
+ extensions.get,
+ )
self.upload = to_streamed_response_wrapper(
extensions.upload,
)
@@ -606,6 +684,9 @@ def __init__(self, extensions: AsyncExtensionsResource) -> None:
extensions.download_from_chrome_store,
AsyncStreamedBinaryAPIResponse,
)
+ self.get = async_to_streamed_response_wrapper(
+ extensions.get,
+ )
self.upload = async_to_streamed_response_wrapper(
extensions.upload,
)
diff --git a/src/kernel/types/__init__.py b/src/kernel/types/__init__.py
index e38ba407..dbdf93a9 100644
--- a/src/kernel/types/__init__.py
+++ b/src/kernel/types/__init__.py
@@ -53,6 +53,7 @@
from .credential_list_params import CredentialListParams as CredentialListParams
from .deployment_list_params import DeploymentListParams as DeploymentListParams
from .deployment_state_event import DeploymentStateEvent as DeploymentStateEvent
+from .extension_get_response import ExtensionGetResponse as ExtensionGetResponse
from .invocation_list_params import InvocationListParams as InvocationListParams
from .invocation_state_event import InvocationStateEvent as InvocationStateEvent
from .api_key_retrieve_params import APIKeyRetrieveParams as APIKeyRetrieveParams
diff --git a/src/kernel/types/extension_get_response.py b/src/kernel/types/extension_get_response.py
new file mode 100644
index 00000000..0143e623
--- /dev/null
+++ b/src/kernel/types/extension_get_response.py
@@ -0,0 +1,30 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from typing import Optional
+from datetime import datetime
+
+from .._models import BaseModel
+
+__all__ = ["ExtensionGetResponse"]
+
+
+class ExtensionGetResponse(BaseModel):
+ """A browser extension uploaded to Kernel."""
+
+ id: str
+ """Unique identifier for the extension"""
+
+ created_at: datetime
+ """Timestamp when the extension was created"""
+
+ size_bytes: int
+ """Size of the extension archive in bytes"""
+
+ last_used_at: Optional[datetime] = None
+ """Timestamp when the extension was last used"""
+
+ name: Optional[str] = None
+ """Optional, easier-to-reference name for the extension.
+
+ Must be unique within the project.
+ """
diff --git a/tests/api_resources/test_extensions.py b/tests/api_resources/test_extensions.py
index fe8a4cb6..cda54a2d 100644
--- a/tests/api_resources/test_extensions.py
+++ b/tests/api_resources/test_extensions.py
@@ -12,6 +12,7 @@
from kernel import Kernel, AsyncKernel
from tests.utils import assert_matches_type
from kernel.types import (
+ ExtensionGetResponse,
ExtensionListResponse,
ExtensionUploadResponse,
)
@@ -214,6 +215,48 @@ def test_streaming_response_download_from_chrome_store(self, client: Kernel, res
assert cast(Any, extension.is_closed) is True
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ def test_method_get(self, client: Kernel) -> None:
+ extension = client.extensions.get(
+ "id_or_name",
+ )
+ assert_matches_type(ExtensionGetResponse, extension, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ def test_raw_response_get(self, client: Kernel) -> None:
+ response = client.extensions.with_raw_response.get(
+ "id_or_name",
+ )
+
+ assert response.is_closed is True
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+ extension = response.parse()
+ assert_matches_type(ExtensionGetResponse, extension, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ def test_streaming_response_get(self, client: Kernel) -> None:
+ with client.extensions.with_streaming_response.get(
+ "id_or_name",
+ ) as response:
+ assert not response.is_closed
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+
+ extension = response.parse()
+ assert_matches_type(ExtensionGetResponse, extension, path=["response"])
+
+ assert cast(Any, response.is_closed) is True
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ def test_path_params_get(self, client: Kernel) -> None:
+ with pytest.raises(ValueError, match=r"Expected a non-empty value for `id_or_name` but received ''"):
+ client.extensions.with_raw_response.get(
+ "",
+ )
+
@pytest.mark.skip(reason="Mock server tests are disabled")
@parametrize
def test_method_upload(self, client: Kernel) -> None:
@@ -454,6 +497,48 @@ async def test_streaming_response_download_from_chrome_store(
assert cast(Any, extension.is_closed) is True
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ async def test_method_get(self, async_client: AsyncKernel) -> None:
+ extension = await async_client.extensions.get(
+ "id_or_name",
+ )
+ assert_matches_type(ExtensionGetResponse, extension, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ async def test_raw_response_get(self, async_client: AsyncKernel) -> None:
+ response = await async_client.extensions.with_raw_response.get(
+ "id_or_name",
+ )
+
+ assert response.is_closed is True
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+ extension = await response.parse()
+ assert_matches_type(ExtensionGetResponse, extension, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ async def test_streaming_response_get(self, async_client: AsyncKernel) -> None:
+ async with async_client.extensions.with_streaming_response.get(
+ "id_or_name",
+ ) as response:
+ assert not response.is_closed
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+
+ extension = await response.parse()
+ assert_matches_type(ExtensionGetResponse, extension, path=["response"])
+
+ assert cast(Any, response.is_closed) is True
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ async def test_path_params_get(self, async_client: AsyncKernel) -> None:
+ with pytest.raises(ValueError, match=r"Expected a non-empty value for `id_or_name` but received ''"):
+ await async_client.extensions.with_raw_response.get(
+ "",
+ )
+
@pytest.mark.skip(reason="Mock server tests are disabled")
@parametrize
async def test_method_upload(self, async_client: AsyncKernel) -> None:
From 26a1e47e8d244da3f796d79ae182113e4746305e Mon Sep 17 00:00:00 2001
From: "stainless-app[bot]"
<142633134+stainless-app[bot]@users.noreply.github.com>
Date: Tue, 23 Jun 2026 14:43:49 +0000
Subject: [PATCH 083/366] feat(api): resolve GET /org/projects/{id} by ID or
name
---
.stats.yml | 4 ++--
src/kernel/resources/projects/projects.py | 10 ++++++----
2 files changed, 8 insertions(+), 6 deletions(-)
diff --git a/.stats.yml b/.stats.yml
index db9991cb..734935c5 100644
--- a/.stats.yml
+++ b/.stats.yml
@@ -1,4 +1,4 @@
configured_endpoints: 121
-openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel/kernel-bccc42bb10d1afe703f0374d11f33e775522069d69a13bb2345cc566a49ba4f3.yml
-openapi_spec_hash: 9f00975c0e741ed84011413674313be0
+openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel/kernel-7d6d64b3998b55057f21aa5e33096cd5468f2bb2c4fda65190e9f299b50e5908.yml
+openapi_spec_hash: aa7d3971d38b16eff41b08cc3cbb549d
config_hash: 3a50aee540dce69a53bb8942f5086f5e
diff --git a/src/kernel/resources/projects/projects.py b/src/kernel/resources/projects/projects.py
index ea977689..786a6c1f 100644
--- a/src/kernel/resources/projects/projects.py
+++ b/src/kernel/resources/projects/projects.py
@@ -104,8 +104,9 @@ def retrieve(
extra_body: Body | None = None,
timeout: float | httpx.Timeout | None | NotGiven = not_given,
) -> Project:
- """
- Get a project by ID.
+ """Get a project by its ID or by its name.
+
+ Names are unique within an organization.
Args:
extra_headers: Send extra headers
@@ -331,8 +332,9 @@ async def retrieve(
extra_body: Body | None = None,
timeout: float | httpx.Timeout | None | NotGiven = not_given,
) -> Project:
- """
- Get a project by ID.
+ """Get a project by its ID or by its name.
+
+ Names are unique within an organization.
Args:
extra_headers: Send extra headers
From 3e4963596296bf3dc84ce4e93f45676459e044c4 Mon Sep 17 00:00:00 2001
From: "stainless-app[bot]"
<142633134+stainless-app[bot]@users.noreply.github.com>
Date: Tue, 23 Jun 2026 16:18:00 +0000
Subject: [PATCH 084/366] feat: Forward replay param through telemetry stream
passthrough
---
.stats.yml | 4 +--
api.md | 2 +-
src/kernel/resources/browsers/telemetry.py | 31 ++++++++++++++++---
src/kernel/types/browsers/__init__.py | 1 +
.../types/browsers/telemetry_stream_params.py | 20 ++++++++++++
.../api_resources/browsers/test_telemetry.py | 2 ++
6 files changed, 52 insertions(+), 8 deletions(-)
create mode 100644 src/kernel/types/browsers/telemetry_stream_params.py
diff --git a/.stats.yml b/.stats.yml
index 734935c5..c487e555 100644
--- a/.stats.yml
+++ b/.stats.yml
@@ -1,4 +1,4 @@
configured_endpoints: 121
-openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel/kernel-7d6d64b3998b55057f21aa5e33096cd5468f2bb2c4fda65190e9f299b50e5908.yml
-openapi_spec_hash: aa7d3971d38b16eff41b08cc3cbb549d
+openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel/kernel-01f7f21a2fc892da284edf6b7cfd340d2ce5d0a9eb7ad2b39a8daec27b093134.yml
+openapi_spec_hash: 21972930082b23d985faf285fd656c31
config_hash: 3a50aee540dce69a53bb8942f5086f5e
diff --git a/api.md b/api.md
index fd5d40cb..58f1e221 100644
--- a/api.md
+++ b/api.md
@@ -152,7 +152,7 @@ from kernel.types.browsers import (
Methods:
-- client.browsers.telemetry.stream(id) -> TelemetryStreamResponse
+- client.browsers.telemetry.stream(id, \*\*params) -> TelemetryStreamResponse
## Replays
diff --git a/src/kernel/resources/browsers/telemetry.py b/src/kernel/resources/browsers/telemetry.py
index 008b1d29..3da7f865 100644
--- a/src/kernel/resources/browsers/telemetry.py
+++ b/src/kernel/resources/browsers/telemetry.py
@@ -5,7 +5,7 @@
import httpx
from ..._types import Body, Omit, Query, Headers, NotGiven, omit, not_given
-from ..._utils import path_template, strip_not_given
+from ..._utils import path_template, maybe_transform, strip_not_given, async_maybe_transform
from ..._compat import cached_property
from ..._resource import SyncAPIResource, AsyncAPIResource
from ..._response import (
@@ -16,6 +16,7 @@
)
from ..._streaming import Stream, AsyncStream
from ..._base_client import make_request_options
+from ...types.browsers import telemetry_stream_params
from ...types.browsers.telemetry_stream_response import TelemetryStreamResponse
__all__ = ["TelemetryResource", "AsyncTelemetryResource"]
@@ -47,6 +48,7 @@ def stream(
self,
id: str,
*,
+ replay: str | Omit = omit,
last_event_id: str | Omit = omit,
# Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
# The extra values given here take precedence over values defined on the client or passed to this method.
@@ -64,9 +66,14 @@ def stream(
set; all frames carry JSON in the data: field. A keepalive comment frame is sent
every 15 seconds when no events arrive. Returns 404 if the browser session does
not exist. If telemetry was not enabled on the session, the stream opens but no
- events are delivered.
+ events are delivered. Fresh connections only see new events; pass replay=all to
+ start from the oldest retained event instead.
Args:
+ replay: Pass `all` to start from the oldest retained event instead of only new events;
+ any other value is treated as from-now. The buffer is bounded, so the first
+ event id may be greater than 1 if older events were evicted.
+
extra_headers: Send extra headers
extra_query: Add additional query parameters to the request
@@ -82,7 +89,11 @@ def stream(
return self._get(
path_template("/browsers/{id}/telemetry/stream", id=id),
options=make_request_options(
- extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
+ extra_headers=extra_headers,
+ extra_query=extra_query,
+ extra_body=extra_body,
+ timeout=timeout,
+ query=maybe_transform({"replay": replay}, telemetry_stream_params.TelemetryStreamParams),
),
cast_to=TelemetryStreamResponse,
stream=True,
@@ -116,6 +127,7 @@ async def stream(
self,
id: str,
*,
+ replay: str | Omit = omit,
last_event_id: str | Omit = omit,
# Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
# The extra values given here take precedence over values defined on the client or passed to this method.
@@ -133,9 +145,14 @@ async def stream(
set; all frames carry JSON in the data: field. A keepalive comment frame is sent
every 15 seconds when no events arrive. Returns 404 if the browser session does
not exist. If telemetry was not enabled on the session, the stream opens but no
- events are delivered.
+ events are delivered. Fresh connections only see new events; pass replay=all to
+ start from the oldest retained event instead.
Args:
+ replay: Pass `all` to start from the oldest retained event instead of only new events;
+ any other value is treated as from-now. The buffer is bounded, so the first
+ event id may be greater than 1 if older events were evicted.
+
extra_headers: Send extra headers
extra_query: Add additional query parameters to the request
@@ -151,7 +168,11 @@ async def stream(
return await self._get(
path_template("/browsers/{id}/telemetry/stream", id=id),
options=make_request_options(
- extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
+ extra_headers=extra_headers,
+ extra_query=extra_query,
+ extra_body=extra_body,
+ timeout=timeout,
+ query=await async_maybe_transform({"replay": replay}, telemetry_stream_params.TelemetryStreamParams),
),
cast_to=TelemetryStreamResponse,
stream=True,
diff --git a/src/kernel/types/browsers/__init__.py b/src/kernel/types/browsers/__init__.py
index 5f14ecd1..b81f7eae 100644
--- a/src/kernel/types/browsers/__init__.py
+++ b/src/kernel/types/browsers/__init__.py
@@ -37,6 +37,7 @@
from .browser_telemetry_event import BrowserTelemetryEvent as BrowserTelemetryEvent
from .process_resize_response import ProcessResizeResponse as ProcessResizeResponse
from .process_status_response import ProcessStatusResponse as ProcessStatusResponse
+from .telemetry_stream_params import TelemetryStreamParams as TelemetryStreamParams
from .browser_telemetry_config import BrowserTelemetryConfig as BrowserTelemetryConfig
from .browser_cdp_connect_event import BrowserCdpConnectEvent as BrowserCdpConnectEvent
from .browser_console_log_event import BrowserConsoleLogEvent as BrowserConsoleLogEvent
diff --git a/src/kernel/types/browsers/telemetry_stream_params.py b/src/kernel/types/browsers/telemetry_stream_params.py
new file mode 100644
index 00000000..d9703bc7
--- /dev/null
+++ b/src/kernel/types/browsers/telemetry_stream_params.py
@@ -0,0 +1,20 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from __future__ import annotations
+
+from typing_extensions import Annotated, TypedDict
+
+from ..._utils import PropertyInfo
+
+__all__ = ["TelemetryStreamParams"]
+
+
+class TelemetryStreamParams(TypedDict, total=False):
+ replay: str
+ """
+ Pass `all` to start from the oldest retained event instead of only new events;
+ any other value is treated as from-now. The buffer is bounded, so the first
+ event id may be greater than 1 if older events were evicted.
+ """
+
+ last_event_id: Annotated[str, PropertyInfo(alias="Last-Event-ID")]
diff --git a/tests/api_resources/browsers/test_telemetry.py b/tests/api_resources/browsers/test_telemetry.py
index 0fcd2715..480e557f 100644
--- a/tests/api_resources/browsers/test_telemetry.py
+++ b/tests/api_resources/browsers/test_telemetry.py
@@ -28,6 +28,7 @@ def test_method_stream(self, client: Kernel) -> None:
def test_method_stream_with_all_params(self, client: Kernel) -> None:
telemetry_stream = client.browsers.telemetry.stream(
id="id",
+ replay="replay",
last_event_id="Last-Event-ID",
)
telemetry_stream.response.close()
@@ -84,6 +85,7 @@ async def test_method_stream(self, async_client: AsyncKernel) -> None:
async def test_method_stream_with_all_params(self, async_client: AsyncKernel) -> None:
telemetry_stream = await async_client.browsers.telemetry.stream(
id="id",
+ replay="replay",
last_event_id="Last-Event-ID",
)
await telemetry_stream.response.aclose()
From ca8e108fb21babf79b58261cdba6c5c4f53bba67 Mon Sep 17 00:00:00 2001
From: "stainless-app[bot]"
<142633134+stainless-app[bot]@users.noreply.github.com>
Date: Tue, 23 Jun 2026 16:36:04 +0000
Subject: [PATCH 085/366] feat: Add GET /browsers/{id}/telemetry/events (read
from S2)
---
.stats.yml | 8 +-
api.md | 2 +
src/kernel/resources/browsers/telemetry.py | 191 +++++++++++++++++-
src/kernel/types/__init__.py | 2 +
src/kernel/types/browsers/__init__.py | 2 +
.../types/browsers/telemetry_events_params.py | 52 +++++
.../browsers/telemetry_events_response.py | 36 ++++
.../api_resources/browsers/test_telemetry.py | 113 +++++++++++
8 files changed, 400 insertions(+), 6 deletions(-)
create mode 100644 src/kernel/types/browsers/telemetry_events_params.py
create mode 100644 src/kernel/types/browsers/telemetry_events_response.py
diff --git a/.stats.yml b/.stats.yml
index c487e555..310bb248 100644
--- a/.stats.yml
+++ b/.stats.yml
@@ -1,4 +1,4 @@
-configured_endpoints: 121
-openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel/kernel-01f7f21a2fc892da284edf6b7cfd340d2ce5d0a9eb7ad2b39a8daec27b093134.yml
-openapi_spec_hash: 21972930082b23d985faf285fd656c31
-config_hash: 3a50aee540dce69a53bb8942f5086f5e
+configured_endpoints: 122
+openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel/kernel-7cde13c82c71b7a5fd64b592f853a7c999c0e0c02dce161e6126d0e99df3412b.yml
+openapi_spec_hash: 52cda226c70a248cbea3af6c52822b44
+config_hash: 99b2b2a25e8067ad9c9214e38e01d64c
diff --git a/api.md b/api.md
index 58f1e221..88588c40 100644
--- a/api.md
+++ b/api.md
@@ -146,12 +146,14 @@ from kernel.types.browsers import (
BrowserTelemetryCategoryConfig,
BrowserTelemetryConfig,
BrowserTelemetryEvent,
+ TelemetryEventsResponse,
TelemetryStreamResponse,
)
```
Methods:
+- client.browsers.telemetry.events(id, \*\*params) -> SyncOffsetPagination[TelemetryEventsResponse]
- client.browsers.telemetry.stream(id, \*\*params) -> TelemetryStreamResponse
## Replays
diff --git a/src/kernel/resources/browsers/telemetry.py b/src/kernel/resources/browsers/telemetry.py
index 3da7f865..aa3e9ed2 100644
--- a/src/kernel/resources/browsers/telemetry.py
+++ b/src/kernel/resources/browsers/telemetry.py
@@ -2,6 +2,9 @@
from __future__ import annotations
+from typing import List
+from typing_extensions import Literal
+
import httpx
from ..._types import Body, Omit, Query, Headers, NotGiven, omit, not_given
@@ -15,8 +18,10 @@
async_to_streamed_response_wrapper,
)
from ..._streaming import Stream, AsyncStream
-from ..._base_client import make_request_options
-from ...types.browsers import telemetry_stream_params
+from ...pagination import SyncOffsetPagination, AsyncOffsetPagination
+from ..._base_client import AsyncPaginator, make_request_options
+from ...types.browsers import telemetry_events_params, telemetry_stream_params
+from ...types.browsers.telemetry_events_response import TelemetryEventsResponse
from ...types.browsers.telemetry_stream_response import TelemetryStreamResponse
__all__ = ["TelemetryResource", "AsyncTelemetryResource"]
@@ -44,6 +49,91 @@ def with_streaming_response(self) -> TelemetryResourceWithStreamingResponse:
"""
return TelemetryResourceWithStreamingResponse(self)
+ def events(
+ self,
+ id: str,
+ *,
+ category: List[
+ Literal[
+ "console",
+ "network",
+ "page",
+ "interaction",
+ "control",
+ "connection",
+ "system",
+ "screenshot",
+ "captcha",
+ "monitor",
+ ]
+ ]
+ | Omit = omit,
+ limit: int | Omit = omit,
+ offset: int | Omit = omit,
+ since: str | Omit = omit,
+ until: str | Omit = omit,
+ # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
+ # The extra values given here take precedence over values defined on the client or passed to this method.
+ extra_headers: Headers | None = None,
+ extra_query: Query | None = None,
+ extra_body: Body | None = None,
+ timeout: float | httpx.Timeout | None | NotGiven = not_given,
+ ) -> SyncOffsetPagination[TelemetryEventsResponse]:
+ """
+ Reads a page of telemetry events for the browser session in ascending sequence
+ order. To page through results, pass the X-Next-Offset value from the previous
+ response as offset and repeat while X-Has-More is true. Returns an empty list
+ when telemetry data is unavailable.
+
+ Args:
+ category: Restrict results to these event categories. Repeat the parameter for multiple
+ values.
+
+ limit: Maximum number of events per page. Defaults to 20.
+
+ offset: Opaque pagination cursor: pass the X-Next-Offset value from the previous
+ response to fetch the next page. When set, paging continues from this cursor and
+ since is ignored, while until still bounds the page. It is not an event's seq
+ field, so do not derive it from the response body.
+
+ since: Start of the window: an RFC-3339 timestamp, or a duration like 5m meaning that
+ long ago. Defaults to 5m. Ignored when offset is set.
+
+ until: End of the window (exclusive): an RFC-3339 timestamp, or a duration like 5m
+ meaning that long ago.
+
+ extra_headers: Send extra headers
+
+ extra_query: Add additional query parameters to the request
+
+ extra_body: Add additional JSON properties to the request
+
+ timeout: Override the client-level default timeout for this request, in seconds
+ """
+ if not id:
+ raise ValueError(f"Expected a non-empty value for `id` but received {id!r}")
+ return self._get_api_list(
+ path_template("/browsers/{id}/telemetry/events", id=id),
+ page=SyncOffsetPagination[TelemetryEventsResponse],
+ options=make_request_options(
+ extra_headers=extra_headers,
+ extra_query=extra_query,
+ extra_body=extra_body,
+ timeout=timeout,
+ query=maybe_transform(
+ {
+ "category": category,
+ "limit": limit,
+ "offset": offset,
+ "since": since,
+ "until": until,
+ },
+ telemetry_events_params.TelemetryEventsParams,
+ ),
+ ),
+ model=TelemetryEventsResponse,
+ )
+
def stream(
self,
id: str,
@@ -123,6 +213,91 @@ def with_streaming_response(self) -> AsyncTelemetryResourceWithStreamingResponse
"""
return AsyncTelemetryResourceWithStreamingResponse(self)
+ def events(
+ self,
+ id: str,
+ *,
+ category: List[
+ Literal[
+ "console",
+ "network",
+ "page",
+ "interaction",
+ "control",
+ "connection",
+ "system",
+ "screenshot",
+ "captcha",
+ "monitor",
+ ]
+ ]
+ | Omit = omit,
+ limit: int | Omit = omit,
+ offset: int | Omit = omit,
+ since: str | Omit = omit,
+ until: str | Omit = omit,
+ # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
+ # The extra values given here take precedence over values defined on the client or passed to this method.
+ extra_headers: Headers | None = None,
+ extra_query: Query | None = None,
+ extra_body: Body | None = None,
+ timeout: float | httpx.Timeout | None | NotGiven = not_given,
+ ) -> AsyncPaginator[TelemetryEventsResponse, AsyncOffsetPagination[TelemetryEventsResponse]]:
+ """
+ Reads a page of telemetry events for the browser session in ascending sequence
+ order. To page through results, pass the X-Next-Offset value from the previous
+ response as offset and repeat while X-Has-More is true. Returns an empty list
+ when telemetry data is unavailable.
+
+ Args:
+ category: Restrict results to these event categories. Repeat the parameter for multiple
+ values.
+
+ limit: Maximum number of events per page. Defaults to 20.
+
+ offset: Opaque pagination cursor: pass the X-Next-Offset value from the previous
+ response to fetch the next page. When set, paging continues from this cursor and
+ since is ignored, while until still bounds the page. It is not an event's seq
+ field, so do not derive it from the response body.
+
+ since: Start of the window: an RFC-3339 timestamp, or a duration like 5m meaning that
+ long ago. Defaults to 5m. Ignored when offset is set.
+
+ until: End of the window (exclusive): an RFC-3339 timestamp, or a duration like 5m
+ meaning that long ago.
+
+ extra_headers: Send extra headers
+
+ extra_query: Add additional query parameters to the request
+
+ extra_body: Add additional JSON properties to the request
+
+ timeout: Override the client-level default timeout for this request, in seconds
+ """
+ if not id:
+ raise ValueError(f"Expected a non-empty value for `id` but received {id!r}")
+ return self._get_api_list(
+ path_template("/browsers/{id}/telemetry/events", id=id),
+ page=AsyncOffsetPagination[TelemetryEventsResponse],
+ options=make_request_options(
+ extra_headers=extra_headers,
+ extra_query=extra_query,
+ extra_body=extra_body,
+ timeout=timeout,
+ query=maybe_transform(
+ {
+ "category": category,
+ "limit": limit,
+ "offset": offset,
+ "since": since,
+ "until": until,
+ },
+ telemetry_events_params.TelemetryEventsParams,
+ ),
+ ),
+ model=TelemetryEventsResponse,
+ )
+
async def stream(
self,
id: str,
@@ -184,6 +359,9 @@ class TelemetryResourceWithRawResponse:
def __init__(self, telemetry: TelemetryResource) -> None:
self._telemetry = telemetry
+ self.events = to_raw_response_wrapper(
+ telemetry.events,
+ )
self.stream = to_raw_response_wrapper(
telemetry.stream,
)
@@ -193,6 +371,9 @@ class AsyncTelemetryResourceWithRawResponse:
def __init__(self, telemetry: AsyncTelemetryResource) -> None:
self._telemetry = telemetry
+ self.events = async_to_raw_response_wrapper(
+ telemetry.events,
+ )
self.stream = async_to_raw_response_wrapper(
telemetry.stream,
)
@@ -202,6 +383,9 @@ class TelemetryResourceWithStreamingResponse:
def __init__(self, telemetry: TelemetryResource) -> None:
self._telemetry = telemetry
+ self.events = to_streamed_response_wrapper(
+ telemetry.events,
+ )
self.stream = to_streamed_response_wrapper(
telemetry.stream,
)
@@ -211,6 +395,9 @@ class AsyncTelemetryResourceWithStreamingResponse:
def __init__(self, telemetry: AsyncTelemetryResource) -> None:
self._telemetry = telemetry
+ self.events = async_to_streamed_response_wrapper(
+ telemetry.events,
+ )
self.stream = async_to_streamed_response_wrapper(
telemetry.stream,
)
diff --git a/src/kernel/types/__init__.py b/src/kernel/types/__init__.py
index dbdf93a9..660875d7 100644
--- a/src/kernel/types/__init__.py
+++ b/src/kernel/types/__init__.py
@@ -111,9 +111,11 @@
browsers.browser_call_stack.BrowserCallStack.update_forward_refs() # type: ignore
browsers.browser_console_error_event.BrowserConsoleErrorEvent.update_forward_refs() # type: ignore
browsers.browser_console_log_event.BrowserConsoleLogEvent.update_forward_refs() # type: ignore
+ browsers.telemetry_events_response.TelemetryEventsResponse.update_forward_refs() # type: ignore
browsers.telemetry_stream_response.TelemetryStreamResponse.update_forward_refs() # type: ignore
else:
browsers.browser_call_stack.BrowserCallStack.model_rebuild(_parent_namespace_depth=0)
browsers.browser_console_error_event.BrowserConsoleErrorEvent.model_rebuild(_parent_namespace_depth=0)
browsers.browser_console_log_event.BrowserConsoleLogEvent.model_rebuild(_parent_namespace_depth=0)
+ browsers.telemetry_events_response.TelemetryEventsResponse.model_rebuild(_parent_namespace_depth=0)
browsers.telemetry_stream_response.TelemetryStreamResponse.model_rebuild(_parent_namespace_depth=0)
diff --git a/src/kernel/types/browsers/__init__.py b/src/kernel/types/browsers/__init__.py
index b81f7eae..990e8e19 100644
--- a/src/kernel/types/browsers/__init__.py
+++ b/src/kernel/types/browsers/__init__.py
@@ -37,6 +37,7 @@
from .browser_telemetry_event import BrowserTelemetryEvent as BrowserTelemetryEvent
from .process_resize_response import ProcessResizeResponse as ProcessResizeResponse
from .process_status_response import ProcessStatusResponse as ProcessStatusResponse
+from .telemetry_events_params import TelemetryEventsParams as TelemetryEventsParams
from .telemetry_stream_params import TelemetryStreamParams as TelemetryStreamParams
from .browser_telemetry_config import BrowserTelemetryConfig as BrowserTelemetryConfig
from .browser_cdp_connect_event import BrowserCdpConnectEvent as BrowserCdpConnectEvent
@@ -47,6 +48,7 @@
from .f_delete_directory_params import FDeleteDirectoryParams as FDeleteDirectoryParams
from .f_download_dir_zip_params import FDownloadDirZipParams as FDownloadDirZipParams
from .playwright_execute_params import PlaywrightExecuteParams as PlaywrightExecuteParams
+from .telemetry_events_response import TelemetryEventsResponse as TelemetryEventsResponse
from .telemetry_stream_response import TelemetryStreamResponse as TelemetryStreamResponse
from .browser_network_idle_event import BrowserNetworkIdleEvent as BrowserNetworkIdleEvent
from .computer_drag_mouse_params import ComputerDragMouseParams as ComputerDragMouseParams
diff --git a/src/kernel/types/browsers/telemetry_events_params.py b/src/kernel/types/browsers/telemetry_events_params.py
new file mode 100644
index 00000000..223bb2fc
--- /dev/null
+++ b/src/kernel/types/browsers/telemetry_events_params.py
@@ -0,0 +1,52 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from __future__ import annotations
+
+from typing import List
+from typing_extensions import Literal, TypedDict
+
+__all__ = ["TelemetryEventsParams"]
+
+
+class TelemetryEventsParams(TypedDict, total=False):
+ category: List[
+ Literal[
+ "console",
+ "network",
+ "page",
+ "interaction",
+ "control",
+ "connection",
+ "system",
+ "screenshot",
+ "captcha",
+ "monitor",
+ ]
+ ]
+ """Restrict results to these event categories.
+
+ Repeat the parameter for multiple values.
+ """
+
+ limit: int
+ """Maximum number of events per page. Defaults to 20."""
+
+ offset: int
+ """
+ Opaque pagination cursor: pass the X-Next-Offset value from the previous
+ response to fetch the next page. When set, paging continues from this cursor and
+ since is ignored, while until still bounds the page. It is not an event's seq
+ field, so do not derive it from the response body.
+ """
+
+ since: str
+ """
+ Start of the window: an RFC-3339 timestamp, or a duration like 5m meaning that
+ long ago. Defaults to 5m. Ignored when offset is set.
+ """
+
+ until: str
+ """
+ End of the window (exclusive): an RFC-3339 timestamp, or a duration like 5m
+ meaning that long ago.
+ """
diff --git a/src/kernel/types/browsers/telemetry_events_response.py b/src/kernel/types/browsers/telemetry_events_response.py
new file mode 100644
index 00000000..3002f470
--- /dev/null
+++ b/src/kernel/types/browsers/telemetry_events_response.py
@@ -0,0 +1,36 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from __future__ import annotations
+
+from ..._models import BaseModel
+
+__all__ = ["TelemetryEventsResponse"]
+
+
+class TelemetryEventsResponse(BaseModel):
+ """Envelope wrapping a browser telemetry event with its monotonic sequence number.
+
+ Each SSE data: frame carries one envelope as JSON. The seq value is also emitted as the SSE id: field so clients can pass it as Last-Event-ID on reconnect.
+ """
+
+ event: "BrowserTelemetryEvent"
+ """Union type representing any browser telemetry event.
+
+ Discriminated on `type`. Each event's `category` determines when it is captured.
+ The CDP collector-health events (monitor_disconnected, monitor_reconnected,
+ monitor_reconnect_failed, monitor_init_failed) use the `monitor` category, which
+ is not user-configurable: it flows automatically whenever any CDP category
+ (console, network, page, interaction) is captured, and is silent otherwise.
+ monitor_screenshot uses the opt-in `screenshot` category. All other event types
+ are controlled by their per-category enable/disable flags.
+ """
+
+ seq: int
+ """Process-monotonic sequence number assigned by the browser VM.
+
+ Pass as Last-Event-ID on reconnect to resume without gaps. Gaps in received seq
+ values indicate dropped events.
+ """
+
+
+from .browser_telemetry_event import BrowserTelemetryEvent
diff --git a/tests/api_resources/browsers/test_telemetry.py b/tests/api_resources/browsers/test_telemetry.py
index 480e557f..7ad9ca02 100644
--- a/tests/api_resources/browsers/test_telemetry.py
+++ b/tests/api_resources/browsers/test_telemetry.py
@@ -8,6 +8,9 @@
import pytest
from kernel import Kernel, AsyncKernel
+from tests.utils import assert_matches_type
+from kernel.pagination import SyncOffsetPagination, AsyncOffsetPagination
+from kernel.types.browsers import TelemetryEventsResponse
base_url = os.environ.get("TEST_API_BASE_URL", "http://127.0.0.1:4010")
@@ -15,6 +18,61 @@
class TestTelemetry:
parametrize = pytest.mark.parametrize("client", [False, True], indirect=True, ids=["loose", "strict"])
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ def test_method_events(self, client: Kernel) -> None:
+ telemetry = client.browsers.telemetry.events(
+ id="id",
+ )
+ assert_matches_type(SyncOffsetPagination[TelemetryEventsResponse], telemetry, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ def test_method_events_with_all_params(self, client: Kernel) -> None:
+ telemetry = client.browsers.telemetry.events(
+ id="id",
+ category=["console"],
+ limit=1,
+ offset=0,
+ since="since",
+ until="until",
+ )
+ assert_matches_type(SyncOffsetPagination[TelemetryEventsResponse], telemetry, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ def test_raw_response_events(self, client: Kernel) -> None:
+ response = client.browsers.telemetry.with_raw_response.events(
+ id="id",
+ )
+
+ assert response.is_closed is True
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+ telemetry = response.parse()
+ assert_matches_type(SyncOffsetPagination[TelemetryEventsResponse], telemetry, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ def test_streaming_response_events(self, client: Kernel) -> None:
+ with client.browsers.telemetry.with_streaming_response.events(
+ id="id",
+ ) as response:
+ assert not response.is_closed
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+
+ telemetry = response.parse()
+ assert_matches_type(SyncOffsetPagination[TelemetryEventsResponse], telemetry, path=["response"])
+
+ assert cast(Any, response.is_closed) is True
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ def test_path_params_events(self, client: Kernel) -> None:
+ with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"):
+ client.browsers.telemetry.with_raw_response.events(
+ id="",
+ )
+
@pytest.mark.skip(reason="Mock server tests are disabled")
@parametrize
def test_method_stream(self, client: Kernel) -> None:
@@ -72,6 +130,61 @@ class TestAsyncTelemetry:
"async_client", [False, True, {"http_client": "aiohttp"}], indirect=True, ids=["loose", "strict", "aiohttp"]
)
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ async def test_method_events(self, async_client: AsyncKernel) -> None:
+ telemetry = await async_client.browsers.telemetry.events(
+ id="id",
+ )
+ assert_matches_type(AsyncOffsetPagination[TelemetryEventsResponse], telemetry, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ async def test_method_events_with_all_params(self, async_client: AsyncKernel) -> None:
+ telemetry = await async_client.browsers.telemetry.events(
+ id="id",
+ category=["console"],
+ limit=1,
+ offset=0,
+ since="since",
+ until="until",
+ )
+ assert_matches_type(AsyncOffsetPagination[TelemetryEventsResponse], telemetry, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ async def test_raw_response_events(self, async_client: AsyncKernel) -> None:
+ response = await async_client.browsers.telemetry.with_raw_response.events(
+ id="id",
+ )
+
+ assert response.is_closed is True
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+ telemetry = await response.parse()
+ assert_matches_type(AsyncOffsetPagination[TelemetryEventsResponse], telemetry, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ async def test_streaming_response_events(self, async_client: AsyncKernel) -> None:
+ async with async_client.browsers.telemetry.with_streaming_response.events(
+ id="id",
+ ) as response:
+ assert not response.is_closed
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+
+ telemetry = await response.parse()
+ assert_matches_type(AsyncOffsetPagination[TelemetryEventsResponse], telemetry, path=["response"])
+
+ assert cast(Any, response.is_closed) is True
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ async def test_path_params_events(self, async_client: AsyncKernel) -> None:
+ with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"):
+ await async_client.browsers.telemetry.with_raw_response.events(
+ id="",
+ )
+
@pytest.mark.skip(reason="Mock server tests are disabled")
@parametrize
async def test_method_stream(self, async_client: AsyncKernel) -> None:
From d619ce4bd819fc5899a6dc6cb0d58c436f1b4396 Mon Sep 17 00:00:00 2001
From: "stainless-app[bot]"
<142633134+stainless-app[bot]@users.noreply.github.com>
Date: Tue, 23 Jun 2026 20:45:24 +0000
Subject: [PATCH 086/366] feat: api: support per-acquire start_url override on
browser pool acquire
---
.stats.yml | 4 ++--
src/kernel/resources/browser_pools.py | 12 ++++++++++++
src/kernel/types/browser_pool_acquire_params.py | 7 +++++++
tests/api_resources/test_browser_pools.py | 2 ++
4 files changed, 23 insertions(+), 2 deletions(-)
diff --git a/.stats.yml b/.stats.yml
index 310bb248..2018924b 100644
--- a/.stats.yml
+++ b/.stats.yml
@@ -1,4 +1,4 @@
configured_endpoints: 122
-openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel/kernel-7cde13c82c71b7a5fd64b592f853a7c999c0e0c02dce161e6126d0e99df3412b.yml
-openapi_spec_hash: 52cda226c70a248cbea3af6c52822b44
+openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel/kernel-d9b82fc5346c9be1bf9c2b18792fdcdec6a80a86153ca765c9e93e597b46fa24.yml
+openapi_spec_hash: 9cbaab975acfa421b795d11aa635c57e
config_hash: 99b2b2a25e8067ad9c9214e38e01d64c
diff --git a/src/kernel/resources/browser_pools.py b/src/kernel/resources/browser_pools.py
index 831b93e0..019489fa 100644
--- a/src/kernel/resources/browser_pools.py
+++ b/src/kernel/resources/browser_pools.py
@@ -427,6 +427,7 @@ def acquire(
*,
acquire_timeout_seconds: int | Omit = omit,
name: str | Omit = omit,
+ start_url: str | Omit = omit,
tags: TagsParam | Omit = omit,
# Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
# The extra values given here take precedence over values defined on the client or passed to this method.
@@ -452,6 +453,10 @@ def acquire(
project. Applies to this lease only and is cleared when the browser is released
back to the pool.
+ start_url: Optional URL to navigate the acquired browser to. Overrides the pool's start_url
+ for this acquire only. Best-effort: failures to navigate do not fail the
+ acquire.
+
tags: Optional user-defined key-value tags for the acquired browser session, used to
find and group sessions later. Applies to this lease only and are cleared when
the browser is released back to the pool. Up to 50 pairs.
@@ -472,6 +477,7 @@ def acquire(
{
"acquire_timeout_seconds": acquire_timeout_seconds,
"name": name,
+ "start_url": start_url,
"tags": tags,
},
browser_pool_acquire_params.BrowserPoolAcquireParams,
@@ -956,6 +962,7 @@ async def acquire(
*,
acquire_timeout_seconds: int | Omit = omit,
name: str | Omit = omit,
+ start_url: str | Omit = omit,
tags: TagsParam | Omit = omit,
# Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
# The extra values given here take precedence over values defined on the client or passed to this method.
@@ -981,6 +988,10 @@ async def acquire(
project. Applies to this lease only and is cleared when the browser is released
back to the pool.
+ start_url: Optional URL to navigate the acquired browser to. Overrides the pool's start_url
+ for this acquire only. Best-effort: failures to navigate do not fail the
+ acquire.
+
tags: Optional user-defined key-value tags for the acquired browser session, used to
find and group sessions later. Applies to this lease only and are cleared when
the browser is released back to the pool. Up to 50 pairs.
@@ -1001,6 +1012,7 @@ async def acquire(
{
"acquire_timeout_seconds": acquire_timeout_seconds,
"name": name,
+ "start_url": start_url,
"tags": tags,
},
browser_pool_acquire_params.BrowserPoolAcquireParams,
diff --git a/src/kernel/types/browser_pool_acquire_params.py b/src/kernel/types/browser_pool_acquire_params.py
index 022d69bd..80b9be26 100644
--- a/src/kernel/types/browser_pool_acquire_params.py
+++ b/src/kernel/types/browser_pool_acquire_params.py
@@ -25,6 +25,13 @@ class BrowserPoolAcquireParams(TypedDict, total=False):
back to the pool.
"""
+ start_url: str
+ """Optional URL to navigate the acquired browser to.
+
+ Overrides the pool's start_url for this acquire only. Best-effort: failures to
+ navigate do not fail the acquire.
+ """
+
tags: TagsParam
"""
Optional user-defined key-value tags for the acquired browser session, used to
diff --git a/tests/api_resources/test_browser_pools.py b/tests/api_resources/test_browser_pools.py
index 2c77704d..ba603e7c 100644
--- a/tests/api_resources/test_browser_pools.py
+++ b/tests/api_resources/test_browser_pools.py
@@ -311,6 +311,7 @@ def test_method_acquire_with_all_params(self, client: Kernel) -> None:
id_or_name="id_or_name",
acquire_timeout_seconds=0,
name="checkout-flow-1",
+ start_url="https://example.com",
tags={
"team": "backend",
"env": "staging",
@@ -746,6 +747,7 @@ async def test_method_acquire_with_all_params(self, async_client: AsyncKernel) -
id_or_name="id_or_name",
acquire_timeout_seconds=0,
name="checkout-flow-1",
+ start_url="https://example.com",
tags={
"team": "backend",
"env": "staging",
From a0c273b2754226441dae2dd725d6efd1e8a6f225 Mon Sep 17 00:00:00 2001
From: "stainless-app[bot]"
<142633134+stainless-app[bot]@users.noreply.github.com>
Date: Wed, 24 Jun 2026 17:48:58 +0000
Subject: [PATCH 087/366] release: 0.70.0
---
.release-please-manifest.json | 2 +-
CHANGELOG.md | 18 ++++++++++++++++++
pyproject.toml | 2 +-
src/kernel/_version.py | 2 +-
4 files changed, 21 insertions(+), 3 deletions(-)
diff --git a/.release-please-manifest.json b/.release-please-manifest.json
index a9d0cc14..56ee145c 100644
--- a/.release-please-manifest.json
+++ b/.release-please-manifest.json
@@ -1,3 +1,3 @@
{
- ".": "0.69.0"
+ ".": "0.70.0"
}
\ No newline at end of file
diff --git a/CHANGELOG.md b/CHANGELOG.md
index a818acee..acc9883f 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,23 @@
# Changelog
+## 0.70.0 (2026-06-24)
+
+Full Changelog: [v0.69.0...v0.70.0](https://github.com/kernel/kernel-python-sdk/compare/v0.69.0...v0.70.0)
+
+### Features
+
+* Add GET /browsers/{id}/telemetry/events (read from S2) ([57eb393](https://github.com/kernel/kernel-python-sdk/commit/57eb393ea1eb5fb0d2e5211845f338610198a0fd))
+* Align browser-pool timeout/viewport/fill-rate contract with implementation; reject save_changes on update ([4fddd6b](https://github.com/kernel/kernel-python-sdk/commit/4fddd6bb3f2ccb86ec3cc492311d5fe9c061ba49))
+* api: support per-acquire start_url override on browser pool acquire ([3e11311](https://github.com/kernel/kernel-python-sdk/commit/3e11311f835d558c4fa6ab0c0fb8a9c7e3536a13))
+* **api:** add GET /extensions/{id_or_name}/metadata ([681a8fc](https://github.com/kernel/kernel-python-sdk/commit/681a8fcd7d8ec5c3373a66c81fb405a6844308eb))
+* **api:** resolve GET /org/projects/{id} by ID or name ([52eb598](https://github.com/kernel/kernel-python-sdk/commit/52eb598c7bfd6e696f0d510585439ba5658dc161))
+* Forward replay param through telemetry stream passthrough ([a8001d2](https://github.com/kernel/kernel-python-sdk/commit/a8001d239ae583fa177164e1ce900e767797c853))
+
+
+### Bug Fixes
+
+* don't misroute telemetry/events to the browser VM ([9bcc418](https://github.com/kernel/kernel-python-sdk/commit/9bcc418b4294295c0d8377adfc921c3748d42019))
+
## 0.69.0 (2026-06-18)
Full Changelog: [v0.68.0...v0.69.0](https://github.com/kernel/kernel-python-sdk/compare/v0.68.0...v0.69.0)
diff --git a/pyproject.toml b/pyproject.toml
index 2f216eb5..84fce8f7 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -1,6 +1,6 @@
[project]
name = "kernel"
-version = "0.69.0"
+version = "0.70.0"
description = "The official Python library for the kernel API"
dynamic = ["readme"]
license = "Apache-2.0"
diff --git a/src/kernel/_version.py b/src/kernel/_version.py
index 1d6a78bf..6d6da8f0 100644
--- a/src/kernel/_version.py
+++ b/src/kernel/_version.py
@@ -1,4 +1,4 @@
# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
__title__ = "kernel"
-__version__ = "0.69.0" # x-release-please-version
+__version__ = "0.70.0" # x-release-please-version
From 871ff5c9d9a328159115cbfee7aa0a4f62d12014 Mon Sep 17 00:00:00 2001
From: "stainless-app[bot]"
<142633134+stainless-app[bot]@users.noreply.github.com>
Date: Wed, 24 Jun 2026 18:41:49 +0000
Subject: [PATCH 088/366] feat: Expose audit logs in public SDK
---
.stats.yml | 8 +-
api.md | 12 +
src/kernel/_client.py | 44 ++++
src/kernel/pagination.py | 82 ++++++-
src/kernel/resources/__init__.py | 14 ++
src/kernel/resources/audit_logs.py | 269 ++++++++++++++++++++++
src/kernel/types/__init__.py | 2 +
src/kernel/types/audit_log_entry.py | 45 ++++
src/kernel/types/audit_log_list_params.py | 44 ++++
tests/api_resources/test_audit_logs.py | 134 +++++++++++
10 files changed, 649 insertions(+), 5 deletions(-)
create mode 100644 src/kernel/resources/audit_logs.py
create mode 100644 src/kernel/types/audit_log_entry.py
create mode 100644 src/kernel/types/audit_log_list_params.py
create mode 100644 tests/api_resources/test_audit_logs.py
diff --git a/.stats.yml b/.stats.yml
index 2018924b..04bb94ae 100644
--- a/.stats.yml
+++ b/.stats.yml
@@ -1,4 +1,4 @@
-configured_endpoints: 122
-openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel/kernel-d9b82fc5346c9be1bf9c2b18792fdcdec6a80a86153ca765c9e93e597b46fa24.yml
-openapi_spec_hash: 9cbaab975acfa421b795d11aa635c57e
-config_hash: 99b2b2a25e8067ad9c9214e38e01d64c
+configured_endpoints: 123
+openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel/kernel-784ee80ab17dd58e4b44292a4732d1669263d9928df5685e2683c59398e7a043.yml
+openapi_spec_hash: d34b07cf9549aef3469c82379f526f95
+config_hash: d52b040438a187c46050e0e8395b2f47
diff --git a/api.md b/api.md
index 88588c40..b2c96ebc 100644
--- a/api.md
+++ b/api.md
@@ -441,6 +441,18 @@ Methods:
- client.organization.limits.retrieve() -> OrgLimits
- client.organization.limits.update(\*\*params) -> OrgLimits
+# AuditLogs
+
+Types:
+
+```python
+from kernel.types import AuditLogEntry
+```
+
+Methods:
+
+- client.audit_logs.list(\*\*params) -> SyncPageTokenPagination[AuditLogEntry]
+
# APIKeys
Types:
diff --git a/src/kernel/_client.py b/src/kernel/_client.py
index 977b83d1..0238b203 100644
--- a/src/kernel/_client.py
+++ b/src/kernel/_client.py
@@ -54,6 +54,7 @@
browsers,
profiles,
projects,
+ audit_logs,
extensions,
credentials,
deployments,
@@ -67,6 +68,7 @@
from .resources.api_keys import APIKeysResource, AsyncAPIKeysResource
from .resources.profiles import ProfilesResource, AsyncProfilesResource
from .resources.auth.auth import AuthResource, AsyncAuthResource
+ from .resources.audit_logs import AuditLogsResource, AsyncAuditLogsResource
from .resources.extensions import ExtensionsResource, AsyncExtensionsResource
from .resources.credentials import CredentialsResource, AsyncCredentialsResource
from .resources.deployments import DeploymentsResource, AsyncDeploymentsResource
@@ -275,6 +277,13 @@ def organization(self) -> OrganizationResource:
return OrganizationResource(self)
+ @cached_property
+ def audit_logs(self) -> AuditLogsResource:
+ """Read audit log records for the authenticated organization."""
+ from .resources.audit_logs import AuditLogsResource
+
+ return AuditLogsResource(self)
+
@cached_property
def api_keys(self) -> APIKeysResource:
"""Create and manage API keys for organization and project-scoped access."""
@@ -620,6 +629,13 @@ def organization(self) -> AsyncOrganizationResource:
return AsyncOrganizationResource(self)
+ @cached_property
+ def audit_logs(self) -> AsyncAuditLogsResource:
+ """Read audit log records for the authenticated organization."""
+ from .resources.audit_logs import AsyncAuditLogsResource
+
+ return AsyncAuditLogsResource(self)
+
@cached_property
def api_keys(self) -> AsyncAPIKeysResource:
"""Create and manage API keys for organization and project-scoped access."""
@@ -873,6 +889,13 @@ def organization(self) -> organization.OrganizationResourceWithRawResponse:
return OrganizationResourceWithRawResponse(self._client.organization)
+ @cached_property
+ def audit_logs(self) -> audit_logs.AuditLogsResourceWithRawResponse:
+ """Read audit log records for the authenticated organization."""
+ from .resources.audit_logs import AuditLogsResourceWithRawResponse
+
+ return AuditLogsResourceWithRawResponse(self._client.audit_logs)
+
@cached_property
def api_keys(self) -> api_keys.APIKeysResourceWithRawResponse:
"""Create and manage API keys for organization and project-scoped access."""
@@ -976,6 +999,13 @@ def organization(self) -> organization.AsyncOrganizationResourceWithRawResponse:
return AsyncOrganizationResourceWithRawResponse(self._client.organization)
+ @cached_property
+ def audit_logs(self) -> audit_logs.AsyncAuditLogsResourceWithRawResponse:
+ """Read audit log records for the authenticated organization."""
+ from .resources.audit_logs import AsyncAuditLogsResourceWithRawResponse
+
+ return AsyncAuditLogsResourceWithRawResponse(self._client.audit_logs)
+
@cached_property
def api_keys(self) -> api_keys.AsyncAPIKeysResourceWithRawResponse:
"""Create and manage API keys for organization and project-scoped access."""
@@ -1079,6 +1109,13 @@ def organization(self) -> organization.OrganizationResourceWithStreamingResponse
return OrganizationResourceWithStreamingResponse(self._client.organization)
+ @cached_property
+ def audit_logs(self) -> audit_logs.AuditLogsResourceWithStreamingResponse:
+ """Read audit log records for the authenticated organization."""
+ from .resources.audit_logs import AuditLogsResourceWithStreamingResponse
+
+ return AuditLogsResourceWithStreamingResponse(self._client.audit_logs)
+
@cached_property
def api_keys(self) -> api_keys.APIKeysResourceWithStreamingResponse:
"""Create and manage API keys for organization and project-scoped access."""
@@ -1182,6 +1219,13 @@ def organization(self) -> organization.AsyncOrganizationResourceWithStreamingRes
return AsyncOrganizationResourceWithStreamingResponse(self._client.organization)
+ @cached_property
+ def audit_logs(self) -> audit_logs.AsyncAuditLogsResourceWithStreamingResponse:
+ """Read audit log records for the authenticated organization."""
+ from .resources.audit_logs import AsyncAuditLogsResourceWithStreamingResponse
+
+ return AsyncAuditLogsResourceWithStreamingResponse(self._client.audit_logs)
+
@cached_property
def api_keys(self) -> api_keys.AsyncAPIKeysResourceWithStreamingResponse:
"""Create and manage API keys for organization and project-scoped access."""
diff --git a/src/kernel/pagination.py b/src/kernel/pagination.py
index 39d5a085..98152da2 100644
--- a/src/kernel/pagination.py
+++ b/src/kernel/pagination.py
@@ -9,13 +9,93 @@
from ._models import BaseModel
from ._base_client import BasePage, PageInfo, BaseSyncPage, BaseAsyncPage
-__all__ = ["SyncOffsetPagination", "AsyncOffsetPagination"]
+__all__ = ["SyncPageTokenPagination", "AsyncPageTokenPagination", "SyncOffsetPagination", "AsyncOffsetPagination"]
_BaseModelT = TypeVar("_BaseModelT", bound=BaseModel)
_T = TypeVar("_T")
+class SyncPageTokenPagination(BaseSyncPage[_T], BasePage[_T], Generic[_T]):
+ items: List[_T]
+ next_page_token: Optional[str] = None
+ has_more: Optional[bool] = None
+
+ @override
+ def _get_page_items(self) -> List[_T]:
+ items = self.items
+ if not items:
+ return []
+ return items
+
+ @override
+ def has_next_page(self) -> bool:
+ has_more = self.has_more
+ if has_more is not None and has_more is False:
+ return False
+
+ return super().has_next_page()
+
+ @override
+ def next_page_info(self) -> Optional[PageInfo]:
+ next_page_token = self.next_page_token
+ if not next_page_token:
+ return None
+
+ return PageInfo(params={"page_token": next_page_token})
+
+ @classmethod
+ def build(cls: Type[_BaseModelT], *, response: Response, data: object) -> _BaseModelT: # noqa: ARG003
+ return cls.construct(
+ None,
+ **{
+ **(cast(Mapping[str, Any], data) if is_mapping(data) else {"items": data}),
+ "next_page_token": response.headers.get("X-Next-Page-Token"),
+ "has_more": maybe_coerce_boolean(response.headers.get("X-Has-More")),
+ },
+ )
+
+
+class AsyncPageTokenPagination(BaseAsyncPage[_T], BasePage[_T], Generic[_T]):
+ items: List[_T]
+ next_page_token: Optional[str] = None
+ has_more: Optional[bool] = None
+
+ @override
+ def _get_page_items(self) -> List[_T]:
+ items = self.items
+ if not items:
+ return []
+ return items
+
+ @override
+ def has_next_page(self) -> bool:
+ has_more = self.has_more
+ if has_more is not None and has_more is False:
+ return False
+
+ return super().has_next_page()
+
+ @override
+ def next_page_info(self) -> Optional[PageInfo]:
+ next_page_token = self.next_page_token
+ if not next_page_token:
+ return None
+
+ return PageInfo(params={"page_token": next_page_token})
+
+ @classmethod
+ def build(cls: Type[_BaseModelT], *, response: Response, data: object) -> _BaseModelT: # noqa: ARG003
+ return cls.construct(
+ None,
+ **{
+ **(cast(Mapping[str, Any], data) if is_mapping(data) else {"items": data}),
+ "next_page_token": response.headers.get("X-Next-Page-Token"),
+ "has_more": maybe_coerce_boolean(response.headers.get("X-Has-More")),
+ },
+ )
+
+
class SyncOffsetPagination(BaseSyncPage[_T], BasePage[_T], Generic[_T]):
items: List[_T]
has_more: Optional[bool] = None
diff --git a/src/kernel/resources/__init__.py b/src/kernel/resources/__init__.py
index 1497ad67..586c499a 100644
--- a/src/kernel/resources/__init__.py
+++ b/src/kernel/resources/__init__.py
@@ -56,6 +56,14 @@
ProjectsResourceWithStreamingResponse,
AsyncProjectsResourceWithStreamingResponse,
)
+from .audit_logs import (
+ AuditLogsResource,
+ AsyncAuditLogsResource,
+ AuditLogsResourceWithRawResponse,
+ AsyncAuditLogsResourceWithRawResponse,
+ AuditLogsResourceWithStreamingResponse,
+ AsyncAuditLogsResourceWithStreamingResponse,
+)
from .extensions import (
ExtensionsResource,
AsyncExtensionsResource,
@@ -186,6 +194,12 @@
"AsyncOrganizationResourceWithRawResponse",
"OrganizationResourceWithStreamingResponse",
"AsyncOrganizationResourceWithStreamingResponse",
+ "AuditLogsResource",
+ "AsyncAuditLogsResource",
+ "AuditLogsResourceWithRawResponse",
+ "AsyncAuditLogsResourceWithRawResponse",
+ "AuditLogsResourceWithStreamingResponse",
+ "AsyncAuditLogsResourceWithStreamingResponse",
"APIKeysResource",
"AsyncAPIKeysResource",
"APIKeysResourceWithRawResponse",
diff --git a/src/kernel/resources/audit_logs.py b/src/kernel/resources/audit_logs.py
new file mode 100644
index 00000000..bce8b719
--- /dev/null
+++ b/src/kernel/resources/audit_logs.py
@@ -0,0 +1,269 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from __future__ import annotations
+
+from typing import Union
+from datetime import datetime
+
+import httpx
+
+from ..types import audit_log_list_params
+from .._types import Body, Omit, Query, Headers, NotGiven, SequenceNotStr, omit, not_given
+from .._utils import maybe_transform
+from .._compat import cached_property
+from .._resource import SyncAPIResource, AsyncAPIResource
+from .._response import (
+ to_raw_response_wrapper,
+ to_streamed_response_wrapper,
+ async_to_raw_response_wrapper,
+ async_to_streamed_response_wrapper,
+)
+from ..pagination import SyncPageTokenPagination, AsyncPageTokenPagination
+from .._base_client import AsyncPaginator, make_request_options
+from ..types.audit_log_entry import AuditLogEntry
+
+__all__ = ["AuditLogsResource", "AsyncAuditLogsResource"]
+
+
+class AuditLogsResource(SyncAPIResource):
+ """Read audit log records for the authenticated organization."""
+
+ @cached_property
+ def with_raw_response(self) -> AuditLogsResourceWithRawResponse:
+ """
+ This property can be used as a prefix for any HTTP method call to return
+ the raw response object instead of the parsed content.
+
+ For more information, see https://www.github.com/kernel/kernel-python-sdk#accessing-raw-response-data-eg-headers
+ """
+ return AuditLogsResourceWithRawResponse(self)
+
+ @cached_property
+ def with_streaming_response(self) -> AuditLogsResourceWithStreamingResponse:
+ """
+ An alternative to `.with_raw_response` that doesn't eagerly read the response body.
+
+ For more information, see https://www.github.com/kernel/kernel-python-sdk#with_streaming_response
+ """
+ return AuditLogsResourceWithStreamingResponse(self)
+
+ def list(
+ self,
+ *,
+ end: Union[str, datetime],
+ start: Union[str, datetime],
+ auth_strategy: str | Omit = omit,
+ exclude_method: str | Omit = omit,
+ limit: int | Omit = omit,
+ method: str | Omit = omit,
+ page_token: str | Omit = omit,
+ search: str | Omit = omit,
+ search_user_id: SequenceNotStr[str] | Omit = omit,
+ service: str | Omit = omit,
+ # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
+ # The extra values given here take precedence over values defined on the client or passed to this method.
+ extra_headers: Headers | None = None,
+ extra_query: Query | None = None,
+ extra_body: Body | None = None,
+ timeout: float | httpx.Timeout | None | NotGiven = not_given,
+ ) -> SyncPageTokenPagination[AuditLogEntry]:
+ """API for searching audit logs.
+
+ Limited to at most 30 day search, returns up to
+ 100 records per page. Not recommended for bulk export.
+
+ Args:
+ end: Upper bound (exclusive) for the audit record timestamp.
+
+ start: Lower bound (inclusive) for the audit record timestamp.
+
+ auth_strategy: Filter by authentication strategy.
+
+ exclude_method: Filter out results by HTTP method.
+
+ limit: Maximum number of results to return.
+
+ method: Filter by HTTP method.
+
+ page_token: Opaque page token from X-Next-Page-Token for the next page of older records.
+
+ search: Free-text search over path, user ID, email, client IP, and status.
+
+ search_user_id: Additional user IDs to OR into free-text search.
+
+ service: Filter by service name.
+
+ 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
+ """
+ return self._get_api_list(
+ "/audit-logs",
+ page=SyncPageTokenPagination[AuditLogEntry],
+ options=make_request_options(
+ extra_headers=extra_headers,
+ extra_query=extra_query,
+ extra_body=extra_body,
+ timeout=timeout,
+ query=maybe_transform(
+ {
+ "end": end,
+ "start": start,
+ "auth_strategy": auth_strategy,
+ "exclude_method": exclude_method,
+ "limit": limit,
+ "method": method,
+ "page_token": page_token,
+ "search": search,
+ "search_user_id": search_user_id,
+ "service": service,
+ },
+ audit_log_list_params.AuditLogListParams,
+ ),
+ ),
+ model=AuditLogEntry,
+ )
+
+
+class AsyncAuditLogsResource(AsyncAPIResource):
+ """Read audit log records for the authenticated organization."""
+
+ @cached_property
+ def with_raw_response(self) -> AsyncAuditLogsResourceWithRawResponse:
+ """
+ This property can be used as a prefix for any HTTP method call to return
+ the raw response object instead of the parsed content.
+
+ For more information, see https://www.github.com/kernel/kernel-python-sdk#accessing-raw-response-data-eg-headers
+ """
+ return AsyncAuditLogsResourceWithRawResponse(self)
+
+ @cached_property
+ def with_streaming_response(self) -> AsyncAuditLogsResourceWithStreamingResponse:
+ """
+ An alternative to `.with_raw_response` that doesn't eagerly read the response body.
+
+ For more information, see https://www.github.com/kernel/kernel-python-sdk#with_streaming_response
+ """
+ return AsyncAuditLogsResourceWithStreamingResponse(self)
+
+ def list(
+ self,
+ *,
+ end: Union[str, datetime],
+ start: Union[str, datetime],
+ auth_strategy: str | Omit = omit,
+ exclude_method: str | Omit = omit,
+ limit: int | Omit = omit,
+ method: str | Omit = omit,
+ page_token: str | Omit = omit,
+ search: str | Omit = omit,
+ search_user_id: SequenceNotStr[str] | Omit = omit,
+ service: str | Omit = omit,
+ # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
+ # The extra values given here take precedence over values defined on the client or passed to this method.
+ extra_headers: Headers | None = None,
+ extra_query: Query | None = None,
+ extra_body: Body | None = None,
+ timeout: float | httpx.Timeout | None | NotGiven = not_given,
+ ) -> AsyncPaginator[AuditLogEntry, AsyncPageTokenPagination[AuditLogEntry]]:
+ """API for searching audit logs.
+
+ Limited to at most 30 day search, returns up to
+ 100 records per page. Not recommended for bulk export.
+
+ Args:
+ end: Upper bound (exclusive) for the audit record timestamp.
+
+ start: Lower bound (inclusive) for the audit record timestamp.
+
+ auth_strategy: Filter by authentication strategy.
+
+ exclude_method: Filter out results by HTTP method.
+
+ limit: Maximum number of results to return.
+
+ method: Filter by HTTP method.
+
+ page_token: Opaque page token from X-Next-Page-Token for the next page of older records.
+
+ search: Free-text search over path, user ID, email, client IP, and status.
+
+ search_user_id: Additional user IDs to OR into free-text search.
+
+ service: Filter by service name.
+
+ 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
+ """
+ return self._get_api_list(
+ "/audit-logs",
+ page=AsyncPageTokenPagination[AuditLogEntry],
+ options=make_request_options(
+ extra_headers=extra_headers,
+ extra_query=extra_query,
+ extra_body=extra_body,
+ timeout=timeout,
+ query=maybe_transform(
+ {
+ "end": end,
+ "start": start,
+ "auth_strategy": auth_strategy,
+ "exclude_method": exclude_method,
+ "limit": limit,
+ "method": method,
+ "page_token": page_token,
+ "search": search,
+ "search_user_id": search_user_id,
+ "service": service,
+ },
+ audit_log_list_params.AuditLogListParams,
+ ),
+ ),
+ model=AuditLogEntry,
+ )
+
+
+class AuditLogsResourceWithRawResponse:
+ def __init__(self, audit_logs: AuditLogsResource) -> None:
+ self._audit_logs = audit_logs
+
+ self.list = to_raw_response_wrapper(
+ audit_logs.list,
+ )
+
+
+class AsyncAuditLogsResourceWithRawResponse:
+ def __init__(self, audit_logs: AsyncAuditLogsResource) -> None:
+ self._audit_logs = audit_logs
+
+ self.list = async_to_raw_response_wrapper(
+ audit_logs.list,
+ )
+
+
+class AuditLogsResourceWithStreamingResponse:
+ def __init__(self, audit_logs: AuditLogsResource) -> None:
+ self._audit_logs = audit_logs
+
+ self.list = to_streamed_response_wrapper(
+ audit_logs.list,
+ )
+
+
+class AsyncAuditLogsResourceWithStreamingResponse:
+ def __init__(self, audit_logs: AsyncAuditLogsResource) -> None:
+ self._audit_logs = audit_logs
+
+ self.list = async_to_streamed_response_wrapper(
+ audit_logs.list,
+ )
diff --git a/src/kernel/types/__init__.py b/src/kernel/types/__init__.py
index 660875d7..357afc98 100644
--- a/src/kernel/types/__init__.py
+++ b/src/kernel/types/__init__.py
@@ -24,6 +24,7 @@
from .browser_pool import BrowserPool as BrowserPool
from .browser_usage import BrowserUsage as BrowserUsage
from .app_list_params import AppListParams as AppListParams
+from .audit_log_entry import AuditLogEntry as AuditLogEntry
from .created_api_key import CreatedAPIKey as CreatedAPIKey
from .browser_pool_ref import BrowserPoolRef as BrowserPoolRef
from .app_list_response import AppListResponse as AppListResponse
@@ -41,6 +42,7 @@
from .api_key_create_params import APIKeyCreateParams as APIKeyCreateParams
from .api_key_rotate_params import APIKeyRotateParams as APIKeyRotateParams
from .api_key_update_params import APIKeyUpdateParams as APIKeyUpdateParams
+from .audit_log_list_params import AuditLogListParams as AuditLogListParams
from .browser_create_params import BrowserCreateParams as BrowserCreateParams
from .browser_curl_response import BrowserCurlResponse as BrowserCurlResponse
from .browser_list_response import BrowserListResponse as BrowserListResponse
diff --git a/src/kernel/types/audit_log_entry.py b/src/kernel/types/audit_log_entry.py
new file mode 100644
index 00000000..dd998098
--- /dev/null
+++ b/src/kernel/types/audit_log_entry.py
@@ -0,0 +1,45 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from datetime import datetime
+
+from .._models import BaseModel
+
+__all__ = ["AuditLogEntry"]
+
+
+class AuditLogEntry(BaseModel):
+ auth_strategy: str
+ """Authentication strategy used for the request."""
+
+ client_ip: str
+ """Client IP address."""
+
+ domain: str
+ """Request host."""
+
+ duration_ms: int
+ """Request duration in milliseconds."""
+
+ email: str
+ """Email of the authenticated user at request time, if any."""
+
+ method: str
+ """HTTP method."""
+
+ path: str
+ """Request path."""
+
+ route: str
+ """Matched API route pattern, if available."""
+
+ status: int
+ """HTTP response status code."""
+
+ timestamp: datetime
+ """UTC time when the request was received."""
+
+ user_agent: str
+ """User agent header."""
+
+ user_id: str
+ """ID of the authenticated user, if any."""
diff --git a/src/kernel/types/audit_log_list_params.py b/src/kernel/types/audit_log_list_params.py
new file mode 100644
index 00000000..f1e18f58
--- /dev/null
+++ b/src/kernel/types/audit_log_list_params.py
@@ -0,0 +1,44 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from __future__ import annotations
+
+from typing import Union
+from datetime import datetime
+from typing_extensions import Required, Annotated, TypedDict
+
+from .._types import SequenceNotStr
+from .._utils import PropertyInfo
+
+__all__ = ["AuditLogListParams"]
+
+
+class AuditLogListParams(TypedDict, total=False):
+ end: Required[Annotated[Union[str, datetime], PropertyInfo(format="iso8601")]]
+ """Upper bound (exclusive) for the audit record timestamp."""
+
+ start: Required[Annotated[Union[str, datetime], PropertyInfo(format="iso8601")]]
+ """Lower bound (inclusive) for the audit record timestamp."""
+
+ auth_strategy: str
+ """Filter by authentication strategy."""
+
+ exclude_method: str
+ """Filter out results by HTTP method."""
+
+ limit: int
+ """Maximum number of results to return."""
+
+ method: str
+ """Filter by HTTP method."""
+
+ page_token: str
+ """Opaque page token from X-Next-Page-Token for the next page of older records."""
+
+ search: str
+ """Free-text search over path, user ID, email, client IP, and status."""
+
+ search_user_id: SequenceNotStr[str]
+ """Additional user IDs to OR into free-text search."""
+
+ service: str
+ """Filter by service name."""
diff --git a/tests/api_resources/test_audit_logs.py b/tests/api_resources/test_audit_logs.py
new file mode 100644
index 00000000..669a8872
--- /dev/null
+++ b/tests/api_resources/test_audit_logs.py
@@ -0,0 +1,134 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from __future__ import annotations
+
+import os
+from typing import Any, cast
+
+import pytest
+
+from kernel import Kernel, AsyncKernel
+from tests.utils import assert_matches_type
+from kernel.types import AuditLogEntry
+from kernel._utils import parse_datetime
+from kernel.pagination import SyncPageTokenPagination, AsyncPageTokenPagination
+
+base_url = os.environ.get("TEST_API_BASE_URL", "http://127.0.0.1:4010")
+
+
+class TestAuditLogs:
+ parametrize = pytest.mark.parametrize("client", [False, True], indirect=True, ids=["loose", "strict"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ def test_method_list(self, client: Kernel) -> None:
+ audit_log = client.audit_logs.list(
+ end=parse_datetime("2026-01-02T00:00:00Z"),
+ start=parse_datetime("2026-01-01T00:00:00Z"),
+ )
+ assert_matches_type(SyncPageTokenPagination[AuditLogEntry], audit_log, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ def test_method_list_with_all_params(self, client: Kernel) -> None:
+ audit_log = client.audit_logs.list(
+ end=parse_datetime("2026-01-02T00:00:00Z"),
+ start=parse_datetime("2026-01-01T00:00:00Z"),
+ auth_strategy="auth_strategy",
+ exclude_method="exclude_method",
+ limit=1,
+ method="method",
+ page_token="page_token",
+ search="search",
+ search_user_id=["string"],
+ service="service",
+ )
+ assert_matches_type(SyncPageTokenPagination[AuditLogEntry], audit_log, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ def test_raw_response_list(self, client: Kernel) -> None:
+ response = client.audit_logs.with_raw_response.list(
+ end=parse_datetime("2026-01-02T00:00:00Z"),
+ start=parse_datetime("2026-01-01T00:00:00Z"),
+ )
+
+ assert response.is_closed is True
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+ audit_log = response.parse()
+ assert_matches_type(SyncPageTokenPagination[AuditLogEntry], audit_log, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ def test_streaming_response_list(self, client: Kernel) -> None:
+ with client.audit_logs.with_streaming_response.list(
+ end=parse_datetime("2026-01-02T00:00:00Z"),
+ start=parse_datetime("2026-01-01T00:00:00Z"),
+ ) as response:
+ assert not response.is_closed
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+
+ audit_log = response.parse()
+ assert_matches_type(SyncPageTokenPagination[AuditLogEntry], audit_log, path=["response"])
+
+ assert cast(Any, response.is_closed) is True
+
+
+class TestAsyncAuditLogs:
+ parametrize = pytest.mark.parametrize(
+ "async_client", [False, True, {"http_client": "aiohttp"}], indirect=True, ids=["loose", "strict", "aiohttp"]
+ )
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ async def test_method_list(self, async_client: AsyncKernel) -> None:
+ audit_log = await async_client.audit_logs.list(
+ end=parse_datetime("2026-01-02T00:00:00Z"),
+ start=parse_datetime("2026-01-01T00:00:00Z"),
+ )
+ assert_matches_type(AsyncPageTokenPagination[AuditLogEntry], audit_log, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ async def test_method_list_with_all_params(self, async_client: AsyncKernel) -> None:
+ audit_log = await async_client.audit_logs.list(
+ end=parse_datetime("2026-01-02T00:00:00Z"),
+ start=parse_datetime("2026-01-01T00:00:00Z"),
+ auth_strategy="auth_strategy",
+ exclude_method="exclude_method",
+ limit=1,
+ method="method",
+ page_token="page_token",
+ search="search",
+ search_user_id=["string"],
+ service="service",
+ )
+ assert_matches_type(AsyncPageTokenPagination[AuditLogEntry], audit_log, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ async def test_raw_response_list(self, async_client: AsyncKernel) -> None:
+ response = await async_client.audit_logs.with_raw_response.list(
+ end=parse_datetime("2026-01-02T00:00:00Z"),
+ start=parse_datetime("2026-01-01T00:00:00Z"),
+ )
+
+ assert response.is_closed is True
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+ audit_log = await response.parse()
+ assert_matches_type(AsyncPageTokenPagination[AuditLogEntry], audit_log, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ async def test_streaming_response_list(self, async_client: AsyncKernel) -> None:
+ async with async_client.audit_logs.with_streaming_response.list(
+ end=parse_datetime("2026-01-02T00:00:00Z"),
+ start=parse_datetime("2026-01-01T00:00:00Z"),
+ ) as response:
+ assert not response.is_closed
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+
+ audit_log = await response.parse()
+ assert_matches_type(AsyncPageTokenPagination[AuditLogEntry], audit_log, path=["response"])
+
+ assert cast(Any, response.is_closed) is True
From aafd5c26f1943d3989ca657a4da03c3dba3f03ec Mon Sep 17 00:00:00 2001
From: "stainless-app[bot]"
<142633134+stainless-app[bot]@users.noreply.github.com>
Date: Thu, 25 Jun 2026 13:13:00 +0000
Subject: [PATCH 089/366] codegen metadata
---
.stats.yml | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/.stats.yml b/.stats.yml
index 04bb94ae..397b820e 100644
--- a/.stats.yml
+++ b/.stats.yml
@@ -1,4 +1,4 @@
configured_endpoints: 123
-openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel/kernel-784ee80ab17dd58e4b44292a4732d1669263d9928df5685e2683c59398e7a043.yml
-openapi_spec_hash: d34b07cf9549aef3469c82379f526f95
+openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel/kernel-ac06dd66a7a0da0dd6dd6cd98127652891b914e880dbaed90d7ab666b13af8a9.yml
+openapi_spec_hash: 1d35ae59d6570497f8b379ded59f0434
config_hash: d52b040438a187c46050e0e8395b2f47
From 7a974b5694b0c6e9fec9122424342b9c61a8b503 Mon Sep 17 00:00:00 2001
From: "stainless-app[bot]"
<142633134+stainless-app[bot]@users.noreply.github.com>
Date: Fri, 26 Jun 2026 17:24:40 +0000
Subject: [PATCH 090/366] feat: Add auth connection event timeline endpoint
---
.stats.yml | 8 +-
api.md | 2 +
src/kernel/resources/auth/connections.py | 127 ++++++++++++++++++
src/kernel/types/auth/__init__.py | 2 +
.../types/auth/connection_timeline_params.py | 18 +++
.../types/auth/managed_auth_timeline_event.py | 77 +++++++++++
tests/api_resources/auth/test_connections.py | 107 +++++++++++++++
7 files changed, 337 insertions(+), 4 deletions(-)
create mode 100644 src/kernel/types/auth/connection_timeline_params.py
create mode 100644 src/kernel/types/auth/managed_auth_timeline_event.py
diff --git a/.stats.yml b/.stats.yml
index 397b820e..ef32d38d 100644
--- a/.stats.yml
+++ b/.stats.yml
@@ -1,4 +1,4 @@
-configured_endpoints: 123
-openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel/kernel-ac06dd66a7a0da0dd6dd6cd98127652891b914e880dbaed90d7ab666b13af8a9.yml
-openapi_spec_hash: 1d35ae59d6570497f8b379ded59f0434
-config_hash: d52b040438a187c46050e0e8395b2f47
+configured_endpoints: 124
+openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel/kernel-4fb45d71a99648425c84bdc8e5780920105cede4ee2d4eac67276d0609ac1e94.yml
+openapi_spec_hash: 1f04cb5b36e92db81dfa264c2a59c32a
+config_hash: fb167e754ebb3a14649463725891c9d0
diff --git a/api.md b/api.md
index b2c96ebc..5c1c6b9c 100644
--- a/api.md
+++ b/api.md
@@ -300,6 +300,7 @@ from kernel.types.auth import (
LoginResponse,
ManagedAuth,
ManagedAuthCreateRequest,
+ ManagedAuthTimelineEvent,
ManagedAuthUpdateRequest,
SubmitFieldsRequest,
SubmitFieldsResponse,
@@ -317,6 +318,7 @@ Methods:
- client.auth.connections.follow(id) -> ConnectionFollowResponse
- client.auth.connections.login(id, \*\*params) -> LoginResponse
- client.auth.connections.submit(id, \*\*params) -> SubmitFieldsResponse
+- client.auth.connections.timeline(id, \*\*params) -> SyncOffsetPagination[ManagedAuthTimelineEvent]
# Proxies
diff --git a/src/kernel/resources/auth/connections.py b/src/kernel/resources/auth/connections.py
index c901fdb8..24a560c2 100644
--- a/src/kernel/resources/auth/connections.py
+++ b/src/kernel/resources/auth/connections.py
@@ -3,6 +3,7 @@
from __future__ import annotations
from typing import Any, Dict, cast
+from typing_extensions import Literal
import httpx
@@ -24,12 +25,14 @@
connection_create_params,
connection_submit_params,
connection_update_params,
+ connection_timeline_params,
)
from ..._base_client import AsyncPaginator, make_request_options
from ...types.auth.managed_auth import ManagedAuth
from ...types.auth.login_response import LoginResponse
from ...types.auth.submit_fields_response import SubmitFieldsResponse
from ...types.auth.connection_follow_response import ConnectionFollowResponse
+from ...types.auth.managed_auth_timeline_event import ManagedAuthTimelineEvent
__all__ = ["ConnectionsResource", "AsyncConnectionsResource"]
@@ -554,6 +557,62 @@ def submit(
cast_to=SubmitFieldsResponse,
)
+ def timeline(
+ self,
+ id: str,
+ *,
+ limit: int | Omit = omit,
+ offset: int | Omit = omit,
+ type: Literal["login", "reauth", "health_check"] | Omit = omit,
+ # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
+ # The extra values given here take precedence over values defined on the client or passed to this method.
+ extra_headers: Headers | None = None,
+ extra_query: Query | None = None,
+ extra_body: Body | None = None,
+ timeout: float | httpx.Timeout | None | NotGiven = not_given,
+ ) -> SyncOffsetPagination[ManagedAuthTimelineEvent]:
+ """
+ Returns a chronological timeline of events for an auth connection — login
+ attempts, automatic re-auth attempts, and health checks. Events are returned
+ newest-first.
+
+ Args:
+ limit: Maximum number of events to return
+
+ offset: Number of events to skip
+
+ type: Filter the timeline to a single event type.
+
+ extra_headers: Send extra headers
+
+ extra_query: Add additional query parameters to the request
+
+ extra_body: Add additional JSON properties to the request
+
+ timeout: Override the client-level default timeout for this request, in seconds
+ """
+ if not id:
+ raise ValueError(f"Expected a non-empty value for `id` but received {id!r}")
+ return self._get_api_list(
+ path_template("/auth/connections/{id}/timeline", id=id),
+ page=SyncOffsetPagination[ManagedAuthTimelineEvent],
+ options=make_request_options(
+ extra_headers=extra_headers,
+ extra_query=extra_query,
+ extra_body=extra_body,
+ timeout=timeout,
+ query=maybe_transform(
+ {
+ "limit": limit,
+ "offset": offset,
+ "type": type,
+ },
+ connection_timeline_params.ConnectionTimelineParams,
+ ),
+ ),
+ model=ManagedAuthTimelineEvent,
+ )
+
class AsyncConnectionsResource(AsyncAPIResource):
"""Create and manage auth connections for automated credential capture and login."""
@@ -1075,6 +1134,62 @@ async def submit(
cast_to=SubmitFieldsResponse,
)
+ def timeline(
+ self,
+ id: str,
+ *,
+ limit: int | Omit = omit,
+ offset: int | Omit = omit,
+ type: Literal["login", "reauth", "health_check"] | Omit = omit,
+ # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
+ # The extra values given here take precedence over values defined on the client or passed to this method.
+ extra_headers: Headers | None = None,
+ extra_query: Query | None = None,
+ extra_body: Body | None = None,
+ timeout: float | httpx.Timeout | None | NotGiven = not_given,
+ ) -> AsyncPaginator[ManagedAuthTimelineEvent, AsyncOffsetPagination[ManagedAuthTimelineEvent]]:
+ """
+ Returns a chronological timeline of events for an auth connection — login
+ attempts, automatic re-auth attempts, and health checks. Events are returned
+ newest-first.
+
+ Args:
+ limit: Maximum number of events to return
+
+ offset: Number of events to skip
+
+ type: Filter the timeline to a single event type.
+
+ extra_headers: Send extra headers
+
+ extra_query: Add additional query parameters to the request
+
+ extra_body: Add additional JSON properties to the request
+
+ timeout: Override the client-level default timeout for this request, in seconds
+ """
+ if not id:
+ raise ValueError(f"Expected a non-empty value for `id` but received {id!r}")
+ return self._get_api_list(
+ path_template("/auth/connections/{id}/timeline", id=id),
+ page=AsyncOffsetPagination[ManagedAuthTimelineEvent],
+ options=make_request_options(
+ extra_headers=extra_headers,
+ extra_query=extra_query,
+ extra_body=extra_body,
+ timeout=timeout,
+ query=maybe_transform(
+ {
+ "limit": limit,
+ "offset": offset,
+ "type": type,
+ },
+ connection_timeline_params.ConnectionTimelineParams,
+ ),
+ ),
+ model=ManagedAuthTimelineEvent,
+ )
+
class ConnectionsResourceWithRawResponse:
def __init__(self, connections: ConnectionsResource) -> None:
@@ -1104,6 +1219,9 @@ def __init__(self, connections: ConnectionsResource) -> None:
self.submit = to_raw_response_wrapper(
connections.submit,
)
+ self.timeline = to_raw_response_wrapper(
+ connections.timeline,
+ )
class AsyncConnectionsResourceWithRawResponse:
@@ -1134,6 +1252,9 @@ def __init__(self, connections: AsyncConnectionsResource) -> None:
self.submit = async_to_raw_response_wrapper(
connections.submit,
)
+ self.timeline = async_to_raw_response_wrapper(
+ connections.timeline,
+ )
class ConnectionsResourceWithStreamingResponse:
@@ -1164,6 +1285,9 @@ def __init__(self, connections: ConnectionsResource) -> None:
self.submit = to_streamed_response_wrapper(
connections.submit,
)
+ self.timeline = to_streamed_response_wrapper(
+ connections.timeline,
+ )
class AsyncConnectionsResourceWithStreamingResponse:
@@ -1194,3 +1318,6 @@ def __init__(self, connections: AsyncConnectionsResource) -> None:
self.submit = async_to_streamed_response_wrapper(
connections.submit,
)
+ self.timeline = async_to_streamed_response_wrapper(
+ connections.timeline,
+ )
diff --git a/src/kernel/types/auth/__init__.py b/src/kernel/types/auth/__init__.py
index db897944..a70816fb 100644
--- a/src/kernel/types/auth/__init__.py
+++ b/src/kernel/types/auth/__init__.py
@@ -11,3 +11,5 @@
from .connection_submit_params import ConnectionSubmitParams as ConnectionSubmitParams
from .connection_update_params import ConnectionUpdateParams as ConnectionUpdateParams
from .connection_follow_response import ConnectionFollowResponse as ConnectionFollowResponse
+from .connection_timeline_params import ConnectionTimelineParams as ConnectionTimelineParams
+from .managed_auth_timeline_event import ManagedAuthTimelineEvent as ManagedAuthTimelineEvent
diff --git a/src/kernel/types/auth/connection_timeline_params.py b/src/kernel/types/auth/connection_timeline_params.py
new file mode 100644
index 00000000..79c4f5dc
--- /dev/null
+++ b/src/kernel/types/auth/connection_timeline_params.py
@@ -0,0 +1,18 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from __future__ import annotations
+
+from typing_extensions import Literal, TypedDict
+
+__all__ = ["ConnectionTimelineParams"]
+
+
+class ConnectionTimelineParams(TypedDict, total=False):
+ limit: int
+ """Maximum number of events to return"""
+
+ offset: int
+ """Number of events to skip"""
+
+ type: Literal["login", "reauth", "health_check"]
+ """Filter the timeline to a single event type."""
diff --git a/src/kernel/types/auth/managed_auth_timeline_event.py b/src/kernel/types/auth/managed_auth_timeline_event.py
new file mode 100644
index 00000000..1f79a526
--- /dev/null
+++ b/src/kernel/types/auth/managed_auth_timeline_event.py
@@ -0,0 +1,77 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from typing import Optional
+from datetime import datetime
+from typing_extensions import Literal
+
+from ..._models import BaseModel
+
+__all__ = ["ManagedAuthTimelineEvent"]
+
+
+class ManagedAuthTimelineEvent(BaseModel):
+ """
+ A single event in an auth connection's history — a login attempt, an automatic re-auth attempt, or a health check.
+ """
+
+ id: str
+ """Identifier of the underlying login/reauth session or health check."""
+
+ status: Literal["IN_PROGRESS", "SUCCESS", "EXPIRED", "CANCELED", "FAILED", "AUTHENTICATED", "NEEDS_AUTH"]
+ """Outcome of the event.
+
+ For login/reauth events this is the flow status (IN_PROGRESS, SUCCESS, EXPIRED,
+ CANCELED, FAILED). For health_check events it is the observed session state
+ (AUTHENTICATED, NEEDS_AUTH).
+ """
+
+ timestamp: datetime
+ """When the event occurred."""
+
+ type: Literal["login", "reauth", "health_check"]
+ """The kind of event.
+
+ "login" and "reauth" are authentication attempts; "health_check" is a periodic
+ session-validity check.
+ """
+
+ error_code: Optional[str] = None
+ """Machine-readable error code. Present when a login/reauth event failed."""
+
+ error_message: Optional[str] = None
+ """Human-readable error message. Present when a login/reauth event failed."""
+
+ previous_status: Optional[Literal["AUTHENTICATED", "NEEDS_AUTH"]] = None
+ """The session state observed before this event.
+
+ Present for health_check events that recorded a prior state.
+ """
+
+ replay_id: Optional[str] = None
+ """
+ Replay recording ID for the event's browser session, if session recording was
+ enabled.
+ """
+
+ step: Optional[
+ Literal[
+ "INITIALIZED",
+ "DISCOVERING",
+ "AWAITING_INPUT",
+ "AWAITING_EXTERNAL_ACTION",
+ "AWAITING_HUMAN_INTERVENTION",
+ "SUBMITTING",
+ "COMPLETED",
+ "EXPIRED",
+ ]
+ ] = None
+ """The step the flow reached. Present for login/reauth events."""
+
+ updated_at: Optional[datetime] = None
+ """When the event was last updated. Present for login/reauth events."""
+
+ website_error: Optional[str] = None
+ """Visible error message from the website (e.g., 'Incorrect password').
+
+ Present when the website displayed an error during the attempt.
+ """
diff --git a/tests/api_resources/auth/test_connections.py b/tests/api_resources/auth/test_connections.py
index 2b9ce1c4..b92cd790 100644
--- a/tests/api_resources/auth/test_connections.py
+++ b/tests/api_resources/auth/test_connections.py
@@ -14,6 +14,7 @@
ManagedAuth,
LoginResponse,
SubmitFieldsResponse,
+ ManagedAuthTimelineEvent,
)
base_url = os.environ.get("TEST_API_BASE_URL", "http://127.0.0.1:4010")
@@ -430,6 +431,59 @@ def test_path_params_submit(self, client: Kernel) -> None:
id="",
)
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ def test_method_timeline(self, client: Kernel) -> None:
+ connection = client.auth.connections.timeline(
+ id="id",
+ )
+ assert_matches_type(SyncOffsetPagination[ManagedAuthTimelineEvent], connection, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ def test_method_timeline_with_all_params(self, client: Kernel) -> None:
+ connection = client.auth.connections.timeline(
+ id="id",
+ limit=100,
+ offset=0,
+ type="login",
+ )
+ assert_matches_type(SyncOffsetPagination[ManagedAuthTimelineEvent], connection, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ def test_raw_response_timeline(self, client: Kernel) -> None:
+ response = client.auth.connections.with_raw_response.timeline(
+ id="id",
+ )
+
+ assert response.is_closed is True
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+ connection = response.parse()
+ assert_matches_type(SyncOffsetPagination[ManagedAuthTimelineEvent], connection, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ def test_streaming_response_timeline(self, client: Kernel) -> None:
+ with client.auth.connections.with_streaming_response.timeline(
+ 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(SyncOffsetPagination[ManagedAuthTimelineEvent], 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_timeline(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.timeline(
+ id="",
+ )
+
class TestAsyncConnections:
parametrize = pytest.mark.parametrize(
@@ -843,3 +897,56 @@ async def test_path_params_submit(self, async_client: AsyncKernel) -> None:
await async_client.auth.connections.with_raw_response.submit(
id="",
)
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ async def test_method_timeline(self, async_client: AsyncKernel) -> None:
+ connection = await async_client.auth.connections.timeline(
+ id="id",
+ )
+ assert_matches_type(AsyncOffsetPagination[ManagedAuthTimelineEvent], connection, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ async def test_method_timeline_with_all_params(self, async_client: AsyncKernel) -> None:
+ connection = await async_client.auth.connections.timeline(
+ id="id",
+ limit=100,
+ offset=0,
+ type="login",
+ )
+ assert_matches_type(AsyncOffsetPagination[ManagedAuthTimelineEvent], connection, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ async def test_raw_response_timeline(self, async_client: AsyncKernel) -> None:
+ response = await async_client.auth.connections.with_raw_response.timeline(
+ 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(AsyncOffsetPagination[ManagedAuthTimelineEvent], connection, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ async def test_streaming_response_timeline(self, async_client: AsyncKernel) -> None:
+ async with async_client.auth.connections.with_streaming_response.timeline(
+ 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(AsyncOffsetPagination[ManagedAuthTimelineEvent], 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_timeline(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.timeline(
+ id="",
+ )
From 0beb2b9ecb8bd93da2528550b7a598ec2e60ca2c Mon Sep 17 00:00:00 2001
From: "stainless-app[bot]"
<142633134+stainless-app[bot]@users.noreply.github.com>
Date: Fri, 26 Jun 2026 17:25:09 +0000
Subject: [PATCH 091/366] release: 0.71.0
---
.release-please-manifest.json | 2 +-
CHANGELOG.md | 9 +++++++++
pyproject.toml | 2 +-
src/kernel/_version.py | 2 +-
4 files changed, 12 insertions(+), 3 deletions(-)
diff --git a/.release-please-manifest.json b/.release-please-manifest.json
index 56ee145c..6772f01d 100644
--- a/.release-please-manifest.json
+++ b/.release-please-manifest.json
@@ -1,3 +1,3 @@
{
- ".": "0.70.0"
+ ".": "0.71.0"
}
\ No newline at end of file
diff --git a/CHANGELOG.md b/CHANGELOG.md
index acc9883f..35090e2b 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,14 @@
# Changelog
+## 0.71.0 (2026-06-26)
+
+Full Changelog: [v0.70.0...v0.71.0](https://github.com/kernel/kernel-python-sdk/compare/v0.70.0...v0.71.0)
+
+### Features
+
+* Add auth connection event timeline endpoint ([871432f](https://github.com/kernel/kernel-python-sdk/commit/871432f827c95b11d132f60b3547c9986e266f6b))
+* Expose audit logs in public SDK ([9c8e9ac](https://github.com/kernel/kernel-python-sdk/commit/9c8e9ace1cdddc6ce727b38fec62beaba6c6fa20))
+
## 0.70.0 (2026-06-24)
Full Changelog: [v0.69.0...v0.70.0](https://github.com/kernel/kernel-python-sdk/compare/v0.69.0...v0.70.0)
diff --git a/pyproject.toml b/pyproject.toml
index 84fce8f7..9187ed7e 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -1,6 +1,6 @@
[project]
name = "kernel"
-version = "0.70.0"
+version = "0.71.0"
description = "The official Python library for the kernel API"
dynamic = ["readme"]
license = "Apache-2.0"
diff --git a/src/kernel/_version.py b/src/kernel/_version.py
index 6d6da8f0..0bf99948 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.70.0" # x-release-please-version
+__version__ = "0.71.0" # x-release-please-version
From a1e5d35255cecd198856f8c6aea6c443b2d97638 Mon Sep 17 00:00:00 2001
From: "stainless-app[bot]"
<142633134+stainless-app[bot]@users.noreply.github.com>
Date: Fri, 26 Jun 2026 18:24:10 +0000
Subject: [PATCH 092/366] fix(api): browser pool profile omits save_changes
(BrowserPoolProfile)
---
.stats.yml | 4 +-
src/kernel/resources/browser_pools.py | 75 ++++++++++++-------
src/kernel/types/browser_pool.py | 35 +++++++--
.../types/browser_pool_create_params.py | 35 +++++++--
.../types/browser_pool_update_params.py | 35 +++++++--
tests/api_resources/test_browser_pools.py | 4 -
6 files changed, 139 insertions(+), 49 deletions(-)
diff --git a/.stats.yml b/.stats.yml
index ef32d38d..10d90226 100644
--- a/.stats.yml
+++ b/.stats.yml
@@ -1,4 +1,4 @@
configured_endpoints: 124
-openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel/kernel-4fb45d71a99648425c84bdc8e5780920105cede4ee2d4eac67276d0609ac1e94.yml
-openapi_spec_hash: 1f04cb5b36e92db81dfa264c2a59c32a
+openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel/kernel-4c243ff089133bd49322d98a6943647589972f71ecadc993fe9e5029972b3995.yml
+openapi_spec_hash: a2cb637a19a070d07a1a4343c75444ee
config_hash: fb167e754ebb3a14649463725891c9d0
diff --git a/src/kernel/resources/browser_pools.py b/src/kernel/resources/browser_pools.py
index 019489fa..842d3ec4 100644
--- a/src/kernel/resources/browser_pools.py
+++ b/src/kernel/resources/browser_pools.py
@@ -29,7 +29,6 @@
from ..types.tags_param import TagsParam
from ..types.browser_pool import BrowserPool
from ..types.browser_pool_acquire_response import BrowserPoolAcquireResponse
-from ..types.shared_params.browser_profile import BrowserProfile
from ..types.shared_params.browser_viewport import BrowserViewport
from ..types.shared_params.browser_extension import BrowserExtension
@@ -68,7 +67,7 @@ def create(
headless: bool | Omit = omit,
kiosk_mode: bool | Omit = omit,
name: str | Omit = omit,
- profile: BrowserProfile | Omit = omit,
+ profile: browser_pool_create_params.Profile | Omit = omit,
proxy_id: str | Omit = omit,
start_url: str | Omit = omit,
stealth: bool | Omit = omit,
@@ -81,8 +80,12 @@ def create(
extra_body: Body | None = None,
timeout: float | httpx.Timeout | None | NotGiven = not_given,
) -> BrowserPool:
- """
- Create a new browser pool with the specified configuration and size.
+ """Create a new browser pool with the specified configuration and size.
+
+ Pooled
+ browsers load their profile read-only: any save_changes on the profile is
+ ignored (not rejected), so pooled browsers never persist changes back to the
+ profile.
Args:
size: Number of browsers to maintain in the pool. The maximum size is determined by
@@ -107,9 +110,12 @@ def create(
name: Optional name for the browser pool. Must be unique within the project.
- profile: Profile selection for the browser session. Provide either id or name. If
- specified, the matching profile will be loaded into the browser session.
- Profiles must be created beforehand.
+ profile: Profile selection for browsers in a pool. Provide either id or name. The
+ matching profile is loaded into every browser in the pool. Profiles must be
+ created beforehand. Unlike single browser sessions, pools load the profile
+ read-only and never persist changes back to it, so save_changes is omitted here.
+ Any save_changes value sent on a pool profile is silently ignored rather than
+ rejected, so callers reusing a single-session profile object will not error.
proxy_id: Optional proxy to associate to the browser session. Must reference a proxy in
the same project as the browser session.
@@ -217,7 +223,7 @@ def update(
headless: bool | Omit = omit,
kiosk_mode: bool | Omit = omit,
name: str | Omit = omit,
- profile: BrowserProfile | Omit = omit,
+ profile: browser_pool_update_params.Profile | Omit = omit,
proxy_id: str | Omit = omit,
size: int | Omit = omit,
start_url: str | Omit = omit,
@@ -231,8 +237,11 @@ def update(
extra_body: Body | None = None,
timeout: float | httpx.Timeout | None | NotGiven = not_given,
) -> BrowserPool:
- """
- Updates the configuration used to create browsers in the pool.
+ """Updates the configuration used to create browsers in the pool.
+
+ As with creation,
+ save_changes on the pool profile is ignored (not rejected); pooled browsers
+ never persist changes back to the profile.
Args:
chrome_policy: Custom Chrome enterprise policy overrides applied to all browsers in this pool.
@@ -256,9 +265,12 @@ def update(
name: Optional name for the browser pool. Must be unique within the project.
- profile: Profile selection for the browser session. Provide either id or name. If
- specified, the matching profile will be loaded into the browser session.
- Profiles must be created beforehand.
+ profile: Profile selection for browsers in a pool. Provide either id or name. The
+ matching profile is loaded into every browser in the pool. Profiles must be
+ created beforehand. Unlike single browser sessions, pools load the profile
+ read-only and never persist changes back to it, so save_changes is omitted here.
+ Any save_changes value sent on a pool profile is silently ignored rather than
+ rejected, so callers reusing a single-session profile object will not error.
proxy_id: Optional proxy to associate to the browser session. Must reference a proxy in
the same project as the browser session.
@@ -603,7 +615,7 @@ async def create(
headless: bool | Omit = omit,
kiosk_mode: bool | Omit = omit,
name: str | Omit = omit,
- profile: BrowserProfile | Omit = omit,
+ profile: browser_pool_create_params.Profile | Omit = omit,
proxy_id: str | Omit = omit,
start_url: str | Omit = omit,
stealth: bool | Omit = omit,
@@ -616,8 +628,12 @@ async def create(
extra_body: Body | None = None,
timeout: float | httpx.Timeout | None | NotGiven = not_given,
) -> BrowserPool:
- """
- Create a new browser pool with the specified configuration and size.
+ """Create a new browser pool with the specified configuration and size.
+
+ Pooled
+ browsers load their profile read-only: any save_changes on the profile is
+ ignored (not rejected), so pooled browsers never persist changes back to the
+ profile.
Args:
size: Number of browsers to maintain in the pool. The maximum size is determined by
@@ -642,9 +658,12 @@ async def create(
name: Optional name for the browser pool. Must be unique within the project.
- profile: Profile selection for the browser session. Provide either id or name. If
- specified, the matching profile will be loaded into the browser session.
- Profiles must be created beforehand.
+ profile: Profile selection for browsers in a pool. Provide either id or name. The
+ matching profile is loaded into every browser in the pool. Profiles must be
+ created beforehand. Unlike single browser sessions, pools load the profile
+ read-only and never persist changes back to it, so save_changes is omitted here.
+ Any save_changes value sent on a pool profile is silently ignored rather than
+ rejected, so callers reusing a single-session profile object will not error.
proxy_id: Optional proxy to associate to the browser session. Must reference a proxy in
the same project as the browser session.
@@ -752,7 +771,7 @@ async def update(
headless: bool | Omit = omit,
kiosk_mode: bool | Omit = omit,
name: str | Omit = omit,
- profile: BrowserProfile | Omit = omit,
+ profile: browser_pool_update_params.Profile | Omit = omit,
proxy_id: str | Omit = omit,
size: int | Omit = omit,
start_url: str | Omit = omit,
@@ -766,8 +785,11 @@ async def update(
extra_body: Body | None = None,
timeout: float | httpx.Timeout | None | NotGiven = not_given,
) -> BrowserPool:
- """
- Updates the configuration used to create browsers in the pool.
+ """Updates the configuration used to create browsers in the pool.
+
+ As with creation,
+ save_changes on the pool profile is ignored (not rejected); pooled browsers
+ never persist changes back to the profile.
Args:
chrome_policy: Custom Chrome enterprise policy overrides applied to all browsers in this pool.
@@ -791,9 +813,12 @@ async def update(
name: Optional name for the browser pool. Must be unique within the project.
- profile: Profile selection for the browser session. Provide either id or name. If
- specified, the matching profile will be loaded into the browser session.
- Profiles must be created beforehand.
+ profile: Profile selection for browsers in a pool. Provide either id or name. The
+ matching profile is loaded into every browser in the pool. Profiles must be
+ created beforehand. Unlike single browser sessions, pools load the profile
+ read-only and never persist changes back to it, so save_changes is omitted here.
+ Any save_changes value sent on a pool profile is silently ignored rather than
+ rejected, so callers reusing a single-session profile object will not error.
proxy_id: Optional proxy to associate to the browser session. Must reference a proxy in
the same project as the browser session.
diff --git a/src/kernel/types/browser_pool.py b/src/kernel/types/browser_pool.py
index 877fbcde..6bceffe4 100644
--- a/src/kernel/types/browser_pool.py
+++ b/src/kernel/types/browser_pool.py
@@ -4,11 +4,30 @@
from datetime import datetime
from .._models import BaseModel
-from .shared.browser_profile import BrowserProfile
from .shared.browser_viewport import BrowserViewport
from .shared.browser_extension import BrowserExtension
-__all__ = ["BrowserPool", "BrowserPoolConfig"]
+__all__ = ["BrowserPool", "BrowserPoolConfig", "BrowserPoolConfigProfile"]
+
+
+class BrowserPoolConfigProfile(BaseModel):
+ """Profile selection for browsers in a pool.
+
+ Provide either id or name. The matching profile is
+ loaded into every browser in the pool. Profiles must be created beforehand. Unlike single
+ browser sessions, pools load the profile read-only and never persist changes back to it, so
+ save_changes is omitted here. Any save_changes value sent on a pool profile is silently
+ ignored rather than rejected, so callers reusing a single-session profile object will not error.
+ """
+
+ id: Optional[str] = None
+ """Profile ID to load for browsers in this pool"""
+
+ name: Optional[str] = None
+ """Profile name to load for browsers in this pool (instead of id).
+
+ Must be 1-255 characters, using letters, numbers, dots, underscores, or hyphens.
+ """
class BrowserPoolConfig(BaseModel):
@@ -54,11 +73,15 @@ class BrowserPoolConfig(BaseModel):
name: Optional[str] = None
"""Optional name for the browser pool. Must be unique within the project."""
- profile: Optional[BrowserProfile] = None
- """Profile selection for the browser session.
+ profile: Optional[BrowserPoolConfigProfile] = None
+ """Profile selection for browsers in a pool.
- Provide either id or name. If specified, the matching profile will be loaded
- into the browser session. Profiles must be created beforehand.
+ Provide either id or name. The matching profile is loaded into every browser in
+ the pool. Profiles must be created beforehand. Unlike single browser sessions,
+ pools load the profile read-only and never persist changes back to it, so
+ save_changes is omitted here. Any save_changes value sent on a pool profile is
+ silently ignored rather than rejected, so callers reusing a single-session
+ profile object will not error.
"""
proxy_id: Optional[str] = None
diff --git a/src/kernel/types/browser_pool_create_params.py b/src/kernel/types/browser_pool_create_params.py
index d404d4e8..f629691f 100644
--- a/src/kernel/types/browser_pool_create_params.py
+++ b/src/kernel/types/browser_pool_create_params.py
@@ -5,11 +5,10 @@
from typing import Dict, Iterable
from typing_extensions import Required, TypedDict
-from .shared_params.browser_profile import BrowserProfile
from .shared_params.browser_viewport import BrowserViewport
from .shared_params.browser_extension import BrowserExtension
-__all__ = ["BrowserPoolCreateParams"]
+__all__ = ["BrowserPoolCreateParams", "Profile"]
class BrowserPoolCreateParams(TypedDict, total=False):
@@ -53,11 +52,15 @@ class BrowserPoolCreateParams(TypedDict, total=False):
name: str
"""Optional name for the browser pool. Must be unique within the project."""
- profile: BrowserProfile
- """Profile selection for the browser session.
+ profile: Profile
+ """Profile selection for browsers in a pool.
- Provide either id or name. If specified, the matching profile will be loaded
- into the browser session. Profiles must be created beforehand.
+ Provide either id or name. The matching profile is loaded into every browser in
+ the pool. Profiles must be created beforehand. Unlike single browser sessions,
+ pools load the profile read-only and never persist changes back to it, so
+ save_changes is omitted here. Any save_changes value sent on a pool profile is
+ silently ignored rather than rejected, so callers reusing a single-session
+ profile object will not error.
"""
proxy_id: str
@@ -102,3 +105,23 @@ class BrowserPoolCreateParams(TypedDict, total=False):
based on the resolution (higher resolutions use lower refresh rates to keep
bandwidth reasonable).
"""
+
+
+class Profile(TypedDict, total=False):
+ """Profile selection for browsers in a pool.
+
+ Provide either id or name. The matching profile is
+ loaded into every browser in the pool. Profiles must be created beforehand. Unlike single
+ browser sessions, pools load the profile read-only and never persist changes back to it, so
+ save_changes is omitted here. Any save_changes value sent on a pool profile is silently
+ ignored rather than rejected, so callers reusing a single-session profile object will not error.
+ """
+
+ id: str
+ """Profile ID to load for browsers in this pool"""
+
+ name: str
+ """Profile name to load for browsers in this pool (instead of id).
+
+ Must be 1-255 characters, using letters, numbers, dots, underscores, or hyphens.
+ """
diff --git a/src/kernel/types/browser_pool_update_params.py b/src/kernel/types/browser_pool_update_params.py
index 79e8172b..2688f235 100644
--- a/src/kernel/types/browser_pool_update_params.py
+++ b/src/kernel/types/browser_pool_update_params.py
@@ -5,11 +5,10 @@
from typing import Dict, Iterable
from typing_extensions import TypedDict
-from .shared_params.browser_profile import BrowserProfile
from .shared_params.browser_viewport import BrowserViewport
from .shared_params.browser_extension import BrowserExtension
-__all__ = ["BrowserPoolUpdateParams"]
+__all__ = ["BrowserPoolUpdateParams", "Profile"]
class BrowserPoolUpdateParams(TypedDict, total=False):
@@ -52,11 +51,15 @@ class BrowserPoolUpdateParams(TypedDict, total=False):
name: str
"""Optional name for the browser pool. Must be unique within the project."""
- profile: BrowserProfile
- """Profile selection for the browser session.
+ profile: Profile
+ """Profile selection for browsers in a pool.
- Provide either id or name. If specified, the matching profile will be loaded
- into the browser session. Profiles must be created beforehand.
+ Provide either id or name. The matching profile is loaded into every browser in
+ the pool. Profiles must be created beforehand. Unlike single browser sessions,
+ pools load the profile read-only and never persist changes back to it, so
+ save_changes is omitted here. Any save_changes value sent on a pool profile is
+ silently ignored rather than rejected, so callers reusing a single-session
+ profile object will not error.
"""
proxy_id: str
@@ -108,3 +111,23 @@ class BrowserPoolUpdateParams(TypedDict, total=False):
based on the resolution (higher resolutions use lower refresh rates to keep
bandwidth reasonable).
"""
+
+
+class Profile(TypedDict, total=False):
+ """Profile selection for browsers in a pool.
+
+ Provide either id or name. The matching profile is
+ loaded into every browser in the pool. Profiles must be created beforehand. Unlike single
+ browser sessions, pools load the profile read-only and never persist changes back to it, so
+ save_changes is omitted here. Any save_changes value sent on a pool profile is silently
+ ignored rather than rejected, so callers reusing a single-session profile object will not error.
+ """
+
+ id: str
+ """Profile ID to load for browsers in this pool"""
+
+ name: str
+ """Profile name to load for browsers in this pool (instead of id).
+
+ Must be 1-255 characters, using letters, numbers, dots, underscores, or hyphens.
+ """
diff --git a/tests/api_resources/test_browser_pools.py b/tests/api_resources/test_browser_pools.py
index ba603e7c..c7e8bdb4 100644
--- a/tests/api_resources/test_browser_pools.py
+++ b/tests/api_resources/test_browser_pools.py
@@ -48,7 +48,6 @@ def test_method_create_with_all_params(self, client: Kernel) -> None:
profile={
"id": "id",
"name": "name",
- "save_changes": True,
},
proxy_id="proxy_id",
start_url="https://example.com",
@@ -158,7 +157,6 @@ def test_method_update_with_all_params(self, client: Kernel) -> None:
profile={
"id": "id",
"name": "name",
- "save_changes": True,
},
proxy_id="proxy_id",
size=10,
@@ -484,7 +482,6 @@ async def test_method_create_with_all_params(self, async_client: AsyncKernel) ->
profile={
"id": "id",
"name": "name",
- "save_changes": True,
},
proxy_id="proxy_id",
start_url="https://example.com",
@@ -594,7 +591,6 @@ async def test_method_update_with_all_params(self, async_client: AsyncKernel) ->
profile={
"id": "id",
"name": "name",
- "save_changes": True,
},
proxy_id="proxy_id",
size=10,
From 70d42140842bd9baa98c762277d09415bd6dd01f Mon Sep 17 00:00:00 2001
From: "stainless-app[bot]"
<142633134+stainless-app[bot]@users.noreply.github.com>
Date: Fri, 26 Jun 2026 19:10:49 +0000
Subject: [PATCH 093/366] release: 0.72.0
---
.release-please-manifest.json | 2 +-
CHANGELOG.md | 8 ++++++++
pyproject.toml | 2 +-
src/kernel/_version.py | 2 +-
4 files changed, 11 insertions(+), 3 deletions(-)
diff --git a/.release-please-manifest.json b/.release-please-manifest.json
index 6772f01d..6f257c8d 100644
--- a/.release-please-manifest.json
+++ b/.release-please-manifest.json
@@ -1,3 +1,3 @@
{
- ".": "0.71.0"
+ ".": "0.72.0"
}
\ No newline at end of file
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 35090e2b..ab94a974 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,13 @@
# Changelog
+## 0.72.0 (2026-06-26)
+
+Full Changelog: [v0.71.0...v0.72.0](https://github.com/kernel/kernel-python-sdk/compare/v0.71.0...v0.72.0)
+
+### Bug Fixes
+
+* **api:** browser pool profile omits save_changes (BrowserPoolProfile) ([34423fb](https://github.com/kernel/kernel-python-sdk/commit/34423fb99af7f43b7b9a6a023a4ec85902b1bcb2))
+
## 0.71.0 (2026-06-26)
Full Changelog: [v0.70.0...v0.71.0](https://github.com/kernel/kernel-python-sdk/compare/v0.70.0...v0.71.0)
diff --git a/pyproject.toml b/pyproject.toml
index 9187ed7e..1649adae 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -1,6 +1,6 @@
[project]
name = "kernel"
-version = "0.71.0"
+version = "0.72.0"
description = "The official Python library for the kernel API"
dynamic = ["readme"]
license = "Apache-2.0"
diff --git a/src/kernel/_version.py b/src/kernel/_version.py
index 0bf99948..4584cbe8 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.71.0" # x-release-please-version
+__version__ = "0.72.0" # x-release-please-version
From e80d1502de9fe59ebf47b985be63515b11d61086 Mon Sep 17 00:00:00 2001
From: "stainless-app[bot]"
<142633134+stainless-app[bot]@users.noreply.github.com>
Date: Wed, 1 Jul 2026 20:46:46 +0000
Subject: [PATCH 094/366] feat: Add hidden audit-logs export endpoint
---
.stats.yml | 8 +-
api.md | 1 +
src/kernel/resources/audit_logs.py | 199 +++++++++++++++++-
src/kernel/types/__init__.py | 1 +
.../types/audit_log_export_chunk_params.py | 47 +++++
tests/api_resources/test_audit_logs.py | 142 +++++++++++++
6 files changed, 392 insertions(+), 6 deletions(-)
create mode 100644 src/kernel/types/audit_log_export_chunk_params.py
diff --git a/.stats.yml b/.stats.yml
index 10d90226..91180822 100644
--- a/.stats.yml
+++ b/.stats.yml
@@ -1,4 +1,4 @@
-configured_endpoints: 124
-openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel/kernel-4c243ff089133bd49322d98a6943647589972f71ecadc993fe9e5029972b3995.yml
-openapi_spec_hash: a2cb637a19a070d07a1a4343c75444ee
-config_hash: fb167e754ebb3a14649463725891c9d0
+configured_endpoints: 125
+openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel/kernel-55409ab573762d8bc010fb34c885651ca858a97d4353b4776b7aafeaaa313257.yml
+openapi_spec_hash: 0cf678d80f2a2b73fb9ec82d05c8cc0a
+config_hash: 06186eb40e0058a2a87ac251fc07415d
diff --git a/api.md b/api.md
index 5c1c6b9c..300e937b 100644
--- a/api.md
+++ b/api.md
@@ -454,6 +454,7 @@ from kernel.types import AuditLogEntry
Methods:
- client.audit_logs.list(\*\*params) -> SyncPageTokenPagination[AuditLogEntry]
+- client.audit_logs.export_chunk(\*\*params) -> BinaryAPIResponse
# APIKeys
diff --git a/src/kernel/resources/audit_logs.py b/src/kernel/resources/audit_logs.py
index bce8b719..814db9c7 100644
--- a/src/kernel/resources/audit_logs.py
+++ b/src/kernel/resources/audit_logs.py
@@ -4,19 +4,28 @@
from typing import Union
from datetime import datetime
+from typing_extensions import Literal
import httpx
-from ..types import audit_log_list_params
+from ..types import audit_log_list_params, audit_log_export_chunk_params
from .._types import Body, Omit, Query, Headers, NotGiven, SequenceNotStr, omit, not_given
-from .._utils import maybe_transform
+from .._utils import maybe_transform, async_maybe_transform
from .._compat import cached_property
from .._resource import SyncAPIResource, AsyncAPIResource
from .._response import (
+ BinaryAPIResponse,
+ AsyncBinaryAPIResponse,
+ StreamedBinaryAPIResponse,
+ AsyncStreamedBinaryAPIResponse,
to_raw_response_wrapper,
to_streamed_response_wrapper,
async_to_raw_response_wrapper,
+ to_custom_raw_response_wrapper,
async_to_streamed_response_wrapper,
+ to_custom_streamed_response_wrapper,
+ async_to_custom_raw_response_wrapper,
+ async_to_custom_streamed_response_wrapper,
)
from ..pagination import SyncPageTokenPagination, AsyncPageTokenPagination
from .._base_client import AsyncPaginator, make_request_options
@@ -128,6 +137,91 @@ def list(
model=AuditLogEntry,
)
+ def export_chunk(
+ self,
+ *,
+ end: Union[str, datetime],
+ start: Union[str, datetime],
+ auth_strategy: str | Omit = omit,
+ cursor: str | Omit = omit,
+ exclude_method: str | Omit = omit,
+ format: Literal["jsonl", "jsonl.gz"] | Omit = omit,
+ limit: int | Omit = omit,
+ method: str | Omit = omit,
+ search: str | Omit = omit,
+ search_user_id: SequenceNotStr[str] | Omit = omit,
+ service: str | Omit = omit,
+ # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
+ # The extra values given here take precedence over values defined on the client or passed to this method.
+ extra_headers: Headers | None = None,
+ extra_query: Query | None = None,
+ extra_body: Body | None = None,
+ timeout: float | httpx.Timeout | None | NotGiven = not_given,
+ ) -> BinaryAPIResponse:
+ """
+ Download an organization's audit log records for a time range as a file, for
+ archival, compliance, or offline analysis. For interactive browsing, use GET
+ /audit-logs.
+
+ Args:
+ end: Upper bound (exclusive) for the audit record timestamp.
+
+ start: Lower bound (inclusive) for the audit record timestamp.
+
+ auth_strategy: Filter by authentication strategy.
+
+ cursor: Opaque cursor from X-Next-Cursor for the next chunk of older records.
+
+ exclude_method: Filter out results by HTTP method.
+
+ format: Encoding for the returned chunk.
+
+ limit: Maximum number of records to return in this chunk.
+
+ method: Filter by HTTP method.
+
+ search: Free-text search over path, user ID, email, client IP, and status.
+
+ search_user_id: Additional user IDs to OR into free-text search.
+
+ service: Filter by service name.
+
+ 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
+ """
+ extra_headers = {"Accept": "application/octet-stream", **(extra_headers or {})}
+ return self._get(
+ "/audit-logs/export/chunk",
+ options=make_request_options(
+ extra_headers=extra_headers,
+ extra_query=extra_query,
+ extra_body=extra_body,
+ timeout=timeout,
+ query=maybe_transform(
+ {
+ "end": end,
+ "start": start,
+ "auth_strategy": auth_strategy,
+ "cursor": cursor,
+ "exclude_method": exclude_method,
+ "format": format,
+ "limit": limit,
+ "method": method,
+ "search": search,
+ "search_user_id": search_user_id,
+ "service": service,
+ },
+ audit_log_export_chunk_params.AuditLogExportChunkParams,
+ ),
+ ),
+ cast_to=BinaryAPIResponse,
+ )
+
class AsyncAuditLogsResource(AsyncAPIResource):
"""Read audit log records for the authenticated organization."""
@@ -232,6 +326,91 @@ def list(
model=AuditLogEntry,
)
+ async def export_chunk(
+ self,
+ *,
+ end: Union[str, datetime],
+ start: Union[str, datetime],
+ auth_strategy: str | Omit = omit,
+ cursor: str | Omit = omit,
+ exclude_method: str | Omit = omit,
+ format: Literal["jsonl", "jsonl.gz"] | Omit = omit,
+ limit: int | Omit = omit,
+ method: str | Omit = omit,
+ search: str | Omit = omit,
+ search_user_id: SequenceNotStr[str] | Omit = omit,
+ service: str | Omit = omit,
+ # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
+ # The extra values given here take precedence over values defined on the client or passed to this method.
+ extra_headers: Headers | None = None,
+ extra_query: Query | None = None,
+ extra_body: Body | None = None,
+ timeout: float | httpx.Timeout | None | NotGiven = not_given,
+ ) -> AsyncBinaryAPIResponse:
+ """
+ Download an organization's audit log records for a time range as a file, for
+ archival, compliance, or offline analysis. For interactive browsing, use GET
+ /audit-logs.
+
+ Args:
+ end: Upper bound (exclusive) for the audit record timestamp.
+
+ start: Lower bound (inclusive) for the audit record timestamp.
+
+ auth_strategy: Filter by authentication strategy.
+
+ cursor: Opaque cursor from X-Next-Cursor for the next chunk of older records.
+
+ exclude_method: Filter out results by HTTP method.
+
+ format: Encoding for the returned chunk.
+
+ limit: Maximum number of records to return in this chunk.
+
+ method: Filter by HTTP method.
+
+ search: Free-text search over path, user ID, email, client IP, and status.
+
+ search_user_id: Additional user IDs to OR into free-text search.
+
+ service: Filter by service name.
+
+ 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
+ """
+ extra_headers = {"Accept": "application/octet-stream", **(extra_headers or {})}
+ return await self._get(
+ "/audit-logs/export/chunk",
+ options=make_request_options(
+ extra_headers=extra_headers,
+ extra_query=extra_query,
+ extra_body=extra_body,
+ timeout=timeout,
+ query=await async_maybe_transform(
+ {
+ "end": end,
+ "start": start,
+ "auth_strategy": auth_strategy,
+ "cursor": cursor,
+ "exclude_method": exclude_method,
+ "format": format,
+ "limit": limit,
+ "method": method,
+ "search": search,
+ "search_user_id": search_user_id,
+ "service": service,
+ },
+ audit_log_export_chunk_params.AuditLogExportChunkParams,
+ ),
+ ),
+ cast_to=AsyncBinaryAPIResponse,
+ )
+
class AuditLogsResourceWithRawResponse:
def __init__(self, audit_logs: AuditLogsResource) -> None:
@@ -240,6 +419,10 @@ def __init__(self, audit_logs: AuditLogsResource) -> None:
self.list = to_raw_response_wrapper(
audit_logs.list,
)
+ self.export_chunk = to_custom_raw_response_wrapper(
+ audit_logs.export_chunk,
+ BinaryAPIResponse,
+ )
class AsyncAuditLogsResourceWithRawResponse:
@@ -249,6 +432,10 @@ def __init__(self, audit_logs: AsyncAuditLogsResource) -> None:
self.list = async_to_raw_response_wrapper(
audit_logs.list,
)
+ self.export_chunk = async_to_custom_raw_response_wrapper(
+ audit_logs.export_chunk,
+ AsyncBinaryAPIResponse,
+ )
class AuditLogsResourceWithStreamingResponse:
@@ -258,6 +445,10 @@ def __init__(self, audit_logs: AuditLogsResource) -> None:
self.list = to_streamed_response_wrapper(
audit_logs.list,
)
+ self.export_chunk = to_custom_streamed_response_wrapper(
+ audit_logs.export_chunk,
+ StreamedBinaryAPIResponse,
+ )
class AsyncAuditLogsResourceWithStreamingResponse:
@@ -267,3 +458,7 @@ def __init__(self, audit_logs: AsyncAuditLogsResource) -> None:
self.list = async_to_streamed_response_wrapper(
audit_logs.list,
)
+ self.export_chunk = async_to_custom_streamed_response_wrapper(
+ audit_logs.export_chunk,
+ AsyncStreamedBinaryAPIResponse,
+ )
diff --git a/src/kernel/types/__init__.py b/src/kernel/types/__init__.py
index 357afc98..49af1d5c 100644
--- a/src/kernel/types/__init__.py
+++ b/src/kernel/types/__init__.py
@@ -90,6 +90,7 @@
from .browser_pool_release_params import BrowserPoolReleaseParams as BrowserPoolReleaseParams
from .deployment_retrieve_response import DeploymentRetrieveResponse as DeploymentRetrieveResponse
from .invocation_retrieve_response import InvocationRetrieveResponse as InvocationRetrieveResponse
+from .audit_log_export_chunk_params import AuditLogExportChunkParams as AuditLogExportChunkParams
from .browser_pool_acquire_response import BrowserPoolAcquireResponse as BrowserPoolAcquireResponse
from .credential_totp_code_response import CredentialTotpCodeResponse as CredentialTotpCodeResponse
from .browser_load_extensions_params import BrowserLoadExtensionsParams as BrowserLoadExtensionsParams
diff --git a/src/kernel/types/audit_log_export_chunk_params.py b/src/kernel/types/audit_log_export_chunk_params.py
new file mode 100644
index 00000000..8e759c9f
--- /dev/null
+++ b/src/kernel/types/audit_log_export_chunk_params.py
@@ -0,0 +1,47 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from __future__ import annotations
+
+from typing import Union
+from datetime import datetime
+from typing_extensions import Literal, Required, Annotated, TypedDict
+
+from .._types import SequenceNotStr
+from .._utils import PropertyInfo
+
+__all__ = ["AuditLogExportChunkParams"]
+
+
+class AuditLogExportChunkParams(TypedDict, total=False):
+ end: Required[Annotated[Union[str, datetime], PropertyInfo(format="iso8601")]]
+ """Upper bound (exclusive) for the audit record timestamp."""
+
+ start: Required[Annotated[Union[str, datetime], PropertyInfo(format="iso8601")]]
+ """Lower bound (inclusive) for the audit record timestamp."""
+
+ auth_strategy: str
+ """Filter by authentication strategy."""
+
+ cursor: str
+ """Opaque cursor from X-Next-Cursor for the next chunk of older records."""
+
+ exclude_method: str
+ """Filter out results by HTTP method."""
+
+ format: Literal["jsonl", "jsonl.gz"]
+ """Encoding for the returned chunk."""
+
+ limit: int
+ """Maximum number of records to return in this chunk."""
+
+ method: str
+ """Filter by HTTP method."""
+
+ search: str
+ """Free-text search over path, user ID, email, client IP, and status."""
+
+ search_user_id: SequenceNotStr[str]
+ """Additional user IDs to OR into free-text search."""
+
+ service: str
+ """Filter by service name."""
diff --git a/tests/api_resources/test_audit_logs.py b/tests/api_resources/test_audit_logs.py
index 669a8872..47193f05 100644
--- a/tests/api_resources/test_audit_logs.py
+++ b/tests/api_resources/test_audit_logs.py
@@ -5,12 +5,20 @@
import os
from typing import Any, cast
+import httpx
import pytest
+from respx import MockRouter
from kernel import Kernel, AsyncKernel
from tests.utils import assert_matches_type
from kernel.types import AuditLogEntry
from kernel._utils import parse_datetime
+from kernel._response import (
+ BinaryAPIResponse,
+ AsyncBinaryAPIResponse,
+ StreamedBinaryAPIResponse,
+ AsyncStreamedBinaryAPIResponse,
+)
from kernel.pagination import SyncPageTokenPagination, AsyncPageTokenPagination
base_url = os.environ.get("TEST_API_BASE_URL", "http://127.0.0.1:4010")
@@ -73,6 +81,73 @@ def test_streaming_response_list(self, client: Kernel) -> None:
assert cast(Any, response.is_closed) is True
+ @parametrize
+ @pytest.mark.respx(base_url=base_url)
+ def test_method_export_chunk(self, client: Kernel, respx_mock: MockRouter) -> None:
+ respx_mock.get("/audit-logs/export/chunk").mock(return_value=httpx.Response(200, json={"foo": "bar"}))
+ audit_log = client.audit_logs.export_chunk(
+ end=parse_datetime("2026-01-02T00:00:00Z"),
+ start=parse_datetime("2026-01-01T00:00:00Z"),
+ )
+ assert audit_log.is_closed
+ assert audit_log.json() == {"foo": "bar"}
+ assert cast(Any, audit_log.is_closed) is True
+ assert isinstance(audit_log, BinaryAPIResponse)
+
+ @parametrize
+ @pytest.mark.respx(base_url=base_url)
+ def test_method_export_chunk_with_all_params(self, client: Kernel, respx_mock: MockRouter) -> None:
+ respx_mock.get("/audit-logs/export/chunk").mock(return_value=httpx.Response(200, json={"foo": "bar"}))
+ audit_log = client.audit_logs.export_chunk(
+ end=parse_datetime("2026-01-02T00:00:00Z"),
+ start=parse_datetime("2026-01-01T00:00:00Z"),
+ auth_strategy="auth_strategy",
+ cursor="cursor",
+ exclude_method="exclude_method",
+ format="jsonl",
+ limit=1,
+ method="method",
+ search="search",
+ search_user_id=["string"],
+ service="service",
+ )
+ assert audit_log.is_closed
+ assert audit_log.json() == {"foo": "bar"}
+ assert cast(Any, audit_log.is_closed) is True
+ assert isinstance(audit_log, BinaryAPIResponse)
+
+ @parametrize
+ @pytest.mark.respx(base_url=base_url)
+ def test_raw_response_export_chunk(self, client: Kernel, respx_mock: MockRouter) -> None:
+ respx_mock.get("/audit-logs/export/chunk").mock(return_value=httpx.Response(200, json={"foo": "bar"}))
+
+ audit_log = client.audit_logs.with_raw_response.export_chunk(
+ end=parse_datetime("2026-01-02T00:00:00Z"),
+ start=parse_datetime("2026-01-01T00:00:00Z"),
+ )
+
+ assert audit_log.is_closed is True
+ assert audit_log.http_request.headers.get("X-Stainless-Lang") == "python"
+ assert audit_log.json() == {"foo": "bar"}
+ assert isinstance(audit_log, BinaryAPIResponse)
+
+ @parametrize
+ @pytest.mark.respx(base_url=base_url)
+ def test_streaming_response_export_chunk(self, client: Kernel, respx_mock: MockRouter) -> None:
+ respx_mock.get("/audit-logs/export/chunk").mock(return_value=httpx.Response(200, json={"foo": "bar"}))
+ with client.audit_logs.with_streaming_response.export_chunk(
+ end=parse_datetime("2026-01-02T00:00:00Z"),
+ start=parse_datetime("2026-01-01T00:00:00Z"),
+ ) as audit_log:
+ assert not audit_log.is_closed
+ assert audit_log.http_request.headers.get("X-Stainless-Lang") == "python"
+
+ assert audit_log.json() == {"foo": "bar"}
+ assert cast(Any, audit_log.is_closed) is True
+ assert isinstance(audit_log, StreamedBinaryAPIResponse)
+
+ assert cast(Any, audit_log.is_closed) is True
+
class TestAsyncAuditLogs:
parametrize = pytest.mark.parametrize(
@@ -132,3 +207,70 @@ async def test_streaming_response_list(self, async_client: AsyncKernel) -> None:
assert_matches_type(AsyncPageTokenPagination[AuditLogEntry], audit_log, path=["response"])
assert cast(Any, response.is_closed) is True
+
+ @parametrize
+ @pytest.mark.respx(base_url=base_url)
+ async def test_method_export_chunk(self, async_client: AsyncKernel, respx_mock: MockRouter) -> None:
+ respx_mock.get("/audit-logs/export/chunk").mock(return_value=httpx.Response(200, json={"foo": "bar"}))
+ audit_log = await async_client.audit_logs.export_chunk(
+ end=parse_datetime("2026-01-02T00:00:00Z"),
+ start=parse_datetime("2026-01-01T00:00:00Z"),
+ )
+ assert audit_log.is_closed
+ assert await audit_log.json() == {"foo": "bar"}
+ assert cast(Any, audit_log.is_closed) is True
+ assert isinstance(audit_log, AsyncBinaryAPIResponse)
+
+ @parametrize
+ @pytest.mark.respx(base_url=base_url)
+ async def test_method_export_chunk_with_all_params(self, async_client: AsyncKernel, respx_mock: MockRouter) -> None:
+ respx_mock.get("/audit-logs/export/chunk").mock(return_value=httpx.Response(200, json={"foo": "bar"}))
+ audit_log = await async_client.audit_logs.export_chunk(
+ end=parse_datetime("2026-01-02T00:00:00Z"),
+ start=parse_datetime("2026-01-01T00:00:00Z"),
+ auth_strategy="auth_strategy",
+ cursor="cursor",
+ exclude_method="exclude_method",
+ format="jsonl",
+ limit=1,
+ method="method",
+ search="search",
+ search_user_id=["string"],
+ service="service",
+ )
+ assert audit_log.is_closed
+ assert await audit_log.json() == {"foo": "bar"}
+ assert cast(Any, audit_log.is_closed) is True
+ assert isinstance(audit_log, AsyncBinaryAPIResponse)
+
+ @parametrize
+ @pytest.mark.respx(base_url=base_url)
+ async def test_raw_response_export_chunk(self, async_client: AsyncKernel, respx_mock: MockRouter) -> None:
+ respx_mock.get("/audit-logs/export/chunk").mock(return_value=httpx.Response(200, json={"foo": "bar"}))
+
+ audit_log = await async_client.audit_logs.with_raw_response.export_chunk(
+ end=parse_datetime("2026-01-02T00:00:00Z"),
+ start=parse_datetime("2026-01-01T00:00:00Z"),
+ )
+
+ assert audit_log.is_closed is True
+ assert audit_log.http_request.headers.get("X-Stainless-Lang") == "python"
+ assert await audit_log.json() == {"foo": "bar"}
+ assert isinstance(audit_log, AsyncBinaryAPIResponse)
+
+ @parametrize
+ @pytest.mark.respx(base_url=base_url)
+ async def test_streaming_response_export_chunk(self, async_client: AsyncKernel, respx_mock: MockRouter) -> None:
+ respx_mock.get("/audit-logs/export/chunk").mock(return_value=httpx.Response(200, json={"foo": "bar"}))
+ async with async_client.audit_logs.with_streaming_response.export_chunk(
+ end=parse_datetime("2026-01-02T00:00:00Z"),
+ start=parse_datetime("2026-01-01T00:00:00Z"),
+ ) as audit_log:
+ assert not audit_log.is_closed
+ assert audit_log.http_request.headers.get("X-Stainless-Lang") == "python"
+
+ assert await audit_log.json() == {"foo": "bar"}
+ assert cast(Any, audit_log.is_closed) is True
+ assert isinstance(audit_log, AsyncStreamedBinaryAPIResponse)
+
+ assert cast(Any, audit_log.is_closed) is True
From 6ebd62aac4f448d3cb5bfd68de664b796924a693 Mon Sep 17 00:00:00 2001
From: "stainless-app[bot]"
<142633134+stainless-app[bot]@users.noreply.github.com>
Date: Wed, 1 Jul 2026 20:47:13 +0000
Subject: [PATCH 095/366] release: 0.73.0
---
.release-please-manifest.json | 2 +-
CHANGELOG.md | 8 ++++++++
pyproject.toml | 2 +-
src/kernel/_version.py | 2 +-
4 files changed, 11 insertions(+), 3 deletions(-)
diff --git a/.release-please-manifest.json b/.release-please-manifest.json
index 6f257c8d..f910f1a9 100644
--- a/.release-please-manifest.json
+++ b/.release-please-manifest.json
@@ -1,3 +1,3 @@
{
- ".": "0.72.0"
+ ".": "0.73.0"
}
\ No newline at end of file
diff --git a/CHANGELOG.md b/CHANGELOG.md
index ab94a974..238e659b 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,13 @@
# Changelog
+## 0.73.0 (2026-07-01)
+
+Full Changelog: [v0.72.0...v0.73.0](https://github.com/kernel/kernel-python-sdk/compare/v0.72.0...v0.73.0)
+
+### Features
+
+* Add hidden audit-logs export endpoint ([619c672](https://github.com/kernel/kernel-python-sdk/commit/619c6720f986b835393793896bb5ad4b64f8673f))
+
## 0.72.0 (2026-06-26)
Full Changelog: [v0.71.0...v0.72.0](https://github.com/kernel/kernel-python-sdk/compare/v0.71.0...v0.72.0)
diff --git a/pyproject.toml b/pyproject.toml
index 1649adae..8188c8f8 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -1,6 +1,6 @@
[project]
name = "kernel"
-version = "0.72.0"
+version = "0.73.0"
description = "The official Python library for the kernel API"
dynamic = ["readme"]
license = "Apache-2.0"
diff --git a/src/kernel/_version.py b/src/kernel/_version.py
index 4584cbe8..0bd70601 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.72.0" # x-release-please-version
+__version__ = "0.73.0" # x-release-please-version
From bd4f0fff33b22a42637b4e715cafe2679db897f9 Mon Sep 17 00:00:00 2001
From: "stainless-app[bot]"
<142633134+stainless-app[bot]@users.noreply.github.com>
Date: Thu, 2 Jul 2026 16:50:23 +0000
Subject: [PATCH 096/366] feat: Add tablet and mobile viewport presets to pool
dashboard
---
.stats.yml | 4 +-
src/kernel/resources/browser_pools.py | 64 +++++++++----------
src/kernel/resources/browsers/browsers.py | 32 +++++-----
src/kernel/types/browser_create_params.py | 16 ++---
src/kernel/types/browser_create_response.py | 16 ++---
src/kernel/types/browser_list_response.py | 16 ++---
src/kernel/types/browser_pool.py | 16 ++---
.../types/browser_pool_acquire_response.py | 16 ++---
.../types/browser_pool_create_params.py | 16 ++---
.../types/browser_pool_update_params.py | 16 ++---
src/kernel/types/browser_retrieve_response.py | 16 ++---
src/kernel/types/browser_update_response.py | 16 ++---
.../invocation_list_browsers_response.py | 16 ++---
src/kernel/types/shared/browser_viewport.py | 2 +-
.../types/shared_params/browser_viewport.py | 2 +-
15 files changed, 132 insertions(+), 132 deletions(-)
diff --git a/.stats.yml b/.stats.yml
index 91180822..a2cf0466 100644
--- a/.stats.yml
+++ b/.stats.yml
@@ -1,4 +1,4 @@
configured_endpoints: 125
-openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel/kernel-55409ab573762d8bc010fb34c885651ca858a97d4353b4776b7aafeaaa313257.yml
-openapi_spec_hash: 0cf678d80f2a2b73fb9ec82d05c8cc0a
+openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel/kernel-ea36e417a5dbc80a7dab65b38bd6c892520929d31363e626a3341436d697a17c.yml
+openapi_spec_hash: fb15f771cbf9c937a4674f23f9a56050
config_hash: 06186eb40e0058a2a87ac251fc07415d
diff --git a/src/kernel/resources/browser_pools.py b/src/kernel/resources/browser_pools.py
index 842d3ec4..6df691be 100644
--- a/src/kernel/resources/browser_pools.py
+++ b/src/kernel/resources/browser_pools.py
@@ -136,14 +136,14 @@ def create(
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).
+ 1440x900@25, 1280x800@60, 1024x768@60, 1200x800@60, 768x1024@60, 390x844@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).
extra_headers: Send extra headers
@@ -295,14 +295,14 @@ def update(
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).
+ 1440x900@25, 1280x800@60, 1024x768@60, 1200x800@60, 768x1024@60, 390x844@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).
extra_headers: Send extra headers
@@ -684,14 +684,14 @@ async def create(
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).
+ 1440x900@25, 1280x800@60, 1024x768@60, 1200x800@60, 768x1024@60, 390x844@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).
extra_headers: Send extra headers
@@ -843,14 +843,14 @@ async def update(
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).
+ 1440x900@25, 1280x800@60, 1024x768@60, 1200x800@60, 768x1024@60, 390x844@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).
extra_headers: Send extra headers
diff --git a/src/kernel/resources/browsers/browsers.py b/src/kernel/resources/browsers/browsers.py
index a5564b84..ae4279ed 100644
--- a/src/kernel/resources/browsers/browsers.py
+++ b/src/kernel/resources/browsers/browsers.py
@@ -246,14 +246,14 @@ def create(
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).
+ 1440x900@25, 1280x800@60, 1024x768@60, 1200x800@60, 768x1024@60, 390x844@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).
extra_headers: Send extra headers
@@ -824,14 +824,14 @@ async def create(
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).
+ 1440x900@25, 1280x800@60, 1024x768@60, 1200x800@60, 768x1024@60, 390x844@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).
extra_headers: Send extra headers
diff --git a/src/kernel/types/browser_create_params.py b/src/kernel/types/browser_create_params.py
index 10d53936..f76cc23f 100644
--- a/src/kernel/types/browser_create_params.py
+++ b/src/kernel/types/browser_create_params.py
@@ -114,14 +114,14 @@ class BrowserCreateParams(TypedDict, total=False):
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).
+ 1440x900@25, 1280x800@60, 1024x768@60, 1200x800@60, 768x1024@60, 390x844@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 ec54e598..8008e6de 100644
--- a/src/kernel/types/browser_create_response.py
+++ b/src/kernel/types/browser_create_response.py
@@ -103,12 +103,12 @@ class BrowserCreateResponse(BaseModel):
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).
+ 1440x900@25, 1280x800@60, 1024x768@60, 1200x800@60, 768x1024@60, 390x844@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 4de6bb00..79e1c44f 100644
--- a/src/kernel/types/browser_list_response.py
+++ b/src/kernel/types/browser_list_response.py
@@ -103,12 +103,12 @@ class BrowserListResponse(BaseModel):
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).
+ 1440x900@25, 1280x800@60, 1024x768@60, 1200x800@60, 768x1024@60, 390x844@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 6bceffe4..0b77264c 100644
--- a/src/kernel/types/browser_pool.py
+++ b/src/kernel/types/browser_pool.py
@@ -117,14 +117,14 @@ class BrowserPoolConfig(BaseModel):
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).
+ 1440x900@25, 1280x800@60, 1024x768@60, 1200x800@60, 768x1024@60, 390x844@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 6d3b0b68..45fe471d 100644
--- a/src/kernel/types/browser_pool_acquire_response.py
+++ b/src/kernel/types/browser_pool_acquire_response.py
@@ -103,12 +103,12 @@ class BrowserPoolAcquireResponse(BaseModel):
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).
+ 1440x900@25, 1280x800@60, 1024x768@60, 1200x800@60, 768x1024@60, 390x844@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 f629691f..3774c107 100644
--- a/src/kernel/types/browser_pool_create_params.py
+++ b/src/kernel/types/browser_pool_create_params.py
@@ -96,14 +96,14 @@ class BrowserPoolCreateParams(TypedDict, total=False):
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).
+ 1440x900@25, 1280x800@60, 1024x768@60, 1200x800@60, 768x1024@60, 390x844@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 2688f235..fa860f6e 100644
--- a/src/kernel/types/browser_pool_update_params.py
+++ b/src/kernel/types/browser_pool_update_params.py
@@ -102,14 +102,14 @@ class BrowserPoolUpdateParams(TypedDict, total=False):
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).
+ 1440x900@25, 1280x800@60, 1024x768@60, 1200x800@60, 768x1024@60, 390x844@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 48dfa362..66c1420e 100644
--- a/src/kernel/types/browser_retrieve_response.py
+++ b/src/kernel/types/browser_retrieve_response.py
@@ -103,12 +103,12 @@ class BrowserRetrieveResponse(BaseModel):
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).
+ 1440x900@25, 1280x800@60, 1024x768@60, 1200x800@60, 768x1024@60, 390x844@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 15c043ea..5a7c7383 100644
--- a/src/kernel/types/browser_update_response.py
+++ b/src/kernel/types/browser_update_response.py
@@ -103,12 +103,12 @@ class BrowserUpdateResponse(BaseModel):
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).
+ 1440x900@25, 1280x800@60, 1024x768@60, 1200x800@60, 768x1024@60, 390x844@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/invocation_list_browsers_response.py b/src/kernel/types/invocation_list_browsers_response.py
index b060f8bb..a37d180a 100644
--- a/src/kernel/types/invocation_list_browsers_response.py
+++ b/src/kernel/types/invocation_list_browsers_response.py
@@ -103,14 +103,14 @@ class Browser(BaseModel):
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).
+ 1440x900@25, 1280x800@60, 1024x768@60, 1200x800@60, 768x1024@60, 390x844@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 7314603e..bfb82aa4 100644
--- a/src/kernel/types/shared/browser_viewport.py
+++ b/src/kernel/types/shared/browser_viewport.py
@@ -14,7 +14,7 @@ class BrowserViewport(BaseModel):
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.
+ 2560x1440@10, 1920x1080@25, 1920x1200@25, 1440x900@25, 1280x800@60, 1024x768@60, 1200x800@60, 768x1024@60, 390x844@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.
diff --git a/src/kernel/types/shared_params/browser_viewport.py b/src/kernel/types/shared_params/browser_viewport.py
index 1250eada..5b03b2f9 100644
--- a/src/kernel/types/shared_params/browser_viewport.py
+++ b/src/kernel/types/shared_params/browser_viewport.py
@@ -14,7 +14,7 @@ class BrowserViewport(TypedDict, total=False):
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.
+ 2560x1440@10, 1920x1080@25, 1920x1200@25, 1440x900@25, 1280x800@60, 1024x768@60, 1200x800@60, 768x1024@60, 390x844@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.
From 5a5d64f6f68fe075f276ceb235d599e289afed73 Mon Sep 17 00:00:00 2001
From: "stainless-app[bot]"
<142633134+stainless-app[bot]@users.noreply.github.com>
Date: Mon, 6 Jul 2026 17:37:55 +0000
Subject: [PATCH 097/366] feat: Add order=desc pagination to telemetry event
reads
---
.stats.yml | 4 +-
src/kernel/resources/browsers/telemetry.py | 40 ++++++++++++++-----
.../types/browsers/telemetry_events_params.py | 11 +++++
.../api_resources/browsers/test_telemetry.py | 2 +
4 files changed, 45 insertions(+), 12 deletions(-)
diff --git a/.stats.yml b/.stats.yml
index a2cf0466..0297d26a 100644
--- a/.stats.yml
+++ b/.stats.yml
@@ -1,4 +1,4 @@
configured_endpoints: 125
-openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel/kernel-ea36e417a5dbc80a7dab65b38bd6c892520929d31363e626a3341436d697a17c.yml
-openapi_spec_hash: fb15f771cbf9c937a4674f23f9a56050
+openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel/kernel-ebbe079bb2542625826422afd876a3e8b499c579cf45efc5fd50e7982d34df7d.yml
+openapi_spec_hash: db850b61a0d71fe9f4b642df8782d7ae
config_hash: 06186eb40e0058a2a87ac251fc07415d
diff --git a/src/kernel/resources/browsers/telemetry.py b/src/kernel/resources/browsers/telemetry.py
index aa3e9ed2..4c7769e7 100644
--- a/src/kernel/resources/browsers/telemetry.py
+++ b/src/kernel/resources/browsers/telemetry.py
@@ -70,6 +70,7 @@ def events(
| Omit = omit,
limit: int | Omit = omit,
offset: int | Omit = omit,
+ order: str | Omit = omit,
since: str | Omit = omit,
until: str | Omit = omit,
# Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
@@ -79,11 +80,12 @@ def events(
extra_body: Body | None = None,
timeout: float | httpx.Timeout | None | NotGiven = not_given,
) -> SyncOffsetPagination[TelemetryEventsResponse]:
- """
- Reads a page of telemetry events for the browser session in ascending sequence
- order. To page through results, pass the X-Next-Offset value from the previous
- response as offset and repeat while X-Has-More is true. Returns an empty list
- when telemetry data is unavailable.
+ """Reads a page of telemetry events for the browser session.
+
+ To page through
+ results, pass the X-Next-Offset value from the previous response as offset and
+ repeat while X-Has-More is true. Returns an empty list when telemetry data is
+ unavailable.
Args:
category: Restrict results to these event categories. Repeat the parameter for multiple
@@ -96,6 +98,13 @@ def events(
since is ignored, while until still bounds the page. It is not an event's seq
field, so do not derive it from the response body.
+ order: Read direction. asc (default) reads oldest first, starting from since or the
+ offset cursor. desc reads newest first: each request returns one page of up to
+ limit records ending at the offset cursor (or until, or the newest archived
+ event); combining desc with since is rejected with a 400. In either direction
+ the category filter applies within the page, so a filtered page may be empty
+ while X-Has-More is true.
+
since: Start of the window: an RFC-3339 timestamp, or a duration like 5m meaning that
long ago. Defaults to 5m. Ignored when offset is set.
@@ -125,6 +134,7 @@ def events(
"category": category,
"limit": limit,
"offset": offset,
+ "order": order,
"since": since,
"until": until,
},
@@ -234,6 +244,7 @@ def events(
| Omit = omit,
limit: int | Omit = omit,
offset: int | Omit = omit,
+ order: str | Omit = omit,
since: str | Omit = omit,
until: str | Omit = omit,
# Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
@@ -243,11 +254,12 @@ def events(
extra_body: Body | None = None,
timeout: float | httpx.Timeout | None | NotGiven = not_given,
) -> AsyncPaginator[TelemetryEventsResponse, AsyncOffsetPagination[TelemetryEventsResponse]]:
- """
- Reads a page of telemetry events for the browser session in ascending sequence
- order. To page through results, pass the X-Next-Offset value from the previous
- response as offset and repeat while X-Has-More is true. Returns an empty list
- when telemetry data is unavailable.
+ """Reads a page of telemetry events for the browser session.
+
+ To page through
+ results, pass the X-Next-Offset value from the previous response as offset and
+ repeat while X-Has-More is true. Returns an empty list when telemetry data is
+ unavailable.
Args:
category: Restrict results to these event categories. Repeat the parameter for multiple
@@ -260,6 +272,13 @@ def events(
since is ignored, while until still bounds the page. It is not an event's seq
field, so do not derive it from the response body.
+ order: Read direction. asc (default) reads oldest first, starting from since or the
+ offset cursor. desc reads newest first: each request returns one page of up to
+ limit records ending at the offset cursor (or until, or the newest archived
+ event); combining desc with since is rejected with a 400. In either direction
+ the category filter applies within the page, so a filtered page may be empty
+ while X-Has-More is true.
+
since: Start of the window: an RFC-3339 timestamp, or a duration like 5m meaning that
long ago. Defaults to 5m. Ignored when offset is set.
@@ -289,6 +308,7 @@ def events(
"category": category,
"limit": limit,
"offset": offset,
+ "order": order,
"since": since,
"until": until,
},
diff --git a/src/kernel/types/browsers/telemetry_events_params.py b/src/kernel/types/browsers/telemetry_events_params.py
index 223bb2fc..4aa0f13c 100644
--- a/src/kernel/types/browsers/telemetry_events_params.py
+++ b/src/kernel/types/browsers/telemetry_events_params.py
@@ -39,6 +39,17 @@ class TelemetryEventsParams(TypedDict, total=False):
field, so do not derive it from the response body.
"""
+ order: str
+ """Read direction.
+
+ asc (default) reads oldest first, starting from since or the offset cursor. desc
+ reads newest first: each request returns one page of up to limit records ending
+ at the offset cursor (or until, or the newest archived event); combining desc
+ with since is rejected with a 400. In either direction the category filter
+ applies within the page, so a filtered page may be empty while X-Has-More is
+ true.
+ """
+
since: str
"""
Start of the window: an RFC-3339 timestamp, or a duration like 5m meaning that
diff --git a/tests/api_resources/browsers/test_telemetry.py b/tests/api_resources/browsers/test_telemetry.py
index 7ad9ca02..3c5dad44 100644
--- a/tests/api_resources/browsers/test_telemetry.py
+++ b/tests/api_resources/browsers/test_telemetry.py
@@ -34,6 +34,7 @@ def test_method_events_with_all_params(self, client: Kernel) -> None:
category=["console"],
limit=1,
offset=0,
+ order="order",
since="since",
until="until",
)
@@ -146,6 +147,7 @@ async def test_method_events_with_all_params(self, async_client: AsyncKernel) ->
category=["console"],
limit=1,
offset=0,
+ order="order",
since="since",
until="until",
)
From 4a4d82f324efbc75849b3c0700168199ae70e37c Mon Sep 17 00:00:00 2001
From: "stainless-app[bot]"
<142633134+stainless-app[bot]@users.noreply.github.com>
Date: Mon, 6 Jul 2026 17:38:22 +0000
Subject: [PATCH 098/366] release: 0.74.0
---
.release-please-manifest.json | 2 +-
CHANGELOG.md | 9 +++++++++
pyproject.toml | 2 +-
src/kernel/_version.py | 2 +-
4 files changed, 12 insertions(+), 3 deletions(-)
diff --git a/.release-please-manifest.json b/.release-please-manifest.json
index f910f1a9..192d2cdd 100644
--- a/.release-please-manifest.json
+++ b/.release-please-manifest.json
@@ -1,3 +1,3 @@
{
- ".": "0.73.0"
+ ".": "0.74.0"
}
\ No newline at end of file
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 238e659b..66e9f601 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,14 @@
# Changelog
+## 0.74.0 (2026-07-06)
+
+Full Changelog: [v0.73.0...v0.74.0](https://github.com/kernel/kernel-python-sdk/compare/v0.73.0...v0.74.0)
+
+### Features
+
+* Add order=desc pagination to telemetry event reads ([763baf5](https://github.com/kernel/kernel-python-sdk/commit/763baf59f95e879d8d9f31566a99fb863354009c))
+* Add tablet and mobile viewport presets to pool dashboard ([37971a0](https://github.com/kernel/kernel-python-sdk/commit/37971a00d405787eebee58a3237c7d4963a0a764))
+
## 0.73.0 (2026-07-01)
Full Changelog: [v0.72.0...v0.73.0](https://github.com/kernel/kernel-python-sdk/compare/v0.72.0...v0.73.0)
diff --git a/pyproject.toml b/pyproject.toml
index 8188c8f8..7d03baca 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -1,6 +1,6 @@
[project]
name = "kernel"
-version = "0.73.0"
+version = "0.74.0"
description = "The official Python library for the kernel API"
dynamic = ["readme"]
license = "Apache-2.0"
diff --git a/src/kernel/_version.py b/src/kernel/_version.py
index 0bd70601..baf9e642 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.73.0" # x-release-please-version
+__version__ = "0.74.0" # x-release-please-version
From 27c49ddbb94bb145d577dfe05127d0c336ea12a0 Mon Sep 17 00:00:00 2001
From: "stainless-app[bot]"
<142633134+stainless-app[bot]@users.noreply.github.com>
Date: Mon, 6 Jul 2026 19:34:47 +0000
Subject: [PATCH 099/366] docs(api): clarify reuse/discard_all_idle pool config
staleness
---
.stats.yml | 4 +--
src/kernel/resources/browser_pools.py | 26 ++++++++++++++-----
.../types/browser_pool_release_params.py | 5 +++-
.../types/browser_pool_update_params.py | 10 ++++---
4 files changed, 33 insertions(+), 12 deletions(-)
diff --git a/.stats.yml b/.stats.yml
index 0297d26a..40adb553 100644
--- a/.stats.yml
+++ b/.stats.yml
@@ -1,4 +1,4 @@
configured_endpoints: 125
-openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel/kernel-ebbe079bb2542625826422afd876a3e8b499c579cf45efc5fd50e7982d34df7d.yml
-openapi_spec_hash: db850b61a0d71fe9f4b642df8782d7ae
+openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel/kernel-83323f87ab825547b6cd11caa46e2643ebb0a68bbd326d4e07ccab970638fc03.yml
+openapi_spec_hash: 69ed04a66b4ef68e97f7bdc07dfe6df2
config_hash: 06186eb40e0058a2a87ac251fc07415d
diff --git a/src/kernel/resources/browser_pools.py b/src/kernel/resources/browser_pools.py
index 6df691be..ea9be13f 100644
--- a/src/kernel/resources/browser_pools.py
+++ b/src/kernel/resources/browser_pools.py
@@ -249,8 +249,12 @@ def update(
Blocked: kernel-managed policies (extensions, proxy, CDP/automation). See
https://chromeenterprise.google/policies/
- discard_all_idle: Whether to discard all idle browsers and rebuild the pool immediately. Defaults
- to false.
+ discard_all_idle: Whether to discard all idle browsers and rebuild them immediately with the new
+ configuration. Defaults to false. Only browsers that are idle when the update
+ runs are rebuilt. A browser that is in use during the update keeps its original
+ configuration, and if it is later released with `reuse: true` it returns to the
+ pool with that stale configuration until it is discarded (by this flag on a
+ later update, or by flushing the pool).
extensions: List of browser extensions to load into the session. Provide each by id or name.
@@ -554,7 +558,10 @@ def release(
session_id: Browser session ID to release back to the pool
reuse: Whether to reuse the browser instance or destroy it and create a new one.
- Defaults to true.
+ Defaults to true. A reused browser keeps the configuration it was created with,
+ so it does not pick up pool configuration changes made while it was in use.
+ Release with `reuse: false`, or flush the pool afterward, to rebuild it with the
+ current configuration.
extra_headers: Send extra headers
@@ -797,8 +804,12 @@ async def update(
Blocked: kernel-managed policies (extensions, proxy, CDP/automation). See
https://chromeenterprise.google/policies/
- discard_all_idle: Whether to discard all idle browsers and rebuild the pool immediately. Defaults
- to false.
+ discard_all_idle: Whether to discard all idle browsers and rebuild them immediately with the new
+ configuration. Defaults to false. Only browsers that are idle when the update
+ runs are rebuilt. A browser that is in use during the update keeps its original
+ configuration, and if it is later released with `reuse: true` it returns to the
+ pool with that stale configuration until it is discarded (by this flag on a
+ later update, or by flushing the pool).
extensions: List of browser extensions to load into the session. Provide each by id or name.
@@ -1102,7 +1113,10 @@ async def release(
session_id: Browser session ID to release back to the pool
reuse: Whether to reuse the browser instance or destroy it and create a new one.
- Defaults to true.
+ Defaults to true. A reused browser keeps the configuration it was created with,
+ so it does not pick up pool configuration changes made while it was in use.
+ Release with `reuse: false`, or flush the pool afterward, to rebuild it with the
+ current configuration.
extra_headers: Send extra headers
diff --git a/src/kernel/types/browser_pool_release_params.py b/src/kernel/types/browser_pool_release_params.py
index 104b0b0c..944e734b 100644
--- a/src/kernel/types/browser_pool_release_params.py
+++ b/src/kernel/types/browser_pool_release_params.py
@@ -14,5 +14,8 @@ class BrowserPoolReleaseParams(TypedDict, total=False):
reuse: bool
"""Whether to reuse the browser instance or destroy it and create a new one.
- Defaults to true.
+ Defaults to true. A reused browser keeps the configuration it was created with,
+ so it does not pick up pool configuration changes made while it was in use.
+ Release with `reuse: false`, or flush the pool afterward, to rebuild it with the
+ current configuration.
"""
diff --git a/src/kernel/types/browser_pool_update_params.py b/src/kernel/types/browser_pool_update_params.py
index fa860f6e..963de02c 100644
--- a/src/kernel/types/browser_pool_update_params.py
+++ b/src/kernel/types/browser_pool_update_params.py
@@ -21,9 +21,13 @@ class BrowserPoolUpdateParams(TypedDict, total=False):
"""
discard_all_idle: bool
- """Whether to discard all idle browsers and rebuild the pool immediately.
-
- Defaults to false.
+ """
+ Whether to discard all idle browsers and rebuild them immediately with the new
+ configuration. Defaults to false. Only browsers that are idle when the update
+ runs are rebuilt. A browser that is in use during the update keeps its original
+ configuration, and if it is later released with `reuse: true` it returns to the
+ pool with that stale configuration until it is discarded (by this flag on a
+ later update, or by flushing the pool).
"""
extensions: Iterable[BrowserExtension]
From 2fe31e901903ac2381140e58a1a6ac4caeb84d3f Mon Sep 17 00:00:00 2001
From: "stainless-app[bot]"
<142633134+stainless-app[bot]@users.noreply.github.com>
Date: Mon, 6 Jul 2026 20:14:56 +0000
Subject: [PATCH 100/366] feat: Document env var redaction on deployment and
app reads
---
.stats.yml | 4 ++--
src/kernel/types/app_list_response.py | 7 ++++++-
src/kernel/types/deployment_create_response.py | 7 ++++++-
src/kernel/types/deployment_follow_response.py | 5 ++++-
src/kernel/types/deployment_list_response.py | 7 ++++++-
src/kernel/types/deployment_retrieve_response.py | 7 ++++++-
src/kernel/types/deployment_state_event.py | 7 ++++++-
7 files changed, 36 insertions(+), 8 deletions(-)
diff --git a/.stats.yml b/.stats.yml
index 40adb553..34f90982 100644
--- a/.stats.yml
+++ b/.stats.yml
@@ -1,4 +1,4 @@
configured_endpoints: 125
-openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel/kernel-83323f87ab825547b6cd11caa46e2643ebb0a68bbd326d4e07ccab970638fc03.yml
-openapi_spec_hash: 69ed04a66b4ef68e97f7bdc07dfe6df2
+openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel/kernel-89a500b26fe8126183d55aaca53f5615c6a25561d8e01e907f4998239ccc7a0b.yml
+openapi_spec_hash: 50236a5e713d5424ca35c0883926b0a5
config_hash: 06186eb40e0058a2a87ac251fc07415d
diff --git a/src/kernel/types/app_list_response.py b/src/kernel/types/app_list_response.py
index 338f506d..91ad648e 100644
--- a/src/kernel/types/app_list_response.py
+++ b/src/kernel/types/app_list_response.py
@@ -25,7 +25,12 @@ class AppListResponse(BaseModel):
"""Deployment ID"""
env_vars: Dict[str, str]
- """Environment variables configured for this app version"""
+ """Environment variables configured for this app version.
+
+ Values are redacted for API key, OAuth, and managed-auth callers, which receive
+ every key with an empty string value. Only dashboard sessions receive the actual
+ values.
+ """
region: Literal["aws.us-east-1a"]
"""Deployment region code"""
diff --git a/src/kernel/types/deployment_create_response.py b/src/kernel/types/deployment_create_response.py
index 5746c97b..17d17b59 100644
--- a/src/kernel/types/deployment_create_response.py
+++ b/src/kernel/types/deployment_create_response.py
@@ -28,7 +28,12 @@ class DeploymentCreateResponse(BaseModel):
"""Relative path to the application entrypoint"""
env_vars: Optional[Dict[str, str]] = None
- """Environment variables configured for this deployment"""
+ """Environment variables configured for this deployment.
+
+ Values are redacted for API key, OAuth, and managed-auth callers, which receive
+ every key with an empty string value. Only dashboard sessions receive the actual
+ values.
+ """
status_reason: Optional[str] = None
"""Status reason"""
diff --git a/src/kernel/types/deployment_follow_response.py b/src/kernel/types/deployment_follow_response.py
index d6de2223..36087a67 100644
--- a/src/kernel/types/deployment_follow_response.py
+++ b/src/kernel/types/deployment_follow_response.py
@@ -40,7 +40,10 @@ class AppVersionSummaryEvent(BaseModel):
"""Version label for the application"""
env_vars: Optional[Dict[str, str]] = None
- """Environment variables configured for this app version"""
+ """Environment variables configured for this app version.
+
+ Not currently populated on streamed app_version_summary events.
+ """
DeploymentFollowResponse: TypeAlias = Annotated[
diff --git a/src/kernel/types/deployment_list_response.py b/src/kernel/types/deployment_list_response.py
index d7719d48..5806bdaf 100644
--- a/src/kernel/types/deployment_list_response.py
+++ b/src/kernel/types/deployment_list_response.py
@@ -28,7 +28,12 @@ class DeploymentListResponse(BaseModel):
"""Relative path to the application entrypoint"""
env_vars: Optional[Dict[str, str]] = None
- """Environment variables configured for this deployment"""
+ """Environment variables configured for this deployment.
+
+ Values are redacted for API key, OAuth, and managed-auth callers, which receive
+ every key with an empty string value. Only dashboard sessions receive the actual
+ values.
+ """
status_reason: Optional[str] = None
"""Status reason"""
diff --git a/src/kernel/types/deployment_retrieve_response.py b/src/kernel/types/deployment_retrieve_response.py
index 3601c864..addcad4a 100644
--- a/src/kernel/types/deployment_retrieve_response.py
+++ b/src/kernel/types/deployment_retrieve_response.py
@@ -28,7 +28,12 @@ class DeploymentRetrieveResponse(BaseModel):
"""Relative path to the application entrypoint"""
env_vars: Optional[Dict[str, str]] = None
- """Environment variables configured for this deployment"""
+ """Environment variables configured for this deployment.
+
+ Values are redacted for API key, OAuth, and managed-auth callers, which receive
+ every key with an empty string value. Only dashboard sessions receive the actual
+ values.
+ """
status_reason: Optional[str] = None
"""Status reason"""
diff --git a/src/kernel/types/deployment_state_event.py b/src/kernel/types/deployment_state_event.py
index cc221c77..dce183a7 100644
--- a/src/kernel/types/deployment_state_event.py
+++ b/src/kernel/types/deployment_state_event.py
@@ -28,7 +28,12 @@ class Deployment(BaseModel):
"""Relative path to the application entrypoint"""
env_vars: Optional[Dict[str, str]] = None
- """Environment variables configured for this deployment"""
+ """Environment variables configured for this deployment.
+
+ Values are redacted for API key, OAuth, and managed-auth callers, which receive
+ every key with an empty string value. Only dashboard sessions receive the actual
+ values.
+ """
status_reason: Optional[str] = None
"""Status reason"""
From 28956b3636e45f7e24a2b4a0f93da3e380f44372 Mon Sep 17 00:00:00 2001
From: "stainless-app[bot]"
<142633134+stainless-app[bot]@users.noreply.github.com>
Date: Tue, 7 Jul 2026 18:01:42 +0000
Subject: [PATCH 101/366] codegen metadata
---
.stats.yml | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/.stats.yml b/.stats.yml
index 34f90982..199caa16 100644
--- a/.stats.yml
+++ b/.stats.yml
@@ -1,4 +1,4 @@
configured_endpoints: 125
-openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel/kernel-89a500b26fe8126183d55aaca53f5615c6a25561d8e01e907f4998239ccc7a0b.yml
-openapi_spec_hash: 50236a5e713d5424ca35c0883926b0a5
+openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel/kernel-073bade6de836ae1cf0cb3f8551cc5c6635f98649d7cf5059ef6819e9ce0f508.yml
+openapi_spec_hash: e84f887c72b5c7335158b706e4218bc7
config_hash: 06186eb40e0058a2a87ac251fc07415d
From 23ef37db82c2b7cf50820360d5bff6259c822923 Mon Sep 17 00:00:00 2001
From: "stainless-app[bot]"
<142633134+stainless-app[bot]@users.noreply.github.com>
Date: Tue, 7 Jul 2026 21:27:36 +0000
Subject: [PATCH 102/366] feat: Reject API key self-deletion
---
.stats.yml | 4 ++--
src/kernel/resources/api_keys.py | 12 ++++++++----
2 files changed, 10 insertions(+), 6 deletions(-)
diff --git a/.stats.yml b/.stats.yml
index 199caa16..02a158e4 100644
--- a/.stats.yml
+++ b/.stats.yml
@@ -1,4 +1,4 @@
configured_endpoints: 125
-openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel/kernel-073bade6de836ae1cf0cb3f8551cc5c6635f98649d7cf5059ef6819e9ce0f508.yml
-openapi_spec_hash: e84f887c72b5c7335158b706e4218bc7
+openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel/kernel-bd00e13bd6d37c150009e612ebeb6ef03d050610f80aaed517e283812c7b9ba9.yml
+openapi_spec_hash: 08c7c031877d6b882ff291b389937028
config_hash: 06186eb40e0058a2a87ac251fc07415d
diff --git a/src/kernel/resources/api_keys.py b/src/kernel/resources/api_keys.py
index da28774d..502e8897 100644
--- a/src/kernel/resources/api_keys.py
+++ b/src/kernel/resources/api_keys.py
@@ -266,8 +266,10 @@ def delete(
extra_body: Body | None = None,
timeout: float | httpx.Timeout | None | NotGiven = not_given,
) -> None:
- """
- Delete an API key.
+ """Delete an API key.
+
+ A key cannot delete itself; use a different key to delete
+ this one.
Args:
extra_headers: Send extra headers
@@ -575,8 +577,10 @@ async def delete(
extra_body: Body | None = None,
timeout: float | httpx.Timeout | None | NotGiven = not_given,
) -> None:
- """
- Delete an API key.
+ """Delete an API key.
+
+ A key cannot delete itself; use a different key to delete
+ this one.
Args:
extra_headers: Send extra headers
From 5f32c886a36e1bf1b272e041a134e349307c6666 Mon Sep 17 00:00:00 2001
From: "stainless-app[bot]"
<142633134+stainless-app[bot]@users.noreply.github.com>
Date: Tue, 7 Jul 2026 21:51:33 +0000
Subject: [PATCH 103/366] feat: Store and return a sha256 checksum for uploaded
extensions
---
.stats.yml | 4 ++--
src/kernel/types/extension_get_response.py | 9 +++++++++
src/kernel/types/extension_list_response.py | 9 +++++++++
src/kernel/types/extension_upload_response.py | 9 +++++++++
4 files changed, 29 insertions(+), 2 deletions(-)
diff --git a/.stats.yml b/.stats.yml
index 02a158e4..06a81919 100644
--- a/.stats.yml
+++ b/.stats.yml
@@ -1,4 +1,4 @@
configured_endpoints: 125
-openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel/kernel-bd00e13bd6d37c150009e612ebeb6ef03d050610f80aaed517e283812c7b9ba9.yml
-openapi_spec_hash: 08c7c031877d6b882ff291b389937028
+openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel/kernel-e123113e7c86e8a61c763f0078adec5b0a7df621f2c31824e8f553331cf7a624.yml
+openapi_spec_hash: 940fb94967b6cb5e13daef6161d3abbf
config_hash: 06186eb40e0058a2a87ac251fc07415d
diff --git a/src/kernel/types/extension_get_response.py b/src/kernel/types/extension_get_response.py
index 0143e623..a4d10b1f 100644
--- a/src/kernel/types/extension_get_response.py
+++ b/src/kernel/types/extension_get_response.py
@@ -20,6 +20,15 @@ class ExtensionGetResponse(BaseModel):
size_bytes: int
"""Size of the extension archive in bytes"""
+ checksum: Optional[str] = None
+ """
+ SHA-256 checksum, encoded as lowercase hexadecimal, of the exact uploaded
+ extension archive bytes. This is not a normalized checksum of the extension
+ contents; archive metadata, file ordering, and compression can change the
+ checksum for otherwise identical contents. Omitted for legacy rows and
+ server-repackaged Chrome Web Store extensions.
+ """
+
last_used_at: Optional[datetime] = None
"""Timestamp when the extension was last used"""
diff --git a/src/kernel/types/extension_list_response.py b/src/kernel/types/extension_list_response.py
index 6bb33561..d7d0ea0d 100644
--- a/src/kernel/types/extension_list_response.py
+++ b/src/kernel/types/extension_list_response.py
@@ -20,6 +20,15 @@ class ExtensionListResponse(BaseModel):
size_bytes: int
"""Size of the extension archive in bytes"""
+ checksum: Optional[str] = None
+ """
+ SHA-256 checksum, encoded as lowercase hexadecimal, of the exact uploaded
+ extension archive bytes. This is not a normalized checksum of the extension
+ contents; archive metadata, file ordering, and compression can change the
+ checksum for otherwise identical contents. Omitted for legacy rows and
+ server-repackaged Chrome Web Store extensions.
+ """
+
last_used_at: Optional[datetime] = None
"""Timestamp when the extension was last used"""
diff --git a/src/kernel/types/extension_upload_response.py b/src/kernel/types/extension_upload_response.py
index 068b25dd..b44390d4 100644
--- a/src/kernel/types/extension_upload_response.py
+++ b/src/kernel/types/extension_upload_response.py
@@ -20,6 +20,15 @@ class ExtensionUploadResponse(BaseModel):
size_bytes: int
"""Size of the extension archive in bytes"""
+ checksum: Optional[str] = None
+ """
+ SHA-256 checksum, encoded as lowercase hexadecimal, of the exact uploaded
+ extension archive bytes. This is not a normalized checksum of the extension
+ contents; archive metadata, file ordering, and compression can change the
+ checksum for otherwise identical contents. Omitted for legacy rows and
+ server-repackaged Chrome Web Store extensions.
+ """
+
last_used_at: Optional[datetime] = None
"""Timestamp when the extension was last used"""
From 7fe970a4d3da14d4420d72b05244b789bf12d921 Mon Sep 17 00:00:00 2001
From: "stainless-app[bot]"
<142633134+stainless-app[bot]@users.noreply.github.com>
Date: Tue, 7 Jul 2026 22:19:51 +0000
Subject: [PATCH 104/366] =?UTF-8?q?feat:=20Revert=20"Store=20and=20return?=
=?UTF-8?q?=20a=20sha256=20checksum=20for=20uploaded=20extensions=20(#?=
=?UTF-8?q?=E2=80=A6?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.stats.yml | 4 ++--
src/kernel/types/extension_get_response.py | 9 ---------
src/kernel/types/extension_list_response.py | 9 ---------
src/kernel/types/extension_upload_response.py | 9 ---------
4 files changed, 2 insertions(+), 29 deletions(-)
diff --git a/.stats.yml b/.stats.yml
index 06a81919..70c2c4fc 100644
--- a/.stats.yml
+++ b/.stats.yml
@@ -1,4 +1,4 @@
configured_endpoints: 125
-openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel/kernel-e123113e7c86e8a61c763f0078adec5b0a7df621f2c31824e8f553331cf7a624.yml
-openapi_spec_hash: 940fb94967b6cb5e13daef6161d3abbf
+openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel/kernel-4395690aa03bf5e2482d60132f7eebdbf88265af3d53098789f6c2f3c623d769.yml
+openapi_spec_hash: ad7c56d655a6e3899532caa4588a02b7
config_hash: 06186eb40e0058a2a87ac251fc07415d
diff --git a/src/kernel/types/extension_get_response.py b/src/kernel/types/extension_get_response.py
index a4d10b1f..0143e623 100644
--- a/src/kernel/types/extension_get_response.py
+++ b/src/kernel/types/extension_get_response.py
@@ -20,15 +20,6 @@ class ExtensionGetResponse(BaseModel):
size_bytes: int
"""Size of the extension archive in bytes"""
- checksum: Optional[str] = None
- """
- SHA-256 checksum, encoded as lowercase hexadecimal, of the exact uploaded
- extension archive bytes. This is not a normalized checksum of the extension
- contents; archive metadata, file ordering, and compression can change the
- checksum for otherwise identical contents. Omitted for legacy rows and
- server-repackaged Chrome Web Store extensions.
- """
-
last_used_at: Optional[datetime] = None
"""Timestamp when the extension was last used"""
diff --git a/src/kernel/types/extension_list_response.py b/src/kernel/types/extension_list_response.py
index d7d0ea0d..6bb33561 100644
--- a/src/kernel/types/extension_list_response.py
+++ b/src/kernel/types/extension_list_response.py
@@ -20,15 +20,6 @@ class ExtensionListResponse(BaseModel):
size_bytes: int
"""Size of the extension archive in bytes"""
- checksum: Optional[str] = None
- """
- SHA-256 checksum, encoded as lowercase hexadecimal, of the exact uploaded
- extension archive bytes. This is not a normalized checksum of the extension
- contents; archive metadata, file ordering, and compression can change the
- checksum for otherwise identical contents. Omitted for legacy rows and
- server-repackaged Chrome Web Store extensions.
- """
-
last_used_at: Optional[datetime] = None
"""Timestamp when the extension was last used"""
diff --git a/src/kernel/types/extension_upload_response.py b/src/kernel/types/extension_upload_response.py
index b44390d4..068b25dd 100644
--- a/src/kernel/types/extension_upload_response.py
+++ b/src/kernel/types/extension_upload_response.py
@@ -20,15 +20,6 @@ class ExtensionUploadResponse(BaseModel):
size_bytes: int
"""Size of the extension archive in bytes"""
- checksum: Optional[str] = None
- """
- SHA-256 checksum, encoded as lowercase hexadecimal, of the exact uploaded
- extension archive bytes. This is not a normalized checksum of the extension
- contents; archive metadata, file ordering, and compression can change the
- checksum for otherwise identical contents. Omitted for legacy rows and
- server-repackaged Chrome Web Store extensions.
- """
-
last_used_at: Optional[datetime] = None
"""Timestamp when the extension was last used"""
From 1a4d1e08dd435e7feff49a02cb526e5bf8458c03 Mon Sep 17 00:00:00 2001
From: "stainless-app[bot]"
<142633134+stainless-app[bot]@users.noreply.github.com>
Date: Wed, 8 Jul 2026 15:31:22 +0000
Subject: [PATCH 105/366] codegen metadata
---
.stats.yml | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/.stats.yml b/.stats.yml
index 70c2c4fc..2c231f14 100644
--- a/.stats.yml
+++ b/.stats.yml
@@ -1,4 +1,4 @@
configured_endpoints: 125
-openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel/kernel-4395690aa03bf5e2482d60132f7eebdbf88265af3d53098789f6c2f3c623d769.yml
-openapi_spec_hash: ad7c56d655a6e3899532caa4588a02b7
+openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel/kernel-f6795e5d85e37b74f59876434c74b598e34171f99bc598c85b789abbb60614f1.yml
+openapi_spec_hash: 98e86fcbf0710a693bb1e03170ca85c3
config_hash: 06186eb40e0058a2a87ac251fc07415d
From 2f850b37886658677c5551b473d8851d2c13187d Mon Sep 17 00:00:00 2001
From: "stainless-app[bot]"
<142633134+stainless-app[bot]@users.noreply.github.com>
Date: Wed, 8 Jul 2026 17:42:38 +0000
Subject: [PATCH 106/366] feat: Auto-flush pools when a managed auth profile
re-authenticates
---
.stats.yml | 4 +-
src/kernel/resources/browser_pools.py | 76 ++++++++++++-------
src/kernel/types/browser_pool.py | 30 ++++----
.../types/browser_pool_create_params.py | 30 ++++----
.../types/browser_pool_update_params.py | 30 ++++----
tests/api_resources/test_browser_pools.py | 4 +
6 files changed, 107 insertions(+), 67 deletions(-)
diff --git a/.stats.yml b/.stats.yml
index 2c231f14..d888de6d 100644
--- a/.stats.yml
+++ b/.stats.yml
@@ -1,4 +1,4 @@
configured_endpoints: 125
-openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel/kernel-f6795e5d85e37b74f59876434c74b598e34171f99bc598c85b789abbb60614f1.yml
-openapi_spec_hash: 98e86fcbf0710a693bb1e03170ca85c3
+openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel/kernel-58ad412d62b3d94e3258c911d7028716d71191358ff769ce494c4b2d87027254.yml
+openapi_spec_hash: 45d710b7d2a9beadff021514bb997ba1
config_hash: 06186eb40e0058a2a87ac251fc07415d
diff --git a/src/kernel/resources/browser_pools.py b/src/kernel/resources/browser_pools.py
index ea9be13f..12239d00 100644
--- a/src/kernel/resources/browser_pools.py
+++ b/src/kernel/resources/browser_pools.py
@@ -69,6 +69,7 @@ def create(
name: str | Omit = omit,
profile: browser_pool_create_params.Profile | Omit = omit,
proxy_id: str | Omit = omit,
+ refresh_on_profile_update: bool | Omit = omit,
start_url: str | Omit = omit,
stealth: bool | Omit = omit,
timeout_seconds: int | Omit = omit,
@@ -110,16 +111,19 @@ def create(
name: Optional name for the browser pool. Must be unique within the project.
- profile: Profile selection for browsers in a pool. Provide either id or name. The
- matching profile is loaded into every browser in the pool. Profiles must be
- created beforehand. Unlike single browser sessions, pools load the profile
- read-only and never persist changes back to it, so save_changes is omitted here.
- Any save_changes value sent on a pool profile is silently ignored rather than
- rejected, so callers reusing a single-session profile object will not error.
+ profile: Profile configuration for browsers in a pool. Provide either id or name.
+ Profiles must be created beforehand. Unlike single browser sessions, pools load
+ the profile read-only and never persist changes back to it, so save_changes is
+ omitted here. Any save_changes value sent on a pool profile is silently ignored
+ rather than rejected.
proxy_id: Optional proxy to associate to the browser session. Must reference a proxy in
the same project as the browser session.
+ refresh_on_profile_update: When true, flush idle browsers when the profile the pool uses is updated, so
+ pool browsers pick up the latest profile data. Requires a profile to be set on
+ the pool.
+
start_url: Optional URL to navigate to when a new browser is warmed into the pool.
Best-effort: failures to navigate do not fail pool fill. Only applied to
newly-warmed browsers; browsers reused via release/acquire keep whatever URL the
@@ -166,6 +170,7 @@ def create(
"name": name,
"profile": profile,
"proxy_id": proxy_id,
+ "refresh_on_profile_update": refresh_on_profile_update,
"start_url": start_url,
"stealth": stealth,
"timeout_seconds": timeout_seconds,
@@ -225,6 +230,7 @@ def update(
name: str | Omit = omit,
profile: browser_pool_update_params.Profile | Omit = omit,
proxy_id: str | Omit = omit,
+ refresh_on_profile_update: bool | Omit = omit,
size: int | Omit = omit,
start_url: str | Omit = omit,
stealth: bool | Omit = omit,
@@ -241,7 +247,9 @@ def update(
As with creation,
save_changes on the pool profile is ignored (not rejected); pooled browsers
- never persist changes back to the profile.
+ never persist changes back to the profile. To clear the profile reference, send
+ `profile: { "id": "" }`. Clearing the profile also disables
+ `refresh_on_profile_update`.
Args:
chrome_policy: Custom Chrome enterprise policy overrides applied to all browsers in this pool.
@@ -269,16 +277,19 @@ def update(
name: Optional name for the browser pool. Must be unique within the project.
- profile: Profile selection for browsers in a pool. Provide either id or name. The
- matching profile is loaded into every browser in the pool. Profiles must be
- created beforehand. Unlike single browser sessions, pools load the profile
- read-only and never persist changes back to it, so save_changes is omitted here.
- Any save_changes value sent on a pool profile is silently ignored rather than
- rejected, so callers reusing a single-session profile object will not error.
+ profile: Profile configuration for browsers in a pool. Provide either id or name.
+ Profiles must be created beforehand. Unlike single browser sessions, pools load
+ the profile read-only and never persist changes back to it, so save_changes is
+ omitted here. Any save_changes value sent on a pool profile is silently ignored
+ rather than rejected.
proxy_id: Optional proxy to associate to the browser session. Must reference a proxy in
the same project as the browser session.
+ refresh_on_profile_update: When true, flush idle browsers when the profile the pool uses is updated, so
+ pool browsers pick up the latest profile data. Requires a profile to be set on
+ the pool.
+
size: Number of browsers to maintain in the pool. The maximum size is determined by
your organization's pooled sessions limit (the sum of all pool sizes cannot
exceed your limit).
@@ -331,6 +342,7 @@ def update(
"name": name,
"profile": profile,
"proxy_id": proxy_id,
+ "refresh_on_profile_update": refresh_on_profile_update,
"size": size,
"start_url": start_url,
"stealth": stealth,
@@ -624,6 +636,7 @@ async def create(
name: str | Omit = omit,
profile: browser_pool_create_params.Profile | Omit = omit,
proxy_id: str | Omit = omit,
+ refresh_on_profile_update: bool | Omit = omit,
start_url: str | Omit = omit,
stealth: bool | Omit = omit,
timeout_seconds: int | Omit = omit,
@@ -665,16 +678,19 @@ async def create(
name: Optional name for the browser pool. Must be unique within the project.
- profile: Profile selection for browsers in a pool. Provide either id or name. The
- matching profile is loaded into every browser in the pool. Profiles must be
- created beforehand. Unlike single browser sessions, pools load the profile
- read-only and never persist changes back to it, so save_changes is omitted here.
- Any save_changes value sent on a pool profile is silently ignored rather than
- rejected, so callers reusing a single-session profile object will not error.
+ profile: Profile configuration for browsers in a pool. Provide either id or name.
+ Profiles must be created beforehand. Unlike single browser sessions, pools load
+ the profile read-only and never persist changes back to it, so save_changes is
+ omitted here. Any save_changes value sent on a pool profile is silently ignored
+ rather than rejected.
proxy_id: Optional proxy to associate to the browser session. Must reference a proxy in
the same project as the browser session.
+ refresh_on_profile_update: When true, flush idle browsers when the profile the pool uses is updated, so
+ pool browsers pick up the latest profile data. Requires a profile to be set on
+ the pool.
+
start_url: Optional URL to navigate to when a new browser is warmed into the pool.
Best-effort: failures to navigate do not fail pool fill. Only applied to
newly-warmed browsers; browsers reused via release/acquire keep whatever URL the
@@ -721,6 +737,7 @@ async def create(
"name": name,
"profile": profile,
"proxy_id": proxy_id,
+ "refresh_on_profile_update": refresh_on_profile_update,
"start_url": start_url,
"stealth": stealth,
"timeout_seconds": timeout_seconds,
@@ -780,6 +797,7 @@ async def update(
name: str | Omit = omit,
profile: browser_pool_update_params.Profile | Omit = omit,
proxy_id: str | Omit = omit,
+ refresh_on_profile_update: bool | Omit = omit,
size: int | Omit = omit,
start_url: str | Omit = omit,
stealth: bool | Omit = omit,
@@ -796,7 +814,9 @@ async def update(
As with creation,
save_changes on the pool profile is ignored (not rejected); pooled browsers
- never persist changes back to the profile.
+ never persist changes back to the profile. To clear the profile reference, send
+ `profile: { "id": "" }`. Clearing the profile also disables
+ `refresh_on_profile_update`.
Args:
chrome_policy: Custom Chrome enterprise policy overrides applied to all browsers in this pool.
@@ -824,16 +844,19 @@ async def update(
name: Optional name for the browser pool. Must be unique within the project.
- profile: Profile selection for browsers in a pool. Provide either id or name. The
- matching profile is loaded into every browser in the pool. Profiles must be
- created beforehand. Unlike single browser sessions, pools load the profile
- read-only and never persist changes back to it, so save_changes is omitted here.
- Any save_changes value sent on a pool profile is silently ignored rather than
- rejected, so callers reusing a single-session profile object will not error.
+ profile: Profile configuration for browsers in a pool. Provide either id or name.
+ Profiles must be created beforehand. Unlike single browser sessions, pools load
+ the profile read-only and never persist changes back to it, so save_changes is
+ omitted here. Any save_changes value sent on a pool profile is silently ignored
+ rather than rejected.
proxy_id: Optional proxy to associate to the browser session. Must reference a proxy in
the same project as the browser session.
+ refresh_on_profile_update: When true, flush idle browsers when the profile the pool uses is updated, so
+ pool browsers pick up the latest profile data. Requires a profile to be set on
+ the pool.
+
size: Number of browsers to maintain in the pool. The maximum size is determined by
your organization's pooled sessions limit (the sum of all pool sizes cannot
exceed your limit).
@@ -886,6 +909,7 @@ async def update(
"name": name,
"profile": profile,
"proxy_id": proxy_id,
+ "refresh_on_profile_update": refresh_on_profile_update,
"size": size,
"start_url": start_url,
"stealth": stealth,
diff --git a/src/kernel/types/browser_pool.py b/src/kernel/types/browser_pool.py
index 0b77264c..a407009c 100644
--- a/src/kernel/types/browser_pool.py
+++ b/src/kernel/types/browser_pool.py
@@ -11,13 +11,12 @@
class BrowserPoolConfigProfile(BaseModel):
- """Profile selection for browsers in a pool.
+ """Profile configuration for browsers in a pool.
- Provide either id or name. The matching profile is
- loaded into every browser in the pool. Profiles must be created beforehand. Unlike single
- browser sessions, pools load the profile read-only and never persist changes back to it, so
- save_changes is omitted here. Any save_changes value sent on a pool profile is silently
- ignored rather than rejected, so callers reusing a single-session profile object will not error.
+ Provide either id or name. Profiles must
+ be created beforehand. Unlike single browser sessions, pools load the profile read-only
+ and never persist changes back to it, so save_changes is omitted here. Any save_changes
+ value sent on a pool profile is silently ignored rather than rejected.
"""
id: Optional[str] = None
@@ -74,14 +73,12 @@ class BrowserPoolConfig(BaseModel):
"""Optional name for the browser pool. Must be unique within the project."""
profile: Optional[BrowserPoolConfigProfile] = None
- """Profile selection for browsers in a pool.
+ """Profile configuration for browsers in a pool.
- Provide either id or name. The matching profile is loaded into every browser in
- the pool. Profiles must be created beforehand. Unlike single browser sessions,
- pools load the profile read-only and never persist changes back to it, so
- save_changes is omitted here. Any save_changes value sent on a pool profile is
- silently ignored rather than rejected, so callers reusing a single-session
- profile object will not error.
+ Provide either id or name. Profiles must be created beforehand. Unlike single
+ browser sessions, pools load the profile read-only and never persist changes
+ back to it, so save_changes is omitted here. Any save_changes value sent on a
+ pool profile is silently ignored rather than rejected.
"""
proxy_id: Optional[str] = None
@@ -90,6 +87,13 @@ class BrowserPoolConfig(BaseModel):
Must reference a proxy in the same project as the browser session.
"""
+ refresh_on_profile_update: Optional[bool] = None
+ """
+ When true, flush idle browsers when the profile the pool uses is updated, so
+ pool browsers pick up the latest profile data. Requires a profile to be set on
+ the pool.
+ """
+
start_url: Optional[str] = None
"""Optional URL to navigate to when a new browser is warmed into the pool.
diff --git a/src/kernel/types/browser_pool_create_params.py b/src/kernel/types/browser_pool_create_params.py
index 3774c107..5d4d9760 100644
--- a/src/kernel/types/browser_pool_create_params.py
+++ b/src/kernel/types/browser_pool_create_params.py
@@ -53,14 +53,12 @@ class BrowserPoolCreateParams(TypedDict, total=False):
"""Optional name for the browser pool. Must be unique within the project."""
profile: Profile
- """Profile selection for browsers in a pool.
+ """Profile configuration for browsers in a pool.
- Provide either id or name. The matching profile is loaded into every browser in
- the pool. Profiles must be created beforehand. Unlike single browser sessions,
- pools load the profile read-only and never persist changes back to it, so
- save_changes is omitted here. Any save_changes value sent on a pool profile is
- silently ignored rather than rejected, so callers reusing a single-session
- profile object will not error.
+ Provide either id or name. Profiles must be created beforehand. Unlike single
+ browser sessions, pools load the profile read-only and never persist changes
+ back to it, so save_changes is omitted here. Any save_changes value sent on a
+ pool profile is silently ignored rather than rejected.
"""
proxy_id: str
@@ -69,6 +67,13 @@ class BrowserPoolCreateParams(TypedDict, total=False):
Must reference a proxy in the same project as the browser session.
"""
+ refresh_on_profile_update: bool
+ """
+ When true, flush idle browsers when the profile the pool uses is updated, so
+ pool browsers pick up the latest profile data. Requires a profile to be set on
+ the pool.
+ """
+
start_url: str
"""Optional URL to navigate to when a new browser is warmed into the pool.
@@ -108,13 +113,12 @@ class BrowserPoolCreateParams(TypedDict, total=False):
class Profile(TypedDict, total=False):
- """Profile selection for browsers in a pool.
+ """Profile configuration for browsers in a pool.
- Provide either id or name. The matching profile is
- loaded into every browser in the pool. Profiles must be created beforehand. Unlike single
- browser sessions, pools load the profile read-only and never persist changes back to it, so
- save_changes is omitted here. Any save_changes value sent on a pool profile is silently
- ignored rather than rejected, so callers reusing a single-session profile object will not error.
+ Provide either id or name. Profiles must
+ be created beforehand. Unlike single browser sessions, pools load the profile read-only
+ and never persist changes back to it, so save_changes is omitted here. Any save_changes
+ value sent on a pool profile is silently ignored rather than rejected.
"""
id: str
diff --git a/src/kernel/types/browser_pool_update_params.py b/src/kernel/types/browser_pool_update_params.py
index 963de02c..d504e29d 100644
--- a/src/kernel/types/browser_pool_update_params.py
+++ b/src/kernel/types/browser_pool_update_params.py
@@ -56,14 +56,12 @@ class BrowserPoolUpdateParams(TypedDict, total=False):
"""Optional name for the browser pool. Must be unique within the project."""
profile: Profile
- """Profile selection for browsers in a pool.
+ """Profile configuration for browsers in a pool.
- Provide either id or name. The matching profile is loaded into every browser in
- the pool. Profiles must be created beforehand. Unlike single browser sessions,
- pools load the profile read-only and never persist changes back to it, so
- save_changes is omitted here. Any save_changes value sent on a pool profile is
- silently ignored rather than rejected, so callers reusing a single-session
- profile object will not error.
+ Provide either id or name. Profiles must be created beforehand. Unlike single
+ browser sessions, pools load the profile read-only and never persist changes
+ back to it, so save_changes is omitted here. Any save_changes value sent on a
+ pool profile is silently ignored rather than rejected.
"""
proxy_id: str
@@ -72,6 +70,13 @@ class BrowserPoolUpdateParams(TypedDict, total=False):
Must reference a proxy in the same project as the browser session.
"""
+ refresh_on_profile_update: bool
+ """
+ When true, flush idle browsers when the profile the pool uses is updated, so
+ pool browsers pick up the latest profile data. Requires a profile to be set on
+ the pool.
+ """
+
size: int
"""Number of browsers to maintain in the pool.
@@ -118,13 +123,12 @@ class BrowserPoolUpdateParams(TypedDict, total=False):
class Profile(TypedDict, total=False):
- """Profile selection for browsers in a pool.
+ """Profile configuration for browsers in a pool.
- Provide either id or name. The matching profile is
- loaded into every browser in the pool. Profiles must be created beforehand. Unlike single
- browser sessions, pools load the profile read-only and never persist changes back to it, so
- save_changes is omitted here. Any save_changes value sent on a pool profile is silently
- ignored rather than rejected, so callers reusing a single-session profile object will not error.
+ Provide either id or name. Profiles must
+ be created beforehand. Unlike single browser sessions, pools load the profile read-only
+ and never persist changes back to it, so save_changes is omitted here. Any save_changes
+ value sent on a pool profile is silently ignored rather than rejected.
"""
id: str
diff --git a/tests/api_resources/test_browser_pools.py b/tests/api_resources/test_browser_pools.py
index c7e8bdb4..b7ca39a4 100644
--- a/tests/api_resources/test_browser_pools.py
+++ b/tests/api_resources/test_browser_pools.py
@@ -50,6 +50,7 @@ def test_method_create_with_all_params(self, client: Kernel) -> None:
"name": "name",
},
proxy_id="proxy_id",
+ refresh_on_profile_update=True,
start_url="https://example.com",
stealth=True,
timeout_seconds=10,
@@ -159,6 +160,7 @@ def test_method_update_with_all_params(self, client: Kernel) -> None:
"name": "name",
},
proxy_id="proxy_id",
+ refresh_on_profile_update=True,
size=10,
start_url="https://example.com",
stealth=True,
@@ -484,6 +486,7 @@ async def test_method_create_with_all_params(self, async_client: AsyncKernel) ->
"name": "name",
},
proxy_id="proxy_id",
+ refresh_on_profile_update=True,
start_url="https://example.com",
stealth=True,
timeout_seconds=10,
@@ -593,6 +596,7 @@ async def test_method_update_with_all_params(self, async_client: AsyncKernel) ->
"name": "name",
},
proxy_id="proxy_id",
+ refresh_on_profile_update=True,
size=10,
start_url="https://example.com",
stealth=True,
From 77ea046e9085e76590ddc141e97ff07867ad200a Mon Sep 17 00:00:00 2001
From: "stainless-app[bot]"
<142633134+stainless-app[bot]@users.noreply.github.com>
Date: Wed, 8 Jul 2026 17:59:25 +0000
Subject: [PATCH 107/366] codegen metadata
---
.stats.yml | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/.stats.yml b/.stats.yml
index d888de6d..9c64d8cf 100644
--- a/.stats.yml
+++ b/.stats.yml
@@ -1,4 +1,4 @@
configured_endpoints: 125
-openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel/kernel-58ad412d62b3d94e3258c911d7028716d71191358ff769ce494c4b2d87027254.yml
-openapi_spec_hash: 45d710b7d2a9beadff021514bb997ba1
+openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel/kernel-4655211f0824a1eb5d534731697ea215c19e769e3c63ac61ca306c7a3140b0b4.yml
+openapi_spec_hash: 0a440e663f1704d4b9a3a3ac5fa75ffb
config_hash: 06186eb40e0058a2a87ac251fc07415d
From 6e1ac47bb604ae9484bf42b49656b1b241ef443c Mon Sep 17 00:00:00 2001
From: "stainless-app[bot]"
<142633134+stainless-app[bot]@users.noreply.github.com>
Date: Wed, 8 Jul 2026 19:54:52 +0000
Subject: [PATCH 108/366] feat: Store and return a sha256 checksum for uploaded
extensions (reland)
---
.stats.yml | 4 ++--
src/kernel/types/extension_get_response.py | 9 +++++++++
src/kernel/types/extension_list_response.py | 9 +++++++++
src/kernel/types/extension_upload_response.py | 9 +++++++++
4 files changed, 29 insertions(+), 2 deletions(-)
diff --git a/.stats.yml b/.stats.yml
index 9c64d8cf..1d73bab6 100644
--- a/.stats.yml
+++ b/.stats.yml
@@ -1,4 +1,4 @@
configured_endpoints: 125
-openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel/kernel-4655211f0824a1eb5d534731697ea215c19e769e3c63ac61ca306c7a3140b0b4.yml
-openapi_spec_hash: 0a440e663f1704d4b9a3a3ac5fa75ffb
+openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel/kernel-f040d3f5c26d6e0d312206b294d3745b272e26b883e7d704f6b4c45dd438cd01.yml
+openapi_spec_hash: 56f4918d2e3f8d717ffe3643a20feb96
config_hash: 06186eb40e0058a2a87ac251fc07415d
diff --git a/src/kernel/types/extension_get_response.py b/src/kernel/types/extension_get_response.py
index 0143e623..a4d10b1f 100644
--- a/src/kernel/types/extension_get_response.py
+++ b/src/kernel/types/extension_get_response.py
@@ -20,6 +20,15 @@ class ExtensionGetResponse(BaseModel):
size_bytes: int
"""Size of the extension archive in bytes"""
+ checksum: Optional[str] = None
+ """
+ SHA-256 checksum, encoded as lowercase hexadecimal, of the exact uploaded
+ extension archive bytes. This is not a normalized checksum of the extension
+ contents; archive metadata, file ordering, and compression can change the
+ checksum for otherwise identical contents. Omitted for legacy rows and
+ server-repackaged Chrome Web Store extensions.
+ """
+
last_used_at: Optional[datetime] = None
"""Timestamp when the extension was last used"""
diff --git a/src/kernel/types/extension_list_response.py b/src/kernel/types/extension_list_response.py
index 6bb33561..d7d0ea0d 100644
--- a/src/kernel/types/extension_list_response.py
+++ b/src/kernel/types/extension_list_response.py
@@ -20,6 +20,15 @@ class ExtensionListResponse(BaseModel):
size_bytes: int
"""Size of the extension archive in bytes"""
+ checksum: Optional[str] = None
+ """
+ SHA-256 checksum, encoded as lowercase hexadecimal, of the exact uploaded
+ extension archive bytes. This is not a normalized checksum of the extension
+ contents; archive metadata, file ordering, and compression can change the
+ checksum for otherwise identical contents. Omitted for legacy rows and
+ server-repackaged Chrome Web Store extensions.
+ """
+
last_used_at: Optional[datetime] = None
"""Timestamp when the extension was last used"""
diff --git a/src/kernel/types/extension_upload_response.py b/src/kernel/types/extension_upload_response.py
index 068b25dd..b44390d4 100644
--- a/src/kernel/types/extension_upload_response.py
+++ b/src/kernel/types/extension_upload_response.py
@@ -20,6 +20,15 @@ class ExtensionUploadResponse(BaseModel):
size_bytes: int
"""Size of the extension archive in bytes"""
+ checksum: Optional[str] = None
+ """
+ SHA-256 checksum, encoded as lowercase hexadecimal, of the exact uploaded
+ extension archive bytes. This is not a normalized checksum of the extension
+ contents; archive metadata, file ordering, and compression can change the
+ checksum for otherwise identical contents. Omitted for legacy rows and
+ server-repackaged Chrome Web Store extensions.
+ """
+
last_used_at: Optional[datetime] = None
"""Timestamp when the extension was last used"""
From 9699a277bcbf5476dd1d881267dd2ff11d7f8e02 Mon Sep 17 00:00:00 2001
From: "stainless-app[bot]"
<142633134+stainless-app[bot]@users.noreply.github.com>
Date: Wed, 8 Jul 2026 21:17:16 +0000
Subject: [PATCH 109/366] feat: Expose resolved profile_id and extension_ids on
browser pool reads
---
.stats.yml | 4 ++--
src/kernel/types/browser_pool.py | 16 ++++++++++++++++
2 files changed, 18 insertions(+), 2 deletions(-)
diff --git a/.stats.yml b/.stats.yml
index 1d73bab6..f8ffb2dd 100644
--- a/.stats.yml
+++ b/.stats.yml
@@ -1,4 +1,4 @@
configured_endpoints: 125
-openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel/kernel-f040d3f5c26d6e0d312206b294d3745b272e26b883e7d704f6b4c45dd438cd01.yml
-openapi_spec_hash: 56f4918d2e3f8d717ffe3643a20feb96
+openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel/kernel-f0486d0b8e34ddf8ba34e36ff97856cf973e0797e021ff621e04e4840970966e.yml
+openapi_spec_hash: 12a71ce5ac80fc65f6a8779f0ca29223
config_hash: 06186eb40e0058a2a87ac251fc07415d
diff --git a/src/kernel/types/browser_pool.py b/src/kernel/types/browser_pool.py
index a407009c..78860923 100644
--- a/src/kernel/types/browser_pool.py
+++ b/src/kernel/types/browser_pool.py
@@ -150,5 +150,21 @@ class BrowserPool(BaseModel):
created_at: datetime
"""Timestamp when the browser pool was created"""
+ extension_ids: List[str]
+ """Resolved extension IDs attached to the pool, in configured load order.
+
+ Empty when no extensions are attached. Authoritative for programmatic consumers;
+ the extensions inside `browser_pool_config` reflect the configured selector
+ (echoed as sent on create).
+ """
+
name: Optional[str] = None
"""Browser pool name, if set"""
+
+ profile_id: Optional[str] = None
+ """Resolved profile ID the pool is attached to.
+
+ Omitted when no profile is attached. Authoritative for programmatic consumers;
+ the profile inside `browser_pool_config` reflects the configured selector
+ (echoed as sent on create).
+ """
From 8aeb4f493226d54bd358d8bb7fbbccc66de53451 Mon Sep 17 00:00:00 2001
From: "stainless-app[bot]"
<142633134+stainless-app[bot]@users.noreply.github.com>
Date: Wed, 8 Jul 2026 21:17:43 +0000
Subject: [PATCH 110/366] release: 0.75.0
---
.release-please-manifest.json | 2 +-
CHANGELOG.md | 19 +++++++++++++++++++
pyproject.toml | 2 +-
src/kernel/_version.py | 2 +-
4 files changed, 22 insertions(+), 3 deletions(-)
diff --git a/.release-please-manifest.json b/.release-please-manifest.json
index 192d2cdd..1cfe7eb3 100644
--- a/.release-please-manifest.json
+++ b/.release-please-manifest.json
@@ -1,3 +1,3 @@
{
- ".": "0.74.0"
+ ".": "0.75.0"
}
\ No newline at end of file
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 66e9f601..77fec02e 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,24 @@
# Changelog
+## 0.75.0 (2026-07-08)
+
+Full Changelog: [v0.74.0...v0.75.0](https://github.com/kernel/kernel-python-sdk/compare/v0.74.0...v0.75.0)
+
+### Features
+
+* Auto-flush pools when a managed auth profile re-authenticates ([430c07f](https://github.com/kernel/kernel-python-sdk/commit/430c07fc2bfd5ab80fe25cc4677b34729bf4b4ee))
+* Document env var redaction on deployment and app reads ([51d713b](https://github.com/kernel/kernel-python-sdk/commit/51d713b92b4c0ed0c2420ea2e128568530a81a14))
+* Expose resolved profile_id and extension_ids on browser pool reads ([56876e9](https://github.com/kernel/kernel-python-sdk/commit/56876e9a49fca34ec34b58ea8f5f85130c0c0315))
+* Reject API key self-deletion ([482f920](https://github.com/kernel/kernel-python-sdk/commit/482f9209f956e5615bdbaa6ec9c58081df56587b))
+* Revert "Store and return a sha256 checksum for uploaded extensions (#… ([74dbc54](https://github.com/kernel/kernel-python-sdk/commit/74dbc54d4d8d5b330f74985227fa09f7569457a7))
+* Store and return a sha256 checksum for uploaded extensions ([b266775](https://github.com/kernel/kernel-python-sdk/commit/b266775497fec10224e965c787c146494eecdae8))
+* Store and return a sha256 checksum for uploaded extensions (reland) ([98cfb28](https://github.com/kernel/kernel-python-sdk/commit/98cfb2835be3d321799bbfd0400e1bb59b39b218))
+
+
+### Documentation
+
+* **api:** clarify reuse/discard_all_idle pool config staleness ([e8641eb](https://github.com/kernel/kernel-python-sdk/commit/e8641ebe96926b22f8874a8ff79498a4cbe7e4ed))
+
## 0.74.0 (2026-07-06)
Full Changelog: [v0.73.0...v0.74.0](https://github.com/kernel/kernel-python-sdk/compare/v0.73.0...v0.74.0)
diff --git a/pyproject.toml b/pyproject.toml
index 7d03baca..1c215e5a 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -1,6 +1,6 @@
[project]
name = "kernel"
-version = "0.74.0"
+version = "0.75.0"
description = "The official Python library for the kernel API"
dynamic = ["readme"]
license = "Apache-2.0"
diff --git a/src/kernel/_version.py b/src/kernel/_version.py
index baf9e642..98f14c72 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.74.0" # x-release-please-version
+__version__ = "0.75.0" # x-release-please-version
From 873c043fa034565f80cd2b571eb140f135d41b72 Mon Sep 17 00:00:00 2001
From: "stainless-app[bot]"
<142633134+stainless-app[bot]@users.noreply.github.com>
Date: Thu, 9 Jul 2026 16:07:11 +0000
Subject: [PATCH 111/366] codegen metadata
---
.stats.yml | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/.stats.yml b/.stats.yml
index f8ffb2dd..639bc208 100644
--- a/.stats.yml
+++ b/.stats.yml
@@ -1,4 +1,4 @@
configured_endpoints: 125
-openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel/kernel-f0486d0b8e34ddf8ba34e36ff97856cf973e0797e021ff621e04e4840970966e.yml
-openapi_spec_hash: 12a71ce5ac80fc65f6a8779f0ca29223
+openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel/kernel-8af2d28c8540273b1b413f49b8f9bcaa5df25fa2332d187d2693ec01502a2ac2.yml
+openapi_spec_hash: ee71b9231754b4682377a514d1a2c249
config_hash: 06186eb40e0058a2a87ac251fc07415d
From 21b159befc9a971a002211712f8c5e3e909345a1 Mon Sep 17 00:00:00 2001
From: "stainless-app[bot]"
<142633134+stainless-app[bot]@users.noreply.github.com>
Date: Thu, 9 Jul 2026 16:52:24 +0000
Subject: [PATCH 112/366] feat: Layer telemetry request config onto the default
set
---
.stats.yml | 4 ++--
src/kernel/types/browser_create_params.py | 24 +++++++++++++------
src/kernel/types/browser_update_params.py | 24 +++++++++++++------
.../browser_telemetry_categories_config.py | 4 ++--
...owser_telemetry_categories_config_param.py | 4 ++--
.../browser_telemetry_category_config.py | 4 +++-
...browser_telemetry_category_config_param.py | 4 +++-
7 files changed, 46 insertions(+), 22 deletions(-)
diff --git a/.stats.yml b/.stats.yml
index 639bc208..adc4cc21 100644
--- a/.stats.yml
+++ b/.stats.yml
@@ -1,4 +1,4 @@
configured_endpoints: 125
-openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel/kernel-8af2d28c8540273b1b413f49b8f9bcaa5df25fa2332d187d2693ec01502a2ac2.yml
-openapi_spec_hash: ee71b9231754b4682377a514d1a2c249
+openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel/kernel-61dab8d0d01c2838febaf4319114ae35aa169a813a102e93af6598ca0d445da2.yml
+openapi_spec_hash: c9fbe809670384dc5141dccb60e45672
config_hash: 06186eb40e0058a2a87ac251fc07415d
diff --git a/src/kernel/types/browser_create_params.py b/src/kernel/types/browser_create_params.py
index f76cc23f..01ac2598 100644
--- a/src/kernel/types/browser_create_params.py
+++ b/src/kernel/types/browser_create_params.py
@@ -134,16 +134,26 @@ class Telemetry(TypedDict, total=False):
browser: BrowserTelemetryCategoriesConfigParam
"""Per-category capture flags.
- Selection is opt-in: only the categories set to enabled=true are captured;
- anything omitted is off. If enabled is true and browser is omitted or empty, the
- default category set is used. A browser config that enables nothing stops
- capture on update and starts no capture on create.
+ The operational categories (control, connection, system, captcha) are captured
+ whenever telemetry is enabled; set one to enabled=false to opt out. The CDP
+ categories (console, network, page, interaction) and screenshot are off by
+ default; set enabled=true to opt in. On create, provided categories layer onto
+ the default set. On update, provided categories merge onto the session's current
+ config; when no telemetry is active this falls back to the default set (matching
+ create). If browser is omitted or empty, the default set is used. A browser
+ config that disables every category stops capture on update and starts no
+ capture on create.
"""
enabled: bool
"""Request shortcut for browser telemetry capture.
- True enables capture using the default category set unless browser category
- settings are provided. False stops capture on update and starts no capture on
- create. enabled=false cannot be combined with browser category settings.
+ True enables capture; with no browser category settings it captures the default
+ set (control, connection, system, captcha), and any browser category settings
+ are layered onto that default set. On update, enabled=true resolves the config
+ fresh from the default set plus any provided categories, replacing the session's
+ current selection rather than merging onto it; omit enabled to merge categories
+ onto the current selection instead. False stops capture on update and starts no
+ capture on create. enabled=false cannot be combined with browser category
+ settings.
"""
diff --git a/src/kernel/types/browser_update_params.py b/src/kernel/types/browser_update_params.py
index 7488be4f..3c3140ee 100644
--- a/src/kernel/types/browser_update_params.py
+++ b/src/kernel/types/browser_update_params.py
@@ -70,18 +70,28 @@ class Telemetry(TypedDict, total=False):
browser: BrowserTelemetryCategoriesConfigParam
"""Per-category capture flags.
- Selection is opt-in: only the categories set to enabled=true are captured;
- anything omitted is off. If enabled is true and browser is omitted or empty, the
- default category set is used. A browser config that enables nothing stops
- capture on update and starts no capture on create.
+ The operational categories (control, connection, system, captcha) are captured
+ whenever telemetry is enabled; set one to enabled=false to opt out. The CDP
+ categories (console, network, page, interaction) and screenshot are off by
+ default; set enabled=true to opt in. On create, provided categories layer onto
+ the default set. On update, provided categories merge onto the session's current
+ config; when no telemetry is active this falls back to the default set (matching
+ create). If browser is omitted or empty, the default set is used. A browser
+ config that disables every category stops capture on update and starts no
+ capture on create.
"""
enabled: bool
"""Request shortcut for browser telemetry capture.
- True enables capture using the default category set unless browser category
- settings are provided. False stops capture on update and starts no capture on
- create. enabled=false cannot be combined with browser category settings.
+ True enables capture; with no browser category settings it captures the default
+ set (control, connection, system, captcha), and any browser category settings
+ are layered onto that default set. On update, enabled=true resolves the config
+ fresh from the default set plus any provided categories, replacing the session's
+ current selection rather than merging onto it; omit enabled to merge categories
+ onto the current selection instead. False stops capture on update and starts no
+ capture on create. enabled=false cannot be combined with browser category
+ settings.
"""
diff --git a/src/kernel/types/browsers/browser_telemetry_categories_config.py b/src/kernel/types/browsers/browser_telemetry_categories_config.py
index 0013fee3..b89ff17b 100644
--- a/src/kernel/types/browsers/browser_telemetry_categories_config.py
+++ b/src/kernel/types/browsers/browser_telemetry_categories_config.py
@@ -9,9 +9,9 @@
class BrowserTelemetryCategoriesConfig(BaseModel):
- """Per-category telemetry capture settings.
+ """Per-category telemetry capture settings layered onto the default set.
- Selection is opt-in: set a category to enabled=true to capture it; anything omitted is off. The default set (used by enabled=true with no per-category settings) is the lightweight operational signals: control, connection, system, captcha. The CDP categories (console, network, page, interaction) and screenshot are off by default and must be opted into.
+ The operational signals (control, connection, system, captcha) are on by default and are opt-out: set one to enabled=false to stop capturing it. The CDP categories (console, network, page, interaction) and screenshot are off by default and are opt-in: set enabled=true to capture them.
"""
captcha: Optional[BrowserTelemetryCategoryConfig] = None
diff --git a/src/kernel/types/browsers/browser_telemetry_categories_config_param.py b/src/kernel/types/browsers/browser_telemetry_categories_config_param.py
index a1520d7c..add32385 100644
--- a/src/kernel/types/browsers/browser_telemetry_categories_config_param.py
+++ b/src/kernel/types/browsers/browser_telemetry_categories_config_param.py
@@ -10,9 +10,9 @@
class BrowserTelemetryCategoriesConfigParam(TypedDict, total=False):
- """Per-category telemetry capture settings.
+ """Per-category telemetry capture settings layered onto the default set.
- Selection is opt-in: set a category to enabled=true to capture it; anything omitted is off. The default set (used by enabled=true with no per-category settings) is the lightweight operational signals: control, connection, system, captcha. The CDP categories (console, network, page, interaction) and screenshot are off by default and must be opted into.
+ The operational signals (control, connection, system, captcha) are on by default and are opt-out: set one to enabled=false to stop capturing it. The CDP categories (console, network, page, interaction) and screenshot are off by default and are opt-in: set enabled=true to capture them.
"""
captcha: BrowserTelemetryCategoryConfigParam
diff --git a/src/kernel/types/browsers/browser_telemetry_category_config.py b/src/kernel/types/browsers/browser_telemetry_category_config.py
index f75616c9..f63c02f4 100644
--- a/src/kernel/types/browsers/browser_telemetry_category_config.py
+++ b/src/kernel/types/browsers/browser_telemetry_category_config.py
@@ -13,5 +13,7 @@ class BrowserTelemetryCategoryConfig(BaseModel):
enabled: Optional[bool] = None
"""Whether this category is captured.
- Selection is opt-in, so an omitted category is not captured.
+ Operational categories (control, connection, system, captcha) default to true;
+ set false to opt out. CDP categories (console, network, page, interaction) and
+ screenshot default to false; set true to opt in.
"""
diff --git a/src/kernel/types/browsers/browser_telemetry_category_config_param.py b/src/kernel/types/browsers/browser_telemetry_category_config_param.py
index 3958f7d8..09dbe418 100644
--- a/src/kernel/types/browsers/browser_telemetry_category_config_param.py
+++ b/src/kernel/types/browsers/browser_telemetry_category_config_param.py
@@ -13,5 +13,7 @@ class BrowserTelemetryCategoryConfigParam(TypedDict, total=False):
enabled: bool
"""Whether this category is captured.
- Selection is opt-in, so an omitted category is not captured.
+ Operational categories (control, connection, system, captcha) default to true;
+ set false to opt out. CDP categories (console, network, page, interaction) and
+ screenshot default to false; set true to opt in.
"""
From ce6cda74de4f0096f464af5e5c23020cb2c13761 Mon Sep 17 00:00:00 2001
From: "stainless-app[bot]"
<142633134+stainless-app[bot]@users.noreply.github.com>
Date: Thu, 9 Jul 2026 18:32:36 +0000
Subject: [PATCH 113/366] feat: Return credential value keys and support
removing values on update
---
.stats.yml | 4 ++--
src/kernel/resources/credentials.py | 12 +++++++++++-
src/kernel/types/credential.py | 9 ++++++++-
src/kernel/types/credential_update_params.py | 9 +++++++++
tests/api_resources/test_credentials.py | 2 ++
5 files changed, 32 insertions(+), 4 deletions(-)
diff --git a/.stats.yml b/.stats.yml
index adc4cc21..3f7dec3b 100644
--- a/.stats.yml
+++ b/.stats.yml
@@ -1,4 +1,4 @@
configured_endpoints: 125
-openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel/kernel-61dab8d0d01c2838febaf4319114ae35aa169a813a102e93af6598ca0d445da2.yml
-openapi_spec_hash: c9fbe809670384dc5141dccb60e45672
+openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel/kernel-ba32039d3975da7aa6d28e5184f0a44c9fbfe36ab7dbc71985d14e2ecc0867b9.yml
+openapi_spec_hash: a9f32fc90c2add2ae85af828c298e35b
config_hash: 06186eb40e0058a2a87ac251fc07415d
diff --git a/src/kernel/resources/credentials.py b/src/kernel/resources/credentials.py
index 3919ecff..fd073957 100644
--- a/src/kernel/resources/credentials.py
+++ b/src/kernel/resources/credentials.py
@@ -7,7 +7,7 @@
import httpx
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 .._types import Body, Omit, Query, Headers, NoneType, NotGiven, SequenceNotStr, omit, not_given
from .._utils import path_template, maybe_transform, async_maybe_transform
from .._compat import cached_property
from .._resource import SyncAPIResource, AsyncAPIResource
@@ -145,6 +145,7 @@ def update(
id_or_name: str,
*,
name: str | Omit = omit,
+ remove_value_keys: SequenceNotStr[str] | Omit = omit,
sso_provider: Optional[str] | Omit = omit,
totp_secret: str | Omit = omit,
values: Dict[str, str] | Omit = omit,
@@ -163,6 +164,9 @@ def update(
Args:
name: New name for the credential
+ remove_value_keys: Field names to remove from the credential's stored values. Removals are applied
+ before `values` are merged, so a key present in both is kept with its new value.
+
sso_provider: If set, indicates this credential should be used with the specified SSO
provider. Set to empty string or null to remove.
@@ -187,6 +191,7 @@ def update(
body=maybe_transform(
{
"name": name,
+ "remove_value_keys": remove_value_keys,
"sso_provider": sso_provider,
"totp_secret": totp_secret,
"values": values,
@@ -445,6 +450,7 @@ async def update(
id_or_name: str,
*,
name: str | Omit = omit,
+ remove_value_keys: SequenceNotStr[str] | Omit = omit,
sso_provider: Optional[str] | Omit = omit,
totp_secret: str | Omit = omit,
values: Dict[str, str] | Omit = omit,
@@ -463,6 +469,9 @@ async def update(
Args:
name: New name for the credential
+ remove_value_keys: Field names to remove from the credential's stored values. Removals are applied
+ before `values` are merged, so a key present in both is kept with its new value.
+
sso_provider: If set, indicates this credential should be used with the specified SSO
provider. Set to empty string or null to remove.
@@ -487,6 +496,7 @@ async def update(
body=await async_maybe_transform(
{
"name": name,
+ "remove_value_keys": remove_value_keys,
"sso_provider": sso_provider,
"totp_secret": totp_secret,
"values": values,
diff --git a/src/kernel/types/credential.py b/src/kernel/types/credential.py
index 323617c5..fb2ff6e1 100644
--- a/src/kernel/types/credential.py
+++ b/src/kernel/types/credential.py
@@ -1,6 +1,6 @@
# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
-from typing import Optional
+from typing import List, Optional
from datetime import datetime
from .._models import BaseModel
@@ -48,3 +48,10 @@ class Credential(BaseModel):
totp_code_expires_at: Optional[datetime] = None
"""When the totp_code expires. Only included when totp_code is present."""
+
+ value_keys: Optional[List[str]] = None
+ """The field names stored in this credential's values (e.g., username, password).
+
+ Values themselves are never returned. Included on single-credential responses
+ (create, get by id or name, update); omitted from list responses.
+ """
diff --git a/src/kernel/types/credential_update_params.py b/src/kernel/types/credential_update_params.py
index c42209e4..9da3f09c 100644
--- a/src/kernel/types/credential_update_params.py
+++ b/src/kernel/types/credential_update_params.py
@@ -5,6 +5,8 @@
from typing import Dict, Optional
from typing_extensions import TypedDict
+from .._types import SequenceNotStr
+
__all__ = ["CredentialUpdateParams"]
@@ -12,6 +14,13 @@ class CredentialUpdateParams(TypedDict, total=False):
name: str
"""New name for the credential"""
+ remove_value_keys: SequenceNotStr[str]
+ """Field names to remove from the credential's stored values.
+
+ Removals are applied before `values` are merged, so a key present in both is
+ kept with its new value.
+ """
+
sso_provider: Optional[str]
"""If set, indicates this credential should be used with the specified SSO
provider.
diff --git a/tests/api_resources/test_credentials.py b/tests/api_resources/test_credentials.py
index 31704230..9fddf1c0 100644
--- a/tests/api_resources/test_credentials.py
+++ b/tests/api_resources/test_credentials.py
@@ -141,6 +141,7 @@ def test_method_update_with_all_params(self, client: Kernel) -> None:
credential = client.credentials.update(
id_or_name="id_or_name",
name="my-updated-login",
+ remove_value_keys=["old_field"],
sso_provider="google",
totp_secret="JBSWY3DPEHPK3PXP",
values={
@@ -433,6 +434,7 @@ async def test_method_update_with_all_params(self, async_client: AsyncKernel) ->
credential = await async_client.credentials.update(
id_or_name="id_or_name",
name="my-updated-login",
+ remove_value_keys=["old_field"],
sso_provider="google",
totp_secret="JBSWY3DPEHPK3PXP",
values={
From 9b3ba00bdbab209552f9db3d946d4cb6cc985096 Mon Sep 17 00:00:00 2001
From: "stainless-app[bot]"
<142633134+stainless-app[bot]@users.noreply.github.com>
Date: Thu, 9 Jul 2026 18:33:08 +0000
Subject: [PATCH 114/366] release: 0.76.0
---
.release-please-manifest.json | 2 +-
CHANGELOG.md | 9 +++++++++
pyproject.toml | 2 +-
src/kernel/_version.py | 2 +-
4 files changed, 12 insertions(+), 3 deletions(-)
diff --git a/.release-please-manifest.json b/.release-please-manifest.json
index 1cfe7eb3..aa39267a 100644
--- a/.release-please-manifest.json
+++ b/.release-please-manifest.json
@@ -1,3 +1,3 @@
{
- ".": "0.75.0"
+ ".": "0.76.0"
}
\ No newline at end of file
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 77fec02e..e0c979f6 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,14 @@
# Changelog
+## 0.76.0 (2026-07-09)
+
+Full Changelog: [v0.75.0...v0.76.0](https://github.com/kernel/kernel-python-sdk/compare/v0.75.0...v0.76.0)
+
+### Features
+
+* Layer telemetry request config onto the default set ([c6d6b65](https://github.com/kernel/kernel-python-sdk/commit/c6d6b651a377d4283af20ab631f2adf25354954f))
+* Return credential value keys and support removing values on update ([1c4af8e](https://github.com/kernel/kernel-python-sdk/commit/1c4af8ea100bd288182b38792ad87abab4136543))
+
## 0.75.0 (2026-07-08)
Full Changelog: [v0.74.0...v0.75.0](https://github.com/kernel/kernel-python-sdk/compare/v0.74.0...v0.75.0)
diff --git a/pyproject.toml b/pyproject.toml
index 1c215e5a..205c9767 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -1,6 +1,6 @@
[project]
name = "kernel"
-version = "0.75.0"
+version = "0.76.0"
description = "The official Python library for the kernel API"
dynamic = ["readme"]
license = "Apache-2.0"
diff --git a/src/kernel/_version.py b/src/kernel/_version.py
index 98f14c72..443b322f 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.75.0" # x-release-please-version
+__version__ = "0.76.0" # x-release-please-version
From 2298b057abf5356af874ad86d59703a0844fbca3 Mon Sep 17 00:00:00 2001
From: "stainless-app[bot]"
<142633134+stainless-app[bot]@users.noreply.github.com>
Date: Thu, 9 Jul 2026 23:07:49 +0000
Subject: [PATCH 115/366] docs(openapi): describe unified concurrency limit,
deprecate max_pooled_sessions (CUS-275)
---
.stats.yml | 4 +--
src/kernel/resources/organization/limits.py | 28 +++++++++----------
src/kernel/resources/projects/limits.py | 20 +++++++------
.../types/organization/limit_update_params.py | 8 +++---
src/kernel/types/organization/org_limits.py | 7 +++--
.../types/projects/limit_update_params.py | 12 ++++----
src/kernel/types/projects/project_limits.py | 11 ++++----
7 files changed, 49 insertions(+), 41 deletions(-)
diff --git a/.stats.yml b/.stats.yml
index 3f7dec3b..c7028199 100644
--- a/.stats.yml
+++ b/.stats.yml
@@ -1,4 +1,4 @@
configured_endpoints: 125
-openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel/kernel-ba32039d3975da7aa6d28e5184f0a44c9fbfe36ab7dbc71985d14e2ecc0867b9.yml
-openapi_spec_hash: a9f32fc90c2add2ae85af828c298e35b
+openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel/kernel-db45aab5b114bc0848eab997c85bfc02a653be9f929f8d090ec24187462733ee.yml
+openapi_spec_hash: 82f4282312699f8d54bec207e981efe8
config_hash: 06186eb40e0058a2a87ac251fc07415d
diff --git a/src/kernel/resources/organization/limits.py b/src/kernel/resources/organization/limits.py
index 4bf68845..d30bfbfa 100644
--- a/src/kernel/resources/organization/limits.py
+++ b/src/kernel/resources/organization/limits.py
@@ -56,8 +56,9 @@ def retrieve(
timeout: float | httpx.Timeout | None | NotGiven = not_given,
) -> OrgLimits:
"""
- Get the organization's concurrent session ceiling and the default per-project
- concurrency cap applied to projects without an explicit override.
+ Get the organization's concurrency limit — the maximum browsers running at once
+ across on-demand sessions and browser pool reservations — and the default
+ per-project concurrency cap applied to projects without an explicit override.
"""
return self._get(
"/org/limits",
@@ -81,13 +82,12 @@ def update(
"""
Set the default per-project concurrency cap applied to projects without an
explicit override. Set the value to 0 to remove the default; omit to leave it
- unchanged. The default cannot exceed the organization's concurrent session
- ceiling.
+ unchanged. The default cannot exceed the organization's concurrency limit.
Args:
- default_project_max_concurrent_sessions: Default maximum concurrent browser sessions for projects without an explicit
- override. Set to 0 to remove the default; omit to leave unchanged. Cannot exceed
- the organization's concurrent session ceiling.
+ default_project_max_concurrent_sessions: Default maximum concurrent browsers for projects without an explicit override.
+ Set to 0 to remove the default; omit to leave unchanged. Cannot exceed the
+ organization's concurrency limit.
extra_headers: Send extra headers
@@ -143,8 +143,9 @@ async def retrieve(
timeout: float | httpx.Timeout | None | NotGiven = not_given,
) -> OrgLimits:
"""
- Get the organization's concurrent session ceiling and the default per-project
- concurrency cap applied to projects without an explicit override.
+ Get the organization's concurrency limit — the maximum browsers running at once
+ across on-demand sessions and browser pool reservations — and the default
+ per-project concurrency cap applied to projects without an explicit override.
"""
return await self._get(
"/org/limits",
@@ -168,13 +169,12 @@ async def update(
"""
Set the default per-project concurrency cap applied to projects without an
explicit override. Set the value to 0 to remove the default; omit to leave it
- unchanged. The default cannot exceed the organization's concurrent session
- ceiling.
+ unchanged. The default cannot exceed the organization's concurrency limit.
Args:
- default_project_max_concurrent_sessions: Default maximum concurrent browser sessions for projects without an explicit
- override. Set to 0 to remove the default; omit to leave unchanged. Cannot exceed
- the organization's concurrent session ceiling.
+ default_project_max_concurrent_sessions: Default maximum concurrent browsers for projects without an explicit override.
+ Set to 0 to remove the default; omit to leave unchanged. Cannot exceed the
+ organization's concurrency limit.
extra_headers: Send extra headers
diff --git a/src/kernel/resources/projects/limits.py b/src/kernel/resources/projects/limits.py
index ade9302c..1e05711f 100644
--- a/src/kernel/resources/projects/limits.py
+++ b/src/kernel/resources/projects/limits.py
@@ -104,11 +104,13 @@ def update(
max_concurrent_invocations: Maximum concurrent app invocations for this project. Set to 0 to remove the cap;
omit to leave unchanged.
- max_concurrent_sessions: Maximum concurrent browser sessions for this project. Set to 0 to remove the
- cap; omit to leave unchanged.
+ max_concurrent_sessions: Maximum concurrent browsers for this project, covering both on-demand sessions
+ and browser pool reservations. Set to 0 to remove the cap; omit to leave
+ unchanged.
- max_pooled_sessions: Maximum pooled sessions capacity for this project. Set to 0 to remove the cap;
- omit to leave unchanged.
+ max_pooled_sessions: Deprecated: pooled browsers now count toward `max_concurrent_sessions`. Requests
+ that set this field are rejected with a 400 once the unified concurrency limit
+ is enabled for your organization.
extra_headers: Send extra headers
@@ -218,11 +220,13 @@ async def update(
max_concurrent_invocations: Maximum concurrent app invocations for this project. Set to 0 to remove the cap;
omit to leave unchanged.
- max_concurrent_sessions: Maximum concurrent browser sessions for this project. Set to 0 to remove the
- cap; omit to leave unchanged.
+ max_concurrent_sessions: Maximum concurrent browsers for this project, covering both on-demand sessions
+ and browser pool reservations. Set to 0 to remove the cap; omit to leave
+ unchanged.
- max_pooled_sessions: Maximum pooled sessions capacity for this project. Set to 0 to remove the cap;
- omit to leave unchanged.
+ max_pooled_sessions: Deprecated: pooled browsers now count toward `max_concurrent_sessions`. Requests
+ that set this field are rejected with a 400 once the unified concurrency limit
+ is enabled for your organization.
extra_headers: Send extra headers
diff --git a/src/kernel/types/organization/limit_update_params.py b/src/kernel/types/organization/limit_update_params.py
index 619eae91..4c85f9c1 100644
--- a/src/kernel/types/organization/limit_update_params.py
+++ b/src/kernel/types/organization/limit_update_params.py
@@ -10,8 +10,8 @@
class LimitUpdateParams(TypedDict, total=False):
default_project_max_concurrent_sessions: Optional[int]
- """
- Default maximum concurrent browser sessions for projects without an explicit
- override. Set to 0 to remove the default; omit to leave unchanged. Cannot exceed
- the organization's concurrent session ceiling.
+ """Default maximum concurrent browsers for projects without an explicit override.
+
+ Set to 0 to remove the default; omit to leave unchanged. Cannot exceed the
+ organization's concurrency limit.
"""
diff --git a/src/kernel/types/organization/org_limits.py b/src/kernel/types/organization/org_limits.py
index da9153c1..3d614996 100644
--- a/src/kernel/types/organization/org_limits.py
+++ b/src/kernel/types/organization/org_limits.py
@@ -10,7 +10,7 @@
class OrgLimits(BaseModel):
default_project_max_concurrent_sessions: Optional[int] = None
"""
- Default maximum concurrent browser sessions applied to every project that has no
+ Default maximum concurrent browsers applied to every project that has no
explicit per-project override. Null means no org-level default, so such projects
are uncapped (only the org-wide limit applies). Applies to existing and newly
created projects.
@@ -18,7 +18,8 @@ class OrgLimits(BaseModel):
max_concurrent_sessions: Optional[int] = None
"""
- The organization's effective concurrent browser session ceiling, from its plan
- or an override. Read-only and shared across all projects in the org; a
+ The organization's effective concurrency limit — the maximum browsers running at
+ once, covering both on-demand sessions and browser pool reservations — from its
+ plan or an override. Read-only and shared across all projects in the org; a
per-project default cannot exceed it.
"""
diff --git a/src/kernel/types/projects/limit_update_params.py b/src/kernel/types/projects/limit_update_params.py
index 387b4c64..8bfb2775 100644
--- a/src/kernel/types/projects/limit_update_params.py
+++ b/src/kernel/types/projects/limit_update_params.py
@@ -16,13 +16,15 @@ class LimitUpdateParams(TypedDict, total=False):
"""
max_concurrent_sessions: Optional[int]
- """Maximum concurrent browser sessions for this project.
-
- Set to 0 to remove the cap; omit to leave unchanged.
+ """
+ Maximum concurrent browsers for this project, covering both on-demand sessions
+ and browser pool reservations. Set to 0 to remove the cap; omit to leave
+ unchanged.
"""
max_pooled_sessions: Optional[int]
- """Maximum pooled sessions capacity for this project.
+ """Deprecated: pooled browsers now count toward `max_concurrent_sessions`.
- Set to 0 to remove the cap; omit to leave unchanged.
+ Requests that set this field are rejected with a 400 once the unified
+ concurrency limit is enabled for your organization.
"""
diff --git a/src/kernel/types/projects/project_limits.py b/src/kernel/types/projects/project_limits.py
index 99868fc8..a7f5cd74 100644
--- a/src/kernel/types/projects/project_limits.py
+++ b/src/kernel/types/projects/project_limits.py
@@ -15,13 +15,14 @@ class ProjectLimits(BaseModel):
"""
max_concurrent_sessions: Optional[int] = None
- """Maximum concurrent browser sessions for this project.
-
- Null means no project-level cap.
+ """
+ Maximum concurrent browsers for this project, covering both on-demand sessions
+ (`browsers.create()`) and browser pool reservations. Null means no project-level
+ cap.
"""
max_pooled_sessions: Optional[int] = None
- """Maximum pooled sessions capacity for this project.
+ """Deprecated: pooled browsers now count toward `max_concurrent_sessions`.
- Null means no project-level cap.
+ Always null once the unified concurrency limit is enabled for your organization.
"""
From 3c20cdab9483becf6f9658d2a9fc06d5b9c3daed Mon Sep 17 00:00:00 2001
From: "stainless-app[bot]"
<142633134+stainless-app[bot]@users.noreply.github.com>
Date: Fri, 10 Jul 2026 13:46:00 +0000
Subject: [PATCH 116/366] feat: Add name-only rename for profiles and proxies
---
.stats.yml | 8 +-
api.md | 3 +
src/kernel/resources/profiles.py | 114 ++++++++++++++++++-
src/kernel/resources/proxies.py | 99 ++++++++++++++++-
src/kernel/types/__init__.py | 3 +
src/kernel/types/profile_create_params.py | 7 +-
src/kernel/types/profile_update_params.py | 17 +++
src/kernel/types/proxy_update_params.py | 16 +++
src/kernel/types/proxy_update_response.py | 128 ++++++++++++++++++++++
tests/api_resources/test_profiles.py | 92 ++++++++++++++++
tests/api_resources/test_proxies.py | 93 ++++++++++++++++
11 files changed, 571 insertions(+), 9 deletions(-)
create mode 100644 src/kernel/types/profile_update_params.py
create mode 100644 src/kernel/types/proxy_update_params.py
create mode 100644 src/kernel/types/proxy_update_response.py
diff --git a/.stats.yml b/.stats.yml
index c7028199..09109a9b 100644
--- a/.stats.yml
+++ b/.stats.yml
@@ -1,4 +1,4 @@
-configured_endpoints: 125
-openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel/kernel-db45aab5b114bc0848eab997c85bfc02a653be9f929f8d090ec24187462733ee.yml
-openapi_spec_hash: 82f4282312699f8d54bec207e981efe8
-config_hash: 06186eb40e0058a2a87ac251fc07415d
+configured_endpoints: 127
+openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel/kernel-bc33d13e669972493e9ab0da82a71999822dc049eb14b113f446753c917ffcdb.yml
+openapi_spec_hash: 133a2e0d0cdc4023f98b3c2e589cf5d8
+config_hash: 77ee715aa17061166f9a02b264a21b8d
diff --git a/api.md b/api.md
index 300e937b..7feea4cd 100644
--- a/api.md
+++ b/api.md
@@ -285,6 +285,7 @@ Methods:
- client.profiles.create(\*\*params) -> Profile
- client.profiles.retrieve(id_or_name) -> Profile
+- client.profiles.update(id_or_name, \*\*params) -> Profile
- client.profiles.list(\*\*params) -> SyncOffsetPagination[Profile]
- client.profiles.delete(id_or_name) -> None
- client.profiles.download(id_or_name) -> BinaryAPIResponse
@@ -328,6 +329,7 @@ Types:
from kernel.types import (
ProxyCreateResponse,
ProxyRetrieveResponse,
+ ProxyUpdateResponse,
ProxyListResponse,
ProxyCheckResponse,
)
@@ -337,6 +339,7 @@ Methods:
- client.proxies.create(\*\*params) -> ProxyCreateResponse
- client.proxies.retrieve(id) -> ProxyRetrieveResponse
+- client.proxies.update(id, \*\*params) -> ProxyUpdateResponse
- client.proxies.list(\*\*params) -> SyncOffsetPagination[ProxyListResponse]
- client.proxies.delete(id) -> None
- client.proxies.check(id, \*\*params) -> ProxyCheckResponse
diff --git a/src/kernel/resources/profiles.py b/src/kernel/resources/profiles.py
index 4083f12d..ef7f6220 100644
--- a/src/kernel/resources/profiles.py
+++ b/src/kernel/resources/profiles.py
@@ -4,7 +4,7 @@
import httpx
-from ..types import profile_list_params, profile_create_params
+from ..types import profile_list_params, profile_create_params, profile_update_params
from .._types import Body, Omit, Query, Headers, NoneType, NotGiven, omit, not_given
from .._utils import path_template, maybe_transform, async_maybe_transform
from .._compat import cached_property
@@ -68,7 +68,9 @@ def create(
sessions.
Args:
- name: Optional name of the profile. Must be unique within the project.
+ name: Optional name of the profile. Must be unique within the logical project; during
+ the default-project migration, unscoped profiles and profiles in the org default
+ project are treated as the same project.
extra_headers: Send extra headers
@@ -120,6 +122,52 @@ def retrieve(
cast_to=Profile,
)
+ def update(
+ self,
+ id_or_name: str,
+ *,
+ name: str,
+ # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
+ # The extra values given here take precedence over values defined on the client or passed to this method.
+ extra_headers: Headers | None = None,
+ extra_query: Query | None = None,
+ extra_body: Body | None = None,
+ timeout: float | httpx.Timeout | None | NotGiven = not_given,
+ ) -> Profile:
+ """Update a profile's name.
+
+ Names must be unique within the logical project; during
+ the default-project migration, unscoped profiles and profiles in the org default
+ project are treated as the same project. Duplicate-name conflicts are checked
+ before update but are best-effort because there is no backing unique index.
+ Renaming a profile while a browser session references it by name may prevent
+ that session's changes from saving; prefer renaming when the profile is not in
+ use.
+
+ Args:
+ name: New profile name. Must be unique within the logical project; during the
+ default-project migration, unscoped profiles and profiles in the org default
+ project are treated as the same project.
+
+ extra_headers: Send extra headers
+
+ extra_query: Add additional query parameters to the request
+
+ extra_body: Add additional JSON properties to the request
+
+ timeout: Override the client-level default timeout for this request, in seconds
+ """
+ if not id_or_name:
+ raise ValueError(f"Expected a non-empty value for `id_or_name` but received {id_or_name!r}")
+ return self._patch(
+ path_template("/profiles/{id_or_name}", id_or_name=id_or_name),
+ body=maybe_transform({"name": name}, profile_update_params.ProfileUpdateParams),
+ options=make_request_options(
+ extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
+ ),
+ cast_to=Profile,
+ )
+
def list(
self,
*,
@@ -278,7 +326,9 @@ async def create(
sessions.
Args:
- name: Optional name of the profile. Must be unique within the project.
+ name: Optional name of the profile. Must be unique within the logical project; during
+ the default-project migration, unscoped profiles and profiles in the org default
+ project are treated as the same project.
extra_headers: Send extra headers
@@ -330,6 +380,52 @@ async def retrieve(
cast_to=Profile,
)
+ async def update(
+ self,
+ id_or_name: str,
+ *,
+ name: str,
+ # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
+ # The extra values given here take precedence over values defined on the client or passed to this method.
+ extra_headers: Headers | None = None,
+ extra_query: Query | None = None,
+ extra_body: Body | None = None,
+ timeout: float | httpx.Timeout | None | NotGiven = not_given,
+ ) -> Profile:
+ """Update a profile's name.
+
+ Names must be unique within the logical project; during
+ the default-project migration, unscoped profiles and profiles in the org default
+ project are treated as the same project. Duplicate-name conflicts are checked
+ before update but are best-effort because there is no backing unique index.
+ Renaming a profile while a browser session references it by name may prevent
+ that session's changes from saving; prefer renaming when the profile is not in
+ use.
+
+ Args:
+ name: New profile name. Must be unique within the logical project; during the
+ default-project migration, unscoped profiles and profiles in the org default
+ project are treated as the same project.
+
+ extra_headers: Send extra headers
+
+ extra_query: Add additional query parameters to the request
+
+ extra_body: Add additional JSON properties to the request
+
+ timeout: Override the client-level default timeout for this request, in seconds
+ """
+ if not id_or_name:
+ raise ValueError(f"Expected a non-empty value for `id_or_name` but received {id_or_name!r}")
+ return await self._patch(
+ path_template("/profiles/{id_or_name}", id_or_name=id_or_name),
+ body=await async_maybe_transform({"name": name}, profile_update_params.ProfileUpdateParams),
+ options=make_request_options(
+ extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
+ ),
+ cast_to=Profile,
+ )
+
def list(
self,
*,
@@ -460,6 +556,9 @@ def __init__(self, profiles: ProfilesResource) -> None:
self.retrieve = to_raw_response_wrapper(
profiles.retrieve,
)
+ self.update = to_raw_response_wrapper(
+ profiles.update,
+ )
self.list = to_raw_response_wrapper(
profiles.list,
)
@@ -482,6 +581,9 @@ def __init__(self, profiles: AsyncProfilesResource) -> None:
self.retrieve = async_to_raw_response_wrapper(
profiles.retrieve,
)
+ self.update = async_to_raw_response_wrapper(
+ profiles.update,
+ )
self.list = async_to_raw_response_wrapper(
profiles.list,
)
@@ -504,6 +606,9 @@ def __init__(self, profiles: ProfilesResource) -> None:
self.retrieve = to_streamed_response_wrapper(
profiles.retrieve,
)
+ self.update = to_streamed_response_wrapper(
+ profiles.update,
+ )
self.list = to_streamed_response_wrapper(
profiles.list,
)
@@ -526,6 +631,9 @@ def __init__(self, profiles: AsyncProfilesResource) -> None:
self.retrieve = async_to_streamed_response_wrapper(
profiles.retrieve,
)
+ self.update = async_to_streamed_response_wrapper(
+ profiles.update,
+ )
self.list = async_to_streamed_response_wrapper(
profiles.list,
)
diff --git a/src/kernel/resources/proxies.py b/src/kernel/resources/proxies.py
index 0b4902de..6d866bd4 100644
--- a/src/kernel/resources/proxies.py
+++ b/src/kernel/resources/proxies.py
@@ -6,7 +6,7 @@
import httpx
-from ..types import proxy_list_params, proxy_check_params, proxy_create_params
+from ..types import proxy_list_params, proxy_check_params, proxy_create_params, proxy_update_params
from .._types import Body, Omit, Query, Headers, NoneType, NotGiven, SequenceNotStr, omit, not_given
from .._utils import path_template, maybe_transform, async_maybe_transform
from .._compat import cached_property
@@ -22,6 +22,7 @@
from ..types.proxy_list_response import ProxyListResponse
from ..types.proxy_check_response import ProxyCheckResponse
from ..types.proxy_create_response import ProxyCreateResponse
+from ..types.proxy_update_response import ProxyUpdateResponse
from ..types.proxy_retrieve_response import ProxyRetrieveResponse
__all__ = ["ProxiesResource", "AsyncProxiesResource"]
@@ -138,6 +139,48 @@ def retrieve(
cast_to=ProxyRetrieveResponse,
)
+ def update(
+ self,
+ id: str,
+ *,
+ name: str,
+ # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
+ # The extra values given here take precedence over values defined on the client or passed to this method.
+ extra_headers: Headers | None = None,
+ extra_query: Query | None = None,
+ extra_body: Body | None = None,
+ timeout: float | httpx.Timeout | None | NotGiven = not_given,
+ ) -> ProxyUpdateResponse:
+ """Update a proxy's name.
+
+ Proxy names are not unique and are not ID-or-name
+ addressable on this endpoint; duplicate names are allowed. Name-based
+ session-create lookups can remain ambiguous until callers resolve proxies by ID
+ or the API adds a stronger uniqueness contract.
+
+ Args:
+ name: New proxy name. Proxy names are trimmed and length-checked only; duplicates are
+ allowed because proxies are updated by ID, not by name.
+
+ 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("/proxies/{id}", id=id),
+ body=maybe_transform({"name": name}, proxy_update_params.ProxyUpdateParams),
+ options=make_request_options(
+ extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
+ ),
+ cast_to=ProxyUpdateResponse,
+ )
+
def list(
self,
*,
@@ -390,6 +433,48 @@ async def retrieve(
cast_to=ProxyRetrieveResponse,
)
+ async def update(
+ self,
+ id: str,
+ *,
+ name: str,
+ # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
+ # The extra values given here take precedence over values defined on the client or passed to this method.
+ extra_headers: Headers | None = None,
+ extra_query: Query | None = None,
+ extra_body: Body | None = None,
+ timeout: float | httpx.Timeout | None | NotGiven = not_given,
+ ) -> ProxyUpdateResponse:
+ """Update a proxy's name.
+
+ Proxy names are not unique and are not ID-or-name
+ addressable on this endpoint; duplicate names are allowed. Name-based
+ session-create lookups can remain ambiguous until callers resolve proxies by ID
+ or the API adds a stronger uniqueness contract.
+
+ Args:
+ name: New proxy name. Proxy names are trimmed and length-checked only; duplicates are
+ allowed because proxies are updated by ID, not by name.
+
+ 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("/proxies/{id}", id=id),
+ body=await async_maybe_transform({"name": name}, proxy_update_params.ProxyUpdateParams),
+ options=make_request_options(
+ extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
+ ),
+ cast_to=ProxyUpdateResponse,
+ )
+
def list(
self,
*,
@@ -541,6 +626,9 @@ def __init__(self, proxies: ProxiesResource) -> None:
self.retrieve = to_raw_response_wrapper(
proxies.retrieve,
)
+ self.update = to_raw_response_wrapper(
+ proxies.update,
+ )
self.list = to_raw_response_wrapper(
proxies.list,
)
@@ -562,6 +650,9 @@ def __init__(self, proxies: AsyncProxiesResource) -> None:
self.retrieve = async_to_raw_response_wrapper(
proxies.retrieve,
)
+ self.update = async_to_raw_response_wrapper(
+ proxies.update,
+ )
self.list = async_to_raw_response_wrapper(
proxies.list,
)
@@ -583,6 +674,9 @@ def __init__(self, proxies: ProxiesResource) -> None:
self.retrieve = to_streamed_response_wrapper(
proxies.retrieve,
)
+ self.update = to_streamed_response_wrapper(
+ proxies.update,
+ )
self.list = to_streamed_response_wrapper(
proxies.list,
)
@@ -604,6 +698,9 @@ def __init__(self, proxies: AsyncProxiesResource) -> None:
self.retrieve = async_to_streamed_response_wrapper(
proxies.retrieve,
)
+ self.update = async_to_streamed_response_wrapper(
+ proxies.update,
+ )
self.list = async_to_streamed_response_wrapper(
proxies.list,
)
diff --git a/src/kernel/types/__init__.py b/src/kernel/types/__init__.py
index 49af1d5c..959865bf 100644
--- a/src/kernel/types/__init__.py
+++ b/src/kernel/types/__init__.py
@@ -38,6 +38,7 @@
from .project_list_params import ProjectListParams as ProjectListParams
from .proxy_create_params import ProxyCreateParams as ProxyCreateParams
from .proxy_list_response import ProxyListResponse as ProxyListResponse
+from .proxy_update_params import ProxyUpdateParams as ProxyUpdateParams
from .proxy_check_response import ProxyCheckResponse as ProxyCheckResponse
from .api_key_create_params import APIKeyCreateParams as APIKeyCreateParams
from .api_key_rotate_params import APIKeyRotateParams as APIKeyRotateParams
@@ -49,9 +50,11 @@
from .browser_update_params import BrowserUpdateParams as BrowserUpdateParams
from .extension_list_params import ExtensionListParams as ExtensionListParams
from .profile_create_params import ProfileCreateParams as ProfileCreateParams
+from .profile_update_params import ProfileUpdateParams as ProfileUpdateParams
from .project_create_params import ProjectCreateParams as ProjectCreateParams
from .project_update_params import ProjectUpdateParams as ProjectUpdateParams
from .proxy_create_response import ProxyCreateResponse as ProxyCreateResponse
+from .proxy_update_response import ProxyUpdateResponse as ProxyUpdateResponse
from .credential_list_params import CredentialListParams as CredentialListParams
from .deployment_list_params import DeploymentListParams as DeploymentListParams
from .deployment_state_event import DeploymentStateEvent as DeploymentStateEvent
diff --git a/src/kernel/types/profile_create_params.py b/src/kernel/types/profile_create_params.py
index bca1e17d..8d1c2f4f 100644
--- a/src/kernel/types/profile_create_params.py
+++ b/src/kernel/types/profile_create_params.py
@@ -9,4 +9,9 @@
class ProfileCreateParams(TypedDict, total=False):
name: str
- """Optional name of the profile. Must be unique within the project."""
+ """Optional name of the profile.
+
+ Must be unique within the logical project; during the default-project migration,
+ unscoped profiles and profiles in the org default project are treated as the
+ same project.
+ """
diff --git a/src/kernel/types/profile_update_params.py b/src/kernel/types/profile_update_params.py
new file mode 100644
index 00000000..bd0165b7
--- /dev/null
+++ b/src/kernel/types/profile_update_params.py
@@ -0,0 +1,17 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from __future__ import annotations
+
+from typing_extensions import Required, TypedDict
+
+__all__ = ["ProfileUpdateParams"]
+
+
+class ProfileUpdateParams(TypedDict, total=False):
+ name: Required[str]
+ """New profile name.
+
+ Must be unique within the logical project; during the default-project migration,
+ unscoped profiles and profiles in the org default project are treated as the
+ same project.
+ """
diff --git a/src/kernel/types/proxy_update_params.py b/src/kernel/types/proxy_update_params.py
new file mode 100644
index 00000000..0db118a4
--- /dev/null
+++ b/src/kernel/types/proxy_update_params.py
@@ -0,0 +1,16 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from __future__ import annotations
+
+from typing_extensions import Required, TypedDict
+
+__all__ = ["ProxyUpdateParams"]
+
+
+class ProxyUpdateParams(TypedDict, total=False):
+ name: Required[str]
+ """New proxy name.
+
+ Proxy names are trimmed and length-checked only; duplicates are allowed because
+ proxies are updated by ID, not by name.
+ """
diff --git a/src/kernel/types/proxy_update_response.py b/src/kernel/types/proxy_update_response.py
new file mode 100644
index 00000000..d90c3013
--- /dev/null
+++ b/src/kernel/types/proxy_update_response.py
@@ -0,0 +1,128 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from typing import List, Union, Optional
+from datetime import datetime
+from typing_extensions import Literal, TypeAlias
+
+from .._models import BaseModel
+
+__all__ = [
+ "ProxyUpdateResponse",
+ "Config",
+ "ConfigDatacenterProxyConfig",
+ "ConfigIspProxyConfig",
+ "ConfigResidentialProxyConfig",
+ "ConfigMobileProxyConfig",
+ "ConfigCustomProxyConfig",
+]
+
+
+class ConfigDatacenterProxyConfig(BaseModel):
+ """Configuration for a datacenter proxy."""
+
+ country: Optional[str] = None
+ """ISO 3166 country code. Defaults to US if not provided."""
+
+
+class ConfigIspProxyConfig(BaseModel):
+ """Configuration for an ISP proxy."""
+
+ country: Optional[str] = None
+ """ISO 3166 country code. Defaults to US if not provided."""
+
+
+class ConfigResidentialProxyConfig(BaseModel):
+ """Configuration for residential proxies."""
+
+ asn: Optional[str] = None
+ """Autonomous system number. See https://bgp.potaroo.net/cidr/autnums.html"""
+
+ city: Optional[str] = None
+ """City name (no spaces, e.g.
+
+ `sanfrancisco`). If provided, `country` must also be provided.
+ """
+
+ country: Optional[str] = None
+ """ISO 3166 country code."""
+
+ os: Optional[Literal["windows", "macos", "android"]] = None
+ """Operating system of the residential device."""
+
+ state: Optional[str] = None
+ """Two-letter state code."""
+
+ zip: Optional[str] = None
+ """US ZIP code."""
+
+
+class ConfigMobileProxyConfig(BaseModel):
+ """Configuration for mobile proxies."""
+
+ city: Optional[str] = None
+ """Provider city alias. Mobile carrier routing can make observed geo vary."""
+
+ country: Optional[str] = None
+ """ISO 3166 country code"""
+
+ state: Optional[str] = None
+ """US-only state code. Mobile carrier routing can make observed geo vary."""
+
+
+class ConfigCustomProxyConfig(BaseModel):
+ """Configuration for a custom proxy (e.g., private proxy server)."""
+
+ host: str
+ """Proxy host address or IP."""
+
+ port: int
+ """Proxy port."""
+
+ has_password: Optional[bool] = None
+ """Whether the proxy has a password."""
+
+ username: Optional[str] = None
+ """Username for proxy authentication."""
+
+
+Config: TypeAlias = Union[
+ ConfigDatacenterProxyConfig,
+ ConfigIspProxyConfig,
+ ConfigResidentialProxyConfig,
+ ConfigMobileProxyConfig,
+ ConfigCustomProxyConfig,
+]
+
+
+class ProxyUpdateResponse(BaseModel):
+ """Configuration for routing traffic through a proxy."""
+
+ type: Literal["datacenter", "isp", "residential", "mobile", "custom"]
+ """Proxy type to use.
+
+ In terms of quality for avoiding bot-detection, from best to worst: `mobile` >
+ `residential` > `isp` > `datacenter`.
+ """
+
+ id: Optional[str] = None
+
+ bypass_hosts: Optional[List[str]] = None
+ """Hostnames that should bypass the parent proxy and connect directly."""
+
+ config: Optional[Config] = None
+ """Configuration specific to the selected proxy `type`."""
+
+ ip_address: Optional[str] = None
+ """IP address that the proxy uses when making requests."""
+
+ last_checked: Optional[datetime] = None
+ """Timestamp of the last health check performed on this proxy."""
+
+ name: Optional[str] = None
+ """Readable name of the proxy."""
+
+ protocol: Optional[Literal["http", "https"]] = None
+ """Protocol to use for the proxy connection."""
+
+ status: Optional[Literal["available", "unavailable"]] = None
+ """Current health status of the proxy."""
diff --git a/tests/api_resources/test_profiles.py b/tests/api_resources/test_profiles.py
index 296593ea..3075a3d9 100644
--- a/tests/api_resources/test_profiles.py
+++ b/tests/api_resources/test_profiles.py
@@ -104,6 +104,52 @@ 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:
+ profile = client.profiles.update(
+ id_or_name="id_or_name",
+ name="my-renamed-profile",
+ )
+ assert_matches_type(Profile, profile, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ def test_raw_response_update(self, client: Kernel) -> None:
+ response = client.profiles.with_raw_response.update(
+ id_or_name="id_or_name",
+ name="my-renamed-profile",
+ )
+
+ assert response.is_closed is True
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+ profile = response.parse()
+ assert_matches_type(Profile, profile, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ def test_streaming_response_update(self, client: Kernel) -> None:
+ with client.profiles.with_streaming_response.update(
+ id_or_name="id_or_name",
+ name="my-renamed-profile",
+ ) as response:
+ assert not response.is_closed
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+
+ profile = response.parse()
+ assert_matches_type(Profile, profile, 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_or_name` but received ''"):
+ client.profiles.with_raw_response.update(
+ id_or_name="",
+ name="my-renamed-profile",
+ )
+
@pytest.mark.skip(reason="Mock server tests are disabled")
@parametrize
def test_method_list(self, client: Kernel) -> None:
@@ -318,6 +364,52 @@ 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:
+ profile = await async_client.profiles.update(
+ id_or_name="id_or_name",
+ name="my-renamed-profile",
+ )
+ assert_matches_type(Profile, profile, 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.profiles.with_raw_response.update(
+ id_or_name="id_or_name",
+ name="my-renamed-profile",
+ )
+
+ assert response.is_closed is True
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+ profile = await response.parse()
+ assert_matches_type(Profile, profile, 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.profiles.with_streaming_response.update(
+ id_or_name="id_or_name",
+ name="my-renamed-profile",
+ ) as response:
+ assert not response.is_closed
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+
+ profile = await response.parse()
+ assert_matches_type(Profile, profile, 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_or_name` but received ''"):
+ await async_client.profiles.with_raw_response.update(
+ id_or_name="",
+ name="my-renamed-profile",
+ )
+
@pytest.mark.skip(reason="Mock server tests are disabled")
@parametrize
async def test_method_list(self, async_client: AsyncKernel) -> None:
diff --git a/tests/api_resources/test_proxies.py b/tests/api_resources/test_proxies.py
index 29df16e4..72750aaa 100644
--- a/tests/api_resources/test_proxies.py
+++ b/tests/api_resources/test_proxies.py
@@ -13,6 +13,7 @@
ProxyListResponse,
ProxyCheckResponse,
ProxyCreateResponse,
+ ProxyUpdateResponse,
ProxyRetrieveResponse,
)
from kernel.pagination import SyncOffsetPagination, AsyncOffsetPagination
@@ -111,6 +112,52 @@ 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:
+ proxy = client.proxies.update(
+ id="id",
+ name="my-renamed-proxy",
+ )
+ assert_matches_type(ProxyUpdateResponse, proxy, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ def test_raw_response_update(self, client: Kernel) -> None:
+ response = client.proxies.with_raw_response.update(
+ id="id",
+ name="my-renamed-proxy",
+ )
+
+ assert response.is_closed is True
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+ proxy = response.parse()
+ assert_matches_type(ProxyUpdateResponse, proxy, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ def test_streaming_response_update(self, client: Kernel) -> None:
+ with client.proxies.with_streaming_response.update(
+ id="id",
+ name="my-renamed-proxy",
+ ) as response:
+ assert not response.is_closed
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+
+ proxy = response.parse()
+ assert_matches_type(ProxyUpdateResponse, proxy, 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.proxies.with_raw_response.update(
+ id="",
+ name="my-renamed-proxy",
+ )
+
@pytest.mark.skip(reason="Mock server tests are disabled")
@parametrize
def test_method_list(self, client: Kernel) -> None:
@@ -336,6 +383,52 @@ 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:
+ proxy = await async_client.proxies.update(
+ id="id",
+ name="my-renamed-proxy",
+ )
+ assert_matches_type(ProxyUpdateResponse, proxy, 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.proxies.with_raw_response.update(
+ id="id",
+ name="my-renamed-proxy",
+ )
+
+ assert response.is_closed is True
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+ proxy = await response.parse()
+ assert_matches_type(ProxyUpdateResponse, proxy, 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.proxies.with_streaming_response.update(
+ id="id",
+ name="my-renamed-proxy",
+ ) as response:
+ assert not response.is_closed
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+
+ proxy = await response.parse()
+ assert_matches_type(ProxyUpdateResponse, proxy, 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.proxies.with_raw_response.update(
+ id="",
+ name="my-renamed-proxy",
+ )
+
@pytest.mark.skip(reason="Mock server tests are disabled")
@parametrize
async def test_method_list(self, async_client: AsyncKernel) -> None:
From ddbda6ee168bdc0308c44c1fe45fec8bbdb0ce48 Mon Sep 17 00:00:00 2001
From: "stainless-app[bot]"
<142633134+stainless-app[bot]@users.noreply.github.com>
Date: Fri, 10 Jul 2026 14:19:16 +0000
Subject: [PATCH 117/366] feat: Document name uniqueness and query match
semantics
---
.stats.yml | 4 ++--
src/kernel/resources/api_keys.py | 4 ++--
src/kernel/resources/auth/connections.py | 24 ++++++++++++++-----
src/kernel/resources/browser_pools.py | 6 +++--
src/kernel/resources/credential_providers.py | 6 +++--
src/kernel/resources/credentials.py | 6 +++--
src/kernel/resources/extensions.py | 6 +++--
src/kernel/resources/profiles.py | 4 ++--
src/kernel/resources/proxies.py | 6 +++--
src/kernel/types/api_key.py | 6 ++++-
src/kernel/types/api_key_create_params.py | 2 +-
.../types/auth/connection_create_params.py | 5 +++-
.../types/auth/connection_login_params.py | 5 +++-
.../types/auth/connection_update_params.py | 5 +++-
src/kernel/types/browser_pool_list_params.py | 5 +++-
src/kernel/types/credential_list_params.py | 5 +++-
.../types/credential_provider_list_params.py | 5 +++-
src/kernel/types/extension_list_params.py | 5 +++-
src/kernel/types/profile_list_params.py | 2 +-
src/kernel/types/proxy_list_params.py | 5 +++-
20 files changed, 83 insertions(+), 33 deletions(-)
diff --git a/.stats.yml b/.stats.yml
index 09109a9b..b058b75d 100644
--- a/.stats.yml
+++ b/.stats.yml
@@ -1,4 +1,4 @@
configured_endpoints: 127
-openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel/kernel-bc33d13e669972493e9ab0da82a71999822dc049eb14b113f446753c917ffcdb.yml
-openapi_spec_hash: 133a2e0d0cdc4023f98b3c2e589cf5d8
+openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel/kernel-6c7cbaefd8a84ec26a1677681f37041323290080dac3fd1a409bb7cd01dd2ae3.yml
+openapi_spec_hash: 75085a25c8a5cdb5417f0dbfad0ed6b6
config_hash: 77ee715aa17061166f9a02b264a21b8d
diff --git a/src/kernel/resources/api_keys.py b/src/kernel/resources/api_keys.py
index 502e8897..73305089 100644
--- a/src/kernel/resources/api_keys.py
+++ b/src/kernel/resources/api_keys.py
@@ -71,7 +71,7 @@ def create(
Create a new API key within the authenticated organization.
Args:
- name: API key name (1-255 characters)
+ name: Label for the API key (1-255 characters). API keys are not addressable by name.
days_to_expire: Number of days until expiry, up to 3650. Use null for never.
@@ -382,7 +382,7 @@ async def create(
Create a new API key within the authenticated organization.
Args:
- name: API key name (1-255 characters)
+ name: Label for the API key (1-255 characters). API keys are not addressable by name.
days_to_expire: Number of days until expiry, up to 3650. Use null for never.
diff --git a/src/kernel/resources/auth/connections.py b/src/kernel/resources/auth/connections.py
index 24a560c2..30f8dca8 100644
--- a/src/kernel/resources/auth/connections.py
+++ b/src/kernel/resources/auth/connections.py
@@ -140,7 +140,9 @@ def create(
login_url: Optional login page URL to skip discovery
proxy: Proxy selection. Provide either id or name. The proxy must be in the same
- project as the resource referencing it.
+ project as the resource referencing it. When selecting by name, the name must
+ match exactly one active proxy in the project. Ambiguous names return a 400; use
+ id for stable references.
record_session: Whether to record browser sessions for this connection by default. Useful for
debugging. Can be overridden per-login. Defaults to false.
@@ -268,7 +270,9 @@ def update(
login_url: Login page URL. Set to empty string to clear.
proxy: Proxy selection. Provide either id or name. The proxy must be in the same
- project as the resource referencing it.
+ project as the resource referencing it. When selecting by name, the name must
+ match exactly one active proxy in the project. Ambiguous names return a 400; use
+ id for stable references.
record_session: Whether to record browser sessions for this connection by default
@@ -465,7 +469,9 @@ def login(
Args:
proxy: Proxy selection. Provide either id or name. The proxy must be in the same
- project as the resource referencing it.
+ project as the resource referencing it. When selecting by name, the name must
+ match exactly one active proxy in the project. Ambiguous names return a 400; use
+ id for stable references.
record_session: Override the connection's default for recording this login's browser session.
When omitted, the connection's record_session default is used.
@@ -717,7 +723,9 @@ async def create(
login_url: Optional login page URL to skip discovery
proxy: Proxy selection. Provide either id or name. The proxy must be in the same
- project as the resource referencing it.
+ project as the resource referencing it. When selecting by name, the name must
+ match exactly one active proxy in the project. Ambiguous names return a 400; use
+ id for stable references.
record_session: Whether to record browser sessions for this connection by default. Useful for
debugging. Can be overridden per-login. Defaults to false.
@@ -845,7 +853,9 @@ async def update(
login_url: Login page URL. Set to empty string to clear.
proxy: Proxy selection. Provide either id or name. The proxy must be in the same
- project as the resource referencing it.
+ project as the resource referencing it. When selecting by name, the name must
+ match exactly one active proxy in the project. Ambiguous names return a 400; use
+ id for stable references.
record_session: Whether to record browser sessions for this connection by default
@@ -1042,7 +1052,9 @@ async def login(
Args:
proxy: Proxy selection. Provide either id or name. The proxy must be in the same
- project as the resource referencing it.
+ project as the resource referencing it. When selecting by name, the name must
+ match exactly one active proxy in the project. Ambiguous names return a 400; use
+ id for stable references.
record_session: Override the connection's default for recording this login's browser session.
When omitted, the connection's record_session default is used.
diff --git a/src/kernel/resources/browser_pools.py b/src/kernel/resources/browser_pools.py
index 12239d00..867319d9 100644
--- a/src/kernel/resources/browser_pools.py
+++ b/src/kernel/resources/browser_pools.py
@@ -378,7 +378,8 @@ def list(
offset: Offset the number of browser pools to return.
- query: Search browser pools by name or ID.
+ query: Case-insensitive substring match against browser pool name. IDs match by exact
+ value.
extra_headers: Send extra headers
@@ -945,7 +946,8 @@ def list(
offset: Offset the number of browser pools to return.
- query: Search browser pools by name or ID.
+ query: Case-insensitive substring match against browser pool name. IDs match by exact
+ value.
extra_headers: Send extra headers
diff --git a/src/kernel/resources/credential_providers.py b/src/kernel/resources/credential_providers.py
index 00db5c5e..de079a8c 100644
--- a/src/kernel/resources/credential_providers.py
+++ b/src/kernel/resources/credential_providers.py
@@ -216,7 +216,8 @@ def list(
offset: Offset the number of credential providers to return.
- query: Search credential providers by name or ID.
+ query: Case-insensitive substring match against credential provider name. IDs match by
+ exact value.
extra_headers: Send extra headers
@@ -534,7 +535,8 @@ def list(
offset: Offset the number of credential providers to return.
- query: Search credential providers by name or ID.
+ query: Case-insensitive substring match against credential provider name. IDs match by
+ exact value.
extra_headers: Send extra headers
diff --git a/src/kernel/resources/credentials.py b/src/kernel/resources/credentials.py
index fd073957..f1a3ac01 100644
--- a/src/kernel/resources/credentials.py
+++ b/src/kernel/resources/credentials.py
@@ -229,7 +229,8 @@ def list(
offset: Number of results to skip
- query: Search credentials by name, domain, or ID.
+ query: Case-insensitive substring match against credential name or domain. IDs match by
+ exact value.
extra_headers: Send extra headers
@@ -534,7 +535,8 @@ def list(
offset: Number of results to skip
- query: Search credentials by name, domain, or ID.
+ query: Case-insensitive substring match against credential name or domain. IDs match by
+ exact value.
extra_headers: Send extra headers
diff --git a/src/kernel/resources/extensions.py b/src/kernel/resources/extensions.py
index 5c023531..a460185a 100644
--- a/src/kernel/resources/extensions.py
+++ b/src/kernel/resources/extensions.py
@@ -79,7 +79,8 @@ def list(
offset: Offset the number of extensions to return.
- query: Search extensions by name or ID.
+ query: Case-insensitive substring match against extension name. IDs match by exact
+ value.
extra_headers: Send extra headers
@@ -355,7 +356,8 @@ def list(
offset: Offset the number of extensions to return.
- query: Search extensions by name or ID.
+ query: Case-insensitive substring match against extension name. IDs match by exact
+ value.
extra_headers: Send extra headers
diff --git a/src/kernel/resources/profiles.py b/src/kernel/resources/profiles.py
index ef7f6220..4bad58bb 100644
--- a/src/kernel/resources/profiles.py
+++ b/src/kernel/resources/profiles.py
@@ -189,7 +189,7 @@ def list(
offset: Offset the number of profiles to return.
- query: Search profiles by name or ID.
+ query: Case-insensitive substring match against profile name or ID.
extra_headers: Send extra headers
@@ -447,7 +447,7 @@ def list(
offset: Offset the number of profiles to return.
- query: Search profiles by name or ID.
+ query: Case-insensitive substring match against profile name or ID.
extra_headers: Send extra headers
diff --git a/src/kernel/resources/proxies.py b/src/kernel/resources/proxies.py
index 6d866bd4..425e81a0 100644
--- a/src/kernel/resources/proxies.py
+++ b/src/kernel/resources/proxies.py
@@ -202,7 +202,8 @@ def list(
offset: Offset the number of proxies to return.
- query: Search proxies by name, host, IP address, or ID.
+ query: Case-insensitive substring match against proxy name, host, or IP address. IDs
+ match by exact value.
extra_headers: Send extra headers
@@ -496,7 +497,8 @@ def list(
offset: Offset the number of proxies to return.
- query: Search proxies by name, host, IP address, or ID.
+ query: Case-insensitive substring match against proxy name, host, or IP address. IDs
+ match by exact value.
extra_headers: Send extra headers
diff --git a/src/kernel/types/api_key.py b/src/kernel/types/api_key.py
index 44519527..b0d6eef2 100644
--- a/src/kernel/types/api_key.py
+++ b/src/kernel/types/api_key.py
@@ -41,7 +41,11 @@ class APIKey(BaseModel):
"""Masked version of the API key"""
name: str
- """API key name"""
+ """Label for the API key.
+
+ API keys are not addressable by name; use the ID or key identifier for stable
+ references.
+ """
project_id: Optional[str] = None
"""Project identifier for project-scoped API keys. Null means org-wide."""
diff --git a/src/kernel/types/api_key_create_params.py b/src/kernel/types/api_key_create_params.py
index 7705108f..2909510e 100644
--- a/src/kernel/types/api_key_create_params.py
+++ b/src/kernel/types/api_key_create_params.py
@@ -10,7 +10,7 @@
class APIKeyCreateParams(TypedDict, total=False):
name: Required[str]
- """API key name (1-255 characters)"""
+ """Label for the API key (1-255 characters). API keys are not addressable by name."""
days_to_expire: Optional[int]
"""Number of days until expiry, up to 3650. Use null for never."""
diff --git a/src/kernel/types/auth/connection_create_params.py b/src/kernel/types/auth/connection_create_params.py
index e5a969ce..0113d062 100644
--- a/src/kernel/types/auth/connection_create_params.py
+++ b/src/kernel/types/auth/connection_create_params.py
@@ -84,7 +84,9 @@ class ConnectionCreateParams(TypedDict, total=False):
"""Proxy selection.
Provide either id or name. The proxy must be in the same project as the resource
- referencing it.
+ referencing it. When selecting by name, the name must match exactly one active
+ proxy in the project. Ambiguous names return a 400; use id for stable
+ references.
"""
record_session: bool
@@ -126,6 +128,7 @@ class Proxy(TypedDict, total=False):
"""Proxy selection.
Provide either id or name. The proxy must be in the same project as the resource referencing it.
+ When selecting by name, the name must match exactly one active proxy in the project. Ambiguous names return a 400; use id for stable references.
"""
id: str
diff --git a/src/kernel/types/auth/connection_login_params.py b/src/kernel/types/auth/connection_login_params.py
index 5e136cc3..6cfa737a 100644
--- a/src/kernel/types/auth/connection_login_params.py
+++ b/src/kernel/types/auth/connection_login_params.py
@@ -12,7 +12,9 @@ class ConnectionLoginParams(TypedDict, total=False):
"""Proxy selection.
Provide either id or name. The proxy must be in the same project as the resource
- referencing it.
+ referencing it. When selecting by name, the name must match exactly one active
+ proxy in the project. Ambiguous names return a 400; use id for stable
+ references.
"""
record_session: bool
@@ -26,6 +28,7 @@ class Proxy(TypedDict, total=False):
"""Proxy selection.
Provide either id or name. The proxy must be in the same project as the resource referencing it.
+ When selecting by name, the name must match exactly one active proxy in the project. Ambiguous names return a 400; use id for stable references.
"""
id: str
diff --git a/src/kernel/types/auth/connection_update_params.py b/src/kernel/types/auth/connection_update_params.py
index e0dadf70..20bc98ab 100644
--- a/src/kernel/types/auth/connection_update_params.py
+++ b/src/kernel/types/auth/connection_update_params.py
@@ -51,7 +51,9 @@ class ConnectionUpdateParams(TypedDict, total=False):
"""Proxy selection.
Provide either id or name. The proxy must be in the same project as the resource
- referencing it.
+ referencing it. When selecting by name, the name must match exactly one active
+ proxy in the project. Ambiguous names return a 400; use id for stable
+ references.
"""
record_session: bool
@@ -87,6 +89,7 @@ class Proxy(TypedDict, total=False):
"""Proxy selection.
Provide either id or name. The proxy must be in the same project as the resource referencing it.
+ When selecting by name, the name must match exactly one active proxy in the project. Ambiguous names return a 400; use id for stable references.
"""
id: str
diff --git a/src/kernel/types/browser_pool_list_params.py b/src/kernel/types/browser_pool_list_params.py
index 26436a61..e9da55f9 100644
--- a/src/kernel/types/browser_pool_list_params.py
+++ b/src/kernel/types/browser_pool_list_params.py
@@ -15,4 +15,7 @@ class BrowserPoolListParams(TypedDict, total=False):
"""Offset the number of browser pools to return."""
query: str
- """Search browser pools by name or ID."""
+ """Case-insensitive substring match against browser pool name.
+
+ IDs match by exact value.
+ """
diff --git a/src/kernel/types/credential_list_params.py b/src/kernel/types/credential_list_params.py
index 50e25895..ecaf2ef1 100644
--- a/src/kernel/types/credential_list_params.py
+++ b/src/kernel/types/credential_list_params.py
@@ -18,4 +18,7 @@ class CredentialListParams(TypedDict, total=False):
"""Number of results to skip"""
query: str
- """Search credentials by name, domain, or ID."""
+ """Case-insensitive substring match against credential name or domain.
+
+ IDs match by exact value.
+ """
diff --git a/src/kernel/types/credential_provider_list_params.py b/src/kernel/types/credential_provider_list_params.py
index 69005988..97571aa8 100644
--- a/src/kernel/types/credential_provider_list_params.py
+++ b/src/kernel/types/credential_provider_list_params.py
@@ -15,4 +15,7 @@ class CredentialProviderListParams(TypedDict, total=False):
"""Offset the number of credential providers to return."""
query: str
- """Search credential providers by name or ID."""
+ """Case-insensitive substring match against credential provider name.
+
+ IDs match by exact value.
+ """
diff --git a/src/kernel/types/extension_list_params.py b/src/kernel/types/extension_list_params.py
index bade1423..a88698c1 100644
--- a/src/kernel/types/extension_list_params.py
+++ b/src/kernel/types/extension_list_params.py
@@ -15,4 +15,7 @@ class ExtensionListParams(TypedDict, total=False):
"""Offset the number of extensions to return."""
query: str
- """Search extensions by name or ID."""
+ """Case-insensitive substring match against extension name.
+
+ IDs match by exact value.
+ """
diff --git a/src/kernel/types/profile_list_params.py b/src/kernel/types/profile_list_params.py
index 4218f5bc..ea24d6c6 100644
--- a/src/kernel/types/profile_list_params.py
+++ b/src/kernel/types/profile_list_params.py
@@ -15,4 +15,4 @@ class ProfileListParams(TypedDict, total=False):
"""Offset the number of profiles to return."""
query: str
- """Search profiles by name or ID."""
+ """Case-insensitive substring match against profile name or ID."""
diff --git a/src/kernel/types/proxy_list_params.py b/src/kernel/types/proxy_list_params.py
index 75c5f133..0a689b27 100644
--- a/src/kernel/types/proxy_list_params.py
+++ b/src/kernel/types/proxy_list_params.py
@@ -15,4 +15,7 @@ class ProxyListParams(TypedDict, total=False):
"""Offset the number of proxies to return."""
query: str
- """Search proxies by name, host, IP address, or ID."""
+ """Case-insensitive substring match against proxy name, host, or IP address.
+
+ IDs match by exact value.
+ """
From 4f17985d85b2510863ea617b1109ca372658e345 Mon Sep 17 00:00:00 2001
From: "stainless-app[bot]"
<142633134+stainless-app[bot]@users.noreply.github.com>
Date: Fri, 10 Jul 2026 14:37:22 +0000
Subject: [PATCH 118/366] feat: Add exact-match name filter to list endpoints
---
.stats.yml | 4 ++--
src/kernel/resources/api_keys.py | 14 ++++++++++++++
src/kernel/resources/browser_pools.py | 14 ++++++++++++++
src/kernel/resources/extensions.py | 14 ++++++++++++++
src/kernel/resources/profiles.py | 14 ++++++++++++++
src/kernel/resources/projects/projects.py | 10 ++++++++++
src/kernel/resources/proxies.py | 12 ++++++++++++
src/kernel/types/api_key_list_params.py | 9 +++++++++
src/kernel/types/browser_pool_list_params.py | 8 ++++++++
src/kernel/types/extension_list_params.py | 8 ++++++++
src/kernel/types/profile_list_params.py | 8 ++++++++
src/kernel/types/project_list_params.py | 6 ++++++
src/kernel/types/proxy_list_params.py | 7 +++++++
tests/api_resources/test_api_keys.py | 2 ++
tests/api_resources/test_browser_pools.py | 2 ++
tests/api_resources/test_extensions.py | 2 ++
tests/api_resources/test_profiles.py | 2 ++
tests/api_resources/test_projects.py | 2 ++
tests/api_resources/test_proxies.py | 2 ++
19 files changed, 138 insertions(+), 2 deletions(-)
diff --git a/.stats.yml b/.stats.yml
index b058b75d..f7ef7fe6 100644
--- a/.stats.yml
+++ b/.stats.yml
@@ -1,4 +1,4 @@
configured_endpoints: 127
-openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel/kernel-6c7cbaefd8a84ec26a1677681f37041323290080dac3fd1a409bb7cd01dd2ae3.yml
-openapi_spec_hash: 75085a25c8a5cdb5417f0dbfad0ed6b6
+openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel/kernel-b20fcf2fc1cdb91b51ffd354adcd12d63b950ea98b69e13bee6a78a58498c2cd.yml
+openapi_spec_hash: bc8b527a963dd2c2d74b6f9e6ca56652
config_hash: 77ee715aa17061166f9a02b264a21b8d
diff --git a/src/kernel/resources/api_keys.py b/src/kernel/resources/api_keys.py
index 73305089..8cc1ed53 100644
--- a/src/kernel/resources/api_keys.py
+++ b/src/kernel/resources/api_keys.py
@@ -188,6 +188,7 @@ def list(
*,
include_deleted: bool | Omit = omit,
limit: int | Omit = omit,
+ name: str | Omit = omit,
offset: int | Omit = omit,
query: str | Omit = omit,
sort_by: Literal["created_at", "name", "expires_at"] | Omit = omit,
@@ -210,6 +211,11 @@ def list(
limit: Maximum number of results to return
+ name: Exact-match filter on API key name using the database collation. In production,
+ matching is case- and accent-insensitive. Names are not required to be unique,
+ so multiple keys may match. When status=all or include_deleted=true is set,
+ soft-deleted keys with the same name may also match.
+
offset: Number of results to skip
query: Case-insensitive substring match against API key name, creator, and project. API
@@ -243,6 +249,7 @@ def list(
{
"include_deleted": include_deleted,
"limit": limit,
+ "name": name,
"offset": offset,
"query": query,
"sort_by": sort_by,
@@ -499,6 +506,7 @@ def list(
*,
include_deleted: bool | Omit = omit,
limit: int | Omit = omit,
+ name: str | Omit = omit,
offset: int | Omit = omit,
query: str | Omit = omit,
sort_by: Literal["created_at", "name", "expires_at"] | Omit = omit,
@@ -521,6 +529,11 @@ def list(
limit: Maximum number of results to return
+ name: Exact-match filter on API key name using the database collation. In production,
+ matching is case- and accent-insensitive. Names are not required to be unique,
+ so multiple keys may match. When status=all or include_deleted=true is set,
+ soft-deleted keys with the same name may also match.
+
offset: Number of results to skip
query: Case-insensitive substring match against API key name, creator, and project. API
@@ -554,6 +567,7 @@ def list(
{
"include_deleted": include_deleted,
"limit": limit,
+ "name": name,
"offset": offset,
"query": query,
"sort_by": sort_by,
diff --git a/src/kernel/resources/browser_pools.py b/src/kernel/resources/browser_pools.py
index 867319d9..6310cd61 100644
--- a/src/kernel/resources/browser_pools.py
+++ b/src/kernel/resources/browser_pools.py
@@ -361,6 +361,7 @@ def list(
self,
*,
limit: int | Omit = omit,
+ name: str | Omit = omit,
offset: int | Omit = omit,
query: str | Omit = omit,
# Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
@@ -376,6 +377,11 @@ def list(
Args:
limit: Limit the number of browser pools to return.
+ name: Exact-match filter on browser pool name using the database collation. In
+ production, matching is case- and accent-insensitive. During the default-project
+ migration, unscoped requests prefer a concrete default-project browser pool over
+ a legacy unscoped browser pool with the same name.
+
offset: Offset the number of browser pools to return.
query: Case-insensitive substring match against browser pool name. IDs match by exact
@@ -400,6 +406,7 @@ def list(
query=maybe_transform(
{
"limit": limit,
+ "name": name,
"offset": offset,
"query": query,
},
@@ -929,6 +936,7 @@ def list(
self,
*,
limit: int | Omit = omit,
+ name: str | Omit = omit,
offset: int | Omit = omit,
query: str | Omit = omit,
# Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
@@ -944,6 +952,11 @@ def list(
Args:
limit: Limit the number of browser pools to return.
+ name: Exact-match filter on browser pool name using the database collation. In
+ production, matching is case- and accent-insensitive. During the default-project
+ migration, unscoped requests prefer a concrete default-project browser pool over
+ a legacy unscoped browser pool with the same name.
+
offset: Offset the number of browser pools to return.
query: Case-insensitive substring match against browser pool name. IDs match by exact
@@ -968,6 +981,7 @@ def list(
query=maybe_transform(
{
"limit": limit,
+ "name": name,
"offset": offset,
"query": query,
},
diff --git a/src/kernel/resources/extensions.py b/src/kernel/resources/extensions.py
index a460185a..81a1ff9a 100644
--- a/src/kernel/resources/extensions.py
+++ b/src/kernel/resources/extensions.py
@@ -62,6 +62,7 @@ def list(
self,
*,
limit: int | Omit = omit,
+ name: str | Omit = omit,
offset: int | Omit = omit,
query: str | Omit = omit,
# Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
@@ -77,6 +78,11 @@ def list(
Args:
limit: Limit the number of extensions to return.
+ name: Exact-match filter on extension name using the database collation. In
+ production, matching is case- and accent-insensitive. During the default-project
+ migration, unscoped requests prefer a concrete default-project extension over a
+ legacy unscoped extension with the same name.
+
offset: Offset the number of extensions to return.
query: Case-insensitive substring match against extension name. IDs match by exact
@@ -101,6 +107,7 @@ def list(
query=maybe_transform(
{
"limit": limit,
+ "name": name,
"offset": offset,
"query": query,
},
@@ -339,6 +346,7 @@ def list(
self,
*,
limit: int | Omit = omit,
+ name: str | Omit = omit,
offset: int | Omit = omit,
query: str | Omit = omit,
# Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
@@ -354,6 +362,11 @@ def list(
Args:
limit: Limit the number of extensions to return.
+ name: Exact-match filter on extension name using the database collation. In
+ production, matching is case- and accent-insensitive. During the default-project
+ migration, unscoped requests prefer a concrete default-project extension over a
+ legacy unscoped extension with the same name.
+
offset: Offset the number of extensions to return.
query: Case-insensitive substring match against extension name. IDs match by exact
@@ -378,6 +391,7 @@ def list(
query=maybe_transform(
{
"limit": limit,
+ "name": name,
"offset": offset,
"query": query,
},
diff --git a/src/kernel/resources/profiles.py b/src/kernel/resources/profiles.py
index 4bad58bb..e804f559 100644
--- a/src/kernel/resources/profiles.py
+++ b/src/kernel/resources/profiles.py
@@ -172,6 +172,7 @@ def list(
self,
*,
limit: int | Omit = omit,
+ name: str | Omit = omit,
offset: int | Omit = omit,
query: str | Omit = omit,
# Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
@@ -187,6 +188,11 @@ def list(
Args:
limit: Limit the number of profiles to return.
+ name: Exact-match filter on profile name using the database collation. In production,
+ matching is case- and accent-insensitive. During the default-project migration,
+ unscoped requests prefer a concrete default-project profile over a legacy
+ unscoped profile with the same name.
+
offset: Offset the number of profiles to return.
query: Case-insensitive substring match against profile name or ID.
@@ -210,6 +216,7 @@ def list(
query=maybe_transform(
{
"limit": limit,
+ "name": name,
"offset": offset,
"query": query,
},
@@ -430,6 +437,7 @@ def list(
self,
*,
limit: int | Omit = omit,
+ name: str | Omit = omit,
offset: int | Omit = omit,
query: str | Omit = omit,
# Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
@@ -445,6 +453,11 @@ def list(
Args:
limit: Limit the number of profiles to return.
+ name: Exact-match filter on profile name using the database collation. In production,
+ matching is case- and accent-insensitive. During the default-project migration,
+ unscoped requests prefer a concrete default-project profile over a legacy
+ unscoped profile with the same name.
+
offset: Offset the number of profiles to return.
query: Case-insensitive substring match against profile name or ID.
@@ -468,6 +481,7 @@ def list(
query=maybe_transform(
{
"limit": limit,
+ "name": name,
"offset": offset,
"query": query,
},
diff --git a/src/kernel/resources/projects/projects.py b/src/kernel/resources/projects/projects.py
index 786a6c1f..c8ca08f2 100644
--- a/src/kernel/resources/projects/projects.py
+++ b/src/kernel/resources/projects/projects.py
@@ -177,6 +177,7 @@ def list(
self,
*,
limit: int | Omit = omit,
+ name: str | Omit = omit,
offset: int | Omit = omit,
query: str | Omit = omit,
# Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
@@ -192,6 +193,9 @@ def list(
Args:
limit: Maximum number of results to return
+ name: Exact-match filter on project name using the database collation. In production,
+ matching is case- and accent-insensitive.
+
offset: Number of results to skip
query: Case-insensitive substring match against project name
@@ -215,6 +219,7 @@ def list(
query=maybe_transform(
{
"limit": limit,
+ "name": name,
"offset": offset,
"query": query,
},
@@ -405,6 +410,7 @@ def list(
self,
*,
limit: int | Omit = omit,
+ name: str | Omit = omit,
offset: int | Omit = omit,
query: str | Omit = omit,
# Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
@@ -420,6 +426,9 @@ def list(
Args:
limit: Maximum number of results to return
+ name: Exact-match filter on project name using the database collation. In production,
+ matching is case- and accent-insensitive.
+
offset: Number of results to skip
query: Case-insensitive substring match against project name
@@ -443,6 +452,7 @@ def list(
query=maybe_transform(
{
"limit": limit,
+ "name": name,
"offset": offset,
"query": query,
},
diff --git a/src/kernel/resources/proxies.py b/src/kernel/resources/proxies.py
index 425e81a0..08f97e37 100644
--- a/src/kernel/resources/proxies.py
+++ b/src/kernel/resources/proxies.py
@@ -185,6 +185,7 @@ def list(
self,
*,
limit: int | Omit = omit,
+ name: str | Omit = omit,
offset: int | Omit = omit,
query: str | Omit = omit,
# Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
@@ -200,6 +201,10 @@ def list(
Args:
limit: Limit the number of proxies to return.
+ name: Exact-match filter on proxy name using the database collation. In production,
+ matching is case- and accent-insensitive. Names are not required to be unique,
+ so multiple proxies may match.
+
offset: Offset the number of proxies to return.
query: Case-insensitive substring match against proxy name, host, or IP address. IDs
@@ -224,6 +229,7 @@ def list(
query=maybe_transform(
{
"limit": limit,
+ "name": name,
"offset": offset,
"query": query,
},
@@ -480,6 +486,7 @@ def list(
self,
*,
limit: int | Omit = omit,
+ name: str | Omit = omit,
offset: int | Omit = omit,
query: str | Omit = omit,
# Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
@@ -495,6 +502,10 @@ def list(
Args:
limit: Limit the number of proxies to return.
+ name: Exact-match filter on proxy name using the database collation. In production,
+ matching is case- and accent-insensitive. Names are not required to be unique,
+ so multiple proxies may match.
+
offset: Offset the number of proxies to return.
query: Case-insensitive substring match against proxy name, host, or IP address. IDs
@@ -519,6 +530,7 @@ def list(
query=maybe_transform(
{
"limit": limit,
+ "name": name,
"offset": offset,
"query": query,
},
diff --git a/src/kernel/types/api_key_list_params.py b/src/kernel/types/api_key_list_params.py
index 3ddc2a38..95b8af5f 100644
--- a/src/kernel/types/api_key_list_params.py
+++ b/src/kernel/types/api_key_list_params.py
@@ -18,6 +18,15 @@ class APIKeyListParams(TypedDict, total=False):
limit: int
"""Maximum number of results to return"""
+ name: str
+ """Exact-match filter on API key name using the database collation.
+
+ In production, matching is case- and accent-insensitive. Names are not required
+ to be unique, so multiple keys may match. When status=all or
+ include_deleted=true is set, soft-deleted keys with the same name may also
+ match.
+ """
+
offset: int
"""Number of results to skip"""
diff --git a/src/kernel/types/browser_pool_list_params.py b/src/kernel/types/browser_pool_list_params.py
index e9da55f9..75fea5db 100644
--- a/src/kernel/types/browser_pool_list_params.py
+++ b/src/kernel/types/browser_pool_list_params.py
@@ -11,6 +11,14 @@ class BrowserPoolListParams(TypedDict, total=False):
limit: int
"""Limit the number of browser pools to return."""
+ name: str
+ """Exact-match filter on browser pool name using the database collation.
+
+ In production, matching is case- and accent-insensitive. During the
+ default-project migration, unscoped requests prefer a concrete default-project
+ browser pool over a legacy unscoped browser pool with the same name.
+ """
+
offset: int
"""Offset the number of browser pools to return."""
diff --git a/src/kernel/types/extension_list_params.py b/src/kernel/types/extension_list_params.py
index a88698c1..4c6669a9 100644
--- a/src/kernel/types/extension_list_params.py
+++ b/src/kernel/types/extension_list_params.py
@@ -11,6 +11,14 @@ class ExtensionListParams(TypedDict, total=False):
limit: int
"""Limit the number of extensions to return."""
+ name: str
+ """Exact-match filter on extension name using the database collation.
+
+ In production, matching is case- and accent-insensitive. During the
+ default-project migration, unscoped requests prefer a concrete default-project
+ extension over a legacy unscoped extension with the same name.
+ """
+
offset: int
"""Offset the number of extensions to return."""
diff --git a/src/kernel/types/profile_list_params.py b/src/kernel/types/profile_list_params.py
index ea24d6c6..5a572f6b 100644
--- a/src/kernel/types/profile_list_params.py
+++ b/src/kernel/types/profile_list_params.py
@@ -11,6 +11,14 @@ class ProfileListParams(TypedDict, total=False):
limit: int
"""Limit the number of profiles to return."""
+ name: str
+ """Exact-match filter on profile name using the database collation.
+
+ In production, matching is case- and accent-insensitive. During the
+ default-project migration, unscoped requests prefer a concrete default-project
+ profile over a legacy unscoped profile with the same name.
+ """
+
offset: int
"""Offset the number of profiles to return."""
diff --git a/src/kernel/types/project_list_params.py b/src/kernel/types/project_list_params.py
index 9e740e60..dd307d17 100644
--- a/src/kernel/types/project_list_params.py
+++ b/src/kernel/types/project_list_params.py
@@ -11,6 +11,12 @@ class ProjectListParams(TypedDict, total=False):
limit: int
"""Maximum number of results to return"""
+ name: str
+ """Exact-match filter on project name using the database collation.
+
+ In production, matching is case- and accent-insensitive.
+ """
+
offset: int
"""Number of results to skip"""
diff --git a/src/kernel/types/proxy_list_params.py b/src/kernel/types/proxy_list_params.py
index 0a689b27..1c7b30e6 100644
--- a/src/kernel/types/proxy_list_params.py
+++ b/src/kernel/types/proxy_list_params.py
@@ -11,6 +11,13 @@ class ProxyListParams(TypedDict, total=False):
limit: int
"""Limit the number of proxies to return."""
+ name: str
+ """Exact-match filter on proxy name using the database collation.
+
+ In production, matching is case- and accent-insensitive. Names are not required
+ to be unique, so multiple proxies may match.
+ """
+
offset: int
"""Offset the number of proxies to return."""
diff --git a/tests/api_resources/test_api_keys.py b/tests/api_resources/test_api_keys.py
index 5fdb5dc6..03d1ed8e 100644
--- a/tests/api_resources/test_api_keys.py
+++ b/tests/api_resources/test_api_keys.py
@@ -174,6 +174,7 @@ def test_method_list_with_all_params(self, client: Kernel) -> None:
api_key = client.api_keys.list(
include_deleted=True,
limit=100,
+ name="name",
offset=0,
query="query",
sort_by="created_at",
@@ -457,6 +458,7 @@ async def test_method_list_with_all_params(self, async_client: AsyncKernel) -> N
api_key = await async_client.api_keys.list(
include_deleted=True,
limit=100,
+ name="name",
offset=0,
query="query",
sort_by="created_at",
diff --git a/tests/api_resources/test_browser_pools.py b/tests/api_resources/test_browser_pools.py
index b7ca39a4..6f79fc8b 100644
--- a/tests/api_resources/test_browser_pools.py
+++ b/tests/api_resources/test_browser_pools.py
@@ -218,6 +218,7 @@ def test_method_list(self, client: Kernel) -> None:
def test_method_list_with_all_params(self, client: Kernel) -> None:
browser_pool = client.browser_pools.list(
limit=1,
+ name="name",
offset=0,
query="query",
)
@@ -654,6 +655,7 @@ async def test_method_list(self, async_client: AsyncKernel) -> None:
async def test_method_list_with_all_params(self, async_client: AsyncKernel) -> None:
browser_pool = await async_client.browser_pools.list(
limit=1,
+ name="name",
offset=0,
query="query",
)
diff --git a/tests/api_resources/test_extensions.py b/tests/api_resources/test_extensions.py
index cda54a2d..ace8ec6b 100644
--- a/tests/api_resources/test_extensions.py
+++ b/tests/api_resources/test_extensions.py
@@ -41,6 +41,7 @@ def test_method_list(self, client: Kernel) -> None:
def test_method_list_with_all_params(self, client: Kernel) -> None:
extension = client.extensions.list(
limit=1,
+ name="name",
offset=0,
query="query",
)
@@ -317,6 +318,7 @@ async def test_method_list(self, async_client: AsyncKernel) -> None:
async def test_method_list_with_all_params(self, async_client: AsyncKernel) -> None:
extension = await async_client.extensions.list(
limit=1,
+ name="name",
offset=0,
query="query",
)
diff --git a/tests/api_resources/test_profiles.py b/tests/api_resources/test_profiles.py
index 3075a3d9..683a3ce2 100644
--- a/tests/api_resources/test_profiles.py
+++ b/tests/api_resources/test_profiles.py
@@ -161,6 +161,7 @@ def test_method_list(self, client: Kernel) -> None:
def test_method_list_with_all_params(self, client: Kernel) -> None:
profile = client.profiles.list(
limit=1,
+ name="name",
offset=0,
query="query",
)
@@ -421,6 +422,7 @@ async def test_method_list(self, async_client: AsyncKernel) -> None:
async def test_method_list_with_all_params(self, async_client: AsyncKernel) -> None:
profile = await async_client.profiles.list(
limit=1,
+ name="name",
offset=0,
query="query",
)
diff --git a/tests/api_resources/test_projects.py b/tests/api_resources/test_projects.py
index bb301454..007353fe 100644
--- a/tests/api_resources/test_projects.py
+++ b/tests/api_resources/test_projects.py
@@ -157,6 +157,7 @@ def test_method_list(self, client: Kernel) -> None:
def test_method_list_with_all_params(self, client: Kernel) -> None:
project = client.projects.list(
limit=100,
+ name="name",
offset=0,
query="query",
)
@@ -371,6 +372,7 @@ async def test_method_list(self, async_client: AsyncKernel) -> None:
async def test_method_list_with_all_params(self, async_client: AsyncKernel) -> None:
project = await async_client.projects.list(
limit=100,
+ name="name",
offset=0,
query="query",
)
diff --git a/tests/api_resources/test_proxies.py b/tests/api_resources/test_proxies.py
index 72750aaa..cbe6e355 100644
--- a/tests/api_resources/test_proxies.py
+++ b/tests/api_resources/test_proxies.py
@@ -169,6 +169,7 @@ def test_method_list(self, client: Kernel) -> None:
def test_method_list_with_all_params(self, client: Kernel) -> None:
proxy = client.proxies.list(
limit=1,
+ name="name",
offset=0,
query="query",
)
@@ -440,6 +441,7 @@ async def test_method_list(self, async_client: AsyncKernel) -> None:
async def test_method_list_with_all_params(self, async_client: AsyncKernel) -> None:
proxy = await async_client.proxies.list(
limit=1,
+ name="name",
offset=0,
query="query",
)
From d37fe47081718484af66cdd731d83230a81a69f8 Mon Sep 17 00:00:00 2001
From: "stainless-app[bot]"
<142633134+stainless-app[bot]@users.noreply.github.com>
Date: Fri, 10 Jul 2026 14:56:09 +0000
Subject: [PATCH 119/366] feat: Make the browser pool OpenAPI contract truthful
---
.stats.yml | 4 +-
src/kernel/resources/browser_pools.py | 124 +++++++++---------
src/kernel/types/browser_pool.py | 17 +--
.../types/browser_pool_create_params.py | 7 +-
.../types/browser_pool_update_params.py | 64 +++++----
5 files changed, 108 insertions(+), 108 deletions(-)
diff --git a/.stats.yml b/.stats.yml
index f7ef7fe6..c403f2c1 100644
--- a/.stats.yml
+++ b/.stats.yml
@@ -1,4 +1,4 @@
configured_endpoints: 127
-openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel/kernel-b20fcf2fc1cdb91b51ffd354adcd12d63b950ea98b69e13bee6a78a58498c2cd.yml
-openapi_spec_hash: bc8b527a963dd2c2d74b6f9e6ca56652
+openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel/kernel-d7ab1ecb886cb7f86e7ce0926207db423a6650e8e12e7283a71f8682fbb472ae.yml
+openapi_spec_hash: 9323b0ba38d2d50b8506be4d7401c04d
config_hash: 77ee715aa17061166f9a02b264a21b8d
diff --git a/src/kernel/resources/browser_pools.py b/src/kernel/resources/browser_pools.py
index 6310cd61..54fdb7c8 100644
--- a/src/kernel/resources/browser_pools.py
+++ b/src/kernel/resources/browser_pools.py
@@ -96,7 +96,8 @@ def create(
chrome_policy: Custom Chrome enterprise policy overrides applied to all browsers in this pool.
Keys are Chrome enterprise policy names; values must match their expected types.
Blocked: kernel-managed policies (extensions, proxy, CDP/automation). See
- https://chromeenterprise.google/policies/
+ https://chromeenterprise.google/policies/ The serialized JSON payload is capped
+ at 5 MiB.
extensions: List of browser extensions to load into the session. Provide each by id or name.
@@ -107,7 +108,7 @@ def create(
headless: If true, launches the browser using a headless image. Defaults to false.
kiosk_mode: If true, launches the browser in kiosk mode to hide address bar and tabs in live
- view.
+ view. Defaults to false.
name: Optional name for the browser pool. Must be unique within the project.
@@ -131,7 +132,7 @@ def create(
chrome:// pages.
stealth: If true, launches the browser in stealth mode to reduce detection by anti-bot
- mechanisms.
+ mechanisms. Defaults to false.
timeout_seconds: Default idle timeout in seconds for browsers acquired from this pool before they
are destroyed. Defaults to 600 seconds. Minimum 10, maximum 259200 (72 hours).
@@ -252,10 +253,12 @@ def update(
`refresh_on_profile_update`.
Args:
- chrome_policy: Custom Chrome enterprise policy overrides applied to all browsers in this pool.
- Keys are Chrome enterprise policy names; values must match their expected types.
+ chrome_policy: If provided, replaces the custom Chrome enterprise policy overrides applied to
+ all browsers in this pool. Empty object clears any previously-set policy. Keys
+ are Chrome enterprise policy names; values must match their expected types.
Blocked: kernel-managed policies (extensions, proxy, CDP/automation). See
- https://chromeenterprise.google/policies/
+ https://chromeenterprise.google/policies/ The serialized JSON payload is capped
+ at 5 MiB.
discard_all_idle: Whether to discard all idle browsers and rebuild them immediately with the new
configuration. Defaults to false. Only browsers that are idle when the update
@@ -264,18 +267,19 @@ def update(
pool with that stale configuration until it is discarded (by this flag on a
later update, or by flushing the pool).
- extensions: List of browser extensions to load into the session. Provide each by id or name.
+ extensions: If provided, replaces the extension list. Empty array clears all
+ previously-selected extensions. Omit this field to leave extensions unchanged.
- fill_rate_per_minute: Percentage of the pool to fill per minute. Defaults to 10. The cap is 25 for
- most organizations but can be raised per-organization, so only the lower bound
- is enforced here.
+ fill_rate_per_minute: If provided, replaces the percentage of the pool to fill per minute. The cap is
+ 25 for most organizations but can be raised per-organization, so only the lower
+ bound is enforced here.
- headless: If true, launches the browser using a headless image. Defaults to false.
+ headless: If provided, replaces whether browsers launch using a headless image.
- kiosk_mode: If true, launches the browser in kiosk mode to hide address bar and tabs in live
- view.
+ kiosk_mode: If provided, replaces whether browsers launch in kiosk mode.
- name: Optional name for the browser pool. Must be unique within the project.
+ name: If provided, replaces the pool name. Empty string is a no-op; the pool name
+ cannot be cleared or reset to empty once assigned.
profile: Profile configuration for browsers in a pool. Provide either id or name.
Profiles must be created beforehand. Unlike single browser sessions, pools load
@@ -283,28 +287,24 @@ def update(
omitted here. Any save_changes value sent on a pool profile is silently ignored
rather than rejected.
- proxy_id: Optional proxy to associate to the browser session. Must reference a proxy in
- the same project as the browser session.
+ proxy_id: Empty string clears the previously-selected proxy. Omit this field to leave the
+ proxy unchanged.
- refresh_on_profile_update: When true, flush idle browsers when the profile the pool uses is updated, so
- pool browsers pick up the latest profile data. Requires a profile to be set on
- the pool.
+ refresh_on_profile_update: If provided, replaces whether idle browsers are flushed when the profile the
+ pool uses is updated. Requires a profile to be set on the pool.
- size: Number of browsers to maintain in the pool. The maximum size is determined by
- your organization's pooled sessions limit (the sum of all pool sizes cannot
- exceed your limit).
+ size: If provided, replaces the number of browsers to maintain in the pool. The
+ maximum size is determined by your organization's pooled sessions limit (the sum
+ of all pool sizes cannot exceed your limit).
- start_url: Optional URL to navigate to when a new browser is warmed into the pool.
- Best-effort: failures to navigate do not fail pool fill. Only applied to
- newly-warmed browsers; browsers reused via release/acquire keep whatever URL the
- previous lease left them on. Accepts any URL Chromium can resolve, including
- chrome:// pages.
+ start_url: If provided, replaces the URL to navigate to when a new browser is warmed into
+ the pool. Empty string clears the previously-set URL. Omit this field to leave
+ it unchanged.
- stealth: If true, launches the browser in stealth mode to reduce detection by anti-bot
- mechanisms.
+ stealth: If provided, replaces whether browsers launch in stealth mode.
- timeout_seconds: Default idle timeout in seconds for browsers acquired from this pool before they
- are destroyed. Defaults to 600 seconds. Minimum 10, maximum 259200 (72 hours).
+ timeout_seconds: If provided, replaces the default idle timeout in seconds for browsers acquired
+ from this pool before they are destroyed. Minimum 10, maximum 259200 (72 hours).
viewport: Initial browser window size in pixels with optional refresh rate. If omitted,
image defaults apply (1920x1080@25). For GPU images, the default is
@@ -671,7 +671,8 @@ async def create(
chrome_policy: Custom Chrome enterprise policy overrides applied to all browsers in this pool.
Keys are Chrome enterprise policy names; values must match their expected types.
Blocked: kernel-managed policies (extensions, proxy, CDP/automation). See
- https://chromeenterprise.google/policies/
+ https://chromeenterprise.google/policies/ The serialized JSON payload is capped
+ at 5 MiB.
extensions: List of browser extensions to load into the session. Provide each by id or name.
@@ -682,7 +683,7 @@ async def create(
headless: If true, launches the browser using a headless image. Defaults to false.
kiosk_mode: If true, launches the browser in kiosk mode to hide address bar and tabs in live
- view.
+ view. Defaults to false.
name: Optional name for the browser pool. Must be unique within the project.
@@ -706,7 +707,7 @@ async def create(
chrome:// pages.
stealth: If true, launches the browser in stealth mode to reduce detection by anti-bot
- mechanisms.
+ mechanisms. Defaults to false.
timeout_seconds: Default idle timeout in seconds for browsers acquired from this pool before they
are destroyed. Defaults to 600 seconds. Minimum 10, maximum 259200 (72 hours).
@@ -827,10 +828,12 @@ async def update(
`refresh_on_profile_update`.
Args:
- chrome_policy: Custom Chrome enterprise policy overrides applied to all browsers in this pool.
- Keys are Chrome enterprise policy names; values must match their expected types.
+ chrome_policy: If provided, replaces the custom Chrome enterprise policy overrides applied to
+ all browsers in this pool. Empty object clears any previously-set policy. Keys
+ are Chrome enterprise policy names; values must match their expected types.
Blocked: kernel-managed policies (extensions, proxy, CDP/automation). See
- https://chromeenterprise.google/policies/
+ https://chromeenterprise.google/policies/ The serialized JSON payload is capped
+ at 5 MiB.
discard_all_idle: Whether to discard all idle browsers and rebuild them immediately with the new
configuration. Defaults to false. Only browsers that are idle when the update
@@ -839,18 +842,19 @@ async def update(
pool with that stale configuration until it is discarded (by this flag on a
later update, or by flushing the pool).
- extensions: List of browser extensions to load into the session. Provide each by id or name.
+ extensions: If provided, replaces the extension list. Empty array clears all
+ previously-selected extensions. Omit this field to leave extensions unchanged.
- fill_rate_per_minute: Percentage of the pool to fill per minute. Defaults to 10. The cap is 25 for
- most organizations but can be raised per-organization, so only the lower bound
- is enforced here.
+ fill_rate_per_minute: If provided, replaces the percentage of the pool to fill per minute. The cap is
+ 25 for most organizations but can be raised per-organization, so only the lower
+ bound is enforced here.
- headless: If true, launches the browser using a headless image. Defaults to false.
+ headless: If provided, replaces whether browsers launch using a headless image.
- kiosk_mode: If true, launches the browser in kiosk mode to hide address bar and tabs in live
- view.
+ kiosk_mode: If provided, replaces whether browsers launch in kiosk mode.
- name: Optional name for the browser pool. Must be unique within the project.
+ name: If provided, replaces the pool name. Empty string is a no-op; the pool name
+ cannot be cleared or reset to empty once assigned.
profile: Profile configuration for browsers in a pool. Provide either id or name.
Profiles must be created beforehand. Unlike single browser sessions, pools load
@@ -858,28 +862,24 @@ async def update(
omitted here. Any save_changes value sent on a pool profile is silently ignored
rather than rejected.
- proxy_id: Optional proxy to associate to the browser session. Must reference a proxy in
- the same project as the browser session.
+ proxy_id: Empty string clears the previously-selected proxy. Omit this field to leave the
+ proxy unchanged.
- refresh_on_profile_update: When true, flush idle browsers when the profile the pool uses is updated, so
- pool browsers pick up the latest profile data. Requires a profile to be set on
- the pool.
+ refresh_on_profile_update: If provided, replaces whether idle browsers are flushed when the profile the
+ pool uses is updated. Requires a profile to be set on the pool.
- size: Number of browsers to maintain in the pool. The maximum size is determined by
- your organization's pooled sessions limit (the sum of all pool sizes cannot
- exceed your limit).
+ size: If provided, replaces the number of browsers to maintain in the pool. The
+ maximum size is determined by your organization's pooled sessions limit (the sum
+ of all pool sizes cannot exceed your limit).
- start_url: Optional URL to navigate to when a new browser is warmed into the pool.
- Best-effort: failures to navigate do not fail pool fill. Only applied to
- newly-warmed browsers; browsers reused via release/acquire keep whatever URL the
- previous lease left them on. Accepts any URL Chromium can resolve, including
- chrome:// pages.
+ start_url: If provided, replaces the URL to navigate to when a new browser is warmed into
+ the pool. Empty string clears the previously-set URL. Omit this field to leave
+ it unchanged.
- stealth: If true, launches the browser in stealth mode to reduce detection by anti-bot
- mechanisms.
+ stealth: If provided, replaces whether browsers launch in stealth mode.
- timeout_seconds: Default idle timeout in seconds for browsers acquired from this pool before they
- are destroyed. Defaults to 600 seconds. Minimum 10, maximum 259200 (72 hours).
+ timeout_seconds: If provided, replaces the default idle timeout in seconds for browsers acquired
+ from this pool before they are destroyed. Minimum 10, maximum 259200 (72 hours).
viewport: Initial browser window size in pixels with optional refresh rate. If omitted,
image defaults apply (1920x1080@25). For GPU images, the default is
diff --git a/src/kernel/types/browser_pool.py b/src/kernel/types/browser_pool.py
index 78860923..a025e6ff 100644
--- a/src/kernel/types/browser_pool.py
+++ b/src/kernel/types/browser_pool.py
@@ -33,7 +33,7 @@ class BrowserPoolConfig(BaseModel):
"""Configuration used to create all browsers in this pool"""
size: int
- """Number of browsers to maintain in the pool.
+ """Number of browsers maintained in the pool.
The maximum size is determined by your organization's pooled sessions limit (the
sum of all pool sizes cannot exceed your limit).
@@ -44,7 +44,8 @@ class BrowserPoolConfig(BaseModel):
Keys are Chrome enterprise policy names; values must match their expected types.
Blocked: kernel-managed policies (extensions, proxy, CDP/automation). See
- https://chromeenterprise.google/policies/
+ https://chromeenterprise.google/policies/ The serialized JSON payload is capped
+ at 5 MiB.
"""
extensions: Optional[List[BrowserExtension]] = None
@@ -56,12 +57,12 @@ class BrowserPoolConfig(BaseModel):
fill_rate_per_minute: Optional[int] = None
"""Percentage of the pool to fill per minute.
- Defaults to 10. The cap is 25 for most organizations but can be raised
- per-organization, so only the lower bound is enforced here.
+ The cap is 25 for most organizations but can be raised per-organization, so only
+ the lower bound is enforced here.
"""
headless: Optional[bool] = None
- """If true, launches the browser using a headless image. Defaults to false."""
+ """If true, launches the browser using a headless image."""
kiosk_mode: Optional[bool] = None
"""
@@ -82,9 +83,9 @@ class BrowserPoolConfig(BaseModel):
"""
proxy_id: Optional[str] = None
- """Optional proxy to associate to the browser session.
+ """Optional proxy associated to the browser session.
- Must reference a proxy in the same project as the browser session.
+ References a proxy in the same project as the browser session.
"""
refresh_on_profile_update: Optional[bool] = None
@@ -112,7 +113,7 @@ class BrowserPoolConfig(BaseModel):
timeout_seconds: Optional[int] = None
"""
Default idle timeout in seconds for browsers acquired from this pool before they
- are destroyed. Defaults to 600 seconds. Minimum 10, maximum 259200 (72 hours).
+ are destroyed. Minimum 10, maximum 259200 (72 hours).
"""
viewport: Optional[BrowserViewport] = None
diff --git a/src/kernel/types/browser_pool_create_params.py b/src/kernel/types/browser_pool_create_params.py
index 5d4d9760..d0e2fe01 100644
--- a/src/kernel/types/browser_pool_create_params.py
+++ b/src/kernel/types/browser_pool_create_params.py
@@ -24,7 +24,8 @@ class BrowserPoolCreateParams(TypedDict, total=False):
Keys are Chrome enterprise policy names; values must match their expected types.
Blocked: kernel-managed policies (extensions, proxy, CDP/automation). See
- https://chromeenterprise.google/policies/
+ https://chromeenterprise.google/policies/ The serialized JSON payload is capped
+ at 5 MiB.
"""
extensions: Iterable[BrowserExtension]
@@ -46,7 +47,7 @@ class BrowserPoolCreateParams(TypedDict, total=False):
kiosk_mode: bool
"""
If true, launches the browser in kiosk mode to hide address bar and tabs in live
- view.
+ view. Defaults to false.
"""
name: str
@@ -86,7 +87,7 @@ class BrowserPoolCreateParams(TypedDict, total=False):
stealth: bool
"""
If true, launches the browser in stealth mode to reduce detection by anti-bot
- mechanisms.
+ mechanisms. Defaults to false.
"""
timeout_seconds: int
diff --git a/src/kernel/types/browser_pool_update_params.py b/src/kernel/types/browser_pool_update_params.py
index d504e29d..575449c3 100644
--- a/src/kernel/types/browser_pool_update_params.py
+++ b/src/kernel/types/browser_pool_update_params.py
@@ -13,11 +13,13 @@
class BrowserPoolUpdateParams(TypedDict, total=False):
chrome_policy: Dict[str, object]
- """Custom Chrome enterprise policy overrides applied to all browsers in this pool.
-
- Keys are Chrome enterprise policy names; values must match their expected types.
+ """
+ If provided, replaces the custom Chrome enterprise policy overrides applied to
+ all browsers in this pool. Empty object clears any previously-set policy. Keys
+ are Chrome enterprise policy names; values must match their expected types.
Blocked: kernel-managed policies (extensions, proxy, CDP/automation). See
- https://chromeenterprise.google/policies/
+ https://chromeenterprise.google/policies/ The serialized JSON payload is capped
+ at 5 MiB.
"""
discard_all_idle: bool
@@ -31,29 +33,31 @@ class BrowserPoolUpdateParams(TypedDict, total=False):
"""
extensions: Iterable[BrowserExtension]
- """List of browser extensions to load into the session.
+ """If provided, replaces the extension list.
- Provide each by id or name.
+ Empty array clears all previously-selected extensions. Omit this field to leave
+ extensions unchanged.
"""
fill_rate_per_minute: int
- """Percentage of the pool to fill per minute.
+ """If provided, replaces the percentage of the pool to fill per minute.
- Defaults to 10. The cap is 25 for most organizations but can be raised
- per-organization, so only the lower bound is enforced here.
+ The cap is 25 for most organizations but can be raised per-organization, so only
+ the lower bound is enforced here.
"""
headless: bool
- """If true, launches the browser using a headless image. Defaults to false."""
+ """If provided, replaces whether browsers launch using a headless image."""
kiosk_mode: bool
- """
- If true, launches the browser in kiosk mode to hide address bar and tabs in live
- view.
- """
+ """If provided, replaces whether browsers launch in kiosk mode."""
name: str
- """Optional name for the browser pool. Must be unique within the project."""
+ """If provided, replaces the pool name.
+
+ Empty string is a no-op; the pool name cannot be cleared or reset to empty once
+ assigned.
+ """
profile: Profile
"""Profile configuration for browsers in a pool.
@@ -65,44 +69,38 @@ class BrowserPoolUpdateParams(TypedDict, total=False):
"""
proxy_id: str
- """Optional proxy to associate to the browser session.
+ """Empty string clears the previously-selected proxy.
- Must reference a proxy in the same project as the browser session.
+ Omit this field to leave the proxy unchanged.
"""
refresh_on_profile_update: bool
"""
- When true, flush idle browsers when the profile the pool uses is updated, so
- pool browsers pick up the latest profile data. Requires a profile to be set on
- the pool.
+ If provided, replaces whether idle browsers are flushed when the profile the
+ pool uses is updated. Requires a profile to be set on the pool.
"""
size: int
- """Number of browsers to maintain in the pool.
+ """If provided, replaces the number of browsers to maintain in the pool.
The maximum size is determined by your organization's pooled sessions limit (the
sum of all pool sizes cannot exceed your limit).
"""
start_url: str
- """Optional URL to navigate to when a new browser is warmed into the pool.
-
- Best-effort: failures to navigate do not fail pool fill. Only applied to
- newly-warmed browsers; browsers reused via release/acquire keep whatever URL the
- previous lease left them on. Accepts any URL Chromium can resolve, including
- chrome:// pages.
+ """
+ If provided, replaces the URL to navigate to when a new browser is warmed into
+ the pool. Empty string clears the previously-set URL. Omit this field to leave
+ it unchanged.
"""
stealth: bool
- """
- If true, launches the browser in stealth mode to reduce detection by anti-bot
- mechanisms.
- """
+ """If provided, replaces whether browsers launch in stealth mode."""
timeout_seconds: int
"""
- Default idle timeout in seconds for browsers acquired from this pool before they
- are destroyed. Defaults to 600 seconds. Minimum 10, maximum 259200 (72 hours).
+ If provided, replaces the default idle timeout in seconds for browsers acquired
+ from this pool before they are destroyed. Minimum 10, maximum 259200 (72 hours).
"""
viewport: BrowserViewport
From d963791e811307773797b8144d9bf92a16fd2ab0 Mon Sep 17 00:00:00 2001
From: "stainless-app[bot]"
<142633134+stainless-app[bot]@users.noreply.github.com>
Date: Fri, 10 Jul 2026 15:32:43 +0000
Subject: [PATCH 120/366] feat: Persist and echo deployment source identity
---
.stats.yml | 4 +--
.../types/deployment_create_response.py | 33 +++++++++++++++++++
src/kernel/types/deployment_list_response.py | 33 +++++++++++++++++++
.../types/deployment_retrieve_response.py | 33 +++++++++++++++++++
src/kernel/types/deployment_state_event.py | 33 +++++++++++++++++++
5 files changed, 134 insertions(+), 2 deletions(-)
diff --git a/.stats.yml b/.stats.yml
index c403f2c1..f6f61824 100644
--- a/.stats.yml
+++ b/.stats.yml
@@ -1,4 +1,4 @@
configured_endpoints: 127
-openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel/kernel-d7ab1ecb886cb7f86e7ce0926207db423a6650e8e12e7283a71f8682fbb472ae.yml
-openapi_spec_hash: 9323b0ba38d2d50b8506be4d7401c04d
+openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel/kernel-d61b5e5ca18ce30a6b4d05483bcd67969ce0f4aee2e9e8c379be4f22f1362b01.yml
+openapi_spec_hash: 23121d1c1552e41177b472c4c39d154e
config_hash: 77ee715aa17061166f9a02b264a21b8d
diff --git a/src/kernel/types/deployment_create_response.py b/src/kernel/types/deployment_create_response.py
index 17d17b59..439ed52a 100644
--- a/src/kernel/types/deployment_create_response.py
+++ b/src/kernel/types/deployment_create_response.py
@@ -35,6 +35,39 @@ class DeploymentCreateResponse(BaseModel):
values.
"""
+ source_checksum: Optional[str] = None
+ """Hex-encoded SHA-256 checksum of the source archive.
+
+ For file uploads, this hashes the uploaded archive; for GitHub-sourced
+ deployments, this hashes the GitHub archive downloaded by the API. Omitted for
+ deployments created before this field was recorded.
+ """
+
+ source_path: Optional[str] = None
+ """
+ For GitHub-sourced deployments, the subpath within the repository that was used
+ as the deploy root. Omitted when the repo root was used or for file uploads.
+ """
+
+ source_ref: Optional[str] = None
+ """
+ For GitHub-sourced deployments, the git ref as requested at deploy time (branch,
+ tag, or commit SHA — not resolved to a commit). Omitted for file uploads.
+ """
+
+ source_type: Optional[Literal["file", "github"]] = None
+ """Origin of the deployed source code.
+
+ This is read-only response provenance; `file` indicates an uploaded archive and
+ `github` indicates a repository fetched by the API.
+ """
+
+ source_url: Optional[str] = None
+ """For GitHub-sourced deployments, the repository URL that was fetched.
+
+ Omitted for file uploads.
+ """
+
status_reason: Optional[str] = None
"""Status reason"""
diff --git a/src/kernel/types/deployment_list_response.py b/src/kernel/types/deployment_list_response.py
index 5806bdaf..accd0d05 100644
--- a/src/kernel/types/deployment_list_response.py
+++ b/src/kernel/types/deployment_list_response.py
@@ -35,6 +35,39 @@ class DeploymentListResponse(BaseModel):
values.
"""
+ source_checksum: Optional[str] = None
+ """Hex-encoded SHA-256 checksum of the source archive.
+
+ For file uploads, this hashes the uploaded archive; for GitHub-sourced
+ deployments, this hashes the GitHub archive downloaded by the API. Omitted for
+ deployments created before this field was recorded.
+ """
+
+ source_path: Optional[str] = None
+ """
+ For GitHub-sourced deployments, the subpath within the repository that was used
+ as the deploy root. Omitted when the repo root was used or for file uploads.
+ """
+
+ source_ref: Optional[str] = None
+ """
+ For GitHub-sourced deployments, the git ref as requested at deploy time (branch,
+ tag, or commit SHA — not resolved to a commit). Omitted for file uploads.
+ """
+
+ source_type: Optional[Literal["file", "github"]] = None
+ """Origin of the deployed source code.
+
+ This is read-only response provenance; `file` indicates an uploaded archive and
+ `github` indicates a repository fetched by the API.
+ """
+
+ source_url: Optional[str] = None
+ """For GitHub-sourced deployments, the repository URL that was fetched.
+
+ Omitted for file uploads.
+ """
+
status_reason: Optional[str] = None
"""Status reason"""
diff --git a/src/kernel/types/deployment_retrieve_response.py b/src/kernel/types/deployment_retrieve_response.py
index addcad4a..2f4cf49e 100644
--- a/src/kernel/types/deployment_retrieve_response.py
+++ b/src/kernel/types/deployment_retrieve_response.py
@@ -35,6 +35,39 @@ class DeploymentRetrieveResponse(BaseModel):
values.
"""
+ source_checksum: Optional[str] = None
+ """Hex-encoded SHA-256 checksum of the source archive.
+
+ For file uploads, this hashes the uploaded archive; for GitHub-sourced
+ deployments, this hashes the GitHub archive downloaded by the API. Omitted for
+ deployments created before this field was recorded.
+ """
+
+ source_path: Optional[str] = None
+ """
+ For GitHub-sourced deployments, the subpath within the repository that was used
+ as the deploy root. Omitted when the repo root was used or for file uploads.
+ """
+
+ source_ref: Optional[str] = None
+ """
+ For GitHub-sourced deployments, the git ref as requested at deploy time (branch,
+ tag, or commit SHA — not resolved to a commit). Omitted for file uploads.
+ """
+
+ source_type: Optional[Literal["file", "github"]] = None
+ """Origin of the deployed source code.
+
+ This is read-only response provenance; `file` indicates an uploaded archive and
+ `github` indicates a repository fetched by the API.
+ """
+
+ source_url: Optional[str] = None
+ """For GitHub-sourced deployments, the repository URL that was fetched.
+
+ Omitted for file uploads.
+ """
+
status_reason: Optional[str] = None
"""Status reason"""
diff --git a/src/kernel/types/deployment_state_event.py b/src/kernel/types/deployment_state_event.py
index dce183a7..64835725 100644
--- a/src/kernel/types/deployment_state_event.py
+++ b/src/kernel/types/deployment_state_event.py
@@ -35,6 +35,39 @@ class Deployment(BaseModel):
values.
"""
+ source_checksum: Optional[str] = None
+ """Hex-encoded SHA-256 checksum of the source archive.
+
+ For file uploads, this hashes the uploaded archive; for GitHub-sourced
+ deployments, this hashes the GitHub archive downloaded by the API. Omitted for
+ deployments created before this field was recorded.
+ """
+
+ source_path: Optional[str] = None
+ """
+ For GitHub-sourced deployments, the subpath within the repository that was used
+ as the deploy root. Omitted when the repo root was used or for file uploads.
+ """
+
+ source_ref: Optional[str] = None
+ """
+ For GitHub-sourced deployments, the git ref as requested at deploy time (branch,
+ tag, or commit SHA — not resolved to a commit). Omitted for file uploads.
+ """
+
+ source_type: Optional[Literal["file", "github"]] = None
+ """Origin of the deployed source code.
+
+ This is read-only response provenance; `file` indicates an uploaded archive and
+ `github` indicates a repository fetched by the API.
+ """
+
+ source_url: Optional[str] = None
+ """For GitHub-sourced deployments, the repository URL that was fetched.
+
+ Omitted for file uploads.
+ """
+
status_reason: Optional[str] = None
"""Status reason"""
From 4afc0624e119959f27d2662c11c58957dd8c11d7 Mon Sep 17 00:00:00 2001
From: "stainless-app[bot]"
<142633134+stainless-app[bot]@users.noreply.github.com>
Date: Fri, 10 Jul 2026 15:59:40 +0000
Subject: [PATCH 121/366] codegen metadata
---
.stats.yml | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/.stats.yml b/.stats.yml
index f6f61824..38aa69be 100644
--- a/.stats.yml
+++ b/.stats.yml
@@ -1,4 +1,4 @@
configured_endpoints: 127
-openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel/kernel-d61b5e5ca18ce30a6b4d05483bcd67969ce0f4aee2e9e8c379be4f22f1362b01.yml
-openapi_spec_hash: 23121d1c1552e41177b472c4c39d154e
+openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel/kernel-9143bc729ef60beffa0bbd9b601bb095083e41e23c8994553e69302f586882f5.yml
+openapi_spec_hash: bd04c530cfa8188af0e63a784129645d
config_hash: 77ee715aa17061166f9a02b264a21b8d
From 4951f38ae8e81a5245949f23bbbd689a535ccc7f Mon Sep 17 00:00:00 2001
From: "stainless-app[bot]"
<142633134+stainless-app[bot]@users.noreply.github.com>
Date: Fri, 10 Jul 2026 19:29:03 +0000
Subject: [PATCH 122/366] feat: Auto-default refresh_on_profile_update when
browser pool profile changes
---
.stats.yml | 4 ++--
src/kernel/resources/browser_pools.py | 18 ++++++++++++------
src/kernel/types/browser_pool.py | 4 ++--
src/kernel/types/browser_pool_create_params.py | 4 ++--
src/kernel/types/browser_pool_update_params.py | 5 ++++-
5 files changed, 22 insertions(+), 13 deletions(-)
diff --git a/.stats.yml b/.stats.yml
index 38aa69be..5882e06c 100644
--- a/.stats.yml
+++ b/.stats.yml
@@ -1,4 +1,4 @@
configured_endpoints: 127
-openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel/kernel-9143bc729ef60beffa0bbd9b601bb095083e41e23c8994553e69302f586882f5.yml
-openapi_spec_hash: bd04c530cfa8188af0e63a784129645d
+openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel/kernel-25b48b79d356843aeda41e5e5c3c407c5bd5c12f9548500ffc0a6696e6acd941.yml
+openapi_spec_hash: f710169690ab7aca30c91968e752cff7
config_hash: 77ee715aa17061166f9a02b264a21b8d
diff --git a/src/kernel/resources/browser_pools.py b/src/kernel/resources/browser_pools.py
index 54fdb7c8..5810f332 100644
--- a/src/kernel/resources/browser_pools.py
+++ b/src/kernel/resources/browser_pools.py
@@ -122,8 +122,8 @@ def create(
the same project as the browser session.
refresh_on_profile_update: When true, flush idle browsers when the profile the pool uses is updated, so
- pool browsers pick up the latest profile data. Requires a profile to be set on
- the pool.
+ pool browsers pick up the latest profile data. When a profile is provided during
+ creation, this defaults to true. Requires a profile to be set on the pool.
start_url: Optional URL to navigate to when a new browser is warmed into the pool.
Best-effort: failures to navigate do not fail pool fill. Only applied to
@@ -291,7 +291,10 @@ def update(
proxy unchanged.
refresh_on_profile_update: If provided, replaces whether idle browsers are flushed when the profile the
- pool uses is updated. Requires a profile to be set on the pool.
+ pool uses is updated. When the pool's profile reference is changed (including
+ newly attached) and this field is omitted, it defaults to true. Re-sending the
+ same profile reference leaves this setting unchanged. Clearing the profile also
+ disables this setting. Requires a profile to be set on the pool.
size: If provided, replaces the number of browsers to maintain in the pool. The
maximum size is determined by your organization's pooled sessions limit (the sum
@@ -697,8 +700,8 @@ async def create(
the same project as the browser session.
refresh_on_profile_update: When true, flush idle browsers when the profile the pool uses is updated, so
- pool browsers pick up the latest profile data. Requires a profile to be set on
- the pool.
+ pool browsers pick up the latest profile data. When a profile is provided during
+ creation, this defaults to true. Requires a profile to be set on the pool.
start_url: Optional URL to navigate to when a new browser is warmed into the pool.
Best-effort: failures to navigate do not fail pool fill. Only applied to
@@ -866,7 +869,10 @@ async def update(
proxy unchanged.
refresh_on_profile_update: If provided, replaces whether idle browsers are flushed when the profile the
- pool uses is updated. Requires a profile to be set on the pool.
+ pool uses is updated. When the pool's profile reference is changed (including
+ newly attached) and this field is omitted, it defaults to true. Re-sending the
+ same profile reference leaves this setting unchanged. Clearing the profile also
+ disables this setting. Requires a profile to be set on the pool.
size: If provided, replaces the number of browsers to maintain in the pool. The
maximum size is determined by your organization's pooled sessions limit (the sum
diff --git a/src/kernel/types/browser_pool.py b/src/kernel/types/browser_pool.py
index a025e6ff..4f7764ab 100644
--- a/src/kernel/types/browser_pool.py
+++ b/src/kernel/types/browser_pool.py
@@ -91,8 +91,8 @@ class BrowserPoolConfig(BaseModel):
refresh_on_profile_update: Optional[bool] = None
"""
When true, flush idle browsers when the profile the pool uses is updated, so
- pool browsers pick up the latest profile data. Requires a profile to be set on
- the pool.
+ pool browsers pick up the latest profile data. When a profile is provided during
+ creation, this defaults to true. Requires a profile to be set on the pool.
"""
start_url: Optional[str] = None
diff --git a/src/kernel/types/browser_pool_create_params.py b/src/kernel/types/browser_pool_create_params.py
index d0e2fe01..328ac743 100644
--- a/src/kernel/types/browser_pool_create_params.py
+++ b/src/kernel/types/browser_pool_create_params.py
@@ -71,8 +71,8 @@ class BrowserPoolCreateParams(TypedDict, total=False):
refresh_on_profile_update: bool
"""
When true, flush idle browsers when the profile the pool uses is updated, so
- pool browsers pick up the latest profile data. Requires a profile to be set on
- the pool.
+ pool browsers pick up the latest profile data. When a profile is provided during
+ creation, this defaults to true. Requires a profile to be set on the pool.
"""
start_url: str
diff --git a/src/kernel/types/browser_pool_update_params.py b/src/kernel/types/browser_pool_update_params.py
index 575449c3..a4c65578 100644
--- a/src/kernel/types/browser_pool_update_params.py
+++ b/src/kernel/types/browser_pool_update_params.py
@@ -77,7 +77,10 @@ class BrowserPoolUpdateParams(TypedDict, total=False):
refresh_on_profile_update: bool
"""
If provided, replaces whether idle browsers are flushed when the profile the
- pool uses is updated. Requires a profile to be set on the pool.
+ pool uses is updated. When the pool's profile reference is changed (including
+ newly attached) and this field is omitted, it defaults to true. Re-sending the
+ same profile reference leaves this setting unchanged. Clearing the profile also
+ disables this setting. Requires a profile to be set on the pool.
"""
size: int
From 94ec3492c2a066ff94e43d407f51773705a006a3 Mon Sep 17 00:00:00 2001
From: "stainless-app[bot]"
<142633134+stainless-app[bot]@users.noreply.github.com>
Date: Mon, 13 Jul 2026 15:43:46 +0000
Subject: [PATCH 123/366] feat: Support multiple audit log method exclusions
---
.stats.yml | 4 ++--
src/kernel/resources/audit_logs.py | 8 ++++----
src/kernel/types/audit_log_export_chunk_params.py | 2 +-
src/kernel/types/audit_log_list_params.py | 2 +-
tests/api_resources/test_audit_logs.py | 8 ++++----
5 files changed, 12 insertions(+), 12 deletions(-)
diff --git a/.stats.yml b/.stats.yml
index 5882e06c..a855b9ce 100644
--- a/.stats.yml
+++ b/.stats.yml
@@ -1,4 +1,4 @@
configured_endpoints: 127
-openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel/kernel-25b48b79d356843aeda41e5e5c3c407c5bd5c12f9548500ffc0a6696e6acd941.yml
-openapi_spec_hash: f710169690ab7aca30c91968e752cff7
+openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel/kernel-f0003e336fc8dc54d9cac4728c847fb80c32d50a9165738c5b70aa83a09bb0f5.yml
+openapi_spec_hash: a2099eec27728b1cfd9032dc71b9bb57
config_hash: 77ee715aa17061166f9a02b264a21b8d
diff --git a/src/kernel/resources/audit_logs.py b/src/kernel/resources/audit_logs.py
index 814db9c7..2970e153 100644
--- a/src/kernel/resources/audit_logs.py
+++ b/src/kernel/resources/audit_logs.py
@@ -62,7 +62,7 @@ def list(
end: Union[str, datetime],
start: Union[str, datetime],
auth_strategy: str | Omit = omit,
- exclude_method: str | Omit = omit,
+ exclude_method: SequenceNotStr[str] | Omit = omit,
limit: int | Omit = omit,
method: str | Omit = omit,
page_token: str | Omit = omit,
@@ -144,7 +144,7 @@ def export_chunk(
start: Union[str, datetime],
auth_strategy: str | Omit = omit,
cursor: str | Omit = omit,
- exclude_method: str | Omit = omit,
+ exclude_method: SequenceNotStr[str] | Omit = omit,
format: Literal["jsonl", "jsonl.gz"] | Omit = omit,
limit: int | Omit = omit,
method: str | Omit = omit,
@@ -251,7 +251,7 @@ def list(
end: Union[str, datetime],
start: Union[str, datetime],
auth_strategy: str | Omit = omit,
- exclude_method: str | Omit = omit,
+ exclude_method: SequenceNotStr[str] | Omit = omit,
limit: int | Omit = omit,
method: str | Omit = omit,
page_token: str | Omit = omit,
@@ -333,7 +333,7 @@ async def export_chunk(
start: Union[str, datetime],
auth_strategy: str | Omit = omit,
cursor: str | Omit = omit,
- exclude_method: str | Omit = omit,
+ exclude_method: SequenceNotStr[str] | Omit = omit,
format: Literal["jsonl", "jsonl.gz"] | Omit = omit,
limit: int | Omit = omit,
method: str | Omit = omit,
diff --git a/src/kernel/types/audit_log_export_chunk_params.py b/src/kernel/types/audit_log_export_chunk_params.py
index 8e759c9f..5cc07b03 100644
--- a/src/kernel/types/audit_log_export_chunk_params.py
+++ b/src/kernel/types/audit_log_export_chunk_params.py
@@ -25,7 +25,7 @@ class AuditLogExportChunkParams(TypedDict, total=False):
cursor: str
"""Opaque cursor from X-Next-Cursor for the next chunk of older records."""
- exclude_method: str
+ exclude_method: SequenceNotStr[str]
"""Filter out results by HTTP method."""
format: Literal["jsonl", "jsonl.gz"]
diff --git a/src/kernel/types/audit_log_list_params.py b/src/kernel/types/audit_log_list_params.py
index f1e18f58..b3329e70 100644
--- a/src/kernel/types/audit_log_list_params.py
+++ b/src/kernel/types/audit_log_list_params.py
@@ -22,7 +22,7 @@ class AuditLogListParams(TypedDict, total=False):
auth_strategy: str
"""Filter by authentication strategy."""
- exclude_method: str
+ exclude_method: SequenceNotStr[str]
"""Filter out results by HTTP method."""
limit: int
diff --git a/tests/api_resources/test_audit_logs.py b/tests/api_resources/test_audit_logs.py
index 47193f05..7a8ebe41 100644
--- a/tests/api_resources/test_audit_logs.py
+++ b/tests/api_resources/test_audit_logs.py
@@ -43,7 +43,7 @@ def test_method_list_with_all_params(self, client: Kernel) -> None:
end=parse_datetime("2026-01-02T00:00:00Z"),
start=parse_datetime("2026-01-01T00:00:00Z"),
auth_strategy="auth_strategy",
- exclude_method="exclude_method",
+ exclude_method=["string"],
limit=1,
method="method",
page_token="page_token",
@@ -103,7 +103,7 @@ def test_method_export_chunk_with_all_params(self, client: Kernel, respx_mock: M
start=parse_datetime("2026-01-01T00:00:00Z"),
auth_strategy="auth_strategy",
cursor="cursor",
- exclude_method="exclude_method",
+ exclude_method=["string"],
format="jsonl",
limit=1,
method="method",
@@ -170,7 +170,7 @@ async def test_method_list_with_all_params(self, async_client: AsyncKernel) -> N
end=parse_datetime("2026-01-02T00:00:00Z"),
start=parse_datetime("2026-01-01T00:00:00Z"),
auth_strategy="auth_strategy",
- exclude_method="exclude_method",
+ exclude_method=["string"],
limit=1,
method="method",
page_token="page_token",
@@ -230,7 +230,7 @@ async def test_method_export_chunk_with_all_params(self, async_client: AsyncKern
start=parse_datetime("2026-01-01T00:00:00Z"),
auth_strategy="auth_strategy",
cursor="cursor",
- exclude_method="exclude_method",
+ exclude_method=["string"],
format="jsonl",
limit=1,
method="method",
From 5bdacb6bff640f7f700c7263ad1c39ae66874f6e Mon Sep 17 00:00:00 2001
From: "stainless-app[bot]"
<142633134+stainless-app[bot]@users.noreply.github.com>
Date: Mon, 13 Jul 2026 16:13:44 +0000
Subject: [PATCH 124/366] feat: Expose telemetry exception message in API/SDK
---
.stats.yml | 4 ++--
src/kernel/types/browsers/browser_console_error_event.py | 7 ++++++-
2 files changed, 8 insertions(+), 3 deletions(-)
diff --git a/.stats.yml b/.stats.yml
index a855b9ce..04d533eb 100644
--- a/.stats.yml
+++ b/.stats.yml
@@ -1,4 +1,4 @@
configured_endpoints: 127
-openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel/kernel-f0003e336fc8dc54d9cac4728c847fb80c32d50a9165738c5b70aa83a09bb0f5.yml
-openapi_spec_hash: a2099eec27728b1cfd9032dc71b9bb57
+openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel/kernel-1413d61d06059008b4c1e204c783d8b2e7dca9af23f6e3b6d2552493ea2d9853.yml
+openapi_spec_hash: 80aca500caddd300cb03a6daf568fddb
config_hash: 77ee715aa17061166f9a02b264a21b8d
diff --git a/src/kernel/types/browsers/browser_console_error_event.py b/src/kernel/types/browsers/browser_console_error_event.py
index 72a93ede..ef1c8949 100644
--- a/src/kernel/types/browsers/browser_console_error_event.py
+++ b/src/kernel/types/browsers/browser_console_error_event.py
@@ -19,7 +19,12 @@ class Data(BrowserEventContext):
"""
text: str
- """Error message text. Present in both source paths."""
+ """Human-readable error text, as the browser console would display it.
+
+ For console.error() calls, the first argument coerced to a string. For uncaught
+ exceptions, the prefix and error message, e.g. "Uncaught Error: boom" or
+ "Uncaught (in promise) TypeError: x is not a function".
+ """
args: Optional[List[str]] = None
"""All console arguments coerced to strings.
From 033b50200ef234523d3ae19afa267977734344e5 Mon Sep 17 00:00:00 2001
From: "stainless-app[bot]"
<142633134+stainless-app[bot]@users.noreply.github.com>
Date: Mon, 13 Jul 2026 18:30:11 +0000
Subject: [PATCH 125/366] release: 0.78.0
---
.release-please-manifest.json | 2 +-
CHANGELOG.md | 20 ++++++++++++++++++++
pyproject.toml | 2 +-
src/kernel/_version.py | 2 +-
4 files changed, 23 insertions(+), 3 deletions(-)
diff --git a/.release-please-manifest.json b/.release-please-manifest.json
index aa39267a..0d51a367 100644
--- a/.release-please-manifest.json
+++ b/.release-please-manifest.json
@@ -1,3 +1,3 @@
{
- ".": "0.76.0"
+ ".": "0.78.0"
}
\ No newline at end of file
diff --git a/CHANGELOG.md b/CHANGELOG.md
index e0c979f6..f8ef4df6 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,25 @@
# Changelog
+## 0.78.0 (2026-07-13)
+
+Full Changelog: [v0.76.0...v0.78.0](https://github.com/kernel/kernel-python-sdk/compare/v0.76.0...v0.78.0)
+
+### Features
+
+* Add exact-match name filter to list endpoints ([6011556](https://github.com/kernel/kernel-python-sdk/commit/601155642a4d082d19c3b3293eea6d6105f74407))
+* Add name-only rename for profiles and proxies ([1d38266](https://github.com/kernel/kernel-python-sdk/commit/1d3826696cb2005ab6c33a7cd1df19b660d66449))
+* Auto-default refresh_on_profile_update when browser pool profile changes ([4771197](https://github.com/kernel/kernel-python-sdk/commit/4771197c55a1562d465f9b75d8a8e2a12f5e6f76))
+* Document name uniqueness and query match semantics ([e27f37d](https://github.com/kernel/kernel-python-sdk/commit/e27f37d43b1d76628c42f90e77a18af513c86280))
+* Expose telemetry exception message in API/SDK ([6575dd2](https://github.com/kernel/kernel-python-sdk/commit/6575dd21919b092a77de88e4ee8d037f601edadc))
+* Make the browser pool OpenAPI contract truthful ([20b3a7e](https://github.com/kernel/kernel-python-sdk/commit/20b3a7efc61b7de4007fdd9d096595bfab9c06c3))
+* Persist and echo deployment source identity ([5db2b01](https://github.com/kernel/kernel-python-sdk/commit/5db2b01c8ad53441cb3cc7056b8b4b7c8cbcc45c))
+* Support multiple audit log method exclusions ([f9decdd](https://github.com/kernel/kernel-python-sdk/commit/f9decdd9d2f617c4483d3a5ddd568f34ac8052f6))
+
+
+### Documentation
+
+* **openapi:** describe unified concurrency limit, deprecate max_pooled_sessions (CUS-275) ([8c85318](https://github.com/kernel/kernel-python-sdk/commit/8c85318da7d5b064b867b1e4d029937eafc08d13))
+
## 0.76.0 (2026-07-09)
Full Changelog: [v0.75.0...v0.76.0](https://github.com/kernel/kernel-python-sdk/compare/v0.75.0...v0.76.0)
diff --git a/pyproject.toml b/pyproject.toml
index 205c9767..d2f34d9d 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -1,6 +1,6 @@
[project]
name = "kernel"
-version = "0.76.0"
+version = "0.78.0"
description = "The official Python library for the kernel API"
dynamic = ["readme"]
license = "Apache-2.0"
diff --git a/src/kernel/_version.py b/src/kernel/_version.py
index 443b322f..e90c0a29 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.76.0" # x-release-please-version
+__version__ = "0.78.0" # x-release-please-version
From ff3a2682af1d946270d1138742c775dc67c1fce6 Mon Sep 17 00:00:00 2001
From: "stainless-app[bot]"
<142633134+stainless-app[bot]@users.noreply.github.com>
Date: Wed, 15 Jul 2026 15:26:44 +0000
Subject: [PATCH 126/366] feat: Default dashboard browser pools to 25% fill
rate
---
.stats.yml | 4 ++--
src/kernel/resources/browser_pools.py | 4 ++--
src/kernel/types/browser_pool_create_params.py | 2 +-
3 files changed, 5 insertions(+), 5 deletions(-)
diff --git a/.stats.yml b/.stats.yml
index 04d533eb..7f088e43 100644
--- a/.stats.yml
+++ b/.stats.yml
@@ -1,4 +1,4 @@
configured_endpoints: 127
-openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel/kernel-1413d61d06059008b4c1e204c783d8b2e7dca9af23f6e3b6d2552493ea2d9853.yml
-openapi_spec_hash: 80aca500caddd300cb03a6daf568fddb
+openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel/kernel-92c776855f978ae03a778fbbac49bf1edc65852530644772c382838b6eab9fe5.yml
+openapi_spec_hash: 4a9fbcb607be408ad646e207fe90687f
config_hash: 77ee715aa17061166f9a02b264a21b8d
diff --git a/src/kernel/resources/browser_pools.py b/src/kernel/resources/browser_pools.py
index 5810f332..a2725fb8 100644
--- a/src/kernel/resources/browser_pools.py
+++ b/src/kernel/resources/browser_pools.py
@@ -101,7 +101,7 @@ def create(
extensions: List of browser extensions to load into the session. Provide each by id or name.
- fill_rate_per_minute: Percentage of the pool to fill per minute. Defaults to 10. The cap is 25 for
+ fill_rate_per_minute: Percentage of the pool to fill per minute. Defaults to 25. The cap is 25 for
most organizations but can be raised per-organization, so only the lower bound
is enforced here.
@@ -679,7 +679,7 @@ async def create(
extensions: List of browser extensions to load into the session. Provide each by id or name.
- fill_rate_per_minute: Percentage of the pool to fill per minute. Defaults to 10. The cap is 25 for
+ fill_rate_per_minute: Percentage of the pool to fill per minute. Defaults to 25. The cap is 25 for
most organizations but can be raised per-organization, so only the lower bound
is enforced here.
diff --git a/src/kernel/types/browser_pool_create_params.py b/src/kernel/types/browser_pool_create_params.py
index 328ac743..cd6573b5 100644
--- a/src/kernel/types/browser_pool_create_params.py
+++ b/src/kernel/types/browser_pool_create_params.py
@@ -37,7 +37,7 @@ class BrowserPoolCreateParams(TypedDict, total=False):
fill_rate_per_minute: int
"""Percentage of the pool to fill per minute.
- Defaults to 10. The cap is 25 for most organizations but can be raised
+ Defaults to 25. The cap is 25 for most organizations but can be raised
per-organization, so only the lower bound is enforced here.
"""
From 5a2b67d2f5aa75e4a7bea5cb6cc7132103215353 Mon Sep 17 00:00:00 2001
From: "stainless-app[bot]"
<142633134+stainless-app[bot]@users.noreply.github.com>
Date: Wed, 15 Jul 2026 15:35:13 +0000
Subject: [PATCH 127/366] release: 0.78.1
---
.release-please-manifest.json | 2 +-
CHANGELOG.md | 8 ++++++++
pyproject.toml | 2 +-
src/kernel/_version.py | 2 +-
4 files changed, 11 insertions(+), 3 deletions(-)
diff --git a/.release-please-manifest.json b/.release-please-manifest.json
index 0d51a367..7cfa47b4 100644
--- a/.release-please-manifest.json
+++ b/.release-please-manifest.json
@@ -1,3 +1,3 @@
{
- ".": "0.78.0"
+ ".": "0.78.1"
}
\ No newline at end of file
diff --git a/CHANGELOG.md b/CHANGELOG.md
index f8ef4df6..7696e5f6 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,13 @@
# Changelog
+## 0.78.1 (2026-07-15)
+
+Full Changelog: [v0.78.0...v0.78.1](https://github.com/kernel/kernel-python-sdk/compare/v0.78.0...v0.78.1)
+
+### Features
+
+* Default dashboard browser pools to 25% fill rate ([6a4848b](https://github.com/kernel/kernel-python-sdk/commit/6a4848bf17e5caafbd1d04639f30e3c9f9904ff9))
+
## 0.78.0 (2026-07-13)
Full Changelog: [v0.76.0...v0.78.0](https://github.com/kernel/kernel-python-sdk/compare/v0.76.0...v0.78.0)
diff --git a/pyproject.toml b/pyproject.toml
index d2f34d9d..25ced223 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -1,6 +1,6 @@
[project]
name = "kernel"
-version = "0.78.0"
+version = "0.78.1"
description = "The official Python library for the kernel API"
dynamic = ["readme"]
license = "Apache-2.0"
diff --git a/src/kernel/_version.py b/src/kernel/_version.py
index e90c0a29..740df447 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.78.0" # x-release-please-version
+__version__ = "0.78.1" # x-release-please-version
From a7f5d9e4909fed534df873d90ac14481b18b7d20 Mon Sep 17 00:00:00 2001
From: "stainless-app[bot]"
<142633134+stainless-app[bot]@users.noreply.github.com>
Date: Wed, 15 Jul 2026 20:54:36 +0000
Subject: [PATCH 128/366] feat: Add telemetry support for browser pools with
BAA enforcement
---
.stats.yml | 4 +-
src/kernel/resources/browser_pools.py | 62 +++++++++++++-
src/kernel/types/browser_pool.py | 7 ++
.../types/browser_pool_acquire_params.py | 50 ++++++++++-
.../types/browser_pool_create_params.py | 49 ++++++++++-
.../types/browser_pool_update_params.py | 50 ++++++++++-
tests/api_resources/test_browser_pools.py | 84 +++++++++++++++++++
7 files changed, 298 insertions(+), 8 deletions(-)
diff --git a/.stats.yml b/.stats.yml
index 7f088e43..3807b2f3 100644
--- a/.stats.yml
+++ b/.stats.yml
@@ -1,4 +1,4 @@
configured_endpoints: 127
-openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel/kernel-92c776855f978ae03a778fbbac49bf1edc65852530644772c382838b6eab9fe5.yml
-openapi_spec_hash: 4a9fbcb607be408ad646e207fe90687f
+openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel/kernel-e04362b2c82b88f4f7fb43209c764fa9fdf37fe98932e142547be43a1e99c50b.yml
+openapi_spec_hash: b16e79bfd6cac36090c815a8184b9e09
config_hash: 77ee715aa17061166f9a02b264a21b8d
diff --git a/src/kernel/resources/browser_pools.py b/src/kernel/resources/browser_pools.py
index a2725fb8..d71fc272 100644
--- a/src/kernel/resources/browser_pools.py
+++ b/src/kernel/resources/browser_pools.py
@@ -2,7 +2,7 @@
from __future__ import annotations
-from typing import Dict, Iterable
+from typing import Dict, Iterable, Optional
import httpx
@@ -72,6 +72,7 @@ def create(
refresh_on_profile_update: bool | Omit = omit,
start_url: str | Omit = omit,
stealth: bool | Omit = omit,
+ telemetry: Optional[browser_pool_create_params.Telemetry] | Omit = omit,
timeout_seconds: int | Omit = omit,
viewport: BrowserViewport | Omit = omit,
# Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
@@ -134,6 +135,13 @@ def create(
stealth: If true, launches the browser in stealth mode to reduce detection by anti-bot
mechanisms. Defaults to false.
+ telemetry: Telemetry configuration applied to browsers warmed into this pool. Set enabled
+ to true to start capture using the default set, or provide browser category
+ settings. If omitted, null, set to an empty object ({}), set to enabled: false
+ without browser category settings, or all four CDP categories are explicitly
+ disabled, no telemetry is configured on the pool. Only applied to newly-warmed
+ browsers.
+
timeout_seconds: Default idle timeout in seconds for browsers acquired from this pool before they
are destroyed. Defaults to 600 seconds. Minimum 10, maximum 259200 (72 hours).
@@ -174,6 +182,7 @@ def create(
"refresh_on_profile_update": refresh_on_profile_update,
"start_url": start_url,
"stealth": stealth,
+ "telemetry": telemetry,
"timeout_seconds": timeout_seconds,
"viewport": viewport,
},
@@ -235,6 +244,7 @@ def update(
size: int | Omit = omit,
start_url: str | Omit = omit,
stealth: bool | Omit = omit,
+ telemetry: Optional[browser_pool_update_params.Telemetry] | Omit = omit,
timeout_seconds: int | Omit = omit,
viewport: BrowserViewport | Omit = omit,
# Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
@@ -306,6 +316,14 @@ def update(
stealth: If provided, replaces whether browsers launch in stealth mode.
+ telemetry: If provided, updates the pool's telemetry configuration. Omit, set to null, or
+ set to an empty object ({}) to leave the existing configuration unchanged. Set
+ enabled to true to enable capture using the default set. Set enabled to false to
+ clear the pool's telemetry. Provide browser category settings for per-category
+ updates, merged onto the pool's current configuration. Only applied to browsers
+ warmed after the update; browsers already in the pool keep their configuration
+ until discarded.
+
timeout_seconds: If provided, replaces the default idle timeout in seconds for browsers acquired
from this pool before they are destroyed. Minimum 10, maximum 259200 (72 hours).
@@ -349,6 +367,7 @@ def update(
"size": size,
"start_url": start_url,
"stealth": stealth,
+ "telemetry": telemetry,
"timeout_seconds": timeout_seconds,
"viewport": viewport,
},
@@ -468,6 +487,7 @@ def acquire(
name: str | Omit = omit,
start_url: str | Omit = omit,
tags: TagsParam | Omit = omit,
+ telemetry: Optional[browser_pool_acquire_params.Telemetry] | 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,
@@ -500,6 +520,15 @@ def acquire(
find and group sessions later. Applies to this lease only and are cleared when
the browser is released back to the pool. Up to 50 pairs.
+ telemetry: Telemetry override for the acquired browser, applied to this lease only. Merges
+ onto the browser's current (pool-inherited) telemetry using the same
+ per-category semantics as PATCH /browsers: provided categories override the
+ current configuration, omitted categories are inherited. Set enabled to true to
+ resolve the config fresh from the default set, or enabled to false to stop
+ capture. When the browser is released back to the pool with reuse, its telemetry
+ is reset to the pool's baseline, so the override does not carry over to the next
+ lease.
+
extra_headers: Send extra headers
extra_query: Add additional query parameters to the request
@@ -518,6 +547,7 @@ def acquire(
"name": name,
"start_url": start_url,
"tags": tags,
+ "telemetry": telemetry,
},
browser_pool_acquire_params.BrowserPoolAcquireParams,
),
@@ -650,6 +680,7 @@ async def create(
refresh_on_profile_update: bool | Omit = omit,
start_url: str | Omit = omit,
stealth: bool | Omit = omit,
+ telemetry: Optional[browser_pool_create_params.Telemetry] | Omit = omit,
timeout_seconds: int | Omit = omit,
viewport: BrowserViewport | Omit = omit,
# Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
@@ -712,6 +743,13 @@ async def create(
stealth: If true, launches the browser in stealth mode to reduce detection by anti-bot
mechanisms. Defaults to false.
+ telemetry: Telemetry configuration applied to browsers warmed into this pool. Set enabled
+ to true to start capture using the default set, or provide browser category
+ settings. If omitted, null, set to an empty object ({}), set to enabled: false
+ without browser category settings, or all four CDP categories are explicitly
+ disabled, no telemetry is configured on the pool. Only applied to newly-warmed
+ browsers.
+
timeout_seconds: Default idle timeout in seconds for browsers acquired from this pool before they
are destroyed. Defaults to 600 seconds. Minimum 10, maximum 259200 (72 hours).
@@ -752,6 +790,7 @@ async def create(
"refresh_on_profile_update": refresh_on_profile_update,
"start_url": start_url,
"stealth": stealth,
+ "telemetry": telemetry,
"timeout_seconds": timeout_seconds,
"viewport": viewport,
},
@@ -813,6 +852,7 @@ async def update(
size: int | Omit = omit,
start_url: str | Omit = omit,
stealth: bool | Omit = omit,
+ telemetry: Optional[browser_pool_update_params.Telemetry] | Omit = omit,
timeout_seconds: int | Omit = omit,
viewport: BrowserViewport | Omit = omit,
# Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
@@ -884,6 +924,14 @@ async def update(
stealth: If provided, replaces whether browsers launch in stealth mode.
+ telemetry: If provided, updates the pool's telemetry configuration. Omit, set to null, or
+ set to an empty object ({}) to leave the existing configuration unchanged. Set
+ enabled to true to enable capture using the default set. Set enabled to false to
+ clear the pool's telemetry. Provide browser category settings for per-category
+ updates, merged onto the pool's current configuration. Only applied to browsers
+ warmed after the update; browsers already in the pool keep their configuration
+ until discarded.
+
timeout_seconds: If provided, replaces the default idle timeout in seconds for browsers acquired
from this pool before they are destroyed. Minimum 10, maximum 259200 (72 hours).
@@ -927,6 +975,7 @@ async def update(
"size": size,
"start_url": start_url,
"stealth": stealth,
+ "telemetry": telemetry,
"timeout_seconds": timeout_seconds,
"viewport": viewport,
},
@@ -1046,6 +1095,7 @@ async def acquire(
name: str | Omit = omit,
start_url: str | Omit = omit,
tags: TagsParam | Omit = omit,
+ telemetry: Optional[browser_pool_acquire_params.Telemetry] | 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,
@@ -1078,6 +1128,15 @@ async def acquire(
find and group sessions later. Applies to this lease only and are cleared when
the browser is released back to the pool. Up to 50 pairs.
+ telemetry: Telemetry override for the acquired browser, applied to this lease only. Merges
+ onto the browser's current (pool-inherited) telemetry using the same
+ per-category semantics as PATCH /browsers: provided categories override the
+ current configuration, omitted categories are inherited. Set enabled to true to
+ resolve the config fresh from the default set, or enabled to false to stop
+ capture. When the browser is released back to the pool with reuse, its telemetry
+ is reset to the pool's baseline, so the override does not carry over to the next
+ lease.
+
extra_headers: Send extra headers
extra_query: Add additional query parameters to the request
@@ -1096,6 +1155,7 @@ async def acquire(
"name": name,
"start_url": start_url,
"tags": tags,
+ "telemetry": telemetry,
},
browser_pool_acquire_params.BrowserPoolAcquireParams,
),
diff --git a/src/kernel/types/browser_pool.py b/src/kernel/types/browser_pool.py
index 4f7764ab..344ffd77 100644
--- a/src/kernel/types/browser_pool.py
+++ b/src/kernel/types/browser_pool.py
@@ -6,6 +6,7 @@
from .._models import BaseModel
from .shared.browser_viewport import BrowserViewport
from .shared.browser_extension import BrowserExtension
+from .browsers.browser_telemetry_config import BrowserTelemetryConfig
__all__ = ["BrowserPool", "BrowserPoolConfig", "BrowserPoolConfigProfile"]
@@ -110,6 +111,12 @@ class BrowserPoolConfig(BaseModel):
mechanisms.
"""
+ telemetry: Optional[BrowserTelemetryConfig] = None
+ """
+ Active telemetry configuration applied to browsers warmed into this pool, if
+ any.
+ """
+
timeout_seconds: Optional[int] = None
"""
Default idle timeout in seconds for browsers acquired from this pool before they
diff --git a/src/kernel/types/browser_pool_acquire_params.py b/src/kernel/types/browser_pool_acquire_params.py
index 80b9be26..70567ee3 100644
--- a/src/kernel/types/browser_pool_acquire_params.py
+++ b/src/kernel/types/browser_pool_acquire_params.py
@@ -2,11 +2,13 @@
from __future__ import annotations
+from typing import Optional
from typing_extensions import TypedDict
from .tags_param import TagsParam
+from .browsers.browser_telemetry_categories_config_param import BrowserTelemetryCategoriesConfigParam
-__all__ = ["BrowserPoolAcquireParams"]
+__all__ = ["BrowserPoolAcquireParams", "Telemetry"]
class BrowserPoolAcquireParams(TypedDict, total=False):
@@ -38,3 +40,49 @@ class BrowserPoolAcquireParams(TypedDict, total=False):
find and group sessions later. Applies to this lease only and are cleared when
the browser is released back to the pool. Up to 50 pairs.
"""
+
+ telemetry: Optional[Telemetry]
+ """Telemetry override for the acquired browser, applied to this lease only.
+
+ Merges onto the browser's current (pool-inherited) telemetry using the same
+ per-category semantics as PATCH /browsers: provided categories override the
+ current configuration, omitted categories are inherited. Set enabled to true to
+ resolve the config fresh from the default set, or enabled to false to stop
+ capture. When the browser is released back to the pool with reuse, its telemetry
+ is reset to the pool's baseline, so the override does not carry over to the next
+ lease.
+ """
+
+
+class Telemetry(TypedDict, total=False):
+ """Telemetry override for the acquired browser, applied to this lease only.
+
+ Merges onto the browser's current (pool-inherited) telemetry using the same per-category semantics as PATCH /browsers: provided categories override the current configuration, omitted categories are inherited. Set enabled to true to resolve the config fresh from the default set, or enabled to false to stop capture. When the browser is released back to the pool with reuse, its telemetry is reset to the pool's baseline, so the override does not carry over to the next lease.
+ """
+
+ browser: BrowserTelemetryCategoriesConfigParam
+ """Per-category capture flags.
+
+ The operational categories (control, connection, system, captcha) are captured
+ whenever telemetry is enabled; set one to enabled=false to opt out. The CDP
+ categories (console, network, page, interaction) and screenshot are off by
+ default; set enabled=true to opt in. On create, provided categories layer onto
+ the default set. On update, provided categories merge onto the session's current
+ config; when no telemetry is active this falls back to the default set (matching
+ create). If browser is omitted or empty, the default set is used. A browser
+ config that disables every category stops capture on update and starts no
+ capture on create.
+ """
+
+ enabled: bool
+ """Request shortcut for browser telemetry capture.
+
+ True enables capture; with no browser category settings it captures the default
+ set (control, connection, system, captcha), and any browser category settings
+ are layered onto that default set. On update, enabled=true resolves the config
+ fresh from the default set plus any provided categories, replacing the session's
+ current selection rather than merging onto it; omit enabled to merge categories
+ onto the current selection instead. False stops capture on update and starts no
+ capture on create. enabled=false cannot be combined with browser category
+ settings.
+ """
diff --git a/src/kernel/types/browser_pool_create_params.py b/src/kernel/types/browser_pool_create_params.py
index cd6573b5..376af6b6 100644
--- a/src/kernel/types/browser_pool_create_params.py
+++ b/src/kernel/types/browser_pool_create_params.py
@@ -2,13 +2,14 @@
from __future__ import annotations
-from typing import Dict, Iterable
+from typing import Dict, Iterable, Optional
from typing_extensions import Required, TypedDict
from .shared_params.browser_viewport import BrowserViewport
from .shared_params.browser_extension import BrowserExtension
+from .browsers.browser_telemetry_categories_config_param import BrowserTelemetryCategoriesConfigParam
-__all__ = ["BrowserPoolCreateParams", "Profile"]
+__all__ = ["BrowserPoolCreateParams", "Profile", "Telemetry"]
class BrowserPoolCreateParams(TypedDict, total=False):
@@ -90,6 +91,16 @@ class BrowserPoolCreateParams(TypedDict, total=False):
mechanisms. Defaults to false.
"""
+ telemetry: Optional[Telemetry]
+ """Telemetry configuration applied to browsers warmed into this pool.
+
+ Set enabled to true to start capture using the default set, or provide browser
+ category settings. If omitted, null, set to an empty object ({}), set to
+ enabled: false without browser category settings, or all four CDP categories are
+ explicitly disabled, no telemetry is configured on the pool. Only applied to
+ newly-warmed browsers.
+ """
+
timeout_seconds: int
"""
Default idle timeout in seconds for browsers acquired from this pool before they
@@ -130,3 +141,37 @@ class Profile(TypedDict, total=False):
Must be 1-255 characters, using letters, numbers, dots, underscores, or hyphens.
"""
+
+
+class Telemetry(TypedDict, total=False):
+ """Telemetry configuration applied to browsers warmed into this pool.
+
+ Set enabled to true to start capture using the default set, or provide browser category settings. If omitted, null, set to an empty object ({}), set to enabled: false without browser category settings, or all four CDP categories are explicitly disabled, no telemetry is configured on the pool. Only applied to newly-warmed browsers.
+ """
+
+ browser: BrowserTelemetryCategoriesConfigParam
+ """Per-category capture flags.
+
+ The operational categories (control, connection, system, captcha) are captured
+ whenever telemetry is enabled; set one to enabled=false to opt out. The CDP
+ categories (console, network, page, interaction) and screenshot are off by
+ default; set enabled=true to opt in. On create, provided categories layer onto
+ the default set. On update, provided categories merge onto the session's current
+ config; when no telemetry is active this falls back to the default set (matching
+ create). If browser is omitted or empty, the default set is used. A browser
+ config that disables every category stops capture on update and starts no
+ capture on create.
+ """
+
+ enabled: bool
+ """Request shortcut for browser telemetry capture.
+
+ True enables capture; with no browser category settings it captures the default
+ set (control, connection, system, captcha), and any browser category settings
+ are layered onto that default set. On update, enabled=true resolves the config
+ fresh from the default set plus any provided categories, replacing the session's
+ current selection rather than merging onto it; omit enabled to merge categories
+ onto the current selection instead. False stops capture on update and starts no
+ capture on create. enabled=false cannot be combined with browser category
+ settings.
+ """
diff --git a/src/kernel/types/browser_pool_update_params.py b/src/kernel/types/browser_pool_update_params.py
index a4c65578..b0956048 100644
--- a/src/kernel/types/browser_pool_update_params.py
+++ b/src/kernel/types/browser_pool_update_params.py
@@ -2,13 +2,14 @@
from __future__ import annotations
-from typing import Dict, Iterable
+from typing import Dict, Iterable, Optional
from typing_extensions import TypedDict
from .shared_params.browser_viewport import BrowserViewport
from .shared_params.browser_extension import BrowserExtension
+from .browsers.browser_telemetry_categories_config_param import BrowserTelemetryCategoriesConfigParam
-__all__ = ["BrowserPoolUpdateParams", "Profile"]
+__all__ = ["BrowserPoolUpdateParams", "Profile", "Telemetry"]
class BrowserPoolUpdateParams(TypedDict, total=False):
@@ -100,6 +101,17 @@ class BrowserPoolUpdateParams(TypedDict, total=False):
stealth: bool
"""If provided, replaces whether browsers launch in stealth mode."""
+ telemetry: Optional[Telemetry]
+ """If provided, updates the pool's telemetry configuration.
+
+ Omit, set to null, or set to an empty object ({}) to leave the existing
+ configuration unchanged. Set enabled to true to enable capture using the default
+ set. Set enabled to false to clear the pool's telemetry. Provide browser
+ category settings for per-category updates, merged onto the pool's current
+ configuration. Only applied to browsers warmed after the update; browsers
+ already in the pool keep their configuration until discarded.
+ """
+
timeout_seconds: int
"""
If provided, replaces the default idle timeout in seconds for browsers acquired
@@ -140,3 +152,37 @@ class Profile(TypedDict, total=False):
Must be 1-255 characters, using letters, numbers, dots, underscores, or hyphens.
"""
+
+
+class Telemetry(TypedDict, total=False):
+ """If provided, updates the pool's telemetry configuration.
+
+ Omit, set to null, or set to an empty object ({}) to leave the existing configuration unchanged. Set enabled to true to enable capture using the default set. Set enabled to false to clear the pool's telemetry. Provide browser category settings for per-category updates, merged onto the pool's current configuration. Only applied to browsers warmed after the update; browsers already in the pool keep their configuration until discarded.
+ """
+
+ browser: BrowserTelemetryCategoriesConfigParam
+ """Per-category capture flags.
+
+ The operational categories (control, connection, system, captcha) are captured
+ whenever telemetry is enabled; set one to enabled=false to opt out. The CDP
+ categories (console, network, page, interaction) and screenshot are off by
+ default; set enabled=true to opt in. On create, provided categories layer onto
+ the default set. On update, provided categories merge onto the session's current
+ config; when no telemetry is active this falls back to the default set (matching
+ create). If browser is omitted or empty, the default set is used. A browser
+ config that disables every category stops capture on update and starts no
+ capture on create.
+ """
+
+ enabled: bool
+ """Request shortcut for browser telemetry capture.
+
+ True enables capture; with no browser category settings it captures the default
+ set (control, connection, system, captcha), and any browser category settings
+ are layered onto that default set. On update, enabled=true resolves the config
+ fresh from the default set plus any provided categories, replacing the session's
+ current selection rather than merging onto it; omit enabled to merge categories
+ onto the current selection instead. False stops capture on update and starts no
+ capture on create. enabled=false cannot be combined with browser category
+ settings.
+ """
diff --git a/tests/api_resources/test_browser_pools.py b/tests/api_resources/test_browser_pools.py
index 6f79fc8b..31657789 100644
--- a/tests/api_resources/test_browser_pools.py
+++ b/tests/api_resources/test_browser_pools.py
@@ -53,6 +53,20 @@ def test_method_create_with_all_params(self, client: Kernel) -> None:
refresh_on_profile_update=True,
start_url="https://example.com",
stealth=True,
+ telemetry={
+ "browser": {
+ "captcha": {"enabled": True},
+ "connection": {"enabled": True},
+ "console": {"enabled": True},
+ "control": {"enabled": True},
+ "interaction": {"enabled": True},
+ "network": {"enabled": True},
+ "page": {"enabled": True},
+ "screenshot": {"enabled": True},
+ "system": {"enabled": True},
+ },
+ "enabled": True,
+ },
timeout_seconds=10,
viewport={
"height": 800,
@@ -164,6 +178,20 @@ def test_method_update_with_all_params(self, client: Kernel) -> None:
size=10,
start_url="https://example.com",
stealth=True,
+ telemetry={
+ "browser": {
+ "captcha": {"enabled": True},
+ "connection": {"enabled": True},
+ "console": {"enabled": True},
+ "control": {"enabled": True},
+ "interaction": {"enabled": True},
+ "network": {"enabled": True},
+ "page": {"enabled": True},
+ "screenshot": {"enabled": True},
+ "system": {"enabled": True},
+ },
+ "enabled": True,
+ },
timeout_seconds=10,
viewport={
"height": 800,
@@ -317,6 +345,20 @@ def test_method_acquire_with_all_params(self, client: Kernel) -> None:
"team": "backend",
"env": "staging",
},
+ telemetry={
+ "browser": {
+ "captcha": {"enabled": True},
+ "connection": {"enabled": True},
+ "console": {"enabled": True},
+ "control": {"enabled": True},
+ "interaction": {"enabled": True},
+ "network": {"enabled": True},
+ "page": {"enabled": True},
+ "screenshot": {"enabled": True},
+ "system": {"enabled": True},
+ },
+ "enabled": True,
+ },
)
assert_matches_type(BrowserPoolAcquireResponse, browser_pool, path=["response"])
@@ -490,6 +532,20 @@ async def test_method_create_with_all_params(self, async_client: AsyncKernel) ->
refresh_on_profile_update=True,
start_url="https://example.com",
stealth=True,
+ telemetry={
+ "browser": {
+ "captcha": {"enabled": True},
+ "connection": {"enabled": True},
+ "console": {"enabled": True},
+ "control": {"enabled": True},
+ "interaction": {"enabled": True},
+ "network": {"enabled": True},
+ "page": {"enabled": True},
+ "screenshot": {"enabled": True},
+ "system": {"enabled": True},
+ },
+ "enabled": True,
+ },
timeout_seconds=10,
viewport={
"height": 800,
@@ -601,6 +657,20 @@ async def test_method_update_with_all_params(self, async_client: AsyncKernel) ->
size=10,
start_url="https://example.com",
stealth=True,
+ telemetry={
+ "browser": {
+ "captcha": {"enabled": True},
+ "connection": {"enabled": True},
+ "console": {"enabled": True},
+ "control": {"enabled": True},
+ "interaction": {"enabled": True},
+ "network": {"enabled": True},
+ "page": {"enabled": True},
+ "screenshot": {"enabled": True},
+ "system": {"enabled": True},
+ },
+ "enabled": True,
+ },
timeout_seconds=10,
viewport={
"height": 800,
@@ -754,6 +824,20 @@ async def test_method_acquire_with_all_params(self, async_client: AsyncKernel) -
"team": "backend",
"env": "staging",
},
+ telemetry={
+ "browser": {
+ "captcha": {"enabled": True},
+ "connection": {"enabled": True},
+ "console": {"enabled": True},
+ "control": {"enabled": True},
+ "interaction": {"enabled": True},
+ "network": {"enabled": True},
+ "page": {"enabled": True},
+ "screenshot": {"enabled": True},
+ "system": {"enabled": True},
+ },
+ "enabled": True,
+ },
)
assert_matches_type(BrowserPoolAcquireResponse, browser_pool, path=["response"])
From 0a77df5f7b7769e9efcf0db2391798050bb16d5c Mon Sep 17 00:00:00 2001
From: "stainless-app[bot]"
<142633134+stainless-app[bot]@users.noreply.github.com>
Date: Wed, 15 Jul 2026 20:55:05 +0000
Subject: [PATCH 129/366] release: 0.79.0
---
.release-please-manifest.json | 2 +-
CHANGELOG.md | 8 ++++++++
pyproject.toml | 2 +-
src/kernel/_version.py | 2 +-
4 files changed, 11 insertions(+), 3 deletions(-)
diff --git a/.release-please-manifest.json b/.release-please-manifest.json
index 7cfa47b4..fe941d15 100644
--- a/.release-please-manifest.json
+++ b/.release-please-manifest.json
@@ -1,3 +1,3 @@
{
- ".": "0.78.1"
+ ".": "0.79.0"
}
\ No newline at end of file
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 7696e5f6..7c25a068 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,13 @@
# Changelog
+## 0.79.0 (2026-07-15)
+
+Full Changelog: [v0.78.1...v0.79.0](https://github.com/kernel/kernel-python-sdk/compare/v0.78.1...v0.79.0)
+
+### Features
+
+* Add telemetry support for browser pools with BAA enforcement ([a7f5d9e](https://github.com/kernel/kernel-python-sdk/commit/a7f5d9e4909fed534df873d90ac14481b18b7d20))
+
## 0.78.1 (2026-07-15)
Full Changelog: [v0.78.0...v0.78.1](https://github.com/kernel/kernel-python-sdk/compare/v0.78.0...v0.78.1)
diff --git a/pyproject.toml b/pyproject.toml
index 25ced223..7727ba08 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -1,6 +1,6 @@
[project]
name = "kernel"
-version = "0.78.1"
+version = "0.79.0"
description = "The official Python library for the kernel API"
dynamic = ["readme"]
license = "Apache-2.0"
diff --git a/src/kernel/_version.py b/src/kernel/_version.py
index 740df447..73b9f569 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.78.1" # x-release-please-version
+__version__ = "0.79.0" # x-release-please-version
From 431fc3ce3d3a2e7fcbf489545a25a7821aec54d5 Mon Sep 17 00:00:00 2001
From: "stainless-app[bot]"
<142633134+stainless-app[bot]@users.noreply.github.com>
Date: Thu, 16 Jul 2026 19:15:31 +0000
Subject: [PATCH 130/366] feat: Add example to ProxyCheckRequest.url so API
reference sample shows it
---
.stats.yml | 4 ++--
tests/api_resources/test_proxies.py | 4 ++--
2 files changed, 4 insertions(+), 4 deletions(-)
diff --git a/.stats.yml b/.stats.yml
index 3807b2f3..ef859acd 100644
--- a/.stats.yml
+++ b/.stats.yml
@@ -1,4 +1,4 @@
configured_endpoints: 127
-openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel/kernel-e04362b2c82b88f4f7fb43209c764fa9fdf37fe98932e142547be43a1e99c50b.yml
-openapi_spec_hash: b16e79bfd6cac36090c815a8184b9e09
+openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel/kernel-0c99f133dc7e81a8e74e829299f4a7bfdd9f4fadc40e69a9a10c81ab28318f7a.yml
+openapi_spec_hash: 6e7e04300dc9554b2d1cf4284ee46504
config_hash: 77ee715aa17061166f9a02b264a21b8d
diff --git a/tests/api_resources/test_proxies.py b/tests/api_resources/test_proxies.py
index cbe6e355..bdc48364 100644
--- a/tests/api_resources/test_proxies.py
+++ b/tests/api_resources/test_proxies.py
@@ -252,7 +252,7 @@ def test_method_check(self, client: Kernel) -> None:
def test_method_check_with_all_params(self, client: Kernel) -> None:
proxy = client.proxies.check(
id="id",
- url="url",
+ url="https://example.com",
)
assert_matches_type(ProxyCheckResponse, proxy, path=["response"])
@@ -524,7 +524,7 @@ async def test_method_check(self, async_client: AsyncKernel) -> None:
async def test_method_check_with_all_params(self, async_client: AsyncKernel) -> None:
proxy = await async_client.proxies.check(
id="id",
- url="url",
+ url="https://example.com",
)
assert_matches_type(ProxyCheckResponse, proxy, path=["response"])
From 26af55d029f553cc5c1f73aeb035a662e0dde2db Mon Sep 17 00:00:00 2001
From: "stainless-app[bot]"
<142633134+stainless-app[bot]@users.noreply.github.com>
Date: Fri, 17 Jul 2026 00:05:18 +0000
Subject: [PATCH 131/366] feat(managed-auth): add TS CUA worker contract
(KERNEL-1456)
---
.stats.yml | 4 +-
src/kernel/resources/auth/connections.py | 16 +++++
.../types/auth/connection_follow_response.py | 58 ++++++++++++++++
.../types/auth/connection_submit_params.py | 6 ++
src/kernel/types/auth/managed_auth.py | 67 ++++++++++++++++++-
tests/api_resources/auth/test_connections.py | 10 +++
6 files changed, 158 insertions(+), 3 deletions(-)
diff --git a/.stats.yml b/.stats.yml
index ef859acd..9e326663 100644
--- a/.stats.yml
+++ b/.stats.yml
@@ -1,4 +1,4 @@
configured_endpoints: 127
-openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel/kernel-0c99f133dc7e81a8e74e829299f4a7bfdd9f4fadc40e69a9a10c81ab28318f7a.yml
-openapi_spec_hash: 6e7e04300dc9554b2d1cf4284ee46504
+openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel/kernel-5b9eb5cdadbc7a5f404f5c2cec40f726e5fece54bf8e2dd29ab5c7700eb44f0e.yml
+openapi_spec_hash: 63dd0798a4c0ed6477af50fff397f5dc
config_hash: 77ee715aa17061166f9a02b264a21b8d
diff --git a/src/kernel/resources/auth/connections.py b/src/kernel/resources/auth/connections.py
index 30f8dca8..05de4f04 100644
--- a/src/kernel/resources/auth/connections.py
+++ b/src/kernel/resources/auth/connections.py
@@ -505,8 +505,10 @@ def submit(
self,
id: str,
*,
+ field_values: Dict[str, str] | Omit = omit,
fields: Dict[str, str] | Omit = omit,
mfa_option_id: str | Omit = omit,
+ selected_choice_id: str | Omit = omit,
sign_in_option_id: str | Omit = omit,
sso_button_selector: str | Omit = omit,
sso_provider: str | Omit = omit,
@@ -523,10 +525,14 @@ def submit(
progress and get results.
Args:
+ field_values: Canonical map of field ID to submitted value.
+
fields: Map of field name to value
mfa_option_id: The MFA method type to select (when mfa_options were returned)
+ selected_choice_id: Canonical choice ID selected by the user.
+
sign_in_option_id: The sign-in option ID to select (when sign_in_options were returned)
sso_button_selector: XPath selector for the SSO button to click (ODA). Use sso_provider instead for
@@ -549,8 +555,10 @@ def submit(
path_template("/auth/connections/{id}/submit", id=id),
body=maybe_transform(
{
+ "field_values": field_values,
"fields": fields,
"mfa_option_id": mfa_option_id,
+ "selected_choice_id": selected_choice_id,
"sign_in_option_id": sign_in_option_id,
"sso_button_selector": sso_button_selector,
"sso_provider": sso_provider,
@@ -1088,8 +1096,10 @@ async def submit(
self,
id: str,
*,
+ field_values: Dict[str, str] | Omit = omit,
fields: Dict[str, str] | Omit = omit,
mfa_option_id: str | Omit = omit,
+ selected_choice_id: str | Omit = omit,
sign_in_option_id: str | Omit = omit,
sso_button_selector: str | Omit = omit,
sso_provider: str | Omit = omit,
@@ -1106,10 +1116,14 @@ async def submit(
progress and get results.
Args:
+ field_values: Canonical map of field ID to submitted value.
+
fields: Map of field name to value
mfa_option_id: The MFA method type to select (when mfa_options were returned)
+ selected_choice_id: Canonical choice ID selected by the user.
+
sign_in_option_id: The sign-in option ID to select (when sign_in_options were returned)
sso_button_selector: XPath selector for the SSO button to click (ODA). Use sso_provider instead for
@@ -1132,8 +1146,10 @@ async def submit(
path_template("/auth/connections/{id}/submit", id=id),
body=await async_maybe_transform(
{
+ "field_values": field_values,
"fields": fields,
"mfa_option_id": mfa_option_id,
+ "selected_choice_id": selected_choice_id,
"sign_in_option_id": sign_in_option_id,
"sso_button_selector": sso_button_selector,
"sso_provider": sso_provider,
diff --git a/src/kernel/types/auth/connection_follow_response.py b/src/kernel/types/auth/connection_follow_response.py
index 1cd1a141..7d6dfc63 100644
--- a/src/kernel/types/auth/connection_follow_response.py
+++ b/src/kernel/types/auth/connection_follow_response.py
@@ -12,13 +12,36 @@
__all__ = [
"ConnectionFollowResponse",
"ManagedAuthStateEvent",
+ "ManagedAuthStateEventChoice",
"ManagedAuthStateEventDiscoveredField",
+ "ManagedAuthStateEventField",
"ManagedAuthStateEventMfaOption",
"ManagedAuthStateEventPendingSSOButton",
"ManagedAuthStateEventSignInOption",
]
+class ManagedAuthStateEventChoice(BaseModel):
+ """Canonical auth-flow choice awaiting user selection."""
+
+ id: str
+ """Stable choice identifier for canonical submit."""
+
+ label: str
+ """Human-readable choice label."""
+
+ type: Literal[
+ "mfa_method", "sso_provider", "sign_in_method", "auth_method", "identifier_method", "account", "other"
+ ]
+ """Choice type."""
+
+ description: Optional[str] = None
+ """Additional context for the choice."""
+
+ observed_selector: Optional[str] = None
+ """Selector for the visible choice, when available."""
+
+
class ManagedAuthStateEventDiscoveredField(BaseModel):
"""A discovered form field"""
@@ -53,6 +76,28 @@ class ManagedAuthStateEventDiscoveredField(BaseModel):
"""Whether field is required"""
+class ManagedAuthStateEventField(BaseModel):
+ """Canonical field awaiting user input."""
+
+ id: str
+ """Stable field identifier for canonical submit."""
+
+ ref: str
+ """Credential reference name to store the submitted value under."""
+
+ type: Literal["identifier", "password", "code", "totp_code", "totp_secret", "text"]
+ """Managed-auth field type."""
+
+ label: Optional[str] = None
+ """Human-readable label shown to the user."""
+
+ observed_selector: Optional[str] = None
+ """Selector for the visible field, when available."""
+
+ required: Optional[bool] = None
+ """Whether this field is required."""
+
+
class ManagedAuthStateEventMfaOption(BaseModel):
"""An MFA method option for verification"""
@@ -118,6 +163,13 @@ class ManagedAuthStateEvent(BaseModel):
timestamp: datetime
"""Time the state was reported."""
+ choices: Optional[List[ManagedAuthStateEventChoice]] = None
+ """Canonical choices awaiting selection.
+
+ Prefer this over pending_sso_buttons, mfa_options, and sign_in_options when
+ present.
+ """
+
discovered_fields: Optional[List[ManagedAuthStateEventDiscoveredField]] = None
"""
Fields awaiting input (present when flow_step=AWAITING_INPUT; may also be
@@ -136,6 +188,12 @@ class ManagedAuthStateEvent(BaseModel):
flow_step=AWAITING_EXTERNAL_ACTION).
"""
+ fields: Optional[List[ManagedAuthStateEventField]] = None
+ """Canonical fields awaiting input.
+
+ Prefer this over discovered_fields when present.
+ """
+
flow_type: Optional[Literal["LOGIN", "REAUTH"]] = None
"""Type of the current flow."""
diff --git a/src/kernel/types/auth/connection_submit_params.py b/src/kernel/types/auth/connection_submit_params.py
index f785b856..19861ed8 100644
--- a/src/kernel/types/auth/connection_submit_params.py
+++ b/src/kernel/types/auth/connection_submit_params.py
@@ -9,12 +9,18 @@
class ConnectionSubmitParams(TypedDict, total=False):
+ field_values: Dict[str, str]
+ """Canonical map of field ID to submitted value."""
+
fields: Dict[str, str]
"""Map of field name to value"""
mfa_option_id: str
"""The MFA method type to select (when mfa_options were returned)"""
+ selected_choice_id: str
+ """Canonical choice ID selected by the user."""
+
sign_in_option_id: str
"""The sign-in option ID to select (when sign_in_options were returned)"""
diff --git a/src/kernel/types/auth/managed_auth.py b/src/kernel/types/auth/managed_auth.py
index 3addc5e0..6dfb8401 100644
--- a/src/kernel/types/auth/managed_auth.py
+++ b/src/kernel/types/auth/managed_auth.py
@@ -6,7 +6,37 @@
from ..._models import BaseModel
-__all__ = ["ManagedAuth", "Credential", "DiscoveredField", "MfaOption", "PendingSSOButton", "SignInOption"]
+__all__ = [
+ "ManagedAuth",
+ "Choice",
+ "Credential",
+ "DiscoveredField",
+ "Field",
+ "MfaOption",
+ "PendingSSOButton",
+ "SignInOption",
+]
+
+
+class Choice(BaseModel):
+ """Canonical auth-flow choice awaiting user selection."""
+
+ id: str
+ """Stable choice identifier for canonical submit."""
+
+ label: str
+ """Human-readable choice label."""
+
+ type: Literal[
+ "mfa_method", "sso_provider", "sign_in_method", "auth_method", "identifier_method", "account", "other"
+ ]
+ """Choice type."""
+
+ description: Optional[str] = None
+ """Additional context for the choice."""
+
+ observed_selector: Optional[str] = None
+ """Selector for the visible choice, when available."""
class Credential(BaseModel):
@@ -65,6 +95,28 @@ class DiscoveredField(BaseModel):
"""Whether field is required"""
+class Field(BaseModel):
+ """Canonical field awaiting user input."""
+
+ id: str
+ """Stable field identifier for canonical submit."""
+
+ ref: str
+ """Credential reference name to store the submitted value under."""
+
+ type: Literal["identifier", "password", "code", "totp_code", "totp_secret", "text"]
+ """Managed-auth field type."""
+
+ label: Optional[str] = None
+ """Human-readable label shown to the user."""
+
+ observed_selector: Optional[str] = None
+ """Selector for the visible field, when available."""
+
+ required: Optional[bool] = None
+ """Whether this field is required."""
+
+
class MfaOption(BaseModel):
"""An MFA method option for verification"""
@@ -243,6 +295,13 @@ class ManagedAuth(BaseModel):
automatically
"""
+ choices: Optional[List[Choice]] = None
+ """Canonical choices awaiting selection.
+
+ Prefer this over pending_sso_buttons, mfa_options, and sign_in_options when
+ present.
+ """
+
credential: Optional[Credential] = None
"""Reference to credentials for the auth connection. Use one of:
@@ -269,6 +328,12 @@ class ManagedAuth(BaseModel):
flow_step=awaiting_external_action)
"""
+ fields: Optional[List[Field]] = None
+ """Canonical fields awaiting input.
+
+ Prefer this over discovered_fields when present.
+ """
+
flow_expires_at: Optional[datetime] = None
"""When the current flow expires (null when no flow in progress).
diff --git a/tests/api_resources/auth/test_connections.py b/tests/api_resources/auth/test_connections.py
index b92cd790..8bdbc9ad 100644
--- a/tests/api_resources/auth/test_connections.py
+++ b/tests/api_resources/auth/test_connections.py
@@ -386,11 +386,16 @@ def test_method_submit(self, client: Kernel) -> None:
def test_method_submit_with_all_params(self, client: Kernel) -> None:
connection = client.auth.connections.submit(
id="id",
+ field_values={
+ "field_email": "user@example.com",
+ "field_password": "secret",
+ },
fields={
"email": "user@example.com",
"password": "secret",
},
mfa_option_id="sms",
+ selected_choice_id="google",
sign_in_option_id="work-account",
sso_button_selector="xpath=//button[contains(text(), 'Continue with Google')]",
sso_provider="google",
@@ -853,11 +858,16 @@ async def test_method_submit(self, async_client: AsyncKernel) -> None:
async def test_method_submit_with_all_params(self, async_client: AsyncKernel) -> None:
connection = await async_client.auth.connections.submit(
id="id",
+ field_values={
+ "field_email": "user@example.com",
+ "field_password": "secret",
+ },
fields={
"email": "user@example.com",
"password": "secret",
},
mfa_option_id="sms",
+ selected_choice_id="google",
sign_in_option_id="work-account",
sso_button_selector="xpath=//button[contains(text(), 'Continue with Google')]",
sso_provider="google",
From d6cb24aecddc3ca77c57bd7db236d435070400bd Mon Sep 17 00:00:00 2001
From: "stainless-app[bot]"
<142633134+stainless-app[bot]@users.noreply.github.com>
Date: Sat, 18 Jul 2026 02:08:25 +0000
Subject: [PATCH 132/366] feat(stlc): configurable CI runner and
private-production-repo support in workflow templates
---
.github/workflows/ci.yml | 6 +++---
1 file changed, 3 insertions(+), 3 deletions(-)
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index ae545691..1066505c 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -18,7 +18,7 @@ jobs:
lint:
timeout-minutes: 10
name: lint
- runs-on: ${{ github.repository == 'stainless-sdks/kernel-python' && 'depot-ubuntu-24.04' || 'ubuntu-latest' }}
+ runs-on: ${{ startsWith(github.repository, 'stainless-sdks/') && 'depot-ubuntu-24.04' || 'ubuntu-latest' }}
if: (github.event_name == 'push' || github.event.pull_request.head.repo.fork) && (github.event_name != 'push' || github.event.head_commit.message != 'codegen metadata')
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
@@ -44,7 +44,7 @@ jobs:
permissions:
contents: read
id-token: write
- runs-on: ${{ github.repository == 'stainless-sdks/kernel-python' && 'depot-ubuntu-24.04' || 'ubuntu-latest' }}
+ runs-on: ${{ startsWith(github.repository, 'stainless-sdks/') && 'depot-ubuntu-24.04' || 'ubuntu-latest' }}
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
@@ -84,7 +84,7 @@ jobs:
test:
timeout-minutes: 10
name: test
- runs-on: ${{ github.repository == 'stainless-sdks/kernel-python' && 'depot-ubuntu-24.04' || 'ubuntu-latest' }}
+ runs-on: ${{ startsWith(github.repository, 'stainless-sdks/') && 'depot-ubuntu-24.04' || 'ubuntu-latest' }}
if: github.event_name == 'push' || github.event.pull_request.head.repo.fork
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
From 095909509ab72f34c04697ff61bfc7ed9484414c Mon Sep 17 00:00:00 2001
From: "stainless-app[bot]"
<142633134+stainless-app[bot]@users.noreply.github.com>
Date: Sat, 18 Jul 2026 06:13:44 +0000
Subject: [PATCH 133/366] codegen metadata
---
.stats.yml | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/.stats.yml b/.stats.yml
index 9e326663..d995362c 100644
--- a/.stats.yml
+++ b/.stats.yml
@@ -1,4 +1,4 @@
configured_endpoints: 127
-openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel/kernel-5b9eb5cdadbc7a5f404f5c2cec40f726e5fece54bf8e2dd29ab5c7700eb44f0e.yml
-openapi_spec_hash: 63dd0798a4c0ed6477af50fff397f5dc
+openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel/kernel-a7f011596b06b94e4be1999585d4675992a5c08e13bc3943764971b78412f020.yml
+openapi_spec_hash: 6c6b46659fd5220d57d5ef806986154c
config_hash: 77ee715aa17061166f9a02b264a21b8d
From 67fd963cb8082762aeedc42de7bf9c99371cbb01 Mon Sep 17 00:00:00 2001
From: "stainless-app[bot]"
<142633134+stainless-app[bot]@users.noreply.github.com>
Date: Sun, 19 Jul 2026 19:16:35 +0000
Subject: [PATCH 134/366] feat: Expose browser sessions on auth timeline events
---
.stats.yml | 4 ++--
src/kernel/types/auth/managed_auth_timeline_event.py | 3 +++
2 files changed, 5 insertions(+), 2 deletions(-)
diff --git a/.stats.yml b/.stats.yml
index d995362c..c189c7a2 100644
--- a/.stats.yml
+++ b/.stats.yml
@@ -1,4 +1,4 @@
configured_endpoints: 127
-openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel/kernel-a7f011596b06b94e4be1999585d4675992a5c08e13bc3943764971b78412f020.yml
-openapi_spec_hash: 6c6b46659fd5220d57d5ef806986154c
+openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel/kernel-1dffe057847b596dd311ce4b6e0e8036126dce3e97d390636c9c8e6d2f4823e1.yml
+openapi_spec_hash: c06336c199ee64c222ba3122deae9463
config_hash: 77ee715aa17061166f9a02b264a21b8d
diff --git a/src/kernel/types/auth/managed_auth_timeline_event.py b/src/kernel/types/auth/managed_auth_timeline_event.py
index 1f79a526..f83734c5 100644
--- a/src/kernel/types/auth/managed_auth_timeline_event.py
+++ b/src/kernel/types/auth/managed_auth_timeline_event.py
@@ -35,6 +35,9 @@ class ManagedAuthTimelineEvent(BaseModel):
session-validity check.
"""
+ browser_session_id: Optional[str] = None
+ """Browser session that produced the event, if one was created."""
+
error_code: Optional[str] = None
"""Machine-readable error code. Present when a login/reauth event failed."""
From 0a547887a81d8f4d306b9b6253923841a68f4a8c Mon Sep 17 00:00:00 2001
From: "stainless-app[bot]"
<142633134+stainless-app[bot]@users.noreply.github.com>
Date: Sun, 19 Jul 2026 19:17:05 +0000
Subject: [PATCH 135/366] release: 0.80.0
---
.release-please-manifest.json | 2 +-
CHANGELOG.md | 11 +++++++++++
pyproject.toml | 2 +-
src/kernel/_version.py | 2 +-
4 files changed, 14 insertions(+), 3 deletions(-)
diff --git a/.release-please-manifest.json b/.release-please-manifest.json
index fe941d15..c3e137b1 100644
--- a/.release-please-manifest.json
+++ b/.release-please-manifest.json
@@ -1,3 +1,3 @@
{
- ".": "0.79.0"
+ ".": "0.80.0"
}
\ No newline at end of file
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 7c25a068..51c9734b 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,16 @@
# Changelog
+## 0.80.0 (2026-07-19)
+
+Full Changelog: [v0.79.0...v0.80.0](https://github.com/kernel/kernel-python-sdk/compare/v0.79.0...v0.80.0)
+
+### Features
+
+* Add example to ProxyCheckRequest.url so API reference sample shows it ([2a288ca](https://github.com/kernel/kernel-python-sdk/commit/2a288ca546da7760cdc16e566260fd440ae36fb7))
+* Expose browser sessions on auth timeline events ([687b401](https://github.com/kernel/kernel-python-sdk/commit/687b401117d37d6db8e818cd08ee3983671a23f6))
+* **managed-auth:** add TS CUA worker contract (KERNEL-1456) ([de2fc59](https://github.com/kernel/kernel-python-sdk/commit/de2fc590e00152328ac6a4f5165da0fb727644bb))
+* **stlc:** configurable CI runner and private-production-repo support in workflow templates ([79f96e4](https://github.com/kernel/kernel-python-sdk/commit/79f96e4e8d8f0e04e21c37010e1faa765f17afb6))
+
## 0.79.0 (2026-07-15)
Full Changelog: [v0.78.1...v0.79.0](https://github.com/kernel/kernel-python-sdk/compare/v0.78.1...v0.79.0)
diff --git a/pyproject.toml b/pyproject.toml
index 7727ba08..e6a19285 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -1,6 +1,6 @@
[project]
name = "kernel"
-version = "0.79.0"
+version = "0.80.0"
description = "The official Python library for the kernel API"
dynamic = ["readme"]
license = "Apache-2.0"
diff --git a/src/kernel/_version.py b/src/kernel/_version.py
index 73b9f569..222542c7 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.79.0" # x-release-please-version
+__version__ = "0.80.0" # x-release-please-version
From 18aa8c925b3f7699c30a6202685447432df2da6c Mon Sep 17 00:00:00 2001
From: yummybomb <19238148+yummybomb@users.noreply.github.com>
Date: Mon, 20 Jul 2026 20:59:38 +0000
Subject: [PATCH 136/366] feat: add complete audit log downloads
---
src/kernel/__init__.py | 8 ++
src/kernel/lib/audit_log_download.py | 205 +++++++++++++++++++++++++++
src/kernel/resources/audit_logs.py | 106 +++++++++++++-
tests/test_audit_log_download.py | 136 ++++++++++++++++++
4 files changed, 454 insertions(+), 1 deletion(-)
create mode 100644 src/kernel/lib/audit_log_download.py
create mode 100644 tests/test_audit_log_download.py
diff --git a/src/kernel/__init__.py b/src/kernel/__init__.py
index 333d7018..af9462fb 100644
--- a/src/kernel/__init__.py
+++ b/src/kernel/__init__.py
@@ -51,6 +51,11 @@
app_registry,
export_registry,
)
+from .lib.audit_log_download import (
+ AuditLogDownloadError,
+ AuditLogDownloadResult,
+ AuditLogDownloadProgress,
+)
__all__ = [
"types",
@@ -105,6 +110,9 @@
"App",
"app_registry",
"export_registry",
+ "AuditLogDownloadError",
+ "AuditLogDownloadProgress",
+ "AuditLogDownloadResult",
]
if not _t.TYPE_CHECKING:
diff --git a/src/kernel/lib/audit_log_download.py b/src/kernel/lib/audit_log_download.py
new file mode 100644
index 00000000..7f739c8e
--- /dev/null
+++ b/src/kernel/lib/audit_log_download.py
@@ -0,0 +1,205 @@
+from __future__ import annotations
+
+import time
+import hashlib
+from typing import BinaryIO, Callable, Optional, Protocol, Awaitable
+from dataclasses import dataclass
+
+import anyio
+import httpx
+
+from .._exceptions import KernelError, APIStatusError
+
+_DOWNLOAD_ATTEMPTS = 7
+_MAX_RETRY_DELAY = 8.0
+
+
+class AuditLogDownloadError(KernelError):
+ pass
+
+
+@dataclass(frozen=True)
+class AuditLogDownloadResult:
+ bytes_written: int
+ chunks: int
+ rows: int
+
+
+@dataclass(frozen=True)
+class AuditLogDownloadProgress(AuditLogDownloadResult):
+ chunk_rows: int
+
+
+class _SyncChunkResponse(Protocol):
+ @property
+ def headers(self) -> httpx.Headers: ...
+
+ def read(self) -> bytes: ...
+
+ def close(self) -> None: ...
+
+
+class _AsyncChunkResponse(Protocol):
+ @property
+ def headers(self) -> httpx.Headers: ...
+
+ async def read(self) -> bytes: ...
+
+ async def close(self) -> None: ...
+
+
+SyncFetchChunk = Callable[[Optional[str]], _SyncChunkResponse]
+AsyncFetchChunk = Callable[[Optional[str]], Awaitable[_AsyncChunkResponse]]
+ProgressCallback = Callable[[AuditLogDownloadProgress], None]
+
+
+def download_audit_logs(
+ fetch_chunk: SyncFetchChunk,
+ destination: BinaryIO,
+ *,
+ on_progress: ProgressCallback | None = None,
+) -> AuditLogDownloadResult:
+ if not callable(getattr(destination, "write", None)):
+ raise TypeError("audit log download destination must provide write()")
+
+ cursor: str | None = None
+ result = AuditLogDownloadResult(bytes_written=0, chunks=0, rows=0)
+ while True:
+ body, headers = _fetch_verified_chunk(fetch_chunk, cursor)
+ chunk_rows, next_cursor, has_more = _parse_chunk_headers(headers, cursor)
+ _write_chunk(destination, body)
+
+ cursor = next_cursor
+ result = AuditLogDownloadResult(
+ bytes_written=result.bytes_written + len(body),
+ chunks=result.chunks + 1,
+ rows=result.rows + chunk_rows,
+ )
+ if on_progress is not None:
+ on_progress(
+ AuditLogDownloadProgress(
+ bytes_written=result.bytes_written,
+ chunks=result.chunks,
+ rows=result.rows,
+ chunk_rows=chunk_rows,
+ )
+ )
+ if not has_more:
+ return result
+
+
+async def async_download_audit_logs(
+ fetch_chunk: AsyncFetchChunk,
+ destination: BinaryIO,
+ *,
+ on_progress: ProgressCallback | None = None,
+) -> AuditLogDownloadResult:
+ if not callable(getattr(destination, "write", None)):
+ raise TypeError("audit log download destination must provide write()")
+
+ cursor: str | None = None
+ result = AuditLogDownloadResult(bytes_written=0, chunks=0, rows=0)
+ while True:
+ body, headers = await _async_fetch_verified_chunk(fetch_chunk, cursor)
+ chunk_rows, next_cursor, has_more = _parse_chunk_headers(headers, cursor)
+ _write_chunk(destination, body)
+
+ cursor = next_cursor
+ result = AuditLogDownloadResult(
+ bytes_written=result.bytes_written + len(body),
+ chunks=result.chunks + 1,
+ rows=result.rows + chunk_rows,
+ )
+ if on_progress is not None:
+ on_progress(
+ AuditLogDownloadProgress(
+ bytes_written=result.bytes_written,
+ chunks=result.chunks,
+ rows=result.rows,
+ chunk_rows=chunk_rows,
+ )
+ )
+ if not has_more:
+ return result
+
+
+def _fetch_verified_chunk(fetch_chunk: SyncFetchChunk, cursor: str | None) -> tuple[bytes, httpx.Headers]:
+ for attempt in range(1, _DOWNLOAD_ATTEMPTS + 1):
+ try:
+ response = fetch_chunk(cursor)
+ try:
+ body = response.read()
+ headers = httpx.Headers(response.headers)
+ finally:
+ response.close()
+ _verify_checksum(body, headers)
+ return body, headers
+ except Exception as error:
+ if attempt == _DOWNLOAD_ATTEMPTS or not _is_retryable(error):
+ raise
+ time.sleep(min(2 ** (attempt - 1), _MAX_RETRY_DELAY))
+ raise AssertionError("unreachable")
+
+
+async def _async_fetch_verified_chunk(fetch_chunk: AsyncFetchChunk, cursor: str | None) -> tuple[bytes, httpx.Headers]:
+ for attempt in range(1, _DOWNLOAD_ATTEMPTS + 1):
+ try:
+ response = await fetch_chunk(cursor)
+ try:
+ body = await response.read()
+ headers = httpx.Headers(response.headers)
+ finally:
+ await response.close()
+ _verify_checksum(body, headers)
+ return body, headers
+ except Exception as error:
+ if attempt == _DOWNLOAD_ATTEMPTS or not _is_retryable(error):
+ raise
+ await anyio.sleep(min(2 ** (attempt - 1), _MAX_RETRY_DELAY))
+ raise AssertionError("unreachable")
+
+
+def _verify_checksum(body: bytes, headers: httpx.Headers) -> None:
+ expected = headers.get("x-content-sha256")
+ if not expected:
+ raise AuditLogDownloadError("response missing X-Content-Sha256 header")
+ actual = hashlib.sha256(body).hexdigest()
+ if actual != expected:
+ raise AuditLogDownloadError(f"audit log chunk checksum mismatch (got {actual}, want {expected})")
+
+
+def _parse_chunk_headers(headers: httpx.Headers, current_cursor: str | None) -> tuple[int, str | None, bool]:
+ has_more_value = headers.get("x-has-more")
+ if has_more_value not in {"true", "false"}:
+ raise AuditLogDownloadError("response missing or invalid X-Has-More header")
+ has_more = has_more_value == "true"
+
+ row_count = headers.get("x-row-count")
+ try:
+ rows = int(row_count) if row_count is not None else -1
+ except ValueError as error:
+ raise AuditLogDownloadError("response missing or invalid X-Row-Count header") from error
+ if rows < 0:
+ raise AuditLogDownloadError("response missing or invalid X-Row-Count header")
+
+ next_cursor = headers.get("x-next-cursor") or None
+ if has_more and (not next_cursor or next_cursor == current_cursor):
+ raise AuditLogDownloadError("response has invalid X-Next-Cursor header")
+ if not has_more and next_cursor:
+ raise AuditLogDownloadError("response returned a cursor after the final chunk")
+ return rows, next_cursor, has_more
+
+
+def _write_chunk(destination: BinaryIO, body: bytes) -> None:
+ remaining = memoryview(body)
+ while remaining:
+ written = destination.write(remaining)
+ if written <= 0 or written > len(remaining):
+ raise AuditLogDownloadError("audit log download destination performed a short write")
+ remaining = remaining[written:]
+
+
+def _is_retryable(error: Exception) -> bool:
+ if isinstance(error, APIStatusError):
+ return error.status_code == 429 or error.status_code >= 500
+ return True
diff --git a/src/kernel/resources/audit_logs.py b/src/kernel/resources/audit_logs.py
index 2970e153..5b50ad59 100644
--- a/src/kernel/resources/audit_logs.py
+++ b/src/kernel/resources/audit_logs.py
@@ -2,7 +2,7 @@
from __future__ import annotations
-from typing import Union
+from typing import Union, BinaryIO
from datetime import datetime
from typing_extensions import Literal
@@ -30,6 +30,12 @@
from ..pagination import SyncPageTokenPagination, AsyncPageTokenPagination
from .._base_client import AsyncPaginator, make_request_options
from ..types.audit_log_entry import AuditLogEntry
+from ..lib.audit_log_download import (
+ ProgressCallback,
+ AuditLogDownloadResult,
+ download_audit_logs,
+ async_download_audit_logs,
+)
__all__ = ["AuditLogsResource", "AsyncAuditLogsResource"]
@@ -222,6 +228,55 @@ def export_chunk(
cast_to=BinaryAPIResponse,
)
+ def download(
+ self,
+ *,
+ to: BinaryIO,
+ end: Union[str, datetime],
+ start: Union[str, datetime],
+ auth_strategy: str | Omit = omit,
+ exclude_method: SequenceNotStr[str] | Omit = omit,
+ format: Literal["jsonl", "jsonl.gz"] | Omit = omit,
+ limit: int | Omit = omit,
+ method: str | Omit = omit,
+ search: str | Omit = omit,
+ search_user_id: SequenceNotStr[str] | Omit = omit,
+ service: str | Omit = omit,
+ on_progress: ProgressCallback | None = None,
+ extra_headers: Headers | None = None,
+ extra_query: Query | None = None,
+ extra_body: Body | None = None,
+ timeout: float | httpx.Timeout | None | NotGiven = not_given,
+ ) -> AuditLogDownloadResult:
+ """Download a complete audit log export to a writable binary destination.
+
+ The SDK verifies every chunk and retries transient transfer failures. It
+ does not close the destination.
+ """
+
+ resource = self._client.with_options(max_retries=0).audit_logs
+
+ def fetch_chunk(cursor: str | None) -> BinaryAPIResponse:
+ return resource.export_chunk(
+ end=end,
+ start=start,
+ auth_strategy=auth_strategy,
+ cursor=cursor if cursor is not None else omit,
+ exclude_method=exclude_method,
+ format=format,
+ limit=limit,
+ method=method,
+ search=search,
+ search_user_id=search_user_id,
+ service=service,
+ extra_headers=extra_headers,
+ extra_query=extra_query,
+ extra_body=extra_body,
+ timeout=timeout,
+ )
+
+ return download_audit_logs(fetch_chunk, to, on_progress=on_progress)
+
class AsyncAuditLogsResource(AsyncAPIResource):
"""Read audit log records for the authenticated organization."""
@@ -411,6 +466,55 @@ async def export_chunk(
cast_to=AsyncBinaryAPIResponse,
)
+ async def download(
+ self,
+ *,
+ to: BinaryIO,
+ end: Union[str, datetime],
+ start: Union[str, datetime],
+ auth_strategy: str | Omit = omit,
+ exclude_method: SequenceNotStr[str] | Omit = omit,
+ format: Literal["jsonl", "jsonl.gz"] | Omit = omit,
+ limit: int | Omit = omit,
+ method: str | Omit = omit,
+ search: str | Omit = omit,
+ search_user_id: SequenceNotStr[str] | Omit = omit,
+ service: str | Omit = omit,
+ on_progress: ProgressCallback | None = None,
+ extra_headers: Headers | None = None,
+ extra_query: Query | None = None,
+ extra_body: Body | None = None,
+ timeout: float | httpx.Timeout | None | NotGiven = not_given,
+ ) -> AuditLogDownloadResult:
+ """Download a complete audit log export to a writable binary destination.
+
+ The SDK verifies every chunk and retries transient transfer failures. It
+ does not close the destination.
+ """
+
+ resource = self._client.with_options(max_retries=0).audit_logs
+
+ async def fetch_chunk(cursor: str | None) -> AsyncBinaryAPIResponse:
+ return await resource.export_chunk(
+ end=end,
+ start=start,
+ auth_strategy=auth_strategy,
+ cursor=cursor if cursor is not None else omit,
+ exclude_method=exclude_method,
+ format=format,
+ limit=limit,
+ method=method,
+ search=search,
+ search_user_id=search_user_id,
+ service=service,
+ extra_headers=extra_headers,
+ extra_query=extra_query,
+ extra_body=extra_body,
+ timeout=timeout,
+ )
+
+ return await async_download_audit_logs(fetch_chunk, to, on_progress=on_progress)
+
class AuditLogsResourceWithRawResponse:
def __init__(self, audit_logs: AuditLogsResource) -> None:
diff --git a/tests/test_audit_log_download.py b/tests/test_audit_log_download.py
new file mode 100644
index 00000000..9c1a8c59
--- /dev/null
+++ b/tests/test_audit_log_download.py
@@ -0,0 +1,136 @@
+from __future__ import annotations
+
+import hashlib
+from io import BytesIO
+
+import httpx
+import pytest
+from respx import MockRouter
+
+from kernel import Kernel, AsyncKernel, AuditLogDownloadError
+from kernel.lib import audit_log_download
+
+
+def chunk_response(body: bytes, *, rows: int, has_more: bool, next_cursor: str | None = None) -> httpx.Response:
+ headers = {
+ "x-content-sha256": hashlib.sha256(body).hexdigest(),
+ "x-has-more": str(has_more).lower(),
+ "x-row-count": str(rows),
+ }
+ if next_cursor is not None:
+ headers["x-next-cursor"] = next_cursor
+ return httpx.Response(200, content=body, headers=headers)
+
+
+def test_download_writes_verified_chunks(client: Kernel, respx_mock: MockRouter) -> None:
+ responses = iter(
+ [
+ chunk_response(b"first", rows=2, has_more=True, next_cursor="next"),
+ chunk_response(b"second", rows=1, has_more=False),
+ ]
+ )
+ cursors: list[str | None] = []
+
+ def respond(request: httpx.Request) -> httpx.Response:
+ cursors.append(request.url.params.get("cursor"))
+ return next(responses)
+
+ respx_mock.get("/audit-logs/export/chunk").mock(side_effect=respond)
+ destination = BytesIO()
+ progress: list[tuple[int, int, int, int]] = []
+
+ result = client.audit_logs.download(
+ to=destination,
+ start="2026-06-01T00:00:00Z",
+ end="2026-06-02T00:00:00Z",
+ format="jsonl.gz",
+ on_progress=lambda update: progress.append(
+ (update.bytes_written, update.chunks, update.rows, update.chunk_rows)
+ ),
+ )
+
+ assert destination.getvalue() == b"firstsecond"
+ assert result.bytes_written == 11
+ assert result.chunks == 2
+ assert result.rows == 3
+ assert progress == [(5, 1, 2, 2), (11, 2, 3, 1)]
+ assert cursors == [None, "next"]
+
+
+async def test_async_download_writes_verified_chunks(async_client: AsyncKernel, respx_mock: MockRouter) -> None:
+ respx_mock.get("/audit-logs/export/chunk").mock(
+ side_effect=[
+ chunk_response(b"first", rows=2, has_more=True, next_cursor="next"),
+ chunk_response(b"second", rows=1, has_more=False),
+ ]
+ )
+ destination = BytesIO()
+
+ result = await async_client.audit_logs.download(
+ to=destination,
+ start="2026-06-01T00:00:00Z",
+ end="2026-06-02T00:00:00Z",
+ )
+
+ assert destination.getvalue() == b"firstsecond"
+ assert result.bytes_written == 11
+ assert result.chunks == 2
+ assert result.rows == 3
+
+
+def test_download_retries_checksum_mismatch(
+ client: Kernel, respx_mock: MockRouter, monkeypatch: pytest.MonkeyPatch
+) -> None:
+ def no_sleep(_: float) -> None:
+ pass
+
+ monkeypatch.setattr(audit_log_download.time, "sleep", no_sleep)
+ bad = chunk_response(b"bad", rows=1, has_more=False)
+ bad.headers["x-content-sha256"] = hashlib.sha256(b"good").hexdigest()
+ route = respx_mock.get("/audit-logs/export/chunk").mock(
+ side_effect=[bad, chunk_response(b"good", rows=1, has_more=False)]
+ )
+ destination = BytesIO()
+
+ client.audit_logs.download(
+ to=destination,
+ start="2026-06-01T00:00:00Z",
+ end="2026-06-02T00:00:00Z",
+ )
+
+ assert destination.getvalue() == b"good"
+ assert route.call_count == 2
+
+
+def test_download_owns_http_retries(client: Kernel, respx_mock: MockRouter, monkeypatch: pytest.MonkeyPatch) -> None:
+ delays: list[float] = []
+ monkeypatch.setattr(audit_log_download.time, "sleep", delays.append)
+ route = respx_mock.get("/audit-logs/export/chunk").mock(
+ side_effect=[
+ httpx.Response(500, json={"message": "temporary failure"}),
+ chunk_response(b"good", rows=1, has_more=False),
+ ]
+ )
+
+ client.audit_logs.download(
+ to=BytesIO(),
+ start="2026-06-01T00:00:00Z",
+ end="2026-06-02T00:00:00Z",
+ )
+
+ assert route.call_count == 2
+ assert delays == [1]
+
+
+def test_download_rejects_invalid_cursor_before_writing(client: Kernel, respx_mock: MockRouter) -> None:
+ respx_mock.get("/audit-logs/export/chunk").mock(return_value=chunk_response(b"chunk", rows=1, has_more=True))
+ destination = BytesIO()
+
+ with pytest.raises(AuditLogDownloadError, match="response has invalid X-Next-Cursor header"):
+ client.audit_logs.download(
+ to=destination,
+ start="2026-06-01T00:00:00Z",
+ end="2026-06-02T00:00:00Z",
+ )
+
+ assert destination.getvalue() == b""
From 8174c85167700e283e254d0bcae75f8482abb159 Mon Sep 17 00:00:00 2001
From: yummybomb <19238148+yummybomb@users.noreply.github.com>
Date: Tue, 21 Jul 2026 14:44:45 +0000
Subject: [PATCH 137/366] Harden audit log downloads
---
src/kernel/lib/audit_log_download.py | 70 +++++++++++++++---------
src/kernel/resources/audit_logs.py | 35 ++++++++----
tests/test_audit_log_download.py | 80 +++++++++++++++++++++++++---
3 files changed, 144 insertions(+), 41 deletions(-)
diff --git a/src/kernel/lib/audit_log_download.py b/src/kernel/lib/audit_log_download.py
index 7f739c8e..783317e5 100644
--- a/src/kernel/lib/audit_log_download.py
+++ b/src/kernel/lib/audit_log_download.py
@@ -2,15 +2,17 @@
import time
import hashlib
+import inspect
from typing import BinaryIO, Callable, Optional, Protocol, Awaitable
from dataclasses import dataclass
import anyio
import httpx
+from anyio import to_thread
-from .._exceptions import KernelError, APIStatusError
+from .._exceptions import KernelError
-_DOWNLOAD_ATTEMPTS = 7
+_DEFAULT_MAX_TRANSFER_RETRIES = 6
_MAX_RETRY_DELAY = 8.0
@@ -51,6 +53,7 @@ async def close(self) -> None: ...
SyncFetchChunk = Callable[[Optional[str]], _SyncChunkResponse]
AsyncFetchChunk = Callable[[Optional[str]], Awaitable[_AsyncChunkResponse]]
ProgressCallback = Callable[[AuditLogDownloadProgress], None]
+AsyncProgressCallback = Callable[[AuditLogDownloadProgress], Optional[Awaitable[None]]]
def download_audit_logs(
@@ -58,15 +61,22 @@ def download_audit_logs(
destination: BinaryIO,
*,
on_progress: ProgressCallback | None = None,
+ max_transfer_retries: int = _DEFAULT_MAX_TRANSFER_RETRIES,
) -> AuditLogDownloadResult:
if not callable(getattr(destination, "write", None)):
raise TypeError("audit log download destination must provide write()")
+ _validate_max_transfer_retries(max_transfer_retries)
cursor: str | None = None
result = AuditLogDownloadResult(bytes_written=0, chunks=0, rows=0)
+ seen_cursors: set[str] = set()
while True:
- body, headers = _fetch_verified_chunk(fetch_chunk, cursor)
+ body, headers = _fetch_verified_chunk(fetch_chunk, cursor, max_transfer_retries)
chunk_rows, next_cursor, has_more = _parse_chunk_headers(headers, cursor)
+ if has_more and next_cursor is not None:
+ if next_cursor in seen_cursors:
+ raise AuditLogDownloadError("response repeated X-Next-Cursor header")
+ seen_cursors.add(next_cursor)
_write_chunk(destination, body)
cursor = next_cursor
@@ -92,17 +102,24 @@ async def async_download_audit_logs(
fetch_chunk: AsyncFetchChunk,
destination: BinaryIO,
*,
- on_progress: ProgressCallback | None = None,
+ on_progress: AsyncProgressCallback | None = None,
+ max_transfer_retries: int = _DEFAULT_MAX_TRANSFER_RETRIES,
) -> AuditLogDownloadResult:
if not callable(getattr(destination, "write", None)):
raise TypeError("audit log download destination must provide write()")
+ _validate_max_transfer_retries(max_transfer_retries)
cursor: str | None = None
result = AuditLogDownloadResult(bytes_written=0, chunks=0, rows=0)
+ seen_cursors: set[str] = set()
while True:
- body, headers = await _async_fetch_verified_chunk(fetch_chunk, cursor)
+ body, headers = await _async_fetch_verified_chunk(fetch_chunk, cursor, max_transfer_retries)
chunk_rows, next_cursor, has_more = _parse_chunk_headers(headers, cursor)
- _write_chunk(destination, body)
+ if has_more and next_cursor is not None:
+ if next_cursor in seen_cursors:
+ raise AuditLogDownloadError("response repeated X-Next-Cursor header")
+ seen_cursors.add(next_cursor)
+ await to_thread.run_sync(_write_chunk, destination, body)
cursor = next_cursor
result = AuditLogDownloadResult(
@@ -111,7 +128,7 @@ async def async_download_audit_logs(
rows=result.rows + chunk_rows,
)
if on_progress is not None:
- on_progress(
+ callback_result = on_progress(
AuditLogDownloadProgress(
bytes_written=result.bytes_written,
chunks=result.chunks,
@@ -119,14 +136,18 @@ async def async_download_audit_logs(
chunk_rows=chunk_rows,
)
)
+ if inspect.isawaitable(callback_result):
+ await callback_result
if not has_more:
return result
-def _fetch_verified_chunk(fetch_chunk: SyncFetchChunk, cursor: str | None) -> tuple[bytes, httpx.Headers]:
- for attempt in range(1, _DOWNLOAD_ATTEMPTS + 1):
+def _fetch_verified_chunk(
+ fetch_chunk: SyncFetchChunk, cursor: str | None, max_transfer_retries: int
+) -> tuple[bytes, httpx.Headers]:
+ for retries in range(max_transfer_retries + 1):
+ response = fetch_chunk(cursor)
try:
- response = fetch_chunk(cursor)
try:
body = response.read()
headers = httpx.Headers(response.headers)
@@ -134,28 +155,30 @@ def _fetch_verified_chunk(fetch_chunk: SyncFetchChunk, cursor: str | None) -> tu
response.close()
_verify_checksum(body, headers)
return body, headers
- except Exception as error:
- if attempt == _DOWNLOAD_ATTEMPTS or not _is_retryable(error):
+ except Exception:
+ if retries == max_transfer_retries:
raise
- time.sleep(min(2 ** (attempt - 1), _MAX_RETRY_DELAY))
+ time.sleep(min(2**retries, _MAX_RETRY_DELAY))
raise AssertionError("unreachable")
-async def _async_fetch_verified_chunk(fetch_chunk: AsyncFetchChunk, cursor: str | None) -> tuple[bytes, httpx.Headers]:
- for attempt in range(1, _DOWNLOAD_ATTEMPTS + 1):
+async def _async_fetch_verified_chunk(
+ fetch_chunk: AsyncFetchChunk, cursor: str | None, max_transfer_retries: int
+) -> tuple[bytes, httpx.Headers]:
+ for retries in range(max_transfer_retries + 1):
+ response = await fetch_chunk(cursor)
try:
- response = await fetch_chunk(cursor)
try:
body = await response.read()
headers = httpx.Headers(response.headers)
finally:
await response.close()
- _verify_checksum(body, headers)
+ await to_thread.run_sync(_verify_checksum, body, headers)
return body, headers
- except Exception as error:
- if attempt == _DOWNLOAD_ATTEMPTS or not _is_retryable(error):
+ except Exception:
+ if retries == max_transfer_retries:
raise
- await anyio.sleep(min(2 ** (attempt - 1), _MAX_RETRY_DELAY))
+ await anyio.sleep(min(2**retries, _MAX_RETRY_DELAY))
raise AssertionError("unreachable")
@@ -199,7 +222,6 @@ def _write_chunk(destination: BinaryIO, body: bytes) -> None:
remaining = remaining[written:]
-def _is_retryable(error: Exception) -> bool:
- if isinstance(error, APIStatusError):
- return error.status_code == 429 or error.status_code >= 500
- return True
+def _validate_max_transfer_retries(max_transfer_retries: int) -> None:
+ if max_transfer_retries < 0:
+ raise ValueError("max_transfer_retries must be non-negative")
diff --git a/src/kernel/resources/audit_logs.py b/src/kernel/resources/audit_logs.py
index 5b50ad59..25e48993 100644
--- a/src/kernel/resources/audit_logs.py
+++ b/src/kernel/resources/audit_logs.py
@@ -32,6 +32,7 @@
from ..types.audit_log_entry import AuditLogEntry
from ..lib.audit_log_download import (
ProgressCallback,
+ AsyncProgressCallback,
AuditLogDownloadResult,
download_audit_logs,
async_download_audit_logs,
@@ -243,6 +244,7 @@ def download(
search_user_id: SequenceNotStr[str] | Omit = omit,
service: str | Omit = omit,
on_progress: ProgressCallback | None = None,
+ max_transfer_retries: int = 6,
extra_headers: Headers | None = None,
extra_query: Query | None = None,
extra_body: Body | None = None,
@@ -251,13 +253,13 @@ def download(
"""Download a complete audit log export to a writable binary destination.
The SDK verifies every chunk and retries transient transfer failures. It
- does not close the destination.
+ does not close the destination. If the download fails, the destination
+ may contain a partial export; use a temporary file and atomic rename
+ when the completed export must be published atomically.
"""
- resource = self._client.with_options(max_retries=0).audit_logs
-
def fetch_chunk(cursor: str | None) -> BinaryAPIResponse:
- return resource.export_chunk(
+ return self.export_chunk(
end=end,
start=start,
auth_strategy=auth_strategy,
@@ -275,7 +277,12 @@ def fetch_chunk(cursor: str | None) -> BinaryAPIResponse:
timeout=timeout,
)
- return download_audit_logs(fetch_chunk, to, on_progress=on_progress)
+ return download_audit_logs(
+ fetch_chunk,
+ to,
+ on_progress=on_progress,
+ max_transfer_retries=max_transfer_retries,
+ )
class AsyncAuditLogsResource(AsyncAPIResource):
@@ -480,7 +487,8 @@ async def download(
search: str | Omit = omit,
search_user_id: SequenceNotStr[str] | Omit = omit,
service: str | Omit = omit,
- on_progress: ProgressCallback | None = None,
+ on_progress: AsyncProgressCallback | None = None,
+ max_transfer_retries: int = 6,
extra_headers: Headers | None = None,
extra_query: Query | None = None,
extra_body: Body | None = None,
@@ -489,13 +497,13 @@ async def download(
"""Download a complete audit log export to a writable binary destination.
The SDK verifies every chunk and retries transient transfer failures. It
- does not close the destination.
+ does not close the destination. If the download fails, the destination
+ may contain a partial export; use a temporary file and atomic rename
+ when the completed export must be published atomically.
"""
- resource = self._client.with_options(max_retries=0).audit_logs
-
async def fetch_chunk(cursor: str | None) -> AsyncBinaryAPIResponse:
- return await resource.export_chunk(
+ return await self.export_chunk(
end=end,
start=start,
auth_strategy=auth_strategy,
@@ -513,7 +521,12 @@ async def fetch_chunk(cursor: str | None) -> AsyncBinaryAPIResponse:
timeout=timeout,
)
- return await async_download_audit_logs(fetch_chunk, to, on_progress=on_progress)
+ return await async_download_audit_logs(
+ fetch_chunk,
+ to,
+ on_progress=on_progress,
+ max_transfer_retries=max_transfer_retries,
+ )
class AuditLogsResourceWithRawResponse:
diff --git a/tests/test_audit_log_download.py b/tests/test_audit_log_download.py
index 9c1a8c59..ccf95e07 100644
--- a/tests/test_audit_log_download.py
+++ b/tests/test_audit_log_download.py
@@ -1,13 +1,15 @@
from __future__ import annotations
import hashlib
+import threading
from io import BytesIO
+from typing import BinaryIO
import httpx
import pytest
from respx import MockRouter
-from kernel import Kernel, AsyncKernel, AuditLogDownloadError
+from kernel import Kernel, AsyncKernel, InternalServerError, AuditLogDownloadError
from kernel.lib import audit_log_download
@@ -57,25 +59,43 @@ def respond(request: httpx.Request) -> httpx.Response:
assert cursors == [None, "next"]
-async def test_async_download_writes_verified_chunks(async_client: AsyncKernel, respx_mock: MockRouter) -> None:
+async def test_async_download_writes_verified_chunks(
+ async_client: AsyncKernel, respx_mock: MockRouter, monkeypatch: pytest.MonkeyPatch
+) -> None:
respx_mock.get("/audit-logs/export/chunk").mock(
side_effect=[
chunk_response(b"first", rows=2, has_more=True, next_cursor="next"),
chunk_response(b"second", rows=1, has_more=False),
]
)
+ thread_ids: list[int] = []
+ write_chunk = audit_log_download._write_chunk
+
+ def record_write(destination: BinaryIO, body: bytes) -> None:
+ thread_ids.append(threading.get_ident())
+ write_chunk(destination, body)
+
+ monkeypatch.setattr(audit_log_download, "_write_chunk", record_write)
destination = BytesIO()
+ progress_called = False
+
+ async def on_progress(_: object) -> None:
+ nonlocal progress_called
+ progress_called = True
result = await async_client.audit_logs.download(
to=destination,
start="2026-06-01T00:00:00Z",
end="2026-06-02T00:00:00Z",
+ on_progress=on_progress,
)
assert destination.getvalue() == b"firstsecond"
assert result.bytes_written == 11
assert result.chunks == 2
assert result.rows == 3
+ assert progress_called
+ assert thread_ids and all(thread_id != threading.get_ident() for thread_id in thread_ids)
def test_download_retries_checksum_mismatch(
@@ -102,9 +122,7 @@ def no_sleep(_: float) -> None:
assert route.call_count == 2
-def test_download_owns_http_retries(client: Kernel, respx_mock: MockRouter, monkeypatch: pytest.MonkeyPatch) -> None:
- delays: list[float] = []
- monkeypatch.setattr(audit_log_download.time, "sleep", delays.append)
+def test_download_uses_client_http_retries(client: Kernel, respx_mock: MockRouter) -> None:
route = respx_mock.get("/audit-logs/export/chunk").mock(
side_effect=[
httpx.Response(500, json={"message": "temporary failure"}),
@@ -119,7 +137,57 @@ def test_download_owns_http_retries(client: Kernel, respx_mock: MockRouter, monk
)
assert route.call_count == 2
- assert delays == [1]
+
+
+def test_download_respects_disabled_http_retries(client: Kernel, respx_mock: MockRouter) -> None:
+ route = respx_mock.get("/audit-logs/export/chunk").mock(
+ return_value=httpx.Response(500, json={"message": "temporary failure"})
+ )
+
+ with pytest.raises(InternalServerError, match="500"):
+ client.with_options(max_retries=0).audit_logs.download(
+ to=BytesIO(),
+ start="2026-06-01T00:00:00Z",
+ end="2026-06-02T00:00:00Z",
+ )
+
+ assert route.call_count == 1
+
+
+def test_download_respects_transfer_retry_limit(client: Kernel, respx_mock: MockRouter) -> None:
+ bad = chunk_response(b"bad", rows=1, has_more=False)
+ bad.headers["x-content-sha256"] = hashlib.sha256(b"good").hexdigest()
+ route = respx_mock.get("/audit-logs/export/chunk").mock(return_value=bad)
+
+ with pytest.raises(AuditLogDownloadError, match="checksum mismatch"):
+ client.audit_logs.download(
+ to=BytesIO(),
+ start="2026-06-01T00:00:00Z",
+ end="2026-06-02T00:00:00Z",
+ max_transfer_retries=0,
+ )
+
+ assert route.call_count == 1
+
+
+def test_download_rejects_cursor_cycle(client: Kernel, respx_mock: MockRouter) -> None:
+ respx_mock.get("/audit-logs/export/chunk").mock(
+ side_effect=[
+ chunk_response(b"first", rows=1, has_more=True, next_cursor="a"),
+ chunk_response(b"second", rows=1, has_more=True, next_cursor="b"),
+ chunk_response(b"duplicate", rows=1, has_more=True, next_cursor="a"),
+ ]
+ )
+ destination = BytesIO()
+
+ with pytest.raises(AuditLogDownloadError, match="response repeated X-Next-Cursor header"):
+ client.audit_logs.download(
+ to=destination,
+ start="2026-06-01T00:00:00Z",
+ end="2026-06-02T00:00:00Z",
+ )
+
+ assert destination.getvalue() == b"firstsecond"
def test_download_rejects_invalid_cursor_before_writing(client: Kernel, respx_mock: MockRouter) -> None:
From 39e5555ba9d411869e3f5ceb48495f613c00c2ba Mon Sep 17 00:00:00 2001
From: yummybomb <19238148+yummybomb@users.noreply.github.com>
Date: Tue, 21 Jul 2026 15:31:47 +0000
Subject: [PATCH 138/366] Harden audit log download validation
---
src/kernel/lib/audit_log_download.py | 16 ++--
tests/test_audit_log_download.py | 106 ++++++++++++++++++++++++++-
2 files changed, 113 insertions(+), 9 deletions(-)
diff --git a/src/kernel/lib/audit_log_download.py b/src/kernel/lib/audit_log_download.py
index 783317e5..cda07dab 100644
--- a/src/kernel/lib/audit_log_download.py
+++ b/src/kernel/lib/audit_log_download.py
@@ -13,6 +13,7 @@
from .._exceptions import KernelError
_DEFAULT_MAX_TRANSFER_RETRIES = 6
+_MAX_CHUNK_ROWS = 50_000
_MAX_RETRY_DELAY = 8.0
@@ -198,11 +199,10 @@ def _parse_chunk_headers(headers: httpx.Headers, current_cursor: str | None) ->
has_more = has_more_value == "true"
row_count = headers.get("x-row-count")
- try:
- rows = int(row_count) if row_count is not None else -1
- except ValueError as error:
- raise AuditLogDownloadError("response missing or invalid X-Row-Count header") from error
- if rows < 0:
+ if row_count is None or not row_count.isascii() or not row_count.isdecimal():
+ raise AuditLogDownloadError("response missing or invalid X-Row-Count header")
+ rows = int(row_count)
+ if rows > _MAX_CHUNK_ROWS:
raise AuditLogDownloadError("response missing or invalid X-Row-Count header")
next_cursor = headers.get("x-next-cursor") or None
@@ -217,11 +217,11 @@ def _write_chunk(destination: BinaryIO, body: bytes) -> None:
remaining = memoryview(body)
while remaining:
written = destination.write(remaining)
- if written <= 0 or written > len(remaining):
+ if type(written) is not int or written <= 0 or written > len(remaining):
raise AuditLogDownloadError("audit log download destination performed a short write")
remaining = remaining[written:]
def _validate_max_transfer_retries(max_transfer_retries: int) -> None:
- if max_transfer_retries < 0:
- raise ValueError("max_transfer_retries must be non-negative")
+ if type(max_transfer_retries) is not int or max_transfer_retries < 0:
+ raise ValueError("max_transfer_retries must be a non-negative integer")
diff --git a/tests/test_audit_log_download.py b/tests/test_audit_log_download.py
index ccf95e07..262c32a8 100644
--- a/tests/test_audit_log_download.py
+++ b/tests/test_audit_log_download.py
@@ -3,7 +3,7 @@
import hashlib
import threading
from io import BytesIO
-from typing import BinaryIO
+from typing import BinaryIO, cast
import httpx
import pytest
@@ -98,6 +98,110 @@ async def on_progress(_: object) -> None:
assert thread_ids and all(thread_id != threading.get_ident() for thread_id in thread_ids)
+async def test_async_download_retries_checksum_mismatch(
+ async_client: AsyncKernel, respx_mock: MockRouter, monkeypatch: pytest.MonkeyPatch
+) -> None:
+ async def no_sleep(_: float) -> None:
+ pass
+
+ monkeypatch.setattr(audit_log_download.anyio, "sleep", no_sleep)
+ bad = chunk_response(b"bad", rows=1, has_more=False)
+ bad.headers["x-content-sha256"] = hashlib.sha256(b"good").hexdigest()
+ route = respx_mock.get("/audit-logs/export/chunk").mock(
+ side_effect=[bad, chunk_response(b"good", rows=1, has_more=False)]
+ )
+ destination = BytesIO()
+
+ await async_client.audit_logs.download(
+ to=destination,
+ start="2026-06-01T00:00:00Z",
+ end="2026-06-02T00:00:00Z",
+ )
+
+ assert destination.getvalue() == b"good"
+ assert route.call_count == 2
+
+
+async def test_async_download_uses_client_http_retries(async_client: AsyncKernel, respx_mock: MockRouter) -> None:
+ route = respx_mock.get("/audit-logs/export/chunk").mock(
+ side_effect=[
+ httpx.Response(500, json={"message": "temporary failure"}),
+ chunk_response(b"good", rows=1, has_more=False),
+ ]
+ )
+
+ await async_client.audit_logs.download(
+ to=BytesIO(),
+ start="2026-06-01T00:00:00Z",
+ end="2026-06-02T00:00:00Z",
+ )
+
+ assert route.call_count == 2
+
+
+async def test_async_download_respects_disabled_http_retries(async_client: AsyncKernel, respx_mock: MockRouter) -> None:
+ route = respx_mock.get("/audit-logs/export/chunk").mock(
+ return_value=httpx.Response(500, json={"message": "temporary failure"})
+ )
+
+ with pytest.raises(InternalServerError, match="500"):
+ await async_client.with_options(max_retries=0).audit_logs.download(
+ to=BytesIO(),
+ start="2026-06-01T00:00:00Z",
+ end="2026-06-02T00:00:00Z",
+ )
+
+ assert route.call_count == 1
+
+
+@pytest.mark.parametrize("row_count", ["", "1.0", "50001"])
+def test_download_rejects_invalid_row_count(row_count: str, client: Kernel, respx_mock: MockRouter) -> None:
+ response = chunk_response(b"chunk", rows=1, has_more=False)
+ response.headers["x-row-count"] = row_count
+ respx_mock.get("/audit-logs/export/chunk").mock(return_value=response)
+ destination = BytesIO()
+
+ with pytest.raises(AuditLogDownloadError, match="response missing or invalid X-Row-Count header"):
+ client.audit_logs.download(
+ to=destination,
+ start="2026-06-01T00:00:00Z",
+ end="2026-06-02T00:00:00Z",
+ )
+
+ assert destination.getvalue() == b""
+
+
+class InvalidWriteDestination:
+ def __init__(self, result: object) -> None:
+ self.result = result
+
+ def write(self, _: object) -> object:
+ return self.result
+
+
+@pytest.mark.parametrize("write_result", [float("nan"), 0.5, True])
+def test_download_rejects_invalid_write_result(write_result: object, client: Kernel, respx_mock: MockRouter) -> None:
+ respx_mock.get("/audit-logs/export/chunk").mock(return_value=chunk_response(b"chunk", rows=1, has_more=False))
+
+ with pytest.raises(AuditLogDownloadError, match="audit log download destination performed a short write"):
+ client.audit_logs.download(
+ to=cast(BinaryIO, InvalidWriteDestination(write_result)),
+ start="2026-06-01T00:00:00Z",
+ end="2026-06-02T00:00:00Z",
+ )
+
+
+@pytest.mark.parametrize("max_transfer_retries", [True, 1.5])
+def test_download_rejects_invalid_transfer_retry_count(max_transfer_retries: object, client: Kernel) -> None:
+ with pytest.raises(ValueError, match="max_transfer_retries must be a non-negative integer"):
+ client.audit_logs.download(
+ to=BytesIO(),
+ start="2026-06-01T00:00:00Z",
+ end="2026-06-02T00:00:00Z",
+ max_transfer_retries=cast(int, max_transfer_retries),
+ )
+
+
def test_download_retries_checksum_mismatch(
client: Kernel, respx_mock: MockRouter, monkeypatch: pytest.MonkeyPatch
) -> None:
From e25457909c27de921570c872a70e38434adca250 Mon Sep 17 00:00:00 2001
From: yummybomb <19238148+yummybomb@users.noreply.github.com>
Date: Tue, 21 Jul 2026 16:13:20 +0000
Subject: [PATCH 139/366] Retry streamed audit log transfers
---
src/kernel/lib/audit_log_download.py | 55 ++++++++++--------
src/kernel/resources/audit_logs.py | 10 ++--
tests/test_audit_log_download.py | 85 ++++++++++++++++++++++++++--
3 files changed, 117 insertions(+), 33 deletions(-)
diff --git a/src/kernel/lib/audit_log_download.py b/src/kernel/lib/audit_log_download.py
index cda07dab..4cfdcc26 100644
--- a/src/kernel/lib/audit_log_download.py
+++ b/src/kernel/lib/audit_log_download.py
@@ -3,7 +3,7 @@
import time
import hashlib
import inspect
-from typing import BinaryIO, Callable, Optional, Protocol, Awaitable
+from typing import BinaryIO, Callable, Optional, Protocol, Awaitable, ContextManager, AsyncContextManager
from dataclasses import dataclass
import anyio
@@ -51,8 +51,8 @@ async def read(self) -> bytes: ...
async def close(self) -> None: ...
-SyncFetchChunk = Callable[[Optional[str]], _SyncChunkResponse]
-AsyncFetchChunk = Callable[[Optional[str]], Awaitable[_AsyncChunkResponse]]
+SyncFetchChunk = Callable[[Optional[str]], ContextManager[_SyncChunkResponse]]
+AsyncFetchChunk = Callable[[Optional[str]], AsyncContextManager[_AsyncChunkResponse]]
ProgressCallback = Callable[[AuditLogDownloadProgress], None]
AsyncProgressCallback = Callable[[AuditLogDownloadProgress], Optional[Awaitable[None]]]
@@ -147,19 +147,20 @@ def _fetch_verified_chunk(
fetch_chunk: SyncFetchChunk, cursor: str | None, max_transfer_retries: int
) -> tuple[bytes, httpx.Headers]:
for retries in range(max_transfer_retries + 1):
- response = fetch_chunk(cursor)
- try:
+ transfer_error: Exception | None = None
+ with fetch_chunk(cursor) as response:
try:
body = response.read()
headers = httpx.Headers(response.headers)
- finally:
- response.close()
- _verify_checksum(body, headers)
- return body, headers
- except Exception:
- if retries == max_transfer_retries:
- raise
- time.sleep(min(2**retries, _MAX_RETRY_DELAY))
+ _verify_checksum(body, headers)
+ except Exception as error:
+ transfer_error = error
+ else:
+ return body, headers
+ assert transfer_error is not None
+ if retries == max_transfer_retries:
+ raise transfer_error
+ time.sleep(min(2**retries, _MAX_RETRY_DELAY))
raise AssertionError("unreachable")
@@ -167,19 +168,20 @@ async def _async_fetch_verified_chunk(
fetch_chunk: AsyncFetchChunk, cursor: str | None, max_transfer_retries: int
) -> tuple[bytes, httpx.Headers]:
for retries in range(max_transfer_retries + 1):
- response = await fetch_chunk(cursor)
- try:
+ transfer_error: Exception | None = None
+ async with fetch_chunk(cursor) as response:
try:
body = await response.read()
headers = httpx.Headers(response.headers)
- finally:
- await response.close()
- await to_thread.run_sync(_verify_checksum, body, headers)
- return body, headers
- except Exception:
- if retries == max_transfer_retries:
- raise
- await anyio.sleep(min(2**retries, _MAX_RETRY_DELAY))
+ await to_thread.run_sync(_verify_checksum, body, headers)
+ except Exception as error:
+ transfer_error = error
+ else:
+ return body, headers
+ assert transfer_error is not None
+ if retries == max_transfer_retries:
+ raise transfer_error
+ await anyio.sleep(min(2**retries, _MAX_RETRY_DELAY))
raise AssertionError("unreachable")
@@ -199,7 +201,12 @@ def _parse_chunk_headers(headers: httpx.Headers, current_cursor: str | None) ->
has_more = has_more_value == "true"
row_count = headers.get("x-row-count")
- if row_count is None or not row_count.isascii() or not row_count.isdecimal():
+ if (
+ row_count is None
+ or not row_count.isascii()
+ or not row_count.isdecimal()
+ or len(row_count) > len(str(_MAX_CHUNK_ROWS))
+ ):
raise AuditLogDownloadError("response missing or invalid X-Row-Count header")
rows = int(row_count)
if rows > _MAX_CHUNK_ROWS:
diff --git a/src/kernel/resources/audit_logs.py b/src/kernel/resources/audit_logs.py
index 25e48993..a5491697 100644
--- a/src/kernel/resources/audit_logs.py
+++ b/src/kernel/resources/audit_logs.py
@@ -2,7 +2,7 @@
from __future__ import annotations
-from typing import Union, BinaryIO
+from typing import Union, BinaryIO, ContextManager, AsyncContextManager
from datetime import datetime
from typing_extensions import Literal
@@ -258,8 +258,8 @@ def download(
when the completed export must be published atomically.
"""
- def fetch_chunk(cursor: str | None) -> BinaryAPIResponse:
- return self.export_chunk(
+ def fetch_chunk(cursor: str | None) -> ContextManager[StreamedBinaryAPIResponse]:
+ return self.with_streaming_response.export_chunk(
end=end,
start=start,
auth_strategy=auth_strategy,
@@ -502,8 +502,8 @@ async def download(
when the completed export must be published atomically.
"""
- async def fetch_chunk(cursor: str | None) -> AsyncBinaryAPIResponse:
- return await self.export_chunk(
+ def fetch_chunk(cursor: str | None) -> AsyncContextManager[AsyncStreamedBinaryAPIResponse]:
+ return self.with_streaming_response.export_chunk(
end=end,
start=start,
auth_strategy=auth_strategy,
diff --git a/tests/test_audit_log_download.py b/tests/test_audit_log_download.py
index 262c32a8..0419ca9e 100644
--- a/tests/test_audit_log_download.py
+++ b/tests/test_audit_log_download.py
@@ -3,7 +3,8 @@
import hashlib
import threading
from io import BytesIO
-from typing import BinaryIO, cast
+from typing import BinaryIO, Iterator, AsyncIterator, cast
+from typing_extensions import override
import httpx
import pytest
@@ -13,7 +14,7 @@
from kernel.lib import audit_log_download
-def chunk_response(body: bytes, *, rows: int, has_more: bool, next_cursor: str | None = None) -> httpx.Response:
+def chunk_headers(body: bytes, *, rows: int, has_more: bool, next_cursor: str | None = None) -> dict[str, str]:
headers = {
"x-content-sha256": hashlib.sha256(body).hexdigest(),
"x-has-more": str(has_more).lower(),
@@ -21,7 +22,29 @@ def chunk_response(body: bytes, *, rows: int, has_more: bool, next_cursor: str |
}
if next_cursor is not None:
headers["x-next-cursor"] = next_cursor
- return httpx.Response(200, content=body, headers=headers)
+ return headers
+
+
+def chunk_response(body: bytes, *, rows: int, has_more: bool, next_cursor: str | None = None) -> httpx.Response:
+ return httpx.Response(
+ 200,
+ content=body,
+ headers=chunk_headers(body, rows=rows, has_more=has_more, next_cursor=next_cursor),
+ )
+
+
+class BrokenSyncByteStream(httpx.SyncByteStream):
+ @override
+ def __iter__(self) -> Iterator[bytes]:
+ yield b"partial"
+ raise httpx.ReadError("truncated response body")
+
+
+class BrokenAsyncByteStream(httpx.AsyncByteStream):
+ @override
+ async def __aiter__(self) -> AsyncIterator[bytes]:
+ yield b"partial"
+ raise httpx.ReadError("truncated response body")
def test_download_writes_verified_chunks(client: Kernel, respx_mock: MockRouter) -> None:
@@ -98,6 +121,33 @@ async def on_progress(_: object) -> None:
assert thread_ids and all(thread_id != threading.get_ident() for thread_id in thread_ids)
+async def test_async_download_retries_body_read_failure(
+ async_client: AsyncKernel, respx_mock: MockRouter, monkeypatch: pytest.MonkeyPatch
+) -> None:
+ async def no_sleep(_: float) -> None:
+ pass
+
+ monkeypatch.setattr(audit_log_download.anyio, "sleep", no_sleep)
+ headers = chunk_headers(b"good", rows=1, has_more=False)
+ route = respx_mock.get("/audit-logs/export/chunk").mock(
+ side_effect=[
+ httpx.Response(200, headers=headers, stream=BrokenAsyncByteStream()),
+ chunk_response(b"good", rows=1, has_more=False),
+ ]
+ )
+ destination = BytesIO()
+
+ await async_client.with_options(max_retries=0).audit_logs.download(
+ to=destination,
+ start="2026-06-01T00:00:00Z",
+ end="2026-06-02T00:00:00Z",
+ max_transfer_retries=1,
+ )
+
+ assert destination.getvalue() == b"good"
+ assert route.call_count == 2
+
+
async def test_async_download_retries_checksum_mismatch(
async_client: AsyncKernel, respx_mock: MockRouter, monkeypatch: pytest.MonkeyPatch
) -> None:
@@ -154,7 +204,7 @@ async def test_async_download_respects_disabled_http_retries(async_client: Async
assert route.call_count == 1
-@pytest.mark.parametrize("row_count", ["", "1.0", "50001"])
+@pytest.mark.parametrize("row_count", ["", "1.0", "50001", "9" * 5000])
def test_download_rejects_invalid_row_count(row_count: str, client: Kernel, respx_mock: MockRouter) -> None:
response = chunk_response(b"chunk", rows=1, has_more=False)
response.headers["x-row-count"] = row_count
@@ -202,6 +252,33 @@ def test_download_rejects_invalid_transfer_retry_count(max_transfer_retries: obj
)
+def test_download_retries_body_read_failure(
+ client: Kernel, respx_mock: MockRouter, monkeypatch: pytest.MonkeyPatch
+) -> None:
+ def no_sleep(_: float) -> None:
+ pass
+
+ monkeypatch.setattr(audit_log_download.time, "sleep", no_sleep)
+ headers = chunk_headers(b"good", rows=1, has_more=False)
+ route = respx_mock.get("/audit-logs/export/chunk").mock(
+ side_effect=[
+ httpx.Response(200, headers=headers, stream=BrokenSyncByteStream()),
+ chunk_response(b"good", rows=1, has_more=False),
+ ]
+ )
+ destination = BytesIO()
+
+ client.with_options(max_retries=0).audit_logs.download(
+ to=destination,
+ start="2026-06-01T00:00:00Z",
+ end="2026-06-02T00:00:00Z",
+ max_transfer_retries=1,
+ )
+
+ assert destination.getvalue() == b"good"
+ assert route.call_count == 2
+
+
def test_download_retries_checksum_mismatch(
client: Kernel, respx_mock: MockRouter, monkeypatch: pytest.MonkeyPatch
) -> None:
From 0467576d093f437f1200202d067f216312ea59f5 Mon Sep 17 00:00:00 2001
From: yummybomb <19238148+yummybomb@users.noreply.github.com>
Date: Tue, 21 Jul 2026 16:19:22 +0000
Subject: [PATCH 140/366] Accept padded audit log row counts
---
src/kernel/lib/audit_log_download.py | 12 +++++-------
tests/test_audit_log_download.py | 14 ++++++++++++++
2 files changed, 19 insertions(+), 7 deletions(-)
diff --git a/src/kernel/lib/audit_log_download.py b/src/kernel/lib/audit_log_download.py
index 4cfdcc26..f334d6ae 100644
--- a/src/kernel/lib/audit_log_download.py
+++ b/src/kernel/lib/audit_log_download.py
@@ -201,14 +201,12 @@ def _parse_chunk_headers(headers: httpx.Headers, current_cursor: str | None) ->
has_more = has_more_value == "true"
row_count = headers.get("x-row-count")
- if (
- row_count is None
- or not row_count.isascii()
- or not row_count.isdecimal()
- or len(row_count) > len(str(_MAX_CHUNK_ROWS))
- ):
+ if row_count is None or not row_count.isascii() or not row_count.isdecimal():
raise AuditLogDownloadError("response missing or invalid X-Row-Count header")
- rows = int(row_count)
+ normalized_row_count = row_count.lstrip("0") or "0"
+ if len(normalized_row_count) > len(str(_MAX_CHUNK_ROWS)):
+ raise AuditLogDownloadError("response missing or invalid X-Row-Count header")
+ rows = int(normalized_row_count)
if rows > _MAX_CHUNK_ROWS:
raise AuditLogDownloadError("response missing or invalid X-Row-Count header")
diff --git a/tests/test_audit_log_download.py b/tests/test_audit_log_download.py
index 0419ca9e..c400ac41 100644
--- a/tests/test_audit_log_download.py
+++ b/tests/test_audit_log_download.py
@@ -221,6 +221,20 @@ def test_download_rejects_invalid_row_count(row_count: str, client: Kernel, resp
assert destination.getvalue() == b""
+def test_download_accepts_zero_padded_row_count(client: Kernel, respx_mock: MockRouter) -> None:
+ response = chunk_response(b"chunk", rows=1, has_more=False)
+ response.headers["x-row-count"] = "0" * 5000 + "1"
+ respx_mock.get("/audit-logs/export/chunk").mock(return_value=response)
+
+ result = client.audit_logs.download(
+ to=BytesIO(),
+ start="2026-06-01T00:00:00Z",
+ end="2026-06-02T00:00:00Z",
+ )
+
+ assert result.rows == 1
+
+
class InvalidWriteDestination:
def __init__(self, result: object) -> None:
self.result = result
From 3e2fcf923865a32d0c3768dbfde720f073e47b5e Mon Sep 17 00:00:00 2001
From: yummybomb <19238148+yummybomb@users.noreply.github.com>
Date: Tue, 21 Jul 2026 18:27:20 +0000
Subject: [PATCH 141/366] Default audit log downloads to gzip
---
src/kernel/resources/audit_logs.py | 26 ++++++++++++--------------
tests/test_audit_log_download.py | 16 +++++++++++++---
2 files changed, 25 insertions(+), 17 deletions(-)
diff --git a/src/kernel/resources/audit_logs.py b/src/kernel/resources/audit_logs.py
index a5491697..aac5d71c 100644
--- a/src/kernel/resources/audit_logs.py
+++ b/src/kernel/resources/audit_logs.py
@@ -237,7 +237,6 @@ def download(
start: Union[str, datetime],
auth_strategy: str | Omit = omit,
exclude_method: SequenceNotStr[str] | Omit = omit,
- format: Literal["jsonl", "jsonl.gz"] | Omit = omit,
limit: int | Omit = omit,
method: str | Omit = omit,
search: str | Omit = omit,
@@ -250,12 +249,13 @@ def download(
extra_body: Body | None = None,
timeout: float | httpx.Timeout | None | NotGiven = not_given,
) -> AuditLogDownloadResult:
- """Download a complete audit log export to a writable binary destination.
+ """Download a complete gzip-compressed JSON Lines audit log export.
- The SDK verifies every chunk and retries transient transfer failures. It
- does not close the destination. If the download fails, the destination
- may contain a partial export; use a temporary file and atomic rename
- when the completed export must be published atomically.
+ The SDK writes the export to a writable binary destination, verifies
+ every chunk, and retries transient transfer failures. It does not close
+ the destination. If the download fails, the destination may contain a
+ partial export; use a temporary file and atomic rename when the completed
+ export must be published atomically.
"""
def fetch_chunk(cursor: str | None) -> ContextManager[StreamedBinaryAPIResponse]:
@@ -265,7 +265,6 @@ def fetch_chunk(cursor: str | None) -> ContextManager[StreamedBinaryAPIResponse]
auth_strategy=auth_strategy,
cursor=cursor if cursor is not None else omit,
exclude_method=exclude_method,
- format=format,
limit=limit,
method=method,
search=search,
@@ -481,7 +480,6 @@ async def download(
start: Union[str, datetime],
auth_strategy: str | Omit = omit,
exclude_method: SequenceNotStr[str] | Omit = omit,
- format: Literal["jsonl", "jsonl.gz"] | Omit = omit,
limit: int | Omit = omit,
method: str | Omit = omit,
search: str | Omit = omit,
@@ -494,12 +492,13 @@ async def download(
extra_body: Body | None = None,
timeout: float | httpx.Timeout | None | NotGiven = not_given,
) -> AuditLogDownloadResult:
- """Download a complete audit log export to a writable binary destination.
+ """Download a complete gzip-compressed JSON Lines audit log export.
- The SDK verifies every chunk and retries transient transfer failures. It
- does not close the destination. If the download fails, the destination
- may contain a partial export; use a temporary file and atomic rename
- when the completed export must be published atomically.
+ The SDK writes the export to a writable binary destination, verifies
+ every chunk, and retries transient transfer failures. It does not close
+ the destination. If the download fails, the destination may contain a
+ partial export; use a temporary file and atomic rename when the completed
+ export must be published atomically.
"""
def fetch_chunk(cursor: str | None) -> AsyncContextManager[AsyncStreamedBinaryAPIResponse]:
@@ -509,7 +508,6 @@ def fetch_chunk(cursor: str | None) -> AsyncContextManager[AsyncStreamedBinaryAP
auth_strategy=auth_strategy,
cursor=cursor if cursor is not None else omit,
exclude_method=exclude_method,
- format=format,
limit=limit,
method=method,
search=search,
diff --git a/tests/test_audit_log_download.py b/tests/test_audit_log_download.py
index c400ac41..e254066e 100644
--- a/tests/test_audit_log_download.py
+++ b/tests/test_audit_log_download.py
@@ -55,9 +55,11 @@ def test_download_writes_verified_chunks(client: Kernel, respx_mock: MockRouter)
]
)
cursors: list[str | None] = []
+ formats: list[str | None] = []
def respond(request: httpx.Request) -> httpx.Response:
cursors.append(request.url.params.get("cursor"))
+ formats.append(request.url.params.get("format"))
return next(responses)
respx_mock.get("/audit-logs/export/chunk").mock(side_effect=respond)
@@ -68,7 +70,6 @@ def respond(request: httpx.Request) -> httpx.Response:
to=destination,
start="2026-06-01T00:00:00Z",
end="2026-06-02T00:00:00Z",
- format="jsonl.gz",
on_progress=lambda update: progress.append(
(update.bytes_written, update.chunks, update.rows, update.chunk_rows)
),
@@ -80,17 +81,25 @@ def respond(request: httpx.Request) -> httpx.Response:
assert result.rows == 3
assert progress == [(5, 1, 2, 2), (11, 2, 3, 1)]
assert cursors == [None, "next"]
+ assert formats == [None, None]
async def test_async_download_writes_verified_chunks(
async_client: AsyncKernel, respx_mock: MockRouter, monkeypatch: pytest.MonkeyPatch
) -> None:
- respx_mock.get("/audit-logs/export/chunk").mock(
- side_effect=[
+ responses = iter(
+ [
chunk_response(b"first", rows=2, has_more=True, next_cursor="next"),
chunk_response(b"second", rows=1, has_more=False),
]
)
+ formats: list[str | None] = []
+
+ def respond(request: httpx.Request) -> httpx.Response:
+ formats.append(request.url.params.get("format"))
+ return next(responses)
+
+ respx_mock.get("/audit-logs/export/chunk").mock(side_effect=respond)
thread_ids: list[int] = []
write_chunk = audit_log_download._write_chunk
@@ -119,6 +128,7 @@ async def on_progress(_: object) -> None:
assert result.rows == 3
assert progress_called
assert thread_ids and all(thread_id != threading.get_ident() for thread_id in thread_ids)
+ assert formats == [None, None]
async def test_async_download_retries_body_read_failure(
From d2638c0ed2140eee92573073d90386289eed1406 Mon Sep 17 00:00:00 2001
From: "stainless-app[bot]"
<142633134+stainless-app[bot]@users.noreply.github.com>
Date: Tue, 21 Jul 2026 18:49:59 +0000
Subject: [PATCH 142/366] release: 0.81.0
---
.release-please-manifest.json | 2 +-
CHANGELOG.md | 8 ++++++++
pyproject.toml | 2 +-
src/kernel/_version.py | 2 +-
4 files changed, 11 insertions(+), 3 deletions(-)
diff --git a/.release-please-manifest.json b/.release-please-manifest.json
index c3e137b1..f8e21778 100644
--- a/.release-please-manifest.json
+++ b/.release-please-manifest.json
@@ -1,3 +1,3 @@
{
- ".": "0.80.0"
+ ".": "0.81.0"
}
\ No newline at end of file
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 51c9734b..9e652229 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,13 @@
# Changelog
+## 0.81.0 (2026-07-21)
+
+Full Changelog: [v0.80.0...v0.81.0](https://github.com/kernel/kernel-python-sdk/compare/v0.80.0...v0.81.0)
+
+### Features
+
+* add complete audit log downloads ([18aa8c9](https://github.com/kernel/kernel-python-sdk/commit/18aa8c925b3f7699c30a6202685447432df2da6c))
+
## 0.80.0 (2026-07-19)
Full Changelog: [v0.79.0...v0.80.0](https://github.com/kernel/kernel-python-sdk/compare/v0.79.0...v0.80.0)
diff --git a/pyproject.toml b/pyproject.toml
index e6a19285..3ecf5f86 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -1,6 +1,6 @@
[project]
name = "kernel"
-version = "0.80.0"
+version = "0.81.0"
description = "The official Python library for the kernel API"
dynamic = ["readme"]
license = "Apache-2.0"
diff --git a/src/kernel/_version.py b/src/kernel/_version.py
index 222542c7..55bf102b 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.80.0" # x-release-please-version
+__version__ = "0.81.0" # x-release-please-version
From 041e16b5fb0bdb572cc74515e06299ed409462cb Mon Sep 17 00:00:00 2001
From: "stainless-app[bot]"
<142633134+stainless-app[bot]@users.noreply.github.com>
Date: Wed, 22 Jul 2026 22:04:53 +0000
Subject: [PATCH 143/366] feat: Count project names by Unicode code point
---
.stats.yml | 4 ++--
src/kernel/resources/projects/projects.py | 8 ++++----
src/kernel/types/project_create_params.py | 2 +-
src/kernel/types/project_update_params.py | 2 +-
tests/api_resources/test_projects.py | 4 ++--
5 files changed, 10 insertions(+), 10 deletions(-)
diff --git a/.stats.yml b/.stats.yml
index c189c7a2..f5843501 100644
--- a/.stats.yml
+++ b/.stats.yml
@@ -1,4 +1,4 @@
configured_endpoints: 127
-openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel/kernel-1dffe057847b596dd311ce4b6e0e8036126dce3e97d390636c9c8e6d2f4823e1.yml
-openapi_spec_hash: c06336c199ee64c222ba3122deae9463
+openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel/kernel-6568ec20bad9bc5257619dae6b6817eedc308f8b74453912e660df95e329e44a.yml
+openapi_spec_hash: 17b9ed1a0a3193731c52c4b1aac2b5b8
config_hash: 77ee715aa17061166f9a02b264a21b8d
diff --git a/src/kernel/resources/projects/projects.py b/src/kernel/resources/projects/projects.py
index c8ca08f2..ca3d6189 100644
--- a/src/kernel/resources/projects/projects.py
+++ b/src/kernel/resources/projects/projects.py
@@ -74,7 +74,7 @@ def create(
Create a new project within the authenticated organization.
Args:
- name: Project name (1-255 characters)
+ name: Project name (1-255 Unicode code points)
extra_headers: Send extra headers
@@ -144,7 +144,7 @@ def update(
Update a project's name or status.
Args:
- name: New project name
+ name: New project name (1-255 Unicode code points)
status: New project status
@@ -307,7 +307,7 @@ async def create(
Create a new project within the authenticated organization.
Args:
- name: Project name (1-255 characters)
+ name: Project name (1-255 Unicode code points)
extra_headers: Send extra headers
@@ -377,7 +377,7 @@ async def update(
Update a project's name or status.
Args:
- name: New project name
+ name: New project name (1-255 Unicode code points)
status: New project status
diff --git a/src/kernel/types/project_create_params.py b/src/kernel/types/project_create_params.py
index 99a59864..9c51c570 100644
--- a/src/kernel/types/project_create_params.py
+++ b/src/kernel/types/project_create_params.py
@@ -9,4 +9,4 @@
class ProjectCreateParams(TypedDict, total=False):
name: Required[str]
- """Project name (1-255 characters)"""
+ """Project name (1-255 Unicode code points)"""
diff --git a/src/kernel/types/project_update_params.py b/src/kernel/types/project_update_params.py
index ea7de900..7b4359ff 100644
--- a/src/kernel/types/project_update_params.py
+++ b/src/kernel/types/project_update_params.py
@@ -9,7 +9,7 @@
class ProjectUpdateParams(TypedDict, total=False):
name: str
- """New project name"""
+ """New project name (1-255 Unicode code points)"""
status: Literal["active", "archived"]
"""New project status"""
diff --git a/tests/api_resources/test_projects.py b/tests/api_resources/test_projects.py
index 007353fe..e8d98c29 100644
--- a/tests/api_resources/test_projects.py
+++ b/tests/api_resources/test_projects.py
@@ -107,7 +107,7 @@ def test_method_update(self, client: Kernel) -> None:
def test_method_update_with_all_params(self, client: Kernel) -> None:
project = client.projects.update(
id="id",
- name="name",
+ name="x",
status="active",
)
assert_matches_type(Project, project, path=["response"])
@@ -322,7 +322,7 @@ async def test_method_update(self, async_client: AsyncKernel) -> None:
async def test_method_update_with_all_params(self, async_client: AsyncKernel) -> None:
project = await async_client.projects.update(
id="id",
- name="name",
+ name="x",
status="active",
)
assert_matches_type(Project, project, path=["response"])
From e2f403c8712ba50b7c8f9d8d055565bacd9ff5ec Mon Sep 17 00:00:00 2001
From: "stainless-app[bot]"
<142633134+stainless-app[bot]@users.noreply.github.com>
Date: Thu, 23 Jul 2026 19:39:19 +0000
Subject: [PATCH 144/366] feat: Stream profile downloads as tar archives
---
.stats.yml | 4 +-
api.md | 2 +-
src/kernel/resources/profiles.py | 44 ++++++++++++++++----
src/kernel/types/__init__.py | 1 +
src/kernel/types/profile_download_params.py | 15 +++++++
tests/api_resources/test_profiles.py | 46 +++++++++++++++++----
6 files changed, 91 insertions(+), 21 deletions(-)
create mode 100644 src/kernel/types/profile_download_params.py
diff --git a/.stats.yml b/.stats.yml
index f5843501..3dedd81c 100644
--- a/.stats.yml
+++ b/.stats.yml
@@ -1,4 +1,4 @@
configured_endpoints: 127
-openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel/kernel-6568ec20bad9bc5257619dae6b6817eedc308f8b74453912e660df95e329e44a.yml
-openapi_spec_hash: 17b9ed1a0a3193731c52c4b1aac2b5b8
+openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel/kernel-5163e8941e42f7fb84e10f572f538202b31722c84bb980db74ccd44a960eb87f.yml
+openapi_spec_hash: 8aa974cf7843681d6ae3e6dcf099596d
config_hash: 77ee715aa17061166f9a02b264a21b8d
diff --git a/api.md b/api.md
index 7feea4cd..edc133e0 100644
--- a/api.md
+++ b/api.md
@@ -288,7 +288,7 @@ Methods:
- client.profiles.update(id_or_name, \*\*params) -> Profile
- client.profiles.list(\*\*params) -> SyncOffsetPagination[Profile]
- client.profiles.delete(id_or_name) -> None
-- client.profiles.download(id_or_name) -> BinaryAPIResponse
+- client.profiles.download(id_or_name, \*\*params) -> BinaryAPIResponse
# Auth
diff --git a/src/kernel/resources/profiles.py b/src/kernel/resources/profiles.py
index e804f559..836a9fb0 100644
--- a/src/kernel/resources/profiles.py
+++ b/src/kernel/resources/profiles.py
@@ -2,9 +2,11 @@
from __future__ import annotations
+from typing_extensions import Literal
+
import httpx
-from ..types import profile_list_params, profile_create_params, profile_update_params
+from ..types import profile_list_params, profile_create_params, profile_update_params, profile_download_params
from .._types import Body, Omit, Query, Headers, NoneType, NotGiven, omit, not_given
from .._utils import path_template, maybe_transform, async_maybe_transform
from .._compat import cached_property
@@ -264,6 +266,7 @@ def download(
self,
id_or_name: str,
*,
+ format: Literal["tar.zst", "tar"] | 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,
@@ -271,10 +274,17 @@ def download(
extra_body: Body | None = None,
timeout: float | httpx.Timeout | None | NotGiven = not_given,
) -> BinaryAPIResponse:
- """
- Returns a zstd-compressed tar file of the full user-data directory.
+ """Downloads the profile in its stored format by default.
+
+ Current profiles are
+ returned as zstd-compressed tar archives, while legacy profiles remain JSON. Set
+ `format=tar` to decompress current profiles during download; legacy profiles
+ remain JSON.
Args:
+ format: Response format for current profile archives. Legacy profiles are always
+ returned as JSON.
+
extra_headers: Send extra headers
extra_query: Add additional query parameters to the request
@@ -285,11 +295,15 @@ def download(
"""
if not id_or_name:
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 {})}
+ extra_headers = {"Accept": "application/zstd", **(extra_headers or {})}
return self._get(
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
+ extra_headers=extra_headers,
+ extra_query=extra_query,
+ extra_body=extra_body,
+ timeout=timeout,
+ query=maybe_transform({"format": format}, profile_download_params.ProfileDownloadParams),
),
cast_to=BinaryAPIResponse,
)
@@ -529,6 +543,7 @@ async def download(
self,
id_or_name: str,
*,
+ format: Literal["tar.zst", "tar"] | 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,
@@ -536,10 +551,17 @@ async def download(
extra_body: Body | None = None,
timeout: float | httpx.Timeout | None | NotGiven = not_given,
) -> AsyncBinaryAPIResponse:
- """
- Returns a zstd-compressed tar file of the full user-data directory.
+ """Downloads the profile in its stored format by default.
+
+ Current profiles are
+ returned as zstd-compressed tar archives, while legacy profiles remain JSON. Set
+ `format=tar` to decompress current profiles during download; legacy profiles
+ remain JSON.
Args:
+ format: Response format for current profile archives. Legacy profiles are always
+ returned as JSON.
+
extra_headers: Send extra headers
extra_query: Add additional query parameters to the request
@@ -550,11 +572,15 @@ async def download(
"""
if not id_or_name:
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 {})}
+ extra_headers = {"Accept": "application/zstd", **(extra_headers or {})}
return await self._get(
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
+ extra_headers=extra_headers,
+ extra_query=extra_query,
+ extra_body=extra_body,
+ timeout=timeout,
+ query=await async_maybe_transform({"format": format}, profile_download_params.ProfileDownloadParams),
),
cast_to=AsyncBinaryAPIResponse,
)
diff --git a/src/kernel/types/__init__.py b/src/kernel/types/__init__.py
index 959865bf..ab58edbc 100644
--- a/src/kernel/types/__init__.py
+++ b/src/kernel/types/__init__.py
@@ -67,6 +67,7 @@
from .browser_update_response import BrowserUpdateResponse as BrowserUpdateResponse
from .extension_list_response import ExtensionListResponse as ExtensionListResponse
from .extension_upload_params import ExtensionUploadParams as ExtensionUploadParams
+from .profile_download_params import ProfileDownloadParams as ProfileDownloadParams
from .proxy_retrieve_response import ProxyRetrieveResponse as ProxyRetrieveResponse
from .browser_pool_list_params import BrowserPoolListParams as BrowserPoolListParams
from .credential_create_params import CredentialCreateParams as CredentialCreateParams
diff --git a/src/kernel/types/profile_download_params.py b/src/kernel/types/profile_download_params.py
new file mode 100644
index 00000000..32bfa3dd
--- /dev/null
+++ b/src/kernel/types/profile_download_params.py
@@ -0,0 +1,15 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from __future__ import annotations
+
+from typing_extensions import Literal, TypedDict
+
+__all__ = ["ProfileDownloadParams"]
+
+
+class ProfileDownloadParams(TypedDict, total=False):
+ format: Literal["tar.zst", "tar"]
+ """Response format for current profile archives.
+
+ Legacy profiles are always returned as JSON.
+ """
diff --git a/tests/api_resources/test_profiles.py b/tests/api_resources/test_profiles.py
index 683a3ce2..9317cf48 100644
--- a/tests/api_resources/test_profiles.py
+++ b/tests/api_resources/test_profiles.py
@@ -11,7 +11,9 @@
from kernel import Kernel, AsyncKernel
from tests.utils import assert_matches_type
-from kernel.types import Profile
+from kernel.types import (
+ Profile,
+)
from kernel._response import (
BinaryAPIResponse,
AsyncBinaryAPIResponse,
@@ -236,7 +238,20 @@ def test_path_params_delete(self, client: Kernel) -> None:
def test_method_download(self, client: Kernel, respx_mock: MockRouter) -> None:
respx_mock.get("/profiles/id_or_name/download").mock(return_value=httpx.Response(200, json={"foo": "bar"}))
profile = client.profiles.download(
- "id_or_name",
+ id_or_name="id_or_name",
+ )
+ assert profile.is_closed
+ assert profile.json() == {"foo": "bar"}
+ assert cast(Any, profile.is_closed) is True
+ assert isinstance(profile, BinaryAPIResponse)
+
+ @parametrize
+ @pytest.mark.respx(base_url=base_url)
+ def test_method_download_with_all_params(self, client: Kernel, respx_mock: MockRouter) -> None:
+ respx_mock.get("/profiles/id_or_name/download").mock(return_value=httpx.Response(200, json={"foo": "bar"}))
+ profile = client.profiles.download(
+ id_or_name="id_or_name",
+ format="tar.zst",
)
assert profile.is_closed
assert profile.json() == {"foo": "bar"}
@@ -249,7 +264,7 @@ def test_raw_response_download(self, client: Kernel, respx_mock: MockRouter) ->
respx_mock.get("/profiles/id_or_name/download").mock(return_value=httpx.Response(200, json={"foo": "bar"}))
profile = client.profiles.with_raw_response.download(
- "id_or_name",
+ id_or_name="id_or_name",
)
assert profile.is_closed is True
@@ -262,7 +277,7 @@ def test_raw_response_download(self, client: Kernel, respx_mock: MockRouter) ->
def test_streaming_response_download(self, client: Kernel, respx_mock: MockRouter) -> None:
respx_mock.get("/profiles/id_or_name/download").mock(return_value=httpx.Response(200, json={"foo": "bar"}))
with client.profiles.with_streaming_response.download(
- "id_or_name",
+ id_or_name="id_or_name",
) as profile:
assert not profile.is_closed
assert profile.http_request.headers.get("X-Stainless-Lang") == "python"
@@ -278,7 +293,7 @@ def test_streaming_response_download(self, client: Kernel, respx_mock: MockRoute
def test_path_params_download(self, client: Kernel) -> None:
with pytest.raises(ValueError, match=r"Expected a non-empty value for `id_or_name` but received ''"):
client.profiles.with_raw_response.download(
- "",
+ id_or_name="",
)
@@ -497,7 +512,20 @@ async def test_path_params_delete(self, async_client: AsyncKernel) -> None:
async def test_method_download(self, async_client: AsyncKernel, respx_mock: MockRouter) -> None:
respx_mock.get("/profiles/id_or_name/download").mock(return_value=httpx.Response(200, json={"foo": "bar"}))
profile = await async_client.profiles.download(
- "id_or_name",
+ id_or_name="id_or_name",
+ )
+ assert profile.is_closed
+ assert await profile.json() == {"foo": "bar"}
+ assert cast(Any, profile.is_closed) is True
+ assert isinstance(profile, AsyncBinaryAPIResponse)
+
+ @parametrize
+ @pytest.mark.respx(base_url=base_url)
+ async def test_method_download_with_all_params(self, async_client: AsyncKernel, respx_mock: MockRouter) -> None:
+ respx_mock.get("/profiles/id_or_name/download").mock(return_value=httpx.Response(200, json={"foo": "bar"}))
+ profile = await async_client.profiles.download(
+ id_or_name="id_or_name",
+ format="tar.zst",
)
assert profile.is_closed
assert await profile.json() == {"foo": "bar"}
@@ -510,7 +538,7 @@ async def test_raw_response_download(self, async_client: AsyncKernel, respx_mock
respx_mock.get("/profiles/id_or_name/download").mock(return_value=httpx.Response(200, json={"foo": "bar"}))
profile = await async_client.profiles.with_raw_response.download(
- "id_or_name",
+ id_or_name="id_or_name",
)
assert profile.is_closed is True
@@ -523,7 +551,7 @@ async def test_raw_response_download(self, async_client: AsyncKernel, respx_mock
async def test_streaming_response_download(self, async_client: AsyncKernel, respx_mock: MockRouter) -> None:
respx_mock.get("/profiles/id_or_name/download").mock(return_value=httpx.Response(200, json={"foo": "bar"}))
async with async_client.profiles.with_streaming_response.download(
- "id_or_name",
+ id_or_name="id_or_name",
) as profile:
assert not profile.is_closed
assert profile.http_request.headers.get("X-Stainless-Lang") == "python"
@@ -539,5 +567,5 @@ async def test_streaming_response_download(self, async_client: AsyncKernel, resp
async def test_path_params_download(self, async_client: AsyncKernel) -> None:
with pytest.raises(ValueError, match=r"Expected a non-empty value for `id_or_name` but received ''"):
await async_client.profiles.with_raw_response.download(
- "",
+ id_or_name="",
)
From 8ca280c69edd0f5f268ae46b830d000aa361d8e3 Mon Sep 17 00:00:00 2001
From: "stainless-app[bot]"
<142633134+stainless-app[bot]@users.noreply.github.com>
Date: Thu, 23 Jul 2026 20:16:38 +0000
Subject: [PATCH 145/366] feat: Use format-neutral profile download Accept
header
---
.stats.yml | 4 ++--
src/kernel/resources/profiles.py | 4 ++--
2 files changed, 4 insertions(+), 4 deletions(-)
diff --git a/.stats.yml b/.stats.yml
index 3dedd81c..c9122258 100644
--- a/.stats.yml
+++ b/.stats.yml
@@ -1,4 +1,4 @@
configured_endpoints: 127
-openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel/kernel-5163e8941e42f7fb84e10f572f538202b31722c84bb980db74ccd44a960eb87f.yml
-openapi_spec_hash: 8aa974cf7843681d6ae3e6dcf099596d
+openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel/kernel-477bd1ff6f7053f2bd4687d7c5fc9a6a1d56a584be42f927abc6fc858ff35718.yml
+openapi_spec_hash: 3bf4cc824f6497c4ad109abd3187eede
config_hash: 77ee715aa17061166f9a02b264a21b8d
diff --git a/src/kernel/resources/profiles.py b/src/kernel/resources/profiles.py
index 836a9fb0..8642940c 100644
--- a/src/kernel/resources/profiles.py
+++ b/src/kernel/resources/profiles.py
@@ -295,7 +295,7 @@ def download(
"""
if not id_or_name:
raise ValueError(f"Expected a non-empty value for `id_or_name` but received {id_or_name!r}")
- extra_headers = {"Accept": "application/zstd", **(extra_headers or {})}
+ extra_headers = {"Accept": "application/octet-stream", **(extra_headers or {})}
return self._get(
path_template("/profiles/{id_or_name}/download", id_or_name=id_or_name),
options=make_request_options(
@@ -572,7 +572,7 @@ async def download(
"""
if not id_or_name:
raise ValueError(f"Expected a non-empty value for `id_or_name` but received {id_or_name!r}")
- extra_headers = {"Accept": "application/zstd", **(extra_headers or {})}
+ extra_headers = {"Accept": "application/octet-stream", **(extra_headers or {})}
return await self._get(
path_template("/profiles/{id_or_name}/download", id_or_name=id_or_name),
options=make_request_options(
From 24435d7c7d4df475ed3703b95ec50b31f3ce95c0 Mon Sep 17 00:00:00 2001
From: "stainless-app[bot]"
<142633134+stainless-app[bot]@users.noreply.github.com>
Date: Thu, 23 Jul 2026 20:17:04 +0000
Subject: [PATCH 146/366] release: 0.82.0
---
.release-please-manifest.json | 2 +-
CHANGELOG.md | 10 ++++++++++
pyproject.toml | 2 +-
src/kernel/_version.py | 2 +-
4 files changed, 13 insertions(+), 3 deletions(-)
diff --git a/.release-please-manifest.json b/.release-please-manifest.json
index f8e21778..f24634e6 100644
--- a/.release-please-manifest.json
+++ b/.release-please-manifest.json
@@ -1,3 +1,3 @@
{
- ".": "0.81.0"
+ ".": "0.82.0"
}
\ No newline at end of file
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 9e652229..b2692c28 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,15 @@
# Changelog
+## 0.82.0 (2026-07-23)
+
+Full Changelog: [v0.81.0...v0.82.0](https://github.com/kernel/kernel-python-sdk/compare/v0.81.0...v0.82.0)
+
+### Features
+
+* Count project names by Unicode code point ([cecdf6d](https://github.com/kernel/kernel-python-sdk/commit/cecdf6d4a4ef7919b6eef5d03f76542b6ade0906))
+* Stream profile downloads as tar archives ([3a2eb97](https://github.com/kernel/kernel-python-sdk/commit/3a2eb97686f3ecf02db16d7bccea803476cb730f))
+* Use format-neutral profile download Accept header ([95d1334](https://github.com/kernel/kernel-python-sdk/commit/95d1334660d2ebaed70d2170233e4bdf3b074958))
+
## 0.81.0 (2026-07-21)
Full Changelog: [v0.80.0...v0.81.0](https://github.com/kernel/kernel-python-sdk/compare/v0.80.0...v0.81.0)
diff --git a/pyproject.toml b/pyproject.toml
index 3ecf5f86..f24b2fbe 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -1,6 +1,6 @@
[project]
name = "kernel"
-version = "0.81.0"
+version = "0.82.0"
description = "The official Python library for the kernel API"
dynamic = ["readme"]
license = "Apache-2.0"
diff --git a/src/kernel/_version.py b/src/kernel/_version.py
index 55bf102b..c0377ac4 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.81.0" # x-release-please-version
+__version__ = "0.82.0" # x-release-please-version
From 2630d8e9c2ef930831dd6f799053769bcd424ec8 Mon Sep 17 00:00:00 2001
From: "stainless-app[bot]"
<142633134+stainless-app[bot]@users.noreply.github.com>
Date: Fri, 24 Jul 2026 17:49:43 +0000
Subject: [PATCH 147/366] feat: Add browser telemetry to managed auth
connections
---
.stats.yml | 4 +-
src/kernel/resources/auth/connections.py | 38 ++++++++-
.../types/auth/connection_create_params.py | 44 +++++++++-
.../types/auth/connection_login_params.py | 47 ++++++++++-
.../types/auth/connection_update_params.py | 44 +++++++++-
src/kernel/types/auth/managed_auth.py | 42 ++++++++++
tests/api_resources/auth/test_connections.py | 84 +++++++++++++++++++
7 files changed, 297 insertions(+), 6 deletions(-)
diff --git a/.stats.yml b/.stats.yml
index c9122258..d1ed25e5 100644
--- a/.stats.yml
+++ b/.stats.yml
@@ -1,4 +1,4 @@
configured_endpoints: 127
-openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel/kernel-477bd1ff6f7053f2bd4687d7c5fc9a6a1d56a584be42f927abc6fc858ff35718.yml
-openapi_spec_hash: 3bf4cc824f6497c4ad109abd3187eede
+openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel/kernel-a4b8f40deaf572d0dab92ebe0cafa865e1357a18f8720c3f003810da89aa4203.yml
+openapi_spec_hash: d06e9bf804151f7d9f7fab77f2e1669c
config_hash: 77ee715aa17061166f9a02b264a21b8d
diff --git a/src/kernel/resources/auth/connections.py b/src/kernel/resources/auth/connections.py
index 05de4f04..f5f4529a 100644
--- a/src/kernel/resources/auth/connections.py
+++ b/src/kernel/resources/auth/connections.py
@@ -2,7 +2,7 @@
from __future__ import annotations
-from typing import Any, Dict, cast
+from typing import Any, Dict, Optional, cast
from typing_extensions import Literal
import httpx
@@ -66,6 +66,7 @@ def create(
profile_name: str,
allowed_domains: SequenceNotStr[str] | Omit = omit,
auto_reauth: bool | Omit = omit,
+ browser_telemetry: Optional[connection_create_params.BrowserTelemetry] | Omit = omit,
credential: connection_create_params.Credential | Omit = omit,
health_check_interval: int | Omit = omit,
health_checks: bool | Omit = omit,
@@ -119,6 +120,10 @@ def create(
false, expired sessions are marked as `NEEDS_AUTH` instead of attempting
re-auth. Defaults to true.
+ browser_telemetry: Browser telemetry configuration used by this connection's browser sessions by
+ default. Uses the exact create-browser configuration. Can be overridden
+ per-login.
+
credential:
Reference to credentials for the auth connection. Use one of:
@@ -166,6 +171,7 @@ def create(
"profile_name": profile_name,
"allowed_domains": allowed_domains,
"auto_reauth": auto_reauth,
+ "browser_telemetry": browser_telemetry,
"credential": credential,
"health_check_interval": health_check_interval,
"health_checks": health_checks,
@@ -223,6 +229,7 @@ def update(
*,
allowed_domains: SequenceNotStr[str] | Omit = omit,
auto_reauth: bool | Omit = omit,
+ browser_telemetry: Optional[connection_update_params.BrowserTelemetry] | Omit = omit,
credential: connection_update_params.Credential | Omit = omit,
health_check_interval: int | Omit = omit,
health_checks: bool | Omit = omit,
@@ -253,6 +260,10 @@ def update(
when `health_checks` is false. When false, expired sessions detected by a health
check are marked as `NEEDS_AUTH` instead of attempting re-auth.
+ browser_telemetry: Browser telemetry configuration used by future browser sessions for this
+ connection. Uses the exact create-browser configuration. Set enabled to false to
+ disable telemetry.
+
credential:
Reference to credentials for the auth connection. Use one of:
@@ -294,6 +305,7 @@ def update(
{
"allowed_domains": allowed_domains,
"auto_reauth": auto_reauth,
+ "browser_telemetry": browser_telemetry,
"credential": credential,
"health_check_interval": health_check_interval,
"health_checks": health_checks,
@@ -452,6 +464,7 @@ def login(
self,
id: str,
*,
+ browser_telemetry: Optional[connection_login_params.BrowserTelemetry] | Omit = omit,
proxy: connection_login_params.Proxy | Omit = omit,
record_session: bool | Omit = omit,
# Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
@@ -468,6 +481,10 @@ def login(
credentials are stored.
Args:
+ browser_telemetry: Override the connection's default browser telemetry configuration for this
+ login. When omitted, the connection's browser_telemetry default is used. Uses
+ the exact create-browser configuration.
+
proxy: Proxy selection. Provide either id or name. The proxy must be in the same
project as the resource referencing it. When selecting by name, the name must
match exactly one active proxy in the project. Ambiguous names return a 400; use
@@ -490,6 +507,7 @@ def login(
path_template("/auth/connections/{id}/login", id=id),
body=maybe_transform(
{
+ "browser_telemetry": browser_telemetry,
"proxy": proxy,
"record_session": record_session,
},
@@ -657,6 +675,7 @@ async def create(
profile_name: str,
allowed_domains: SequenceNotStr[str] | Omit = omit,
auto_reauth: bool | Omit = omit,
+ browser_telemetry: Optional[connection_create_params.BrowserTelemetry] | Omit = omit,
credential: connection_create_params.Credential | Omit = omit,
health_check_interval: int | Omit = omit,
health_checks: bool | Omit = omit,
@@ -710,6 +729,10 @@ async def create(
false, expired sessions are marked as `NEEDS_AUTH` instead of attempting
re-auth. Defaults to true.
+ browser_telemetry: Browser telemetry configuration used by this connection's browser sessions by
+ default. Uses the exact create-browser configuration. Can be overridden
+ per-login.
+
credential:
Reference to credentials for the auth connection. Use one of:
@@ -757,6 +780,7 @@ async def create(
"profile_name": profile_name,
"allowed_domains": allowed_domains,
"auto_reauth": auto_reauth,
+ "browser_telemetry": browser_telemetry,
"credential": credential,
"health_check_interval": health_check_interval,
"health_checks": health_checks,
@@ -814,6 +838,7 @@ async def update(
*,
allowed_domains: SequenceNotStr[str] | Omit = omit,
auto_reauth: bool | Omit = omit,
+ browser_telemetry: Optional[connection_update_params.BrowserTelemetry] | Omit = omit,
credential: connection_update_params.Credential | Omit = omit,
health_check_interval: int | Omit = omit,
health_checks: bool | Omit = omit,
@@ -844,6 +869,10 @@ async def update(
when `health_checks` is false. When false, expired sessions detected by a health
check are marked as `NEEDS_AUTH` instead of attempting re-auth.
+ browser_telemetry: Browser telemetry configuration used by future browser sessions for this
+ connection. Uses the exact create-browser configuration. Set enabled to false to
+ disable telemetry.
+
credential:
Reference to credentials for the auth connection. Use one of:
@@ -885,6 +914,7 @@ async def update(
{
"allowed_domains": allowed_domains,
"auto_reauth": auto_reauth,
+ "browser_telemetry": browser_telemetry,
"credential": credential,
"health_check_interval": health_check_interval,
"health_checks": health_checks,
@@ -1043,6 +1073,7 @@ async def login(
self,
id: str,
*,
+ browser_telemetry: Optional[connection_login_params.BrowserTelemetry] | Omit = omit,
proxy: connection_login_params.Proxy | Omit = omit,
record_session: bool | Omit = omit,
# Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
@@ -1059,6 +1090,10 @@ async def login(
credentials are stored.
Args:
+ browser_telemetry: Override the connection's default browser telemetry configuration for this
+ login. When omitted, the connection's browser_telemetry default is used. Uses
+ the exact create-browser configuration.
+
proxy: Proxy selection. Provide either id or name. The proxy must be in the same
project as the resource referencing it. When selecting by name, the name must
match exactly one active proxy in the project. Ambiguous names return a 400; use
@@ -1081,6 +1116,7 @@ async def login(
path_template("/auth/connections/{id}/login", id=id),
body=await async_maybe_transform(
{
+ "browser_telemetry": browser_telemetry,
"proxy": proxy,
"record_session": record_session,
},
diff --git a/src/kernel/types/auth/connection_create_params.py b/src/kernel/types/auth/connection_create_params.py
index 0113d062..ed94bf37 100644
--- a/src/kernel/types/auth/connection_create_params.py
+++ b/src/kernel/types/auth/connection_create_params.py
@@ -2,11 +2,13 @@
from __future__ import annotations
+from typing import Optional
from typing_extensions import Required, TypedDict
from ..._types import SequenceNotStr
+from ..browsers.browser_telemetry_categories_config_param import BrowserTelemetryCategoriesConfigParam
-__all__ = ["ConnectionCreateParams", "Credential", "Proxy"]
+__all__ = ["ConnectionCreateParams", "BrowserTelemetry", "Credential", "Proxy"]
class ConnectionCreateParams(TypedDict, total=False):
@@ -52,6 +54,13 @@ class ConnectionCreateParams(TypedDict, total=False):
re-auth. Defaults to true.
"""
+ browser_telemetry: Optional[BrowserTelemetry]
+ """
+ Browser telemetry configuration used by this connection's browser sessions by
+ default. Uses the exact create-browser configuration. Can be overridden
+ per-login.
+ """
+
credential: Credential
"""Reference to credentials for the auth connection. Use one of:
@@ -102,6 +111,39 @@ class ConnectionCreateParams(TypedDict, total=False):
"""
+class BrowserTelemetry(TypedDict, total=False):
+ """
+ Browser telemetry configuration used by this connection's browser sessions by default. Uses the exact create-browser configuration. Can be overridden per-login.
+ """
+
+ browser: BrowserTelemetryCategoriesConfigParam
+ """Per-category capture flags.
+
+ The operational categories (control, connection, system, captcha) are captured
+ whenever telemetry is enabled; set one to enabled=false to opt out. The CDP
+ categories (console, network, page, interaction) and screenshot are off by
+ default; set enabled=true to opt in. On create, provided categories layer onto
+ the default set. On update, provided categories merge onto the session's current
+ config; when no telemetry is active this falls back to the default set (matching
+ create). If browser is omitted or empty, the default set is used. A browser
+ config that disables every category stops capture on update and starts no
+ capture on create.
+ """
+
+ enabled: bool
+ """Request shortcut for browser telemetry capture.
+
+ True enables capture; with no browser category settings it captures the default
+ set (control, connection, system, captcha), and any browser category settings
+ are layered onto that default set. On update, enabled=true resolves the config
+ fresh from the default set plus any provided categories, replacing the session's
+ current selection rather than merging onto it; omit enabled to merge categories
+ onto the current selection instead. False stops capture on update and starts no
+ capture on create. enabled=false cannot be combined with browser category
+ settings.
+ """
+
+
class Credential(TypedDict, total=False):
"""Reference to credentials for the auth connection.
diff --git a/src/kernel/types/auth/connection_login_params.py b/src/kernel/types/auth/connection_login_params.py
index 6cfa737a..719ea678 100644
--- a/src/kernel/types/auth/connection_login_params.py
+++ b/src/kernel/types/auth/connection_login_params.py
@@ -2,12 +2,23 @@
from __future__ import annotations
+from typing import Optional
from typing_extensions import TypedDict
-__all__ = ["ConnectionLoginParams", "Proxy"]
+from ..browsers.browser_telemetry_categories_config_param import BrowserTelemetryCategoriesConfigParam
+
+__all__ = ["ConnectionLoginParams", "BrowserTelemetry", "Proxy"]
class ConnectionLoginParams(TypedDict, total=False):
+ browser_telemetry: Optional[BrowserTelemetry]
+ """Override the connection's default browser telemetry configuration for this
+ login.
+
+ When omitted, the connection's browser_telemetry default is used. Uses the exact
+ create-browser configuration.
+ """
+
proxy: Proxy
"""Proxy selection.
@@ -24,6 +35,40 @@ class ConnectionLoginParams(TypedDict, total=False):
"""
+class BrowserTelemetry(TypedDict, total=False):
+ """Override the connection's default browser telemetry configuration for this login.
+
+ When omitted, the connection's browser_telemetry default is used. Uses the exact create-browser configuration.
+ """
+
+ browser: BrowserTelemetryCategoriesConfigParam
+ """Per-category capture flags.
+
+ The operational categories (control, connection, system, captcha) are captured
+ whenever telemetry is enabled; set one to enabled=false to opt out. The CDP
+ categories (console, network, page, interaction) and screenshot are off by
+ default; set enabled=true to opt in. On create, provided categories layer onto
+ the default set. On update, provided categories merge onto the session's current
+ config; when no telemetry is active this falls back to the default set (matching
+ create). If browser is omitted or empty, the default set is used. A browser
+ config that disables every category stops capture on update and starts no
+ capture on create.
+ """
+
+ enabled: bool
+ """Request shortcut for browser telemetry capture.
+
+ True enables capture; with no browser category settings it captures the default
+ set (control, connection, system, captcha), and any browser category settings
+ are layered onto that default set. On update, enabled=true resolves the config
+ fresh from the default set plus any provided categories, replacing the session's
+ current selection rather than merging onto it; omit enabled to merge categories
+ onto the current selection instead. False stops capture on update and starts no
+ capture on create. enabled=false cannot be combined with browser category
+ settings.
+ """
+
+
class Proxy(TypedDict, total=False):
"""Proxy selection.
diff --git a/src/kernel/types/auth/connection_update_params.py b/src/kernel/types/auth/connection_update_params.py
index 20bc98ab..a14d486a 100644
--- a/src/kernel/types/auth/connection_update_params.py
+++ b/src/kernel/types/auth/connection_update_params.py
@@ -2,11 +2,13 @@
from __future__ import annotations
+from typing import Optional
from typing_extensions import TypedDict
from ..._types import SequenceNotStr
+from ..browsers.browser_telemetry_categories_config_param import BrowserTelemetryCategoriesConfigParam
-__all__ = ["ConnectionUpdateParams", "Credential", "Proxy"]
+__all__ = ["ConnectionUpdateParams", "BrowserTelemetry", "Credential", "Proxy"]
class ConnectionUpdateParams(TypedDict, total=False):
@@ -25,6 +27,13 @@ class ConnectionUpdateParams(TypedDict, total=False):
re-auth.
"""
+ browser_telemetry: Optional[BrowserTelemetry]
+ """
+ Browser telemetry configuration used by future browser sessions for this
+ connection. Uses the exact create-browser configuration. Set enabled to false to
+ disable telemetry.
+ """
+
credential: Credential
"""Reference to credentials for the auth connection. Use one of:
@@ -63,6 +72,39 @@ class ConnectionUpdateParams(TypedDict, total=False):
"""Whether to save credentials after every successful login"""
+class BrowserTelemetry(TypedDict, total=False):
+ """
+ Browser telemetry configuration used by future browser sessions for this connection. Uses the exact create-browser configuration. Set enabled to false to disable telemetry.
+ """
+
+ browser: BrowserTelemetryCategoriesConfigParam
+ """Per-category capture flags.
+
+ The operational categories (control, connection, system, captcha) are captured
+ whenever telemetry is enabled; set one to enabled=false to opt out. The CDP
+ categories (console, network, page, interaction) and screenshot are off by
+ default; set enabled=true to opt in. On create, provided categories layer onto
+ the default set. On update, provided categories merge onto the session's current
+ config; when no telemetry is active this falls back to the default set (matching
+ create). If browser is omitted or empty, the default set is used. A browser
+ config that disables every category stops capture on update and starts no
+ capture on create.
+ """
+
+ enabled: bool
+ """Request shortcut for browser telemetry capture.
+
+ True enables capture; with no browser category settings it captures the default
+ set (control, connection, system, captcha), and any browser category settings
+ are layered onto that default set. On update, enabled=true resolves the config
+ fresh from the default set plus any provided categories, replacing the session's
+ current selection rather than merging onto it; omit enabled to merge categories
+ onto the current selection instead. False stops capture on update and starts no
+ capture on create. enabled=false cannot be combined with browser category
+ settings.
+ """
+
+
class Credential(TypedDict, total=False):
"""Reference to credentials for the auth connection.
diff --git a/src/kernel/types/auth/managed_auth.py b/src/kernel/types/auth/managed_auth.py
index 6dfb8401..73a4d48a 100644
--- a/src/kernel/types/auth/managed_auth.py
+++ b/src/kernel/types/auth/managed_auth.py
@@ -5,9 +5,11 @@
from typing_extensions import Literal
from ..._models import BaseModel
+from ..browsers.browser_telemetry_categories_config import BrowserTelemetryCategoriesConfig
__all__ = [
"ManagedAuth",
+ "BrowserTelemetry",
"Choice",
"Credential",
"DiscoveredField",
@@ -18,6 +20,39 @@
]
+class BrowserTelemetry(BaseModel):
+ """
+ Browser telemetry configuration used by this connection's browser sessions by default. The exact create-browser configuration is preserved and can be overridden per-login.
+ """
+
+ browser: Optional[BrowserTelemetryCategoriesConfig] = None
+ """Per-category capture flags.
+
+ The operational categories (control, connection, system, captcha) are captured
+ whenever telemetry is enabled; set one to enabled=false to opt out. The CDP
+ categories (console, network, page, interaction) and screenshot are off by
+ default; set enabled=true to opt in. On create, provided categories layer onto
+ the default set. On update, provided categories merge onto the session's current
+ config; when no telemetry is active this falls back to the default set (matching
+ create). If browser is omitted or empty, the default set is used. A browser
+ config that disables every category stops capture on update and starts no
+ capture on create.
+ """
+
+ enabled: Optional[bool] = None
+ """Request shortcut for browser telemetry capture.
+
+ True enables capture; with no browser category settings it captures the default
+ set (control, connection, system, captcha), and any browser category settings
+ are layered onto that default set. On update, enabled=true resolves the config
+ fresh from the default set plus any provided categories, replacing the session's
+ current selection rather than merging onto it; omit enabled to merge categories
+ onto the current selection instead. False stops capture on update and starts no
+ capture on create. enabled=false cannot be combined with browser category
+ settings.
+ """
+
+
class Choice(BaseModel):
"""Canonical auth-flow choice awaiting user selection."""
@@ -237,6 +272,13 @@ class ManagedAuth(BaseModel):
`/browsers` API.
"""
+ browser_telemetry: Optional[BrowserTelemetry] = None
+ """
+ Browser telemetry configuration used by this connection's browser sessions by
+ default. The exact create-browser configuration is preserved and can be
+ overridden per-login.
+ """
+
can_reauth: Optional[bool] = None
"""
Whether Kernel can automatically re-authenticate this connection when the
diff --git a/tests/api_resources/auth/test_connections.py b/tests/api_resources/auth/test_connections.py
index 8bdbc9ad..6c7423e6 100644
--- a/tests/api_resources/auth/test_connections.py
+++ b/tests/api_resources/auth/test_connections.py
@@ -40,6 +40,20 @@ def test_method_create_with_all_params(self, client: Kernel) -> None:
profile_name="user-123",
allowed_domains=["login.netflix.com", "auth.netflix.com"],
auto_reauth=True,
+ browser_telemetry={
+ "browser": {
+ "captcha": {"enabled": True},
+ "connection": {"enabled": True},
+ "console": {"enabled": True},
+ "control": {"enabled": True},
+ "interaction": {"enabled": True},
+ "network": {"enabled": True},
+ "page": {"enabled": True},
+ "screenshot": {"enabled": True},
+ "system": {"enabled": True},
+ },
+ "enabled": True,
+ },
credential={
"auto": True,
"name": "my-netflix-creds",
@@ -143,6 +157,20 @@ def test_method_update_with_all_params(self, client: Kernel) -> None:
id="id",
allowed_domains=["login.netflix.com", "auth.netflix.com"],
auto_reauth=True,
+ browser_telemetry={
+ "browser": {
+ "captcha": {"enabled": True},
+ "connection": {"enabled": True},
+ "console": {"enabled": True},
+ "control": {"enabled": True},
+ "interaction": {"enabled": True},
+ "network": {"enabled": True},
+ "page": {"enabled": True},
+ "screenshot": {"enabled": True},
+ "system": {"enabled": True},
+ },
+ "enabled": True,
+ },
credential={
"auto": True,
"name": "my-netflix-creds",
@@ -331,6 +359,20 @@ def test_method_login(self, client: Kernel) -> None:
def test_method_login_with_all_params(self, client: Kernel) -> None:
connection = client.auth.connections.login(
id="id",
+ browser_telemetry={
+ "browser": {
+ "captcha": {"enabled": True},
+ "connection": {"enabled": True},
+ "console": {"enabled": True},
+ "control": {"enabled": True},
+ "interaction": {"enabled": True},
+ "network": {"enabled": True},
+ "page": {"enabled": True},
+ "screenshot": {"enabled": True},
+ "system": {"enabled": True},
+ },
+ "enabled": True,
+ },
proxy={
"id": "id",
"name": "name",
@@ -512,6 +554,20 @@ async def test_method_create_with_all_params(self, async_client: AsyncKernel) ->
profile_name="user-123",
allowed_domains=["login.netflix.com", "auth.netflix.com"],
auto_reauth=True,
+ browser_telemetry={
+ "browser": {
+ "captcha": {"enabled": True},
+ "connection": {"enabled": True},
+ "console": {"enabled": True},
+ "control": {"enabled": True},
+ "interaction": {"enabled": True},
+ "network": {"enabled": True},
+ "page": {"enabled": True},
+ "screenshot": {"enabled": True},
+ "system": {"enabled": True},
+ },
+ "enabled": True,
+ },
credential={
"auto": True,
"name": "my-netflix-creds",
@@ -615,6 +671,20 @@ async def test_method_update_with_all_params(self, async_client: AsyncKernel) ->
id="id",
allowed_domains=["login.netflix.com", "auth.netflix.com"],
auto_reauth=True,
+ browser_telemetry={
+ "browser": {
+ "captcha": {"enabled": True},
+ "connection": {"enabled": True},
+ "console": {"enabled": True},
+ "control": {"enabled": True},
+ "interaction": {"enabled": True},
+ "network": {"enabled": True},
+ "page": {"enabled": True},
+ "screenshot": {"enabled": True},
+ "system": {"enabled": True},
+ },
+ "enabled": True,
+ },
credential={
"auto": True,
"name": "my-netflix-creds",
@@ -803,6 +873,20 @@ async def test_method_login(self, async_client: AsyncKernel) -> None:
async def test_method_login_with_all_params(self, async_client: AsyncKernel) -> None:
connection = await async_client.auth.connections.login(
id="id",
+ browser_telemetry={
+ "browser": {
+ "captcha": {"enabled": True},
+ "connection": {"enabled": True},
+ "console": {"enabled": True},
+ "control": {"enabled": True},
+ "interaction": {"enabled": True},
+ "network": {"enabled": True},
+ "page": {"enabled": True},
+ "screenshot": {"enabled": True},
+ "system": {"enabled": True},
+ },
+ "enabled": True,
+ },
proxy={
"id": "id",
"name": "name",
From 2af482a3f7de4f4cfeeab0a500e74b4bdc432771 Mon Sep 17 00:00:00 2001
From: "stainless-app[bot]"
<142633134+stainless-app[bot]@users.noreply.github.com>
Date: Fri, 24 Jul 2026 17:50:09 +0000
Subject: [PATCH 148/366] release: 0.83.0
---
.release-please-manifest.json | 2 +-
CHANGELOG.md | 8 ++++++++
pyproject.toml | 2 +-
src/kernel/_version.py | 2 +-
4 files changed, 11 insertions(+), 3 deletions(-)
diff --git a/.release-please-manifest.json b/.release-please-manifest.json
index f24634e6..d34cd697 100644
--- a/.release-please-manifest.json
+++ b/.release-please-manifest.json
@@ -1,3 +1,3 @@
{
- ".": "0.82.0"
+ ".": "0.83.0"
}
\ No newline at end of file
diff --git a/CHANGELOG.md b/CHANGELOG.md
index b2692c28..1d2172a4 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,13 @@
# Changelog
+## 0.83.0 (2026-07-24)
+
+Full Changelog: [v0.82.0...v0.83.0](https://github.com/kernel/kernel-python-sdk/compare/v0.82.0...v0.83.0)
+
+### Features
+
+* Add browser telemetry to managed auth connections ([c19bfbe](https://github.com/kernel/kernel-python-sdk/commit/c19bfbed5eed67758e735fb477c8c6c2afff9e7a))
+
## 0.82.0 (2026-07-23)
Full Changelog: [v0.81.0...v0.82.0](https://github.com/kernel/kernel-python-sdk/compare/v0.81.0...v0.82.0)
diff --git a/pyproject.toml b/pyproject.toml
index f24b2fbe..adb008f1 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -1,6 +1,6 @@
[project]
name = "kernel"
-version = "0.82.0"
+version = "0.83.0"
description = "The official Python library for the kernel API"
dynamic = ["readme"]
license = "Apache-2.0"
diff --git a/src/kernel/_version.py b/src/kernel/_version.py
index c0377ac4..8457ee35 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.82.0" # x-release-please-version
+__version__ = "0.83.0" # x-release-please-version
From 4560b09e66a5240b2ee484de7f320c125b6aefd9 Mon Sep 17 00:00:00 2001
From: "stainless-app[bot]"
<142633134+stainless-app[bot]@users.noreply.github.com>
Date: Mon, 27 Jul 2026 20:34:23 +0000
Subject: [PATCH 149/366] feat: Expose telemetry state on managed auth timeline
events
---
.stats.yml | 4 ++--
src/kernel/types/auth/managed_auth_timeline_event.py | 3 +++
2 files changed, 5 insertions(+), 2 deletions(-)
diff --git a/.stats.yml b/.stats.yml
index d1ed25e5..748759e3 100644
--- a/.stats.yml
+++ b/.stats.yml
@@ -1,4 +1,4 @@
configured_endpoints: 127
-openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel/kernel-a4b8f40deaf572d0dab92ebe0cafa865e1357a18f8720c3f003810da89aa4203.yml
-openapi_spec_hash: d06e9bf804151f7d9f7fab77f2e1669c
+openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel/kernel-904fd6fd50970ea07ec05c51b1ae0819ffb64cbf0921fff9daed6792b64f1dc9.yml
+openapi_spec_hash: cbd6626bdac7beaa768abb346e03fb74
config_hash: 77ee715aa17061166f9a02b264a21b8d
diff --git a/src/kernel/types/auth/managed_auth_timeline_event.py b/src/kernel/types/auth/managed_auth_timeline_event.py
index f83734c5..b2e0213b 100644
--- a/src/kernel/types/auth/managed_auth_timeline_event.py
+++ b/src/kernel/types/auth/managed_auth_timeline_event.py
@@ -70,6 +70,9 @@ class ManagedAuthTimelineEvent(BaseModel):
] = None
"""The step the flow reached. Present for login/reauth events."""
+ telemetry_captured: Optional[bool] = None
+ """Whether browser telemetry capture started for this event's browser session."""
+
updated_at: Optional[datetime] = None
"""When the event was last updated. Present for login/reauth events."""
From b1d01e985b89d312cef61f07097dc489503f44dd Mon Sep 17 00:00:00 2001
From: "stainless-app[bot]"
<142633134+stainless-app[bot]@users.noreply.github.com>
Date: Mon, 27 Jul 2026 20:34:50 +0000
Subject: [PATCH 150/366] release: 0.84.0
---
.release-please-manifest.json | 2 +-
CHANGELOG.md | 8 ++++++++
pyproject.toml | 2 +-
src/kernel/_version.py | 2 +-
4 files changed, 11 insertions(+), 3 deletions(-)
diff --git a/.release-please-manifest.json b/.release-please-manifest.json
index d34cd697..47dd76c1 100644
--- a/.release-please-manifest.json
+++ b/.release-please-manifest.json
@@ -1,3 +1,3 @@
{
- ".": "0.83.0"
+ ".": "0.84.0"
}
\ No newline at end of file
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 1d2172a4..53b7644d 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,13 @@
# Changelog
+## 0.84.0 (2026-07-27)
+
+Full Changelog: [v0.83.0...v0.84.0](https://github.com/kernel/kernel-python-sdk/compare/v0.83.0...v0.84.0)
+
+### Features
+
+* Expose telemetry state on managed auth timeline events ([9a37e8c](https://github.com/kernel/kernel-python-sdk/commit/9a37e8cbd40cd0fdcca0486543ddf30677d5dc16))
+
## 0.83.0 (2026-07-24)
Full Changelog: [v0.82.0...v0.83.0](https://github.com/kernel/kernel-python-sdk/compare/v0.82.0...v0.83.0)
diff --git a/pyproject.toml b/pyproject.toml
index adb008f1..8b7dbfbb 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -1,6 +1,6 @@
[project]
name = "kernel"
-version = "0.83.0"
+version = "0.84.0"
description = "The official Python library for the kernel API"
dynamic = ["readme"]
license = "Apache-2.0"
diff --git a/src/kernel/_version.py b/src/kernel/_version.py
index 8457ee35..c6ab69f7 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.83.0" # x-release-please-version
+__version__ = "0.84.0" # x-release-please-version
From a2c0cf9c4772553fa4846bcfdde96aa9f76cbc4b Mon Sep 17 00:00:00 2001
From: "stainless-app[bot]"
<142633134+stainless-app[bot]@users.noreply.github.com>
Date: Tue, 28 Jul 2026 21:01:04 +0000
Subject: [PATCH 151/366] feat: Add encrypted per-proxy CA bundle for BYO MITM
proxies
---
.stats.yml | 4 ++--
src/kernel/types/proxy_check_response.py | 3 +++
src/kernel/types/proxy_create_params.py | 7 +++++++
src/kernel/types/proxy_create_response.py | 3 +++
src/kernel/types/proxy_list_response.py | 3 +++
src/kernel/types/proxy_retrieve_response.py | 3 +++
src/kernel/types/proxy_update_response.py | 3 +++
7 files changed, 24 insertions(+), 2 deletions(-)
diff --git a/.stats.yml b/.stats.yml
index 748759e3..20e66039 100644
--- a/.stats.yml
+++ b/.stats.yml
@@ -1,4 +1,4 @@
configured_endpoints: 127
-openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel/kernel-904fd6fd50970ea07ec05c51b1ae0819ffb64cbf0921fff9daed6792b64f1dc9.yml
-openapi_spec_hash: cbd6626bdac7beaa768abb346e03fb74
+openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel/kernel-32a5ef4f5def9dde3ed7acc2b6159b143194e224ceaad2dbbbcb766cac90602e.yml
+openapi_spec_hash: 3f514f770bb352d98bd3dbcf865a077f
config_hash: 77ee715aa17061166f9a02b264a21b8d
diff --git a/src/kernel/types/proxy_check_response.py b/src/kernel/types/proxy_check_response.py
index f3cdab80..49833d66 100644
--- a/src/kernel/types/proxy_check_response.py
+++ b/src/kernel/types/proxy_check_response.py
@@ -78,6 +78,9 @@ class ConfigCustomProxyConfig(BaseModel):
port: int
"""Proxy port."""
+ has_ca_bundle: Optional[bool] = None
+ """Whether the proxy has a custom CA bundle configured."""
+
has_password: Optional[bool] = None
"""Whether the proxy has a password."""
diff --git a/src/kernel/types/proxy_create_params.py b/src/kernel/types/proxy_create_params.py
index 331bd776..7069d9b1 100644
--- a/src/kernel/types/proxy_create_params.py
+++ b/src/kernel/types/proxy_create_params.py
@@ -100,6 +100,13 @@ class ConfigCreateCustomProxyConfig(TypedDict, total=False):
port: Required[int]
"""Proxy port."""
+ ca_bundle: str
+ """PEM-encoded CA certificate bundle the proxy re-signs upstream TLS with.
+
+ Provide when the proxy terminates TLS (MITM) so the browser trusts its
+ certificates. May contain multiple concatenated certificates.
+ """
+
password: str
"""Password for proxy authentication."""
diff --git a/src/kernel/types/proxy_create_response.py b/src/kernel/types/proxy_create_response.py
index d71656c8..eeddcc3a 100644
--- a/src/kernel/types/proxy_create_response.py
+++ b/src/kernel/types/proxy_create_response.py
@@ -78,6 +78,9 @@ class ConfigCustomProxyConfig(BaseModel):
port: int
"""Proxy port."""
+ has_ca_bundle: Optional[bool] = None
+ """Whether the proxy has a custom CA bundle configured."""
+
has_password: Optional[bool] = None
"""Whether the proxy has a password."""
diff --git a/src/kernel/types/proxy_list_response.py b/src/kernel/types/proxy_list_response.py
index 6d40a8a4..878ad7f2 100644
--- a/src/kernel/types/proxy_list_response.py
+++ b/src/kernel/types/proxy_list_response.py
@@ -78,6 +78,9 @@ class ConfigCustomProxyConfig(BaseModel):
port: int
"""Proxy port."""
+ has_ca_bundle: Optional[bool] = None
+ """Whether the proxy has a custom CA bundle configured."""
+
has_password: Optional[bool] = None
"""Whether the proxy has a password."""
diff --git a/src/kernel/types/proxy_retrieve_response.py b/src/kernel/types/proxy_retrieve_response.py
index 77d113e5..f2eb1487 100644
--- a/src/kernel/types/proxy_retrieve_response.py
+++ b/src/kernel/types/proxy_retrieve_response.py
@@ -78,6 +78,9 @@ class ConfigCustomProxyConfig(BaseModel):
port: int
"""Proxy port."""
+ has_ca_bundle: Optional[bool] = None
+ """Whether the proxy has a custom CA bundle configured."""
+
has_password: Optional[bool] = None
"""Whether the proxy has a password."""
diff --git a/src/kernel/types/proxy_update_response.py b/src/kernel/types/proxy_update_response.py
index d90c3013..bae95bba 100644
--- a/src/kernel/types/proxy_update_response.py
+++ b/src/kernel/types/proxy_update_response.py
@@ -78,6 +78,9 @@ class ConfigCustomProxyConfig(BaseModel):
port: int
"""Proxy port."""
+ has_ca_bundle: Optional[bool] = None
+ """Whether the proxy has a custom CA bundle configured."""
+
has_password: Optional[bool] = None
"""Whether the proxy has a password."""
From 3b4b3267ecd8d8d10f9461e86518935627ebc89d Mon Sep 17 00:00:00 2001
From: "stainless-app[bot]"
<142633134+stainless-app[bot]@users.noreply.github.com>
Date: Wed, 29 Jul 2026 13:46:39 +0000
Subject: [PATCH 152/366] feat: Stabilize project lifecycle error codes
---
.stats.yml | 4 +-
src/kernel/_client.py | 36 ++++++++++++++---
src/kernel/resources/projects/limits.py | 12 +++++-
src/kernel/resources/projects/projects.py | 48 +++++++++++++++++++----
4 files changed, 82 insertions(+), 18 deletions(-)
diff --git a/.stats.yml b/.stats.yml
index 20e66039..e1475cae 100644
--- a/.stats.yml
+++ b/.stats.yml
@@ -1,4 +1,4 @@
configured_endpoints: 127
-openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel/kernel-32a5ef4f5def9dde3ed7acc2b6159b143194e224ceaad2dbbbcb766cac90602e.yml
-openapi_spec_hash: 3f514f770bb352d98bd3dbcf865a077f
+openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel/kernel-7b03462454d6fa8390eaebc1b411d3e160501bf96abb93b75d38dfc382d4ce51.yml
+openapi_spec_hash: ba55400aafb4759cc06ec139fe528dfc
config_hash: 77ee715aa17061166f9a02b264a21b8d
diff --git a/src/kernel/_client.py b/src/kernel/_client.py
index 0238b203..64c7136b 100644
--- a/src/kernel/_client.py
+++ b/src/kernel/_client.py
@@ -266,7 +266,11 @@ def credentials(self) -> CredentialsResource:
@cached_property
def projects(self) -> ProjectsResource:
- """Create and manage projects for resource isolation within an organization."""
+ """
+ Create and manage projects for resource isolation within an organization.
+ When projects are disabled for the organization, project operations return
+ `404` with code `projects_disabled`.
+ """
from .resources.projects import ProjectsResource
return ProjectsResource(self)
@@ -618,7 +622,11 @@ def credentials(self) -> AsyncCredentialsResource:
@cached_property
def projects(self) -> AsyncProjectsResource:
- """Create and manage projects for resource isolation within an organization."""
+ """
+ Create and manage projects for resource isolation within an organization.
+ When projects are disabled for the organization, project operations return
+ `404` with code `projects_disabled`.
+ """
from .resources.projects import AsyncProjectsResource
return AsyncProjectsResource(self)
@@ -878,7 +886,11 @@ def credentials(self) -> credentials.CredentialsResourceWithRawResponse:
@cached_property
def projects(self) -> projects.ProjectsResourceWithRawResponse:
- """Create and manage projects for resource isolation within an organization."""
+ """
+ Create and manage projects for resource isolation within an organization.
+ When projects are disabled for the organization, project operations return
+ `404` with code `projects_disabled`.
+ """
from .resources.projects import ProjectsResourceWithRawResponse
return ProjectsResourceWithRawResponse(self._client.projects)
@@ -988,7 +1000,11 @@ def credentials(self) -> credentials.AsyncCredentialsResourceWithRawResponse:
@cached_property
def projects(self) -> projects.AsyncProjectsResourceWithRawResponse:
- """Create and manage projects for resource isolation within an organization."""
+ """
+ Create and manage projects for resource isolation within an organization.
+ When projects are disabled for the organization, project operations return
+ `404` with code `projects_disabled`.
+ """
from .resources.projects import AsyncProjectsResourceWithRawResponse
return AsyncProjectsResourceWithRawResponse(self._client.projects)
@@ -1098,7 +1114,11 @@ def credentials(self) -> credentials.CredentialsResourceWithStreamingResponse:
@cached_property
def projects(self) -> projects.ProjectsResourceWithStreamingResponse:
- """Create and manage projects for resource isolation within an organization."""
+ """
+ Create and manage projects for resource isolation within an organization.
+ When projects are disabled for the organization, project operations return
+ `404` with code `projects_disabled`.
+ """
from .resources.projects import ProjectsResourceWithStreamingResponse
return ProjectsResourceWithStreamingResponse(self._client.projects)
@@ -1208,7 +1228,11 @@ def credentials(self) -> credentials.AsyncCredentialsResourceWithStreamingRespon
@cached_property
def projects(self) -> projects.AsyncProjectsResourceWithStreamingResponse:
- """Create and manage projects for resource isolation within an organization."""
+ """
+ Create and manage projects for resource isolation within an organization.
+ When projects are disabled for the organization, project operations return
+ `404` with code `projects_disabled`.
+ """
from .resources.projects import AsyncProjectsResourceWithStreamingResponse
return AsyncProjectsResourceWithStreamingResponse(self._client.projects)
diff --git a/src/kernel/resources/projects/limits.py b/src/kernel/resources/projects/limits.py
index 1e05711f..f8447024 100644
--- a/src/kernel/resources/projects/limits.py
+++ b/src/kernel/resources/projects/limits.py
@@ -24,7 +24,11 @@
class LimitsResource(SyncAPIResource):
- """Create and manage projects for resource isolation within an organization."""
+ """
+ Create and manage projects for resource isolation within an organization.
+ When projects are disabled for the organization, project operations return
+ `404` with code `projects_disabled`.
+ """
@cached_property
def with_raw_response(self) -> LimitsResourceWithRawResponse:
@@ -140,7 +144,11 @@ def update(
class AsyncLimitsResource(AsyncAPIResource):
- """Create and manage projects for resource isolation within an organization."""
+ """
+ Create and manage projects for resource isolation within an organization.
+ When projects are disabled for the organization, project operations return
+ `404` with code `projects_disabled`.
+ """
@cached_property
def with_raw_response(self) -> AsyncLimitsResourceWithRawResponse:
diff --git a/src/kernel/resources/projects/projects.py b/src/kernel/resources/projects/projects.py
index ca3d6189..2c3d3569 100644
--- a/src/kernel/resources/projects/projects.py
+++ b/src/kernel/resources/projects/projects.py
@@ -33,11 +33,19 @@
class ProjectsResource(SyncAPIResource):
- """Create and manage projects for resource isolation within an organization."""
+ """
+ Create and manage projects for resource isolation within an organization.
+ When projects are disabled for the organization, project operations return
+ `404` with code `projects_disabled`.
+ """
@cached_property
def limits(self) -> LimitsResource:
- """Create and manage projects for resource isolation within an organization."""
+ """
+ Create and manage projects for resource isolation within an organization.
+ When projects are disabled for the organization, project operations return
+ `404` with code `projects_disabled`.
+ """
return LimitsResource(self._client)
@cached_property
@@ -266,11 +274,19 @@ def delete(
class AsyncProjectsResource(AsyncAPIResource):
- """Create and manage projects for resource isolation within an organization."""
+ """
+ Create and manage projects for resource isolation within an organization.
+ When projects are disabled for the organization, project operations return
+ `404` with code `projects_disabled`.
+ """
@cached_property
def limits(self) -> AsyncLimitsResource:
- """Create and manage projects for resource isolation within an organization."""
+ """
+ Create and manage projects for resource isolation within an organization.
+ When projects are disabled for the organization, project operations return
+ `404` with code `projects_disabled`.
+ """
return AsyncLimitsResource(self._client)
@cached_property
@@ -520,7 +536,11 @@ def __init__(self, projects: ProjectsResource) -> None:
@cached_property
def limits(self) -> LimitsResourceWithRawResponse:
- """Create and manage projects for resource isolation within an organization."""
+ """
+ Create and manage projects for resource isolation within an organization.
+ When projects are disabled for the organization, project operations return
+ `404` with code `projects_disabled`.
+ """
return LimitsResourceWithRawResponse(self._projects.limits)
@@ -546,7 +566,11 @@ def __init__(self, projects: AsyncProjectsResource) -> None:
@cached_property
def limits(self) -> AsyncLimitsResourceWithRawResponse:
- """Create and manage projects for resource isolation within an organization."""
+ """
+ Create and manage projects for resource isolation within an organization.
+ When projects are disabled for the organization, project operations return
+ `404` with code `projects_disabled`.
+ """
return AsyncLimitsResourceWithRawResponse(self._projects.limits)
@@ -572,7 +596,11 @@ def __init__(self, projects: ProjectsResource) -> None:
@cached_property
def limits(self) -> LimitsResourceWithStreamingResponse:
- """Create and manage projects for resource isolation within an organization."""
+ """
+ Create and manage projects for resource isolation within an organization.
+ When projects are disabled for the organization, project operations return
+ `404` with code `projects_disabled`.
+ """
return LimitsResourceWithStreamingResponse(self._projects.limits)
@@ -598,5 +626,9 @@ def __init__(self, projects: AsyncProjectsResource) -> None:
@cached_property
def limits(self) -> AsyncLimitsResourceWithStreamingResponse:
- """Create and manage projects for resource isolation within an organization."""
+ """
+ Create and manage projects for resource isolation within an organization.
+ When projects are disabled for the organization, project operations return
+ `404` with code `projects_disabled`.
+ """
return AsyncLimitsResourceWithStreamingResponse(self._projects.limits)
From 8769915967ffed4931946e6df78892165ed02615 Mon Sep 17 00:00:00 2001
From: "stainless-app[bot]"
<142633134+stainless-app[bot]@users.noreply.github.com>
Date: Wed, 29 Jul 2026 13:47:10 +0000
Subject: [PATCH 153/366] release: 0.85.0
---
.release-please-manifest.json | 2 +-
CHANGELOG.md | 9 +++++++++
pyproject.toml | 2 +-
src/kernel/_version.py | 2 +-
4 files changed, 12 insertions(+), 3 deletions(-)
diff --git a/.release-please-manifest.json b/.release-please-manifest.json
index 47dd76c1..ad502a4b 100644
--- a/.release-please-manifest.json
+++ b/.release-please-manifest.json
@@ -1,3 +1,3 @@
{
- ".": "0.84.0"
+ ".": "0.85.0"
}
\ No newline at end of file
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 53b7644d..fc63ed28 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,14 @@
# Changelog
+## 0.85.0 (2026-07-29)
+
+Full Changelog: [v0.84.0...v0.85.0](https://github.com/kernel/kernel-python-sdk/compare/v0.84.0...v0.85.0)
+
+### Features
+
+* Add encrypted per-proxy CA bundle for BYO MITM proxies ([c3f4571](https://github.com/kernel/kernel-python-sdk/commit/c3f4571357f66bc38352169f603da1256ea5b152))
+* Stabilize project lifecycle error codes ([5102f24](https://github.com/kernel/kernel-python-sdk/commit/5102f24f2583e0bfa8b21539cb822a65451b3df8))
+
## 0.84.0 (2026-07-27)
Full Changelog: [v0.83.0...v0.84.0](https://github.com/kernel/kernel-python-sdk/compare/v0.83.0...v0.84.0)
diff --git a/pyproject.toml b/pyproject.toml
index 8b7dbfbb..237d3ac1 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -1,6 +1,6 @@
[project]
name = "kernel"
-version = "0.84.0"
+version = "0.85.0"
description = "The official Python library for the kernel API"
dynamic = ["readme"]
license = "Apache-2.0"
diff --git a/src/kernel/_version.py b/src/kernel/_version.py
index c6ab69f7..e7464986 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.84.0" # x-release-please-version
+__version__ = "0.85.0" # x-release-please-version
From 6b5edd27ef92ea267fc52e3ef2344fb8667d440d Mon Sep 17 00:00:00 2001
From: "stainless-app[bot]"
<142633134+stainless-app[bot]@users.noreply.github.com>
Date: Fri, 31 Jul 2026 18:06:49 +0000
Subject: [PATCH 154/366] feat: Report the unified concurrency ceiling from the
org limits endpoint
---
.stats.yml | 4 ++--
src/kernel/resources/projects/limits.py | 6 ++----
src/kernel/types/projects/limit_update_params.py | 3 +--
src/kernel/types/projects/project_limits.py | 2 +-
4 files changed, 6 insertions(+), 9 deletions(-)
diff --git a/.stats.yml b/.stats.yml
index e1475cae..0f14b3da 100644
--- a/.stats.yml
+++ b/.stats.yml
@@ -1,4 +1,4 @@
configured_endpoints: 127
-openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel/kernel-7b03462454d6fa8390eaebc1b411d3e160501bf96abb93b75d38dfc382d4ce51.yml
-openapi_spec_hash: ba55400aafb4759cc06ec139fe528dfc
+openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel/kernel-7d774918a5bbe0a980a1dca0322e74d2b4e3d3372a2cbc9513e48f4b8b6abfa1.yml
+openapi_spec_hash: 8007b5846ff01c221e29d467b1cb8aaf
config_hash: 77ee715aa17061166f9a02b264a21b8d
diff --git a/src/kernel/resources/projects/limits.py b/src/kernel/resources/projects/limits.py
index f8447024..87227768 100644
--- a/src/kernel/resources/projects/limits.py
+++ b/src/kernel/resources/projects/limits.py
@@ -113,8 +113,7 @@ def update(
unchanged.
max_pooled_sessions: Deprecated: pooled browsers now count toward `max_concurrent_sessions`. Requests
- that set this field are rejected with a 400 once the unified concurrency limit
- is enabled for your organization.
+ that set this field are rejected with a 400.
extra_headers: Send extra headers
@@ -233,8 +232,7 @@ async def update(
unchanged.
max_pooled_sessions: Deprecated: pooled browsers now count toward `max_concurrent_sessions`. Requests
- that set this field are rejected with a 400 once the unified concurrency limit
- is enabled for your organization.
+ that set this field are rejected with a 400.
extra_headers: Send extra headers
diff --git a/src/kernel/types/projects/limit_update_params.py b/src/kernel/types/projects/limit_update_params.py
index 8bfb2775..e8bd1b60 100644
--- a/src/kernel/types/projects/limit_update_params.py
+++ b/src/kernel/types/projects/limit_update_params.py
@@ -25,6 +25,5 @@ class LimitUpdateParams(TypedDict, total=False):
max_pooled_sessions: Optional[int]
"""Deprecated: pooled browsers now count toward `max_concurrent_sessions`.
- Requests that set this field are rejected with a 400 once the unified
- concurrency limit is enabled for your organization.
+ Requests that set this field are rejected with a 400.
"""
diff --git a/src/kernel/types/projects/project_limits.py b/src/kernel/types/projects/project_limits.py
index a7f5cd74..712e84c1 100644
--- a/src/kernel/types/projects/project_limits.py
+++ b/src/kernel/types/projects/project_limits.py
@@ -24,5 +24,5 @@ class ProjectLimits(BaseModel):
max_pooled_sessions: Optional[int] = None
"""Deprecated: pooled browsers now count toward `max_concurrent_sessions`.
- Always null once the unified concurrency limit is enabled for your organization.
+ Always null.
"""
From 251fa8b546ae8d39b9e3e913e4757db9dee1d92c Mon Sep 17 00:00:00 2001
From: "stainless-app[bot]"
<142633134+stainless-app[bot]@users.noreply.github.com>
Date: Sat, 1 Aug 2026 15:21:33 +0000
Subject: [PATCH 155/366] feat: Harden managed auth verification and login
recovery
---
.stats.yml | 4 ++--
src/kernel/types/auth/connection_follow_response.py | 6 ++++++
src/kernel/types/auth/managed_auth.py | 6 ++++++
3 files changed, 14 insertions(+), 2 deletions(-)
diff --git a/.stats.yml b/.stats.yml
index 0f14b3da..f433e790 100644
--- a/.stats.yml
+++ b/.stats.yml
@@ -1,4 +1,4 @@
configured_endpoints: 127
-openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel/kernel-7d774918a5bbe0a980a1dca0322e74d2b4e3d3372a2cbc9513e48f4b8b6abfa1.yml
-openapi_spec_hash: 8007b5846ff01c221e29d467b1cb8aaf
+openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel/kernel-3d361d4878b48802a1dc486fa597b6b01ce23099d6627bcf836a849f0080c0c7.yml
+openapi_spec_hash: f21df7b9241e2e8b8dc04fdae30a3e27
config_hash: 77ee715aa17061166f9a02b264a21b8d
diff --git a/src/kernel/types/auth/connection_follow_response.py b/src/kernel/types/auth/connection_follow_response.py
index 7d6dfc63..7a0ecd05 100644
--- a/src/kernel/types/auth/connection_follow_response.py
+++ b/src/kernel/types/auth/connection_follow_response.py
@@ -94,6 +94,12 @@ class ManagedAuthStateEventField(BaseModel):
observed_selector: Optional[str] = None
"""Selector for the visible field, when available."""
+ replace_existing: Optional[bool] = None
+ """
+ Whether the submitted value must replace an existing credential after explicit
+ rejection.
+ """
+
required: Optional[bool] = None
"""Whether this field is required."""
diff --git a/src/kernel/types/auth/managed_auth.py b/src/kernel/types/auth/managed_auth.py
index 73a4d48a..44e32270 100644
--- a/src/kernel/types/auth/managed_auth.py
+++ b/src/kernel/types/auth/managed_auth.py
@@ -148,6 +148,12 @@ class Field(BaseModel):
observed_selector: Optional[str] = None
"""Selector for the visible field, when available."""
+ replace_existing: Optional[bool] = None
+ """
+ Whether the submitted value must replace an existing credential after explicit
+ rejection.
+ """
+
required: Optional[bool] = None
"""Whether this field is required."""
From 1c93a59fa77798c1bf1752d19253a2bc1c8bbd45 Mon Sep 17 00:00:00 2001
From: "stainless-app[bot]"
<142633134+stainless-app[bot]@users.noreply.github.com>
Date: Mon, 3 Aug 2026 20:54:22 +0000
Subject: [PATCH 156/366] codegen metadata
---
.stats.yml | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/.stats.yml b/.stats.yml
index f433e790..3068025f 100644
--- a/.stats.yml
+++ b/.stats.yml
@@ -1,4 +1,4 @@
configured_endpoints: 127
-openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel/kernel-3d361d4878b48802a1dc486fa597b6b01ce23099d6627bcf836a849f0080c0c7.yml
-openapi_spec_hash: f21df7b9241e2e8b8dc04fdae30a3e27
+openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel/kernel-61255bf30e3e3af67416f691a1a6544324415b986ccfcc5995c55e26f25b89d8.yml
+openapi_spec_hash: 4fb35a47c7ef551b00229e47b5aa7bf3
config_hash: 77ee715aa17061166f9a02b264a21b8d
From f289569129269c5c8cefdf202c0bbcbe27ada3ec Mon Sep 17 00:00:00 2001
From: "stainless-app[bot]"
<142633134+stainless-app[bot]@users.noreply.github.com>
Date: Mon, 3 Aug 2026 21:16:22 +0000
Subject: [PATCH 157/366] codegen metadata
---
.stats.yml | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/.stats.yml b/.stats.yml
index 3068025f..fcd634ae 100644
--- a/.stats.yml
+++ b/.stats.yml
@@ -1,4 +1,4 @@
configured_endpoints: 127
-openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel/kernel-61255bf30e3e3af67416f691a1a6544324415b986ccfcc5995c55e26f25b89d8.yml
-openapi_spec_hash: 4fb35a47c7ef551b00229e47b5aa7bf3
+openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel/kernel-3fc18876d59a47ed6993892cc5f69d7461fe2ca6a94fad43905677955a5adc55.yml
+openapi_spec_hash: 75d990158aeb0dac3d3ef8da476e86b9
config_hash: 77ee715aa17061166f9a02b264a21b8d
From 140bfddc004f7699570c80dc35e5d3620e863d24 Mon Sep 17 00:00:00 2001
From: "stainless-app[bot]"
<142633134+stainless-app[bot]@users.noreply.github.com>
Date: Mon, 3 Aug 2026 21:16:48 +0000
Subject: [PATCH 158/366] release: 0.86.0
---
.release-please-manifest.json | 2 +-
CHANGELOG.md | 9 +++++++++
pyproject.toml | 2 +-
src/kernel/_version.py | 2 +-
4 files changed, 12 insertions(+), 3 deletions(-)
diff --git a/.release-please-manifest.json b/.release-please-manifest.json
index ad502a4b..53079579 100644
--- a/.release-please-manifest.json
+++ b/.release-please-manifest.json
@@ -1,3 +1,3 @@
{
- ".": "0.85.0"
+ ".": "0.86.0"
}
\ No newline at end of file
diff --git a/CHANGELOG.md b/CHANGELOG.md
index fc63ed28..b177e1d3 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,14 @@
# Changelog
+## 0.86.0 (2026-08-03)
+
+Full Changelog: [v0.85.0...v0.86.0](https://github.com/kernel/kernel-python-sdk/compare/v0.85.0...v0.86.0)
+
+### Features
+
+* Harden managed auth verification and login recovery ([1d83850](https://github.com/kernel/kernel-python-sdk/commit/1d83850b39851ccfe34a229e2489d834609ffeeb))
+* Report the unified concurrency ceiling from the org limits endpoint ([63775d8](https://github.com/kernel/kernel-python-sdk/commit/63775d8640b52f7b26e40343f870370166ab53dc))
+
## 0.85.0 (2026-07-29)
Full Changelog: [v0.84.0...v0.85.0](https://github.com/kernel/kernel-python-sdk/compare/v0.84.0...v0.85.0)
diff --git a/pyproject.toml b/pyproject.toml
index 237d3ac1..9fbdf513 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -1,6 +1,6 @@
[project]
name = "kernel"
-version = "0.85.0"
+version = "0.86.0"
description = "The official Python library for the kernel API"
dynamic = ["readme"]
license = "Apache-2.0"
diff --git a/src/kernel/_version.py b/src/kernel/_version.py
index e7464986..c2d399cd 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.85.0" # x-release-please-version
+__version__ = "0.86.0" # x-release-please-version
From bf366f0982dde8ddc7dc5030aecb573b66a4c9fa Mon Sep 17 00:00:00 2001
From: stlc-bot
Date: Tue, 4 Aug 2026 21:00:00 +0000
Subject: [PATCH 159/366] ci: add stlc promotion workflows
---
.github/workflows/stlc-promote.yml | 61 ++++++++++++++++
.github/workflows/stlc-sync.yml | 107 +++++++++++++++++++++++++++++
2 files changed, 168 insertions(+)
create mode 100644 .github/workflows/stlc-promote.yml
create mode 100644 .github/workflows/stlc-sync.yml
diff --git a/.github/workflows/stlc-promote.yml b/.github/workflows/stlc-promote.yml
new file mode 100644
index 00000000..b79b456f
--- /dev/null
+++ b/.github/workflows/stlc-promote.yml
@@ -0,0 +1,61 @@
+name: Promote SDKs
+
+# Manually fast-forwards production main to the reviewed staging main. The
+# ancestor check refuses divergent histories; this workflow never force-pushes.
+on:
+ workflow_dispatch: {}
+
+permissions:
+ contents: read
+
+jobs:
+ promote:
+ if: github.repository == 'kernel/kernel-python-sdk-staging'
+ runs-on: ${{ vars.STLC_RUNNER || 'ubuntu-latest' }}
+ environment: production
+ steps:
+ - name: Check out staging
+ uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
+ with:
+ fetch-depth: 0
+ persist-credentials: false
+
+ - name: Mint production token
+ id: production-token
+ uses: actions/create-github-app-token@d72941d797fd3113feb6b93fd0dec494b13a2547 # v1
+ with:
+ app-id: ${{ secrets.ADMIN_APP_ID }}
+ private-key: ${{ secrets.ADMIN_APP_PRIVATE_KEY }}
+ owner: kernel
+ repositories: kernel-python-sdk
+ permission-contents: write
+ permission-workflows: write
+
+ - name: Fetch production main
+ env:
+ GH_TOKEN: ${{ steps.production-token.outputs.token }}
+ PRODUCTION_REPO: kernel/kernel-python-sdk
+ run: |
+ git remote add production "https://x-access-token:${GH_TOKEN}@github.com/${PRODUCTION_REPO}.git"
+ git fetch production main
+
+ - name: Check whether production already has staging's content
+ id: diff
+ run: |
+ MERGED=$(git merge-tree --write-tree production/main origin/main) || MERGED=conflict
+ PRODUCTION_TREE=$(git rev-parse 'production/main^{tree}')
+ if [ "$MERGED" = "$PRODUCTION_TREE" ]; then
+ echo "Production already contains staging's content. Nothing to promote."
+ echo "synced=true" >> "$GITHUB_OUTPUT"
+ else
+ echo "synced=false" >> "$GITHUB_OUTPUT"
+ fi
+
+ - name: Promote staging to production
+ if: steps.diff.outputs.synced == 'false'
+ run: |
+ if ! git merge-base --is-ancestor production/main origin/main; then
+ echo "::error title=Promote blocked::production/main is not an ancestor of staging main. Back-sync production first."
+ exit 1
+ fi
+ git push production origin/main:refs/heads/main
diff --git a/.github/workflows/stlc-sync.yml b/.github/workflows/stlc-sync.yml
new file mode 100644
index 00000000..216712f2
--- /dev/null
+++ b/.github/workflows/stlc-sync.yml
@@ -0,0 +1,107 @@
+name: Sync SDK repos
+
+# Keeps production and staging on one fast-forward-only history. Optional
+# dispatch tokens make the polling loop eager; the scheduled back-sync is the
+# safety backstop when those secrets are absent.
+on:
+ schedule:
+ - cron: '7,37 * * * *'
+ workflow_dispatch: {}
+ repository_dispatch:
+ types: [prod-released]
+ release:
+ types: [published]
+ push:
+ branches: [main]
+
+jobs:
+ back-sync:
+ if: >-
+ github.repository == 'kernel/kernel-python-sdk-staging' &&
+ (github.event_name == 'schedule' || github.event_name == 'workflow_dispatch' || github.event_name == 'repository_dispatch')
+ runs-on: ${{ vars.STLC_RUNNER || 'ubuntu-latest' }}
+ permissions:
+ contents: write
+ concurrency:
+ group: stlc-back-sync
+ cancel-in-progress: true
+ steps:
+ - name: Check out staging
+ uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
+ with:
+ fetch-depth: 0
+
+ - name: Fetch production main
+ run: |
+ git remote add production "https://github.com/kernel/kernel-python-sdk.git"
+ git -c "http.https://github.com/.extraheader=" fetch production main
+
+ - name: Check whether production has content staging lacks
+ id: diff
+ run: |
+ MERGED=$(git merge-tree --write-tree origin/main production/main) || MERGED=conflict
+ STAGING_TREE=$(git rev-parse 'origin/main^{tree}')
+ if [ "$MERGED" = "$STAGING_TREE" ]; then
+ echo "Staging already has production's content. Nothing to pull back."
+ echo "behind=false" >> "$GITHUB_OUTPUT"
+ else
+ echo "behind=true" >> "$GITHUB_OUTPUT"
+ fi
+
+ - name: Sync production to staging
+ if: steps.diff.outputs.behind == 'true'
+ run: |
+ if ! git merge-base --is-ancestor origin/main production/main; then
+ echo "::error title=Back-sync blocked::staging main is not an ancestor of production/main."
+ exit 1
+ fi
+ git push origin production/main:refs/heads/main
+
+ notify-back-sync:
+ if: >-
+ github.repository == 'kernel/kernel-python-sdk' &&
+ (github.event_name == 'release' || github.event_name == 'workflow_dispatch')
+ runs-on: ${{ vars.STLC_RUNNER || 'ubuntu-latest' }}
+ permissions:
+ contents: read
+ steps:
+ - name: Dispatch back-sync to staging
+ env:
+ DISPATCH_TOKEN: ${{ secrets.STAGING_DISPATCH_TOKEN }}
+ REF_NAME: ${{ github.ref_name }}
+ run: |
+ set -euo pipefail
+ if [ -z "${DISPATCH_TOKEN:-}" ]; then
+ echo "::notice::STAGING_DISPATCH_TOKEN not configured; the scheduled back-sync remains active."
+ exit 0
+ fi
+ payload=$(jq -n --arg ref "$REF_NAME" '{event_type:"prod-released",client_payload:{ref:$ref}}')
+ curl --fail-with-body -sS -X POST -H "Authorization: Bearer $DISPATCH_TOKEN" -H "Accept: application/vnd.github+json" -H "X-GitHub-Api-Version: 2022-11-28" "https://api.github.com/repos/kernel/kernel-python-sdk-staging/dispatches" -d "$payload"
+
+ seal-dispatch:
+ if: github.repository == 'kernel/kernel-python-sdk-staging' && github.event_name == 'push'
+ runs-on: ${{ vars.STLC_RUNNER || 'ubuntu-latest' }}
+ permissions:
+ contents: read
+ concurrency:
+ group: seal-dispatch-${{ github.ref }}
+ cancel-in-progress: false
+ steps:
+ - name: Dispatch tracking sync
+ env:
+ DISPATCH_TOKEN: ${{ secrets.CONFIG_DISPATCH_TOKEN }}
+ HEAD_MSG: ${{ github.event.head_commit.message }}
+ HEAD_AUTHOR_NAME: ${{ github.event.head_commit.author.name }}
+ SHA: ${{ github.sha }}
+ run: |
+ set -euo pipefail
+ if printf '%s' "$HEAD_MSG" | grep -q 'Stainless-Generated-From' || [ "$HEAD_AUTHOR_NAME" = "stlc-bot" ]; then
+ echo "Generated commit; skipping tracking dispatch."
+ exit 0
+ fi
+ if [ -z "${DISPATCH_TOKEN:-}" ]; then
+ echo "::notice::CONFIG_DISPATCH_TOKEN not configured; the config repo's scheduled sync remains active."
+ exit 0
+ fi
+ payload=$(jq -n --arg sha "$SHA" --arg repo "${{ github.repository }}" '{event_type:"seal-custom-code",client_payload:{target:"all",sha:$sha,repo:$repo}}')
+ curl --fail-with-body -sS -X POST -H "Authorization: Bearer $DISPATCH_TOKEN" -H "Accept: application/vnd.github+json" -H "X-GitHub-Api-Version: 2022-11-28" "https://api.github.com/repos/kernel/kernel/dispatches" -d "$payload"
From 710780c7e6c7b2c868ab878e6d66d548eda17234 Mon Sep 17 00:00:00 2001
From: Steven Miller
Date: Tue, 4 Aug 2026 21:03:55 +0000
Subject: [PATCH 160/366] ci: promote through merge-commit pull requests
---
.github/workflows/stlc-promote.yml | 32 ++++++++++++++++++++++--------
1 file changed, 24 insertions(+), 8 deletions(-)
diff --git a/.github/workflows/stlc-promote.yml b/.github/workflows/stlc-promote.yml
index b79b456f..9c73f102 100644
--- a/.github/workflows/stlc-promote.yml
+++ b/.github/workflows/stlc-promote.yml
@@ -1,8 +1,11 @@
name: Promote SDKs
-# Manually fast-forwards production main to the reviewed staging main. The
-# ancestor check refuses divergent histories; this workflow never force-pushes.
+# Production requires pull requests, so staging is promoted through a merge-
+# commit PR. Never squash or rebase this cross-repo PR: preserving the incoming
+# commits keeps production and staging on one ancestry chain.
on:
+ push:
+ branches: [main]
workflow_dispatch: {}
permissions:
@@ -12,7 +15,9 @@ jobs:
promote:
if: github.repository == 'kernel/kernel-python-sdk-staging'
runs-on: ${{ vars.STLC_RUNNER || 'ubuntu-latest' }}
- environment: production
+ concurrency:
+ group: stlc-promote
+ cancel-in-progress: true
steps:
- name: Check out staging
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
@@ -30,6 +35,7 @@ jobs:
repositories: kernel-python-sdk
permission-contents: write
permission-workflows: write
+ permission-pull-requests: write
- name: Fetch production main
env:
@@ -51,11 +57,21 @@ jobs:
echo "synced=false" >> "$GITHUB_OUTPUT"
fi
- - name: Promote staging to production
+ - name: Push the production release branch
if: steps.diff.outputs.synced == 'false'
+ env:
+ GH_TOKEN: ${{ steps.production-token.outputs.token }}
+ PRODUCTION_REPO: kernel/kernel-python-sdk
+ run: git push production origin/main:refs/heads/stainless/release --force
+
+ - name: Open or update the promote PR
+ if: steps.diff.outputs.synced == 'false'
+ env:
+ GH_TOKEN: ${{ steps.production-token.outputs.token }}
+ PRODUCTION_REPO: kernel/kernel-python-sdk
run: |
- if ! git merge-base --is-ancestor production/main origin/main; then
- echo "::error title=Promote blocked::production/main is not an ancestor of staging main. Back-sync production first."
- exit 1
+ existing=$(gh pr list --repo "$PRODUCTION_REPO" --head stainless/release --state open --json number --jq '.[0].number')
+ if [ -z "$existing" ]; then
+ gh pr create --repo "$PRODUCTION_REPO" --base main --head stainless/release --title "Release SDK updates" --body "$(git log --oneline production/main..origin/main)"
fi
- git push production origin/main:refs/heads/main
+ gh pr merge stainless/release --repo "$PRODUCTION_REPO" --merge --auto || echo "Auto-merge unavailable; merge the promote PR manually with a merge commit."
From 53eb143fc833c9588b1503740140888e856573d2 Mon Sep 17 00:00:00 2001
From: Steven Miller
Date: Wed, 5 Aug 2026 13:09:02 +0000
Subject: [PATCH 161/366] ci: harden promotion pull request updates
---
.github/workflows/stlc-promote.yml | 12 +++++++++---
1 file changed, 9 insertions(+), 3 deletions(-)
diff --git a/.github/workflows/stlc-promote.yml b/.github/workflows/stlc-promote.yml
index 9c73f102..5123a90a 100644
--- a/.github/workflows/stlc-promote.yml
+++ b/.github/workflows/stlc-promote.yml
@@ -70,8 +70,14 @@ jobs:
GH_TOKEN: ${{ steps.production-token.outputs.token }}
PRODUCTION_REPO: kernel/kernel-python-sdk
run: |
- existing=$(gh pr list --repo "$PRODUCTION_REPO" --head stainless/release --state open --json number --jq '.[0].number')
+ body=$(mktemp)
+ git log --oneline production/main..origin/main > "$body"
+ existing=$(gh pr list --repo "$PRODUCTION_REPO" --head stainless/release --state open --json number --jq 'if length == 0 then "" else .[0].number end')
if [ -z "$existing" ]; then
- gh pr create --repo "$PRODUCTION_REPO" --base main --head stainless/release --title "Release SDK updates" --body "$(git log --oneline production/main..origin/main)"
+ gh pr create --repo "$PRODUCTION_REPO" --base main --head stainless/release --title "Release SDK updates" --body-file "$body"
+ else
+ gh pr edit "$existing" --repo "$PRODUCTION_REPO" --title "Release SDK updates" --body-file "$body"
+ fi
+ if ! gh pr merge stainless/release --repo "$PRODUCTION_REPO" --merge --auto; then
+ echo "::warning title=Manual promotion required::Merge the promote PR with a merge commit."
fi
- gh pr merge stainless/release --repo "$PRODUCTION_REPO" --merge --auto || echo "Auto-merge unavailable; merge the promote PR manually with a merge commit."
From 4fbcc0268140e8438b44d932379aca103219d998 Mon Sep 17 00:00:00 2001
From: "kernel-internal[bot]"
<260533166+kernel-internal[bot]@users.noreply.github.com>
Date: Thu, 6 Aug 2026 00:44:06 +0000
Subject: [PATCH 162/366] Generate SDKs with stlc instead of hosted Stainless
Stainless-Generated-From: efeda6464d2aaef6c7323ef46fd10f2844cf2b39
---
.github/workflows/release-please.yml | 20 +++++++
.stats.yml | 3 -
api.md | 2 +
release-please-config.json | 4 +-
scripts/utils/upload-artifact.sh | 2 +-
src/kernel/resources/browsers/browsers.py | 4 +-
.../types/auth/connection_create_params.py | 60 ++++++++++++++++++-
.../types/auth/connection_login_params.py | 59 +++++++++++++++++-
.../types/auth/connection_update_params.py | 60 ++++++++++++++++++-
src/kernel/types/auth/managed_auth.py | 53 ++++++++++++++++
src/kernel/types/browser_create_params.py | 58 +++++++++++++++++-
src/kernel/types/browser_list_params.py | 4 +-
.../types/browser_pool_acquire_params.py | 58 +++++++++++++++++-
.../types/browser_pool_create_params.py | 59 +++++++++++++++++-
.../types/browser_pool_update_params.py | 59 +++++++++++++++++-
src/kernel/types/browser_update_params.py | 59 +++++++++++++++++-
src/kernel/types/browsers/__init__.py | 2 +
.../browsers/browser_telemetry_config.py | 7 +++
.../browser_telemetry_export_config.py | 15 +++++
.../browser_telemetry_otlp_export_config.py | 20 +++++++
tests/api_resources/auth/test_connections.py | 54 +++++++++++++++++
tests/api_resources/test_browser_pools.py | 54 +++++++++++++++++
tests/api_resources/test_browsers.py | 36 +++++++++++
23 files changed, 735 insertions(+), 17 deletions(-)
create mode 100644 .github/workflows/release-please.yml
create mode 100644 src/kernel/types/browsers/browser_telemetry_export_config.py
create mode 100644 src/kernel/types/browsers/browser_telemetry_otlp_export_config.py
diff --git a/.github/workflows/release-please.yml b/.github/workflows/release-please.yml
new file mode 100644
index 00000000..a18a3f9e
--- /dev/null
+++ b/.github/workflows/release-please.yml
@@ -0,0 +1,20 @@
+name: Release Please
+on:
+ push:
+ branches:
+ - main
+
+permissions:
+ contents: write
+ pull-requests: write
+
+jobs:
+ release-please:
+ if: github.repository == 'kernel/kernel-python-sdk'
+ runs-on: ubuntu-latest
+
+ steps:
+ - uses: googleapis/release-please-action@5c625bfb5d1ff62eadeeb3772007f7f66fdcf071 # v4.4.1
+ id: release
+ with:
+ token: ${{ secrets.RELEASE_PLEASE_TOKEN }}
diff --git a/.stats.yml b/.stats.yml
index fcd634ae..71c320fc 100644
--- a/.stats.yml
+++ b/.stats.yml
@@ -1,4 +1 @@
configured_endpoints: 127
-openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel/kernel-3fc18876d59a47ed6993892cc5f69d7461fe2ca6a94fad43905677955a5adc55.yml
-openapi_spec_hash: 75d990158aeb0dac3d3ef8da476e86b9
-config_hash: 77ee715aa17061166f9a02b264a21b8d
diff --git a/api.md b/api.md
index edc133e0..2685a9d5 100644
--- a/api.md
+++ b/api.md
@@ -146,6 +146,8 @@ from kernel.types.browsers import (
BrowserTelemetryCategoryConfig,
BrowserTelemetryConfig,
BrowserTelemetryEvent,
+ BrowserTelemetryExportConfig,
+ BrowserTelemetryOtlpExportConfig,
TelemetryEventsResponse,
TelemetryStreamResponse,
)
diff --git a/release-please-config.json b/release-please-config.json
index 942ec08a..bdba3b86 100644
--- a/release-please-config.json
+++ b/release-please-config.json
@@ -2,11 +2,11 @@
"packages": {
".": {}
},
- "$schema": "https://raw.githubusercontent.com/stainless-api/release-please/main/schemas/config.json",
+ "$schema": "https://raw.githubusercontent.com/googleapis/release-please/main/schemas/config.json",
"include-v-in-tag": true,
"include-component-in-tag": false,
"versioning": "prerelease",
- "prerelease": true,
+ "prerelease": false,
"bump-minor-pre-major": true,
"bump-patch-for-minor-pre-major": false,
"pull-request-header": "Automated Release PR",
diff --git a/scripts/utils/upload-artifact.sh b/scripts/utils/upload-artifact.sh
index 14b2cc82..960fa64b 100755
--- a/scripts/utils/upload-artifact.sh
+++ b/scripts/utils/upload-artifact.sh
@@ -20,7 +20,7 @@ UPLOAD_RESPONSE=$(curl -v -X PUT \
if echo "$UPLOAD_RESPONSE" | grep -q "HTTP/[0-9.]* 200"; then
echo -e "\033[32mUploaded build to Stainless storage.\033[0m"
- echo -e "\033[32mInstallation: pip install 'https://pkg.stainless.com/s/kernel-python/$SHA/$FILENAME'\033[0m"
+ echo -e "\033[32mInstallation: pip install 'https://pkg.stainless.com/s/kernel-python-sdk-staging/$SHA/$FILENAME'\033[0m"
else
echo -e "\033[31mFailed to upload artifact.\033[0m"
exit 1
diff --git a/src/kernel/resources/browsers/browsers.py b/src/kernel/resources/browsers/browsers.py
index ae4279ed..553a98dd 100644
--- a/src/kernel/resources/browsers/browsers.py
+++ b/src/kernel/resources/browsers/browsers.py
@@ -439,7 +439,7 @@ def list(
offset: Number of results to skip. Defaults to 0.
- query: Search browsers by name, session ID, profile ID, proxy ID, or pool name.
+ query: Search browsers by name, session ID, profile name or ID, proxy ID, or pool name.
status: Filter sessions by status. "active" returns only active sessions (default),
"deleted" returns only soft-deleted sessions, "all" returns both.
@@ -1017,7 +1017,7 @@ def list(
offset: Number of results to skip. Defaults to 0.
- query: Search browsers by name, session ID, profile ID, proxy ID, or pool name.
+ query: Search browsers by name, session ID, profile name or ID, proxy ID, or pool name.
status: Filter sessions by status. "active" returns only active sessions (default),
"deleted" returns only soft-deleted sessions, "all" returns both.
diff --git a/src/kernel/types/auth/connection_create_params.py b/src/kernel/types/auth/connection_create_params.py
index ed94bf37..407857be 100644
--- a/src/kernel/types/auth/connection_create_params.py
+++ b/src/kernel/types/auth/connection_create_params.py
@@ -8,7 +8,15 @@
from ..._types import SequenceNotStr
from ..browsers.browser_telemetry_categories_config_param import BrowserTelemetryCategoriesConfigParam
-__all__ = ["ConnectionCreateParams", "BrowserTelemetry", "Credential", "Proxy"]
+__all__ = [
+ "ConnectionCreateParams",
+ "BrowserTelemetry",
+ "BrowserTelemetryExport",
+ "BrowserTelemetryExportOtlp",
+ "BrowserTelemetryExportOtlpDestination",
+ "Credential",
+ "Proxy",
+]
class ConnectionCreateParams(TypedDict, total=False):
@@ -111,6 +119,50 @@ class ConnectionCreateParams(TypedDict, total=False):
"""
+class BrowserTelemetryExportOtlpDestination(TypedDict, total=False):
+ """OTLP destination to export this session's captured telemetry to.
+
+ Provide either id or name. Requires telemetry capture to be enabled.
+ """
+
+ id: str
+ """OTLP destination ID"""
+
+ name: str
+ """OTLP destination name"""
+
+
+class BrowserTelemetryExportOtlp(TypedDict, total=False):
+ """
+ Export captured telemetry over OTLP to one of the org's configured destinations.
+ """
+
+ destination: BrowserTelemetryExportOtlpDestination
+ """OTLP destination to export this session's captured telemetry to.
+
+ Provide either id or name. Requires telemetry capture to be enabled.
+ """
+
+ enabled: bool
+ """Whether to export captured telemetry over OTLP.
+
+ Setting destination implies enabled=true, so this only needs to be set
+ explicitly to disable export (enabled=false with a destination is rejected).
+ """
+
+
+class BrowserTelemetryExport(TypedDict, total=False):
+ """Where to export this session's captured telemetry.
+
+ Omit to capture without exporting.
+ """
+
+ otlp: BrowserTelemetryExportOtlp
+ """
+ Export captured telemetry over OTLP to one of the org's configured destinations.
+ """
+
+
class BrowserTelemetry(TypedDict, total=False):
"""
Browser telemetry configuration used by this connection's browser sessions by default. Uses the exact create-browser configuration. Can be overridden per-login.
@@ -143,6 +195,12 @@ class BrowserTelemetry(TypedDict, total=False):
settings.
"""
+ export: BrowserTelemetryExport
+ """Where to export this session's captured telemetry.
+
+ Omit to capture without exporting.
+ """
+
class Credential(TypedDict, total=False):
"""Reference to credentials for the auth connection.
diff --git a/src/kernel/types/auth/connection_login_params.py b/src/kernel/types/auth/connection_login_params.py
index 719ea678..117d3c7d 100644
--- a/src/kernel/types/auth/connection_login_params.py
+++ b/src/kernel/types/auth/connection_login_params.py
@@ -7,7 +7,14 @@
from ..browsers.browser_telemetry_categories_config_param import BrowserTelemetryCategoriesConfigParam
-__all__ = ["ConnectionLoginParams", "BrowserTelemetry", "Proxy"]
+__all__ = [
+ "ConnectionLoginParams",
+ "BrowserTelemetry",
+ "BrowserTelemetryExport",
+ "BrowserTelemetryExportOtlp",
+ "BrowserTelemetryExportOtlpDestination",
+ "Proxy",
+]
class ConnectionLoginParams(TypedDict, total=False):
@@ -35,6 +42,50 @@ class ConnectionLoginParams(TypedDict, total=False):
"""
+class BrowserTelemetryExportOtlpDestination(TypedDict, total=False):
+ """OTLP destination to export this session's captured telemetry to.
+
+ Provide either id or name. Requires telemetry capture to be enabled.
+ """
+
+ id: str
+ """OTLP destination ID"""
+
+ name: str
+ """OTLP destination name"""
+
+
+class BrowserTelemetryExportOtlp(TypedDict, total=False):
+ """
+ Export captured telemetry over OTLP to one of the org's configured destinations.
+ """
+
+ destination: BrowserTelemetryExportOtlpDestination
+ """OTLP destination to export this session's captured telemetry to.
+
+ Provide either id or name. Requires telemetry capture to be enabled.
+ """
+
+ enabled: bool
+ """Whether to export captured telemetry over OTLP.
+
+ Setting destination implies enabled=true, so this only needs to be set
+ explicitly to disable export (enabled=false with a destination is rejected).
+ """
+
+
+class BrowserTelemetryExport(TypedDict, total=False):
+ """Where to export this session's captured telemetry.
+
+ Omit to capture without exporting.
+ """
+
+ otlp: BrowserTelemetryExportOtlp
+ """
+ Export captured telemetry over OTLP to one of the org's configured destinations.
+ """
+
+
class BrowserTelemetry(TypedDict, total=False):
"""Override the connection's default browser telemetry configuration for this login.
@@ -68,6 +119,12 @@ class BrowserTelemetry(TypedDict, total=False):
settings.
"""
+ export: BrowserTelemetryExport
+ """Where to export this session's captured telemetry.
+
+ Omit to capture without exporting.
+ """
+
class Proxy(TypedDict, total=False):
"""Proxy selection.
diff --git a/src/kernel/types/auth/connection_update_params.py b/src/kernel/types/auth/connection_update_params.py
index a14d486a..f92a40c7 100644
--- a/src/kernel/types/auth/connection_update_params.py
+++ b/src/kernel/types/auth/connection_update_params.py
@@ -8,7 +8,15 @@
from ..._types import SequenceNotStr
from ..browsers.browser_telemetry_categories_config_param import BrowserTelemetryCategoriesConfigParam
-__all__ = ["ConnectionUpdateParams", "BrowserTelemetry", "Credential", "Proxy"]
+__all__ = [
+ "ConnectionUpdateParams",
+ "BrowserTelemetry",
+ "BrowserTelemetryExport",
+ "BrowserTelemetryExportOtlp",
+ "BrowserTelemetryExportOtlpDestination",
+ "Credential",
+ "Proxy",
+]
class ConnectionUpdateParams(TypedDict, total=False):
@@ -72,6 +80,50 @@ class ConnectionUpdateParams(TypedDict, total=False):
"""Whether to save credentials after every successful login"""
+class BrowserTelemetryExportOtlpDestination(TypedDict, total=False):
+ """OTLP destination to export this session's captured telemetry to.
+
+ Provide either id or name. Requires telemetry capture to be enabled.
+ """
+
+ id: str
+ """OTLP destination ID"""
+
+ name: str
+ """OTLP destination name"""
+
+
+class BrowserTelemetryExportOtlp(TypedDict, total=False):
+ """
+ Export captured telemetry over OTLP to one of the org's configured destinations.
+ """
+
+ destination: BrowserTelemetryExportOtlpDestination
+ """OTLP destination to export this session's captured telemetry to.
+
+ Provide either id or name. Requires telemetry capture to be enabled.
+ """
+
+ enabled: bool
+ """Whether to export captured telemetry over OTLP.
+
+ Setting destination implies enabled=true, so this only needs to be set
+ explicitly to disable export (enabled=false with a destination is rejected).
+ """
+
+
+class BrowserTelemetryExport(TypedDict, total=False):
+ """Where to export this session's captured telemetry.
+
+ Omit to capture without exporting.
+ """
+
+ otlp: BrowserTelemetryExportOtlp
+ """
+ Export captured telemetry over OTLP to one of the org's configured destinations.
+ """
+
+
class BrowserTelemetry(TypedDict, total=False):
"""
Browser telemetry configuration used by future browser sessions for this connection. Uses the exact create-browser configuration. Set enabled to false to disable telemetry.
@@ -104,6 +156,12 @@ class BrowserTelemetry(TypedDict, total=False):
settings.
"""
+ export: BrowserTelemetryExport
+ """Where to export this session's captured telemetry.
+
+ Omit to capture without exporting.
+ """
+
class Credential(TypedDict, total=False):
"""Reference to credentials for the auth connection.
diff --git a/src/kernel/types/auth/managed_auth.py b/src/kernel/types/auth/managed_auth.py
index 44e32270..e3bf7b9c 100644
--- a/src/kernel/types/auth/managed_auth.py
+++ b/src/kernel/types/auth/managed_auth.py
@@ -10,6 +10,9 @@
__all__ = [
"ManagedAuth",
"BrowserTelemetry",
+ "BrowserTelemetryExport",
+ "BrowserTelemetryExportOtlp",
+ "BrowserTelemetryExportOtlpDestination",
"Choice",
"Credential",
"DiscoveredField",
@@ -20,6 +23,50 @@
]
+class BrowserTelemetryExportOtlpDestination(BaseModel):
+ """OTLP destination to export this session's captured telemetry to.
+
+ Provide either id or name. Requires telemetry capture to be enabled.
+ """
+
+ id: Optional[str] = None
+ """OTLP destination ID"""
+
+ name: Optional[str] = None
+ """OTLP destination name"""
+
+
+class BrowserTelemetryExportOtlp(BaseModel):
+ """
+ Export captured telemetry over OTLP to one of the org's configured destinations.
+ """
+
+ destination: Optional[BrowserTelemetryExportOtlpDestination] = None
+ """OTLP destination to export this session's captured telemetry to.
+
+ Provide either id or name. Requires telemetry capture to be enabled.
+ """
+
+ enabled: Optional[bool] = None
+ """Whether to export captured telemetry over OTLP.
+
+ Setting destination implies enabled=true, so this only needs to be set
+ explicitly to disable export (enabled=false with a destination is rejected).
+ """
+
+
+class BrowserTelemetryExport(BaseModel):
+ """Where to export this session's captured telemetry.
+
+ Omit to capture without exporting.
+ """
+
+ otlp: Optional[BrowserTelemetryExportOtlp] = None
+ """
+ Export captured telemetry over OTLP to one of the org's configured destinations.
+ """
+
+
class BrowserTelemetry(BaseModel):
"""
Browser telemetry configuration used by this connection's browser sessions by default. The exact create-browser configuration is preserved and can be overridden per-login.
@@ -52,6 +99,12 @@ class BrowserTelemetry(BaseModel):
settings.
"""
+ export: Optional[BrowserTelemetryExport] = None
+ """Where to export this session's captured telemetry.
+
+ Omit to capture without exporting.
+ """
+
class Choice(BaseModel):
"""Canonical auth-flow choice awaiting user selection."""
diff --git a/src/kernel/types/browser_create_params.py b/src/kernel/types/browser_create_params.py
index 01ac2598..f09a29cf 100644
--- a/src/kernel/types/browser_create_params.py
+++ b/src/kernel/types/browser_create_params.py
@@ -11,7 +11,13 @@
from .shared_params.browser_extension import BrowserExtension
from .browsers.browser_telemetry_categories_config_param import BrowserTelemetryCategoriesConfigParam
-__all__ = ["BrowserCreateParams", "Telemetry"]
+__all__ = [
+ "BrowserCreateParams",
+ "Telemetry",
+ "TelemetryExport",
+ "TelemetryExportOtlp",
+ "TelemetryExportOtlpDestination",
+]
class BrowserCreateParams(TypedDict, total=False):
@@ -125,6 +131,50 @@ class BrowserCreateParams(TypedDict, total=False):
"""
+class TelemetryExportOtlpDestination(TypedDict, total=False):
+ """OTLP destination to export this session's captured telemetry to.
+
+ Provide either id or name. Requires telemetry capture to be enabled.
+ """
+
+ id: str
+ """OTLP destination ID"""
+
+ name: str
+ """OTLP destination name"""
+
+
+class TelemetryExportOtlp(TypedDict, total=False):
+ """
+ Export captured telemetry over OTLP to one of the org's configured destinations.
+ """
+
+ destination: TelemetryExportOtlpDestination
+ """OTLP destination to export this session's captured telemetry to.
+
+ Provide either id or name. Requires telemetry capture to be enabled.
+ """
+
+ enabled: bool
+ """Whether to export captured telemetry over OTLP.
+
+ Setting destination implies enabled=true, so this only needs to be set
+ explicitly to disable export (enabled=false with a destination is rejected).
+ """
+
+
+class TelemetryExport(TypedDict, total=False):
+ """Where to export this session's captured telemetry.
+
+ Omit to capture without exporting.
+ """
+
+ otlp: TelemetryExportOtlp
+ """
+ Export captured telemetry over OTLP to one of the org's configured destinations.
+ """
+
+
class Telemetry(TypedDict, total=False):
"""Telemetry configuration for the browser session.
@@ -157,3 +207,9 @@ class Telemetry(TypedDict, total=False):
capture on create. enabled=false cannot be combined with browser category
settings.
"""
+
+ export: TelemetryExport
+ """Where to export this session's captured telemetry.
+
+ Omit to capture without exporting.
+ """
diff --git a/src/kernel/types/browser_list_params.py b/src/kernel/types/browser_list_params.py
index 4006dfbe..57de2521 100644
--- a/src/kernel/types/browser_list_params.py
+++ b/src/kernel/types/browser_list_params.py
@@ -23,7 +23,9 @@ class BrowserListParams(TypedDict, total=False):
"""Number of results to skip. Defaults to 0."""
query: str
- """Search browsers by name, session ID, profile ID, proxy ID, or pool name."""
+ """
+ Search browsers by name, session ID, profile name or ID, proxy ID, or pool name.
+ """
status: Literal["active", "deleted", "all"]
"""Filter sessions by status.
diff --git a/src/kernel/types/browser_pool_acquire_params.py b/src/kernel/types/browser_pool_acquire_params.py
index 70567ee3..bd50be1b 100644
--- a/src/kernel/types/browser_pool_acquire_params.py
+++ b/src/kernel/types/browser_pool_acquire_params.py
@@ -8,7 +8,13 @@
from .tags_param import TagsParam
from .browsers.browser_telemetry_categories_config_param import BrowserTelemetryCategoriesConfigParam
-__all__ = ["BrowserPoolAcquireParams", "Telemetry"]
+__all__ = [
+ "BrowserPoolAcquireParams",
+ "Telemetry",
+ "TelemetryExport",
+ "TelemetryExportOtlp",
+ "TelemetryExportOtlpDestination",
+]
class BrowserPoolAcquireParams(TypedDict, total=False):
@@ -54,6 +60,50 @@ class BrowserPoolAcquireParams(TypedDict, total=False):
"""
+class TelemetryExportOtlpDestination(TypedDict, total=False):
+ """OTLP destination to export this session's captured telemetry to.
+
+ Provide either id or name. Requires telemetry capture to be enabled.
+ """
+
+ id: str
+ """OTLP destination ID"""
+
+ name: str
+ """OTLP destination name"""
+
+
+class TelemetryExportOtlp(TypedDict, total=False):
+ """
+ Export captured telemetry over OTLP to one of the org's configured destinations.
+ """
+
+ destination: TelemetryExportOtlpDestination
+ """OTLP destination to export this session's captured telemetry to.
+
+ Provide either id or name. Requires telemetry capture to be enabled.
+ """
+
+ enabled: bool
+ """Whether to export captured telemetry over OTLP.
+
+ Setting destination implies enabled=true, so this only needs to be set
+ explicitly to disable export (enabled=false with a destination is rejected).
+ """
+
+
+class TelemetryExport(TypedDict, total=False):
+ """Where to export this session's captured telemetry.
+
+ Omit to capture without exporting.
+ """
+
+ otlp: TelemetryExportOtlp
+ """
+ Export captured telemetry over OTLP to one of the org's configured destinations.
+ """
+
+
class Telemetry(TypedDict, total=False):
"""Telemetry override for the acquired browser, applied to this lease only.
@@ -86,3 +136,9 @@ class Telemetry(TypedDict, total=False):
capture on create. enabled=false cannot be combined with browser category
settings.
"""
+
+ export: TelemetryExport
+ """Where to export this session's captured telemetry.
+
+ Omit to capture without exporting.
+ """
diff --git a/src/kernel/types/browser_pool_create_params.py b/src/kernel/types/browser_pool_create_params.py
index 376af6b6..fb6aa63b 100644
--- a/src/kernel/types/browser_pool_create_params.py
+++ b/src/kernel/types/browser_pool_create_params.py
@@ -9,7 +9,14 @@
from .shared_params.browser_extension import BrowserExtension
from .browsers.browser_telemetry_categories_config_param import BrowserTelemetryCategoriesConfigParam
-__all__ = ["BrowserPoolCreateParams", "Profile", "Telemetry"]
+__all__ = [
+ "BrowserPoolCreateParams",
+ "Profile",
+ "Telemetry",
+ "TelemetryExport",
+ "TelemetryExportOtlp",
+ "TelemetryExportOtlpDestination",
+]
class BrowserPoolCreateParams(TypedDict, total=False):
@@ -143,6 +150,50 @@ class Profile(TypedDict, total=False):
"""
+class TelemetryExportOtlpDestination(TypedDict, total=False):
+ """OTLP destination to export this session's captured telemetry to.
+
+ Provide either id or name. Requires telemetry capture to be enabled.
+ """
+
+ id: str
+ """OTLP destination ID"""
+
+ name: str
+ """OTLP destination name"""
+
+
+class TelemetryExportOtlp(TypedDict, total=False):
+ """
+ Export captured telemetry over OTLP to one of the org's configured destinations.
+ """
+
+ destination: TelemetryExportOtlpDestination
+ """OTLP destination to export this session's captured telemetry to.
+
+ Provide either id or name. Requires telemetry capture to be enabled.
+ """
+
+ enabled: bool
+ """Whether to export captured telemetry over OTLP.
+
+ Setting destination implies enabled=true, so this only needs to be set
+ explicitly to disable export (enabled=false with a destination is rejected).
+ """
+
+
+class TelemetryExport(TypedDict, total=False):
+ """Where to export this session's captured telemetry.
+
+ Omit to capture without exporting.
+ """
+
+ otlp: TelemetryExportOtlp
+ """
+ Export captured telemetry over OTLP to one of the org's configured destinations.
+ """
+
+
class Telemetry(TypedDict, total=False):
"""Telemetry configuration applied to browsers warmed into this pool.
@@ -175,3 +226,9 @@ class Telemetry(TypedDict, total=False):
capture on create. enabled=false cannot be combined with browser category
settings.
"""
+
+ export: TelemetryExport
+ """Where to export this session's captured telemetry.
+
+ Omit to capture without exporting.
+ """
diff --git a/src/kernel/types/browser_pool_update_params.py b/src/kernel/types/browser_pool_update_params.py
index b0956048..069181c3 100644
--- a/src/kernel/types/browser_pool_update_params.py
+++ b/src/kernel/types/browser_pool_update_params.py
@@ -9,7 +9,14 @@
from .shared_params.browser_extension import BrowserExtension
from .browsers.browser_telemetry_categories_config_param import BrowserTelemetryCategoriesConfigParam
-__all__ = ["BrowserPoolUpdateParams", "Profile", "Telemetry"]
+__all__ = [
+ "BrowserPoolUpdateParams",
+ "Profile",
+ "Telemetry",
+ "TelemetryExport",
+ "TelemetryExportOtlp",
+ "TelemetryExportOtlpDestination",
+]
class BrowserPoolUpdateParams(TypedDict, total=False):
@@ -154,6 +161,50 @@ class Profile(TypedDict, total=False):
"""
+class TelemetryExportOtlpDestination(TypedDict, total=False):
+ """OTLP destination to export this session's captured telemetry to.
+
+ Provide either id or name. Requires telemetry capture to be enabled.
+ """
+
+ id: str
+ """OTLP destination ID"""
+
+ name: str
+ """OTLP destination name"""
+
+
+class TelemetryExportOtlp(TypedDict, total=False):
+ """
+ Export captured telemetry over OTLP to one of the org's configured destinations.
+ """
+
+ destination: TelemetryExportOtlpDestination
+ """OTLP destination to export this session's captured telemetry to.
+
+ Provide either id or name. Requires telemetry capture to be enabled.
+ """
+
+ enabled: bool
+ """Whether to export captured telemetry over OTLP.
+
+ Setting destination implies enabled=true, so this only needs to be set
+ explicitly to disable export (enabled=false with a destination is rejected).
+ """
+
+
+class TelemetryExport(TypedDict, total=False):
+ """Where to export this session's captured telemetry.
+
+ Omit to capture without exporting.
+ """
+
+ otlp: TelemetryExportOtlp
+ """
+ Export captured telemetry over OTLP to one of the org's configured destinations.
+ """
+
+
class Telemetry(TypedDict, total=False):
"""If provided, updates the pool's telemetry configuration.
@@ -186,3 +237,9 @@ class Telemetry(TypedDict, total=False):
capture on create. enabled=false cannot be combined with browser category
settings.
"""
+
+ export: TelemetryExport
+ """Where to export this session's captured telemetry.
+
+ Omit to capture without exporting.
+ """
diff --git a/src/kernel/types/browser_update_params.py b/src/kernel/types/browser_update_params.py
index 3c3140ee..01b19c8a 100644
--- a/src/kernel/types/browser_update_params.py
+++ b/src/kernel/types/browser_update_params.py
@@ -10,7 +10,14 @@
from .shared_params.browser_viewport import BrowserViewport
from .browsers.browser_telemetry_categories_config_param import BrowserTelemetryCategoriesConfigParam
-__all__ = ["BrowserUpdateParams", "Telemetry", "Viewport"]
+__all__ = [
+ "BrowserUpdateParams",
+ "Telemetry",
+ "TelemetryExport",
+ "TelemetryExportOtlp",
+ "TelemetryExportOtlpDestination",
+ "Viewport",
+]
class BrowserUpdateParams(TypedDict, total=False):
@@ -61,6 +68,50 @@ class BrowserUpdateParams(TypedDict, total=False):
"""Viewport configuration to apply to the browser session."""
+class TelemetryExportOtlpDestination(TypedDict, total=False):
+ """OTLP destination to export this session's captured telemetry to.
+
+ Provide either id or name. Requires telemetry capture to be enabled.
+ """
+
+ id: str
+ """OTLP destination ID"""
+
+ name: str
+ """OTLP destination name"""
+
+
+class TelemetryExportOtlp(TypedDict, total=False):
+ """
+ Export captured telemetry over OTLP to one of the org's configured destinations.
+ """
+
+ destination: TelemetryExportOtlpDestination
+ """OTLP destination to export this session's captured telemetry to.
+
+ Provide either id or name. Requires telemetry capture to be enabled.
+ """
+
+ enabled: bool
+ """Whether to export captured telemetry over OTLP.
+
+ Setting destination implies enabled=true, so this only needs to be set
+ explicitly to disable export (enabled=false with a destination is rejected).
+ """
+
+
+class TelemetryExport(TypedDict, total=False):
+ """Where to export this session's captured telemetry.
+
+ Omit to capture without exporting.
+ """
+
+ otlp: TelemetryExportOtlp
+ """
+ Export captured telemetry over OTLP to one of the org's configured destinations.
+ """
+
+
class Telemetry(TypedDict, total=False):
"""Telemetry configuration.
@@ -94,6 +145,12 @@ class Telemetry(TypedDict, total=False):
settings.
"""
+ export: TelemetryExport
+ """Where to export this session's captured telemetry.
+
+ Omit to capture without exporting.
+ """
+
class Viewport(BrowserViewport, total=False):
"""Viewport configuration to apply to the browser session."""
diff --git a/src/kernel/types/browsers/__init__.py b/src/kernel/types/browsers/__init__.py
index 990e8e19..bf9d861a 100644
--- a/src/kernel/types/browsers/__init__.py
+++ b/src/kernel/types/browsers/__init__.py
@@ -69,6 +69,7 @@
from .browser_interaction_click_event import BrowserInteractionClickEvent as BrowserInteractionClickEvent
from .browser_live_view_connect_event import BrowserLiveViewConnectEvent as BrowserLiveViewConnectEvent
from .browser_page_layout_shift_event import BrowserPageLayoutShiftEvent as BrowserPageLayoutShiftEvent
+from .browser_telemetry_export_config import BrowserTelemetryExportConfig as BrowserTelemetryExportConfig
from .computer_write_clipboard_params import ComputerWriteClipboardParams as ComputerWriteClipboardParams
from .browser_monitor_screenshot_event import BrowserMonitorScreenshotEvent as BrowserMonitorScreenshotEvent
from .computer_read_clipboard_response import ComputerReadClipboardResponse as ComputerReadClipboardResponse
@@ -82,6 +83,7 @@
from .computer_capture_screenshot_params import ComputerCaptureScreenshotParams as ComputerCaptureScreenshotParams
from .browser_telemetry_categories_config import BrowserTelemetryCategoriesConfig as BrowserTelemetryCategoriesConfig
from .browser_network_loading_failed_event import BrowserNetworkLoadingFailedEvent as BrowserNetworkLoadingFailedEvent
+from .browser_telemetry_otlp_export_config import BrowserTelemetryOtlpExportConfig as BrowserTelemetryOtlpExportConfig
from .computer_get_mouse_position_response import ComputerGetMousePositionResponse as ComputerGetMousePositionResponse
from .browser_page_dom_content_loaded_event import BrowserPageDomContentLoadedEvent as BrowserPageDomContentLoadedEvent
from .browser_page_navigation_settled_event import (
diff --git a/src/kernel/types/browsers/browser_telemetry_config.py b/src/kernel/types/browsers/browser_telemetry_config.py
index 2fae7a3d..8d24e6bf 100644
--- a/src/kernel/types/browsers/browser_telemetry_config.py
+++ b/src/kernel/types/browsers/browser_telemetry_config.py
@@ -3,6 +3,7 @@
from typing import Optional
from ..._models import BaseModel
+from .browser_telemetry_export_config import BrowserTelemetryExportConfig
from .browser_telemetry_categories_config import BrowserTelemetryCategoriesConfig
__all__ = ["BrowserTelemetryConfig"]
@@ -13,3 +14,9 @@ class BrowserTelemetryConfig(BaseModel):
browser: Optional[BrowserTelemetryCategoriesConfig] = None
"""Per-category enable/disable flags."""
+
+ export: Optional[BrowserTelemetryExportConfig] = None
+ """Where the session's captured telemetry is being exported.
+
+ Omitted when the export state is unknown.
+ """
diff --git a/src/kernel/types/browsers/browser_telemetry_export_config.py b/src/kernel/types/browsers/browser_telemetry_export_config.py
new file mode 100644
index 00000000..8aa93704
--- /dev/null
+++ b/src/kernel/types/browsers/browser_telemetry_export_config.py
@@ -0,0 +1,15 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from typing import Optional
+
+from ..._models import BaseModel
+from .browser_telemetry_otlp_export_config import BrowserTelemetryOtlpExportConfig
+
+__all__ = ["BrowserTelemetryExportConfig"]
+
+
+class BrowserTelemetryExportConfig(BaseModel):
+ """Active export state for a session's captured telemetry, by protocol."""
+
+ otlp: Optional[BrowserTelemetryOtlpExportConfig] = None
+ """Active OTLP export state."""
diff --git a/src/kernel/types/browsers/browser_telemetry_otlp_export_config.py b/src/kernel/types/browsers/browser_telemetry_otlp_export_config.py
new file mode 100644
index 00000000..3626a5f6
--- /dev/null
+++ b/src/kernel/types/browsers/browser_telemetry_otlp_export_config.py
@@ -0,0 +1,20 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from typing import Optional
+
+from ..._models import BaseModel
+
+__all__ = ["BrowserTelemetryOtlpExportConfig"]
+
+
+class BrowserTelemetryOtlpExportConfig(BaseModel):
+ """Active OTLP export state for a browser session."""
+
+ destination: Optional[str] = None
+ """ID of the OTLP destination the session is bound to.
+
+ Omitted when the session is not exporting.
+ """
+
+ enabled: Optional[bool] = None
+ """Whether the session is exporting captured telemetry over OTLP."""
diff --git a/tests/api_resources/auth/test_connections.py b/tests/api_resources/auth/test_connections.py
index 6c7423e6..321e84e4 100644
--- a/tests/api_resources/auth/test_connections.py
+++ b/tests/api_resources/auth/test_connections.py
@@ -53,6 +53,15 @@ def test_method_create_with_all_params(self, client: Kernel) -> None:
"system": {"enabled": True},
},
"enabled": True,
+ "export": {
+ "otlp": {
+ "destination": {
+ "id": "id",
+ "name": "name",
+ },
+ "enabled": True,
+ }
+ },
},
credential={
"auto": True,
@@ -170,6 +179,15 @@ def test_method_update_with_all_params(self, client: Kernel) -> None:
"system": {"enabled": True},
},
"enabled": True,
+ "export": {
+ "otlp": {
+ "destination": {
+ "id": "id",
+ "name": "name",
+ },
+ "enabled": True,
+ }
+ },
},
credential={
"auto": True,
@@ -372,6 +390,15 @@ def test_method_login_with_all_params(self, client: Kernel) -> None:
"system": {"enabled": True},
},
"enabled": True,
+ "export": {
+ "otlp": {
+ "destination": {
+ "id": "id",
+ "name": "name",
+ },
+ "enabled": True,
+ }
+ },
},
proxy={
"id": "id",
@@ -567,6 +594,15 @@ async def test_method_create_with_all_params(self, async_client: AsyncKernel) ->
"system": {"enabled": True},
},
"enabled": True,
+ "export": {
+ "otlp": {
+ "destination": {
+ "id": "id",
+ "name": "name",
+ },
+ "enabled": True,
+ }
+ },
},
credential={
"auto": True,
@@ -684,6 +720,15 @@ async def test_method_update_with_all_params(self, async_client: AsyncKernel) ->
"system": {"enabled": True},
},
"enabled": True,
+ "export": {
+ "otlp": {
+ "destination": {
+ "id": "id",
+ "name": "name",
+ },
+ "enabled": True,
+ }
+ },
},
credential={
"auto": True,
@@ -886,6 +931,15 @@ async def test_method_login_with_all_params(self, async_client: AsyncKernel) ->
"system": {"enabled": True},
},
"enabled": True,
+ "export": {
+ "otlp": {
+ "destination": {
+ "id": "id",
+ "name": "name",
+ },
+ "enabled": True,
+ }
+ },
},
proxy={
"id": "id",
diff --git a/tests/api_resources/test_browser_pools.py b/tests/api_resources/test_browser_pools.py
index 31657789..a85d5be7 100644
--- a/tests/api_resources/test_browser_pools.py
+++ b/tests/api_resources/test_browser_pools.py
@@ -66,6 +66,15 @@ def test_method_create_with_all_params(self, client: Kernel) -> None:
"system": {"enabled": True},
},
"enabled": True,
+ "export": {
+ "otlp": {
+ "destination": {
+ "id": "id",
+ "name": "name",
+ },
+ "enabled": True,
+ }
+ },
},
timeout_seconds=10,
viewport={
@@ -191,6 +200,15 @@ def test_method_update_with_all_params(self, client: Kernel) -> None:
"system": {"enabled": True},
},
"enabled": True,
+ "export": {
+ "otlp": {
+ "destination": {
+ "id": "id",
+ "name": "name",
+ },
+ "enabled": True,
+ }
+ },
},
timeout_seconds=10,
viewport={
@@ -358,6 +376,15 @@ def test_method_acquire_with_all_params(self, client: Kernel) -> None:
"system": {"enabled": True},
},
"enabled": True,
+ "export": {
+ "otlp": {
+ "destination": {
+ "id": "id",
+ "name": "name",
+ },
+ "enabled": True,
+ }
+ },
},
)
assert_matches_type(BrowserPoolAcquireResponse, browser_pool, path=["response"])
@@ -545,6 +572,15 @@ async def test_method_create_with_all_params(self, async_client: AsyncKernel) ->
"system": {"enabled": True},
},
"enabled": True,
+ "export": {
+ "otlp": {
+ "destination": {
+ "id": "id",
+ "name": "name",
+ },
+ "enabled": True,
+ }
+ },
},
timeout_seconds=10,
viewport={
@@ -670,6 +706,15 @@ async def test_method_update_with_all_params(self, async_client: AsyncKernel) ->
"system": {"enabled": True},
},
"enabled": True,
+ "export": {
+ "otlp": {
+ "destination": {
+ "id": "id",
+ "name": "name",
+ },
+ "enabled": True,
+ }
+ },
},
timeout_seconds=10,
viewport={
@@ -837,6 +882,15 @@ async def test_method_acquire_with_all_params(self, async_client: AsyncKernel) -
"system": {"enabled": True},
},
"enabled": True,
+ "export": {
+ "otlp": {
+ "destination": {
+ "id": "id",
+ "name": "name",
+ },
+ "enabled": True,
+ }
+ },
},
)
assert_matches_type(BrowserPoolAcquireResponse, browser_pool, path=["response"])
diff --git a/tests/api_resources/test_browsers.py b/tests/api_resources/test_browsers.py
index 322fffed..a8f93efc 100644
--- a/tests/api_resources/test_browsers.py
+++ b/tests/api_resources/test_browsers.py
@@ -71,6 +71,15 @@ def test_method_create_with_all_params(self, client: Kernel) -> None:
"system": {"enabled": True},
},
"enabled": True,
+ "export": {
+ "otlp": {
+ "destination": {
+ "id": "id",
+ "name": "name",
+ },
+ "enabled": True,
+ }
+ },
},
timeout_seconds=10,
viewport={
@@ -192,6 +201,15 @@ def test_method_update_with_all_params(self, client: Kernel) -> None:
"system": {"enabled": True},
},
"enabled": True,
+ "export": {
+ "otlp": {
+ "destination": {
+ "id": "id",
+ "name": "name",
+ },
+ "enabled": True,
+ }
+ },
},
viewport={
"height": 800,
@@ -498,6 +516,15 @@ async def test_method_create_with_all_params(self, async_client: AsyncKernel) ->
"system": {"enabled": True},
},
"enabled": True,
+ "export": {
+ "otlp": {
+ "destination": {
+ "id": "id",
+ "name": "name",
+ },
+ "enabled": True,
+ }
+ },
},
timeout_seconds=10,
viewport={
@@ -619,6 +646,15 @@ async def test_method_update_with_all_params(self, async_client: AsyncKernel) ->
"system": {"enabled": True},
},
"enabled": True,
+ "export": {
+ "otlp": {
+ "destination": {
+ "id": "id",
+ "name": "name",
+ },
+ "enabled": True,
+ }
+ },
},
viewport={
"height": 800,
From 4f3442786becb2650f8550292c16f3daabb09b75 Mon Sep 17 00:00:00 2001
From: sjmiller609 <7516283+sjmiller609@users.noreply.github.com>
Date: Thu, 6 Aug 2026 13:48:14 +0000
Subject: [PATCH 163/366] Build SDK
Stainless-Generated-From: 460714c882fb67c0e6bfb552d23485cd6246c8a2
---
requirements-dev.lock | 22 +-
requirements.lock | 20 +-
uv.lock | 1521 +++++++++++++++++++++++++++++++++++++++++
3 files changed, 1560 insertions(+), 3 deletions(-)
create mode 100644 uv.lock
diff --git a/requirements-dev.lock b/requirements-dev.lock
index 4a38b203..979c7980 100644
--- a/requirements-dev.lock
+++ b/requirements-dev.lock
@@ -29,15 +29,20 @@ async-timeout==5.0.1
attrs==25.4.0
# via aiohttp
# via nox
+ # via standardwebhooks
backports-asyncio-runner==1.2.0
# via pytest-asyncio
certifi==2026.1.4
# via httpcore
# via httpx
+colorama==0.4.6
+ # via griffe
colorlog==6.10.1
# via nox
dependency-groups==1.3.1
# via nox
+deprecated==1.3.1
+ # via standardwebhooks
dirty-equals==0.11
distlib==0.4.0
# via virtualenv
@@ -53,14 +58,16 @@ filelock==3.19.1
frozenlist==1.8.0
# via aiohttp
# via aiosignal
+griffe==1.14.0
h11==0.16.0
# via httpcore
httpcore==1.0.9
# via httpx
httpx==0.28.1
# via httpx-aiohttp
- # via kernel
# via respx
+ # via standardwebhooks
+ # via kernel
httpx-aiohttp==0.1.12
# via kernel
humanize==4.13.0
@@ -112,6 +119,7 @@ pytest==8.4.2
pytest-asyncio==1.2.0
pytest-xdist==3.8.0
python-dateutil==2.9.0.post0
+ # via standardwebhooks
# via time-machine
respx==0.22.0
rich==14.2.0
@@ -120,29 +128,39 @@ six==1.17.0
# via python-dateutil
sniffio==1.3.1
# via kernel
+standardwebhooks==1.0.1
+ # via kernel
time-machine==2.19.0
tomli==2.4.0
# via dependency-groups
# via mypy
# via nox
# via pytest
+types-deprecated==1.3.1.20251101
+ # via standardwebhooks
+types-python-dateutil==2.9.0.20251115
+ # via standardwebhooks
typing-extensions==4.15.0
# via aiosignal
# via anyio
# via exceptiongroup
- # via kernel
# via multidict
# via mypy
# via pydantic
# via pydantic-core
# via pyright
# via pytest-asyncio
+ # via kernel
# via typing-inspection
# via virtualenv
typing-inspection==0.4.2
# via pydantic
virtualenv==20.36.1
# via nox
+websockets==15.0.1
+ # via kernel
+wrapt==2.0.1
+ # via deprecated
yarl==1.22.0
# via aiohttp
zipp==3.23.0
diff --git a/requirements.lock b/requirements.lock
index 5f6c7ff4..64675707 100644
--- a/requirements.lock
+++ b/requirements.lock
@@ -26,9 +26,12 @@ async-timeout==5.0.1
# via aiohttp
attrs==25.4.0
# via aiohttp
+ # via standardwebhooks
certifi==2026.1.4
# via httpcore
# via httpx
+deprecated==1.3.1
+ # via standardwebhooks
distro==1.9.0
# via kernel
exceptiongroup==1.3.1
@@ -42,6 +45,7 @@ httpcore==1.0.9
# via httpx
httpx==0.28.1
# via httpx-aiohttp
+ # via standardwebhooks
# via kernel
httpx-aiohttp==0.1.12
# via kernel
@@ -59,18 +63,32 @@ pydantic==2.12.5
# via kernel
pydantic-core==2.41.5
# via pydantic
+python-dateutil==2.9.0.post0
+ # via standardwebhooks
+six==1.17.0
+ # via python-dateutil
sniffio==1.3.1
# via kernel
+standardwebhooks==1.0.1
+ # via kernel
+types-deprecated==1.3.1.20251101
+ # via standardwebhooks
+types-python-dateutil==2.9.0.20251115
+ # via standardwebhooks
typing-extensions==4.15.0
# via aiosignal
# via anyio
# via exceptiongroup
- # via kernel
# via multidict
# via pydantic
# via pydantic-core
+ # via kernel
# via typing-inspection
typing-inspection==0.4.2
# via pydantic
+websockets==15.0.1
+ # via kernel
+wrapt==2.0.1
+ # via deprecated
yarl==1.22.0
# via aiohttp
diff --git a/uv.lock b/uv.lock
new file mode 100644
index 00000000..79935255
--- /dev/null
+++ b/uv.lock
@@ -0,0 +1,1521 @@
+version = 1
+revision = 3
+requires-python = ">=3.9"
+resolution-markers = [
+ "python_full_version >= '3.10'",
+ "python_full_version < '3.10'",
+]
+
+[[package]]
+name = "aiohappyeyeballs"
+version = "2.6.1"
+source = { registry = "https://pypi.org/simple" }
+resolution-markers = [
+ "python_full_version < '3.10'",
+]
+sdist = { url = "https://files.pythonhosted.org/packages/26/30/f84a107a9c4331c14b2b586036f40965c128aa4fee4dda5d3d51cb14ad54/aiohappyeyeballs-2.6.1.tar.gz", hash = "sha256:c3f9d0113123803ccadfdf3f0faa505bc78e6a72d1cc4806cbd719826e943558", size = 22760, upload-time = "2025-03-12T01:42:48.764Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/0f/15/5bf3b99495fb160b63f95972b81750f18f7f4e02ad051373b669d17d44f2/aiohappyeyeballs-2.6.1-py3-none-any.whl", hash = "sha256:f349ba8f4b75cb25c99c5c2d84e997e485204d2902a9597802b0371f09331fb8", size = 15265, upload-time = "2025-03-12T01:42:47.083Z" },
+]
+
+[[package]]
+name = "aiohappyeyeballs"
+version = "2.7.1"
+source = { registry = "https://pypi.org/simple" }
+resolution-markers = [
+ "python_full_version >= '3.10'",
+]
+sdist = { url = "https://files.pythonhosted.org/packages/ce/f4/eec0465c2f67b2664688d0240b3212d5196fd89e741df67ddb81f8d35658/aiohappyeyeballs-2.7.1.tar.gz", hash = "sha256:065665c041c42a5938ed220bdcd7230f22527fbec085e1853d2402c8a3615d9d", size = 24757, upload-time = "2026-07-01T17:11:55.501Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/71/43/1947f06babed6b3f1d7f38b0c767f52df66bfb2bc10b468c4a7de9eceff2/aiohappyeyeballs-2.7.1-py3-none-any.whl", hash = "sha256:9243213661e29250eb41368e5daa826fc017156c3b8a11440826b2e3ed376472", size = 15038, upload-time = "2026-07-01T17:11:54.055Z" },
+]
+
+[[package]]
+name = "aiohttp"
+version = "3.13.5"
+source = { registry = "https://pypi.org/simple" }
+resolution-markers = [
+ "python_full_version < '3.10'",
+]
+dependencies = [
+ { name = "aiohappyeyeballs", version = "2.6.1", source = { registry = "https://pypi.org/simple" } },
+ { name = "aiosignal" },
+ { name = "async-timeout" },
+ { name = "attrs" },
+ { name = "frozenlist" },
+ { name = "multidict" },
+ { name = "propcache", version = "0.4.1", source = { registry = "https://pypi.org/simple" } },
+ { name = "yarl", version = "1.22.0", source = { registry = "https://pypi.org/simple" } },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/77/9a/152096d4808df8e4268befa55fba462f440f14beab85e8ad9bf990516918/aiohttp-3.13.5.tar.gz", hash = "sha256:9d98cc980ecc96be6eb4c1994ce35d28d8b1f5e5208a23b421187d1209dbb7d1", size = 7858271, upload-time = "2026-03-31T22:01:03.343Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/bd/85/cebc47ee74d8b408749073a1a46c6fcba13d170dc8af7e61996c6c9394ac/aiohttp-3.13.5-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:02222e7e233295f40e011c1b00e3b0bd451f22cf853a0304c3595633ee47da4b", size = 750547, upload-time = "2026-03-31T21:56:30.024Z" },
+ { url = "https://files.pythonhosted.org/packages/05/98/afd308e35b9d3d8c9ec54c0918f1d722c86dc17ddfec272fcdbcce5a3124/aiohttp-3.13.5-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:bace460460ed20614fa6bc8cb09966c0b8517b8c58ad8046828c6078d25333b5", size = 503535, upload-time = "2026-03-31T21:56:31.935Z" },
+ { url = "https://files.pythonhosted.org/packages/6f/4d/926c183e06b09d5270a309eb50fbde7b09782bfd305dec1e800f329834fb/aiohttp-3.13.5-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8f546a4dc1e6a5edbb9fd1fd6ad18134550e096a5a43f4ad74acfbd834fc6670", size = 497830, upload-time = "2026-03-31T21:56:33.654Z" },
+ { url = "https://files.pythonhosted.org/packages/e4/d6/f47d1c690f115a5c2a5e8938cce4a232a5be9aac5c5fb2647efcbbbda333/aiohttp-3.13.5-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c86969d012e51b8e415a8c6ce96f7857d6a87d6207303ab02d5d11ef0cad2274", size = 1682474, upload-time = "2026-03-31T21:56:35.513Z" },
+ { url = "https://files.pythonhosted.org/packages/01/44/056fd37b1bb52eac760303e5196acc74d9d546631b035704ae5927f7b4ac/aiohttp-3.13.5-cp310-cp310-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:b6f6cd1560c5fa427e3b6074bb24d2c64e225afbb7165008903bd42e4e33e28a", size = 1655259, upload-time = "2026-03-31T21:56:37.843Z" },
+ { url = "https://files.pythonhosted.org/packages/91/9f/78eb1a20c1c28ae02f6a3c0f4d7b0dcc66abce5290cadd53d78ce3084175/aiohttp-3.13.5-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:636bc362f0c5bbc7372bc3ae49737f9e3030dbce469f0f422c8f38079780363d", size = 1736204, upload-time = "2026-03-31T21:56:39.822Z" },
+ { url = "https://files.pythonhosted.org/packages/de/6c/d20d7de23f0b52b8c1d9e2033b2db1ac4dacbb470bb74c56de0f5f86bb4f/aiohttp-3.13.5-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:6a7cbeb06d1070f1d14895eeeed4dac5913b22d7b456f2eb969f11f4b3993796", size = 1826198, upload-time = "2026-03-31T21:56:41.378Z" },
+ { url = "https://files.pythonhosted.org/packages/2f/86/a6f3ff1fd795f49545a7c74b2c92f62729135d73e7e4055bf74da5a26c82/aiohttp-3.13.5-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bca9ef7517fd7874a1a08970ae88f497bf5c984610caa0bf40bd7e8450852b95", size = 1681329, upload-time = "2026-03-31T21:56:43.374Z" },
+ { url = "https://files.pythonhosted.org/packages/fb/68/84cd3dab6b7b4f3e6fe9459a961acb142aaab846417f6e8905110d7027e5/aiohttp-3.13.5-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:019a67772e034a0e6b9b17c13d0a8fe56ad9fb150fc724b7f3ffd3724288d9e5", size = 1560023, upload-time = "2026-03-31T21:56:45.031Z" },
+ { url = "https://files.pythonhosted.org/packages/41/2c/db61b64b0249e30f954a65ab4cb4970ced57544b1de2e3c98ee5dc24165f/aiohttp-3.13.5-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:f34ecee82858e41dd217734f0c41a532bd066bcaab636ad830f03a30b2a96f2a", size = 1652372, upload-time = "2026-03-31T21:56:47.075Z" },
+ { url = "https://files.pythonhosted.org/packages/25/6f/e96988a6c982d047810c772e28c43c64c300c943b0ed5c1c0c4ce1e1027c/aiohttp-3.13.5-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:4eac02d9af4813ee289cd63a361576da36dba57f5a1ab36377bc2600db0cbb73", size = 1662031, upload-time = "2026-03-31T21:56:48.835Z" },
+ { url = "https://files.pythonhosted.org/packages/b7/26/a56feace81f3d347b4052403a9d03754a0ab23f7940780dada0849a38c92/aiohttp-3.13.5-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:4beac52e9fe46d6abf98b0176a88154b742e878fdf209d2248e99fcdf73cd297", size = 1708118, upload-time = "2026-03-31T21:56:50.833Z" },
+ { url = "https://files.pythonhosted.org/packages/78/6e/b6173a8ff03d01d5e1a694bc06764b5dad1df2d4ed8f0ceec12bb3277936/aiohttp-3.13.5-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:c180f480207a9b2475f2b8d8bd7204e47aec952d084b2a2be58a782ffcf96074", size = 1548667, upload-time = "2026-03-31T21:56:52.81Z" },
+ { url = "https://files.pythonhosted.org/packages/16/13/13296ffe2c132d888b3fe2c195c8b9c0c24c89c3fa5cc2c44464dc23b22e/aiohttp-3.13.5-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:2837fb92951564d6339cedae4a7231692aa9f73cbc4fb2e04263b96844e03b4e", size = 1724490, upload-time = "2026-03-31T21:56:54.541Z" },
+ { url = "https://files.pythonhosted.org/packages/7a/b4/1f1c287f4a79782ef36e5a6e62954c85343bc30470d862d30bd5f26c9fa2/aiohttp-3.13.5-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:d9010032a0b9710f58012a1e9c222528763d860ba2ee1422c03473eab47703e7", size = 1667109, upload-time = "2026-03-31T21:56:56.21Z" },
+ { url = "https://files.pythonhosted.org/packages/ef/42/8461a2aaf60a8f4ea4549a4056be36b904b0eb03d97ca9a8a2604681a500/aiohttp-3.13.5-cp310-cp310-win32.whl", hash = "sha256:7c4b6668b2b2b9027f209ddf647f2a4407784b5d88b8be4efcc72036f365baf9", size = 439478, upload-time = "2026-03-31T21:56:58.292Z" },
+ { url = "https://files.pythonhosted.org/packages/e5/71/06956304cb5ee439dfe8d86e1b2e70088bd88ed1ced1f42fb29e5d855f0e/aiohttp-3.13.5-cp310-cp310-win_amd64.whl", hash = "sha256:cd3db5927bf9167d5a6157ddb2f036f6b6b0ad001ac82355d43e97a4bde76d76", size = 462047, upload-time = "2026-03-31T21:57:00.257Z" },
+ { url = "https://files.pythonhosted.org/packages/d6/f5/a20c4ac64aeaef1679e25c9983573618ff765d7aa829fa2b84ae7573169e/aiohttp-3.13.5-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:7ab7229b6f9b5c1ba4910d6c41a9eb11f543eadb3f384df1b4c293f4e73d44d6", size = 757513, upload-time = "2026-03-31T21:57:02.146Z" },
+ { url = "https://files.pythonhosted.org/packages/75/0a/39fa6c6b179b53fcb3e4b3d2b6d6cad0180854eda17060c7218540102bef/aiohttp-3.13.5-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:8f14c50708bb156b3a3ca7230b3d820199d56a48e3af76fa21c2d6087190fe3d", size = 506748, upload-time = "2026-03-31T21:57:04.275Z" },
+ { url = "https://files.pythonhosted.org/packages/87/ec/e38ce072e724fd7add6243613f8d1810da084f54175353d25ccf9f9c7e5a/aiohttp-3.13.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e7d2f8616f0ff60bd332022279011776c3ac0faa0f1b463f7bb12326fbc97a1c", size = 501673, upload-time = "2026-03-31T21:57:06.208Z" },
+ { url = "https://files.pythonhosted.org/packages/ba/ba/3bc7525d7e2beaa11b309a70d48b0d3cfc3c2089ec6a7d0820d59c657053/aiohttp-3.13.5-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a2567b72e1ffc3ab25510db43f355b29eeada56c0a622e58dcdb19530eb0a3cb", size = 1763757, upload-time = "2026-03-31T21:57:07.882Z" },
+ { url = "https://files.pythonhosted.org/packages/5e/ab/e87744cf18f1bd78263aba24924d4953b41086bd3a31d22452378e9028a0/aiohttp-3.13.5-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:fb0540c854ac9c0c5ad495908fdfd3e332d553ec731698c0e29b1877ba0d2ec6", size = 1720152, upload-time = "2026-03-31T21:57:09.946Z" },
+ { url = "https://files.pythonhosted.org/packages/6b/f3/ed17a6f2d742af17b50bae2d152315ed1b164b07a5fd5cc1754d99e4dfa5/aiohttp-3.13.5-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c9883051c6972f58bfc4ebb2116345ee2aa151178e99c3f2b2bbe2af712abd13", size = 1818010, upload-time = "2026-03-31T21:57:12.157Z" },
+ { url = "https://files.pythonhosted.org/packages/53/06/ecbc63dc937192e2a5cb46df4d3edb21deb8225535818802f210a6ea5816/aiohttp-3.13.5-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:2294172ce08a82fb7c7273485895de1fa1186cc8294cfeb6aef4af42ad261174", size = 1907251, upload-time = "2026-03-31T21:57:14.023Z" },
+ { url = "https://files.pythonhosted.org/packages/7e/a5/0521aa32c1ddf3aa1e71dcc466be0b7db2771907a13f18cddaa45967d97b/aiohttp-3.13.5-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3a807cabd5115fb55af198b98178997a5e0e57dead43eb74a93d9c07d6d4a7dc", size = 1759969, upload-time = "2026-03-31T21:57:16.146Z" },
+ { url = "https://files.pythonhosted.org/packages/f6/78/a38f8c9105199dd3b9706745865a8a59d0041b6be0ca0cc4b2ccf1bab374/aiohttp-3.13.5-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:aa6d0d932e0f39c02b80744273cd5c388a2d9bc07760a03164f229c8e02662f6", size = 1616871, upload-time = "2026-03-31T21:57:17.856Z" },
+ { url = "https://files.pythonhosted.org/packages/6f/41/27392a61ead8ab38072105c71aa44ff891e71653fe53d576a7067da2b4e8/aiohttp-3.13.5-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:60869c7ac4aaabe7110f26499f3e6e5696eae98144735b12a9c3d9eae2b51a49", size = 1739844, upload-time = "2026-03-31T21:57:19.679Z" },
+ { url = "https://files.pythonhosted.org/packages/6e/55/5564e7ae26d94f3214250009a0b1c65a0c6af4bf88924ccb6fdab901de28/aiohttp-3.13.5-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:26d2f8546f1dfa75efa50c3488215a903c0168d253b75fba4210f57ab77a0fb8", size = 1731969, upload-time = "2026-03-31T21:57:22.006Z" },
+ { url = "https://files.pythonhosted.org/packages/6d/c5/705a3929149865fc941bcbdd1047b238e4a72bcb215a9b16b9d7a2e8d992/aiohttp-3.13.5-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:f1162a1492032c82f14271e831c8f4b49f2b6078f4f5fc74de2c912fa225d51d", size = 1795193, upload-time = "2026-03-31T21:57:24.256Z" },
+ { url = "https://files.pythonhosted.org/packages/a6/19/edabed62f718d02cff7231ca0db4ef1c72504235bc467f7b67adb1679f48/aiohttp-3.13.5-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:8b14eb3262fad0dc2f89c1a43b13727e709504972186ff6a99a3ecaa77102b6c", size = 1606477, upload-time = "2026-03-31T21:57:26.364Z" },
+ { url = "https://files.pythonhosted.org/packages/de/fc/76f80ef008675637d88d0b21584596dc27410a990b0918cb1e5776545b5b/aiohttp-3.13.5-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:ca9ac61ac6db4eb6c2a0cd1d0f7e1357647b638ccc92f7e9d8d133e71ed3c6ac", size = 1813198, upload-time = "2026-03-31T21:57:28.316Z" },
+ { url = "https://files.pythonhosted.org/packages/e5/67/5b3ac26b80adb20ea541c487f73730dc8fa107d632c998f25bbbab98fcda/aiohttp-3.13.5-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:7996023b2ed59489ae4762256c8516df9820f751cf2c5da8ed2fb20ee50abab3", size = 1752321, upload-time = "2026-03-31T21:57:30.549Z" },
+ { url = "https://files.pythonhosted.org/packages/88/06/e4a2e49255ea23fa4feeb5ab092d90240d927c15e47b5b5c48dff5a9ce29/aiohttp-3.13.5-cp311-cp311-win32.whl", hash = "sha256:77dfa48c9f8013271011e51c00f8ada19851f013cde2c48fca1ba5e0caf5bb06", size = 439069, upload-time = "2026-03-31T21:57:32.388Z" },
+ { url = "https://files.pythonhosted.org/packages/c0/43/8c7163a596dab4f8be12c190cf467a1e07e4734cf90eebb39f7f5d53fc6a/aiohttp-3.13.5-cp311-cp311-win_amd64.whl", hash = "sha256:d3a4834f221061624b8887090637db9ad4f61752001eae37d56c52fddade2dc8", size = 462859, upload-time = "2026-03-31T21:57:34.455Z" },
+ { url = "https://files.pythonhosted.org/packages/be/6f/353954c29e7dcce7cf00280a02c75f30e133c00793c7a2ed3776d7b2f426/aiohttp-3.13.5-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:023ecba036ddd840b0b19bf195bfae970083fd7024ce1ac22e9bba90464620e9", size = 748876, upload-time = "2026-03-31T21:57:36.319Z" },
+ { url = "https://files.pythonhosted.org/packages/f5/1b/428a7c64687b3b2e9cd293186695affc0e1e54a445d0361743b231f11066/aiohttp-3.13.5-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:15c933ad7920b7d9a20de151efcd05a6e38302cbf0e10c9b2acb9a42210a2416", size = 499557, upload-time = "2026-03-31T21:57:38.236Z" },
+ { url = "https://files.pythonhosted.org/packages/29/47/7be41556bfbb6917069d6a6634bb7dd5e163ba445b783a90d40f5ac7e3a7/aiohttp-3.13.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ab2899f9fa2f9f741896ebb6fa07c4c883bfa5c7f2ddd8cf2aafa86fa981b2d2", size = 500258, upload-time = "2026-03-31T21:57:39.923Z" },
+ { url = "https://files.pythonhosted.org/packages/67/84/c9ecc5828cb0b3695856c07c0a6817a99d51e2473400f705275a2b3d9239/aiohttp-3.13.5-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a60eaa2d440cd4707696b52e40ed3e2b0f73f65be07fd0ef23b6b539c9c0b0b4", size = 1749199, upload-time = "2026-03-31T21:57:41.938Z" },
+ { url = "https://files.pythonhosted.org/packages/f0/d3/3c6d610e66b495657622edb6ae7c7fd31b2e9086b4ec50b47897ad6042a9/aiohttp-3.13.5-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:55b3bdd3292283295774ab585160c4004f4f2f203946997f49aac032c84649e9", size = 1721013, upload-time = "2026-03-31T21:57:43.904Z" },
+ { url = "https://files.pythonhosted.org/packages/49/a0/24409c12217456df0bae7babe3b014e460b0b38a8e60753d6cb339f6556d/aiohttp-3.13.5-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c2b2355dc094e5f7d45a7bb262fe7207aa0460b37a0d87027dcf21b5d890e7d5", size = 1781501, upload-time = "2026-03-31T21:57:46.285Z" },
+ { url = "https://files.pythonhosted.org/packages/98/9d/b65ec649adc5bccc008b0957a9a9c691070aeac4e41cea18559fef49958b/aiohttp-3.13.5-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b38765950832f7d728297689ad78f5f2cf79ff82487131c4d26fe6ceecdc5f8e", size = 1878981, upload-time = "2026-03-31T21:57:48.734Z" },
+ { url = "https://files.pythonhosted.org/packages/57/d8/8d44036d7eb7b6a8ec4c5494ea0c8c8b94fbc0ed3991c1a7adf230df03bf/aiohttp-3.13.5-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b18f31b80d5a33661e08c89e202edabf1986e9b49c42b4504371daeaa11b47c1", size = 1767934, upload-time = "2026-03-31T21:57:51.171Z" },
+ { url = "https://files.pythonhosted.org/packages/31/04/d3f8211f273356f158e3464e9e45484d3fb8c4ce5eb2f6fe9405c3273983/aiohttp-3.13.5-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:33add2463dde55c4f2d9635c6ab33ce154e5ecf322bd26d09af95c5f81cfa286", size = 1566671, upload-time = "2026-03-31T21:57:53.326Z" },
+ { url = "https://files.pythonhosted.org/packages/41/db/073e4ebe00b78e2dfcacff734291651729a62953b48933d765dc513bf798/aiohttp-3.13.5-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:327cc432fdf1356fb4fbc6fe833ad4e9f6aacb71a8acaa5f1855e4b25910e4a9", size = 1705219, upload-time = "2026-03-31T21:57:55.385Z" },
+ { url = "https://files.pythonhosted.org/packages/48/45/7dfba71a2f9fd97b15c95c06819de7eb38113d2cdb6319669195a7d64270/aiohttp-3.13.5-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:7c35b0bf0b48a70b4cb4fc5d7bed9b932532728e124874355de1a0af8ec4bc88", size = 1743049, upload-time = "2026-03-31T21:57:57.341Z" },
+ { url = "https://files.pythonhosted.org/packages/18/71/901db0061e0f717d226386a7f471bb59b19566f2cae5f0d93874b017271f/aiohttp-3.13.5-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:df23d57718f24badef8656c49743e11a89fd6f5358fa8a7b96e728fda2abf7d3", size = 1749557, upload-time = "2026-03-31T21:57:59.626Z" },
+ { url = "https://files.pythonhosted.org/packages/08/d5/41eebd16066e59cd43728fe74bce953d7402f2b4ddfdfef2c0e9f17ca274/aiohttp-3.13.5-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:02e048037a6501a5ec1f6fc9736135aec6eb8a004ce48838cb951c515f32c80b", size = 1558931, upload-time = "2026-03-31T21:58:01.972Z" },
+ { url = "https://files.pythonhosted.org/packages/30/e6/4a799798bf05740e66c3a1161079bda7a3dd8e22ca392481d7a7f9af82a6/aiohttp-3.13.5-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:31cebae8b26f8a615d2b546fee45d5ffb76852ae6450e2a03f42c9102260d6fe", size = 1774125, upload-time = "2026-03-31T21:58:04.007Z" },
+ { url = "https://files.pythonhosted.org/packages/84/63/7749337c90f92bc2cb18f9560d67aa6258c7060d1397d21529b8004fcf6f/aiohttp-3.13.5-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:888e78eb5ca55a615d285c3c09a7a91b42e9dd6fc699b166ebd5dee87c9ccf14", size = 1732427, upload-time = "2026-03-31T21:58:06.337Z" },
+ { url = "https://files.pythonhosted.org/packages/98/de/cf2f44ff98d307e72fb97d5f5bbae3bfcb442f0ea9790c0bf5c5c2331404/aiohttp-3.13.5-cp312-cp312-win32.whl", hash = "sha256:8bd3ec6376e68a41f9f95f5ed170e2fcf22d4eb27a1f8cb361d0508f6e0557f3", size = 433534, upload-time = "2026-03-31T21:58:08.712Z" },
+ { url = "https://files.pythonhosted.org/packages/aa/ca/eadf6f9c8fa5e31d40993e3db153fb5ed0b11008ad5d9de98a95045bed84/aiohttp-3.13.5-cp312-cp312-win_amd64.whl", hash = "sha256:110e448e02c729bcebb18c60b9214a87ba33bac4a9fa5e9a5f139938b56c6cb1", size = 460446, upload-time = "2026-03-31T21:58:10.945Z" },
+ { url = "https://files.pythonhosted.org/packages/78/e9/d76bf503005709e390122d34e15256b88f7008e246c4bdbe915cd4f1adce/aiohttp-3.13.5-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:a5029cc80718bbd545123cd8fe5d15025eccaaaace5d0eeec6bd556ad6163d61", size = 742930, upload-time = "2026-03-31T21:58:13.155Z" },
+ { url = "https://files.pythonhosted.org/packages/57/00/4b7b70223deaebd9bb85984d01a764b0d7bd6526fcdc73cca83bcbe7243e/aiohttp-3.13.5-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:4bb6bf5811620003614076bdc807ef3b5e38244f9d25ca5fe888eaccea2a9832", size = 496927, upload-time = "2026-03-31T21:58:15.073Z" },
+ { url = "https://files.pythonhosted.org/packages/9c/f5/0fb20fb49f8efdcdce6cd8127604ad2c503e754a8f139f5e02b01626523f/aiohttp-3.13.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a84792f8631bf5a94e52d9cc881c0b824ab42717165a5579c760b830d9392ac9", size = 497141, upload-time = "2026-03-31T21:58:17.009Z" },
+ { url = "https://files.pythonhosted.org/packages/3b/86/b7c870053e36a94e8951b803cb5b909bfbc9b90ca941527f5fcafbf6b0fa/aiohttp-3.13.5-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:57653eac22c6a4c13eb22ecf4d673d64a12f266e72785ab1c8b8e5940d0e8090", size = 1732476, upload-time = "2026-03-31T21:58:18.925Z" },
+ { url = "https://files.pythonhosted.org/packages/b5/e5/4e161f84f98d80c03a238671b4136e6530453d65262867d989bbe78244d0/aiohttp-3.13.5-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:e5e5f7debc7a57af53fdf5c5009f9391d9f4c12867049d509bf7bb164a6e295b", size = 1706507, upload-time = "2026-03-31T21:58:21.094Z" },
+ { url = "https://files.pythonhosted.org/packages/d4/56/ea11a9f01518bd5a2a2fcee869d248c4b8a0cfa0bb13401574fa31adf4d4/aiohttp-3.13.5-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c719f65bebcdf6716f10e9eff80d27567f7892d8988c06de12bbbd39307c6e3a", size = 1773465, upload-time = "2026-03-31T21:58:23.159Z" },
+ { url = "https://files.pythonhosted.org/packages/eb/40/333ca27fb74b0383f17c90570c748f7582501507307350a79d9f9f3c6eb1/aiohttp-3.13.5-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d97f93fdae594d886c5a866636397e2bcab146fd7a132fd6bb9ce182224452f8", size = 1873523, upload-time = "2026-03-31T21:58:25.59Z" },
+ { url = "https://files.pythonhosted.org/packages/f0/d2/e2f77eef1acb7111405433c707dc735e63f67a56e176e72e9e7a2cd3f493/aiohttp-3.13.5-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3df334e39d4c2f899a914f1dba283c1aadc311790733f705182998c6f7cae665", size = 1754113, upload-time = "2026-03-31T21:58:27.624Z" },
+ { url = "https://files.pythonhosted.org/packages/fb/56/3f653d7f53c89669301ec9e42c95233e2a0c0a6dd051269e6e678db4fdb0/aiohttp-3.13.5-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:fe6970addfea9e5e081401bcbadf865d2b6da045472f58af08427e108d618540", size = 1562351, upload-time = "2026-03-31T21:58:29.918Z" },
+ { url = "https://files.pythonhosted.org/packages/ec/a6/9b3e91eb8ae791cce4ee736da02211c85c6f835f1bdfac0594a8a3b7018c/aiohttp-3.13.5-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:7becdf835feff2f4f335d7477f121af787e3504b48b449ff737afb35869ba7bb", size = 1693205, upload-time = "2026-03-31T21:58:32.214Z" },
+ { url = "https://files.pythonhosted.org/packages/98/fc/bfb437a99a2fcebd6b6eaec609571954de2ed424f01c352f4b5504371dd3/aiohttp-3.13.5-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:676e5651705ad5d8a70aeb8eb6936c436d8ebbd56e63436cb7dd9bb36d2a9a46", size = 1730618, upload-time = "2026-03-31T21:58:34.728Z" },
+ { url = "https://files.pythonhosted.org/packages/e4/b6/c8534862126191a034f68153194c389addc285a0f1347d85096d349bbc15/aiohttp-3.13.5-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:9b16c653d38eb1a611cc898c41e76859ca27f119d25b53c12875fd0474ae31a8", size = 1745185, upload-time = "2026-03-31T21:58:36.909Z" },
+ { url = "https://files.pythonhosted.org/packages/0b/93/4ca8ee2ef5236e2707e0fd5fecb10ce214aee1ff4ab307af9c558bda3b37/aiohttp-3.13.5-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:999802d5fa0389f58decd24b537c54aa63c01c3219ce17d1214cbda3c2b22d2d", size = 1557311, upload-time = "2026-03-31T21:58:39.38Z" },
+ { url = "https://files.pythonhosted.org/packages/57/ae/76177b15f18c5f5d094f19901d284025db28eccc5ae374d1d254181d33f4/aiohttp-3.13.5-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:ec707059ee75732b1ba130ed5f9580fe10ff75180c812bc267ded039db5128c6", size = 1773147, upload-time = "2026-03-31T21:58:41.476Z" },
+ { url = "https://files.pythonhosted.org/packages/01/a4/62f05a0a98d88af59d93b7fcac564e5f18f513cb7471696ac286db970d6a/aiohttp-3.13.5-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:2d6d44a5b48132053c2f6cd5c8cb14bc67e99a63594e336b0f2af81e94d5530c", size = 1730356, upload-time = "2026-03-31T21:58:44.049Z" },
+ { url = "https://files.pythonhosted.org/packages/e4/85/fc8601f59dfa8c9523808281f2da571f8b4699685f9809a228adcc90838d/aiohttp-3.13.5-cp313-cp313-win32.whl", hash = "sha256:329f292ed14d38a6c4c435e465f48bebb47479fd676a0411936cc371643225cc", size = 432637, upload-time = "2026-03-31T21:58:46.167Z" },
+ { url = "https://files.pythonhosted.org/packages/c0/1b/ac685a8882896acf0f6b31d689e3792199cfe7aba37969fa91da63a7fa27/aiohttp-3.13.5-cp313-cp313-win_amd64.whl", hash = "sha256:69f571de7500e0557801c0b51f4780482c0ec5fe2ac851af5a92cfce1af1cb83", size = 458896, upload-time = "2026-03-31T21:58:48.119Z" },
+ { url = "https://files.pythonhosted.org/packages/5d/ce/46572759afc859e867a5bc8ec3487315869013f59281ce61764f76d879de/aiohttp-3.13.5-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:eb4639f32fd4a9904ab8fb45bf3383ba71137f3d9d4ba25b3b3f3109977c5b8c", size = 745721, upload-time = "2026-03-31T21:58:50.229Z" },
+ { url = "https://files.pythonhosted.org/packages/13/fe/8a2efd7626dbe6049b2ef8ace18ffda8a4dfcbe1bcff3ac30c0c7575c20b/aiohttp-3.13.5-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:7e5dc4311bd5ac493886c63cbf76ab579dbe4641268e7c74e48e774c74b6f2be", size = 497663, upload-time = "2026-03-31T21:58:52.232Z" },
+ { url = "https://files.pythonhosted.org/packages/9b/91/cc8cc78a111826c54743d88651e1687008133c37e5ee615fee9b57990fac/aiohttp-3.13.5-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:756c3c304d394977519824449600adaf2be0ccee76d206ee339c5e76b70ded25", size = 499094, upload-time = "2026-03-31T21:58:54.566Z" },
+ { url = "https://files.pythonhosted.org/packages/0a/33/a8362cb15cf16a3af7e86ed11962d5cd7d59b449202dc576cdc731310bde/aiohttp-3.13.5-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ecc26751323224cf8186efcf7fbcbc30f4e1d8c7970659daf25ad995e4032a56", size = 1726701, upload-time = "2026-03-31T21:58:56.864Z" },
+ { url = "https://files.pythonhosted.org/packages/45/0c/c091ac5c3a17114bd76cbf85d674650969ddf93387876cf67f754204bd77/aiohttp-3.13.5-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:10a75acfcf794edf9d8db50e5a7ec5fc818b2a8d3f591ce93bc7b1210df016d2", size = 1683360, upload-time = "2026-03-31T21:58:59.072Z" },
+ { url = "https://files.pythonhosted.org/packages/23/73/bcee1c2b79bc275e964d1446c55c54441a461938e70267c86afaae6fba27/aiohttp-3.13.5-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:0f7a18f258d124cd678c5fe072fe4432a4d5232b0657fca7c1847f599233c83a", size = 1773023, upload-time = "2026-03-31T21:59:01.776Z" },
+ { url = "https://files.pythonhosted.org/packages/c7/ef/720e639df03004fee2d869f771799d8c23046dec47d5b81e396c7cda583a/aiohttp-3.13.5-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:df6104c009713d3a89621096f3e3e88cc323fd269dbd7c20afe18535094320be", size = 1853795, upload-time = "2026-03-31T21:59:04.568Z" },
+ { url = "https://files.pythonhosted.org/packages/bd/c9/989f4034fb46841208de7aeeac2c6d8300745ab4f28c42f629ba77c2d916/aiohttp-3.13.5-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:241a94f7de7c0c3b616627aaad530fe2cb620084a8b144d3be7b6ecfe95bae3b", size = 1730405, upload-time = "2026-03-31T21:59:07.221Z" },
+ { url = "https://files.pythonhosted.org/packages/ce/75/ee1fd286ca7dc599d824b5651dad7b3be7ff8d9a7e7b3fe9820d9180f7db/aiohttp-3.13.5-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c974fb66180e58709b6fc402846f13791240d180b74de81d23913abe48e96d94", size = 1558082, upload-time = "2026-03-31T21:59:09.484Z" },
+ { url = "https://files.pythonhosted.org/packages/c3/20/1e9e6650dfc436340116b7aa89ff8cb2bbdf0abc11dfaceaad8f74273a10/aiohttp-3.13.5-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:6e27ea05d184afac78aabbac667450c75e54e35f62238d44463131bd3f96753d", size = 1692346, upload-time = "2026-03-31T21:59:12.068Z" },
+ { url = "https://files.pythonhosted.org/packages/d8/40/8ebc6658d48ea630ac7903912fe0dd4e262f0e16825aa4c833c56c9f1f56/aiohttp-3.13.5-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:a79a6d399cef33a11b6f004c67bb07741d91f2be01b8d712d52c75711b1e07c7", size = 1698891, upload-time = "2026-03-31T21:59:14.552Z" },
+ { url = "https://files.pythonhosted.org/packages/d8/78/ea0ae5ec8ba7a5c10bdd6e318f1ba5e76fcde17db8275188772afc7917a4/aiohttp-3.13.5-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:c632ce9c0b534fbe25b52c974515ed674937c5b99f549a92127c85f771a78772", size = 1742113, upload-time = "2026-03-31T21:59:17.068Z" },
+ { url = "https://files.pythonhosted.org/packages/8a/66/9d308ed71e3f2491be1acb8769d96c6f0c47d92099f3bc9119cada27b357/aiohttp-3.13.5-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:fceedde51fbd67ee2bcc8c0b33d0126cc8b51ef3bbde2f86662bd6d5a6f10ec5", size = 1553088, upload-time = "2026-03-31T21:59:19.541Z" },
+ { url = "https://files.pythonhosted.org/packages/da/a6/6cc25ed8dfc6e00c90f5c6d126a98e2cf28957ad06fa1036bd34b6f24a2c/aiohttp-3.13.5-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:f92995dfec9420bb69ae629abf422e516923ba79ba4403bc750d94fb4a6c68c1", size = 1757976, upload-time = "2026-03-31T21:59:22.311Z" },
+ { url = "https://files.pythonhosted.org/packages/c1/2b/cce5b0ffe0de99c83e5e36d8f828e4161e415660a9f3e58339d07cce3006/aiohttp-3.13.5-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:20ae0ff08b1f2c8788d6fb85afcb798654ae6ba0b747575f8562de738078457b", size = 1712444, upload-time = "2026-03-31T21:59:24.635Z" },
+ { url = "https://files.pythonhosted.org/packages/6c/cf/9e1795b4160c58d29421eafd1a69c6ce351e2f7c8d3c6b7e4ca44aea1a5b/aiohttp-3.13.5-cp314-cp314-win32.whl", hash = "sha256:b20df693de16f42b2472a9c485e1c948ee55524786a0a34345511afdd22246f3", size = 438128, upload-time = "2026-03-31T21:59:27.291Z" },
+ { url = "https://files.pythonhosted.org/packages/22/4d/eaedff67fc805aeba4ba746aec891b4b24cebb1a7d078084b6300f79d063/aiohttp-3.13.5-cp314-cp314-win_amd64.whl", hash = "sha256:f85c6f327bf0b8c29da7d93b1cabb6363fb5e4e160a32fa241ed2dce21b73162", size = 464029, upload-time = "2026-03-31T21:59:29.429Z" },
+ { url = "https://files.pythonhosted.org/packages/79/11/c27d9332ee20d68dd164dc12a6ecdef2e2e35ecc97ed6cf0d2442844624b/aiohttp-3.13.5-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:1efb06900858bb618ff5cee184ae2de5828896c448403d51fb633f09e109be0a", size = 778758, upload-time = "2026-03-31T21:59:31.547Z" },
+ { url = "https://files.pythonhosted.org/packages/04/fb/377aead2e0a3ba5f09b7624f702a964bdf4f08b5b6728a9799830c80041e/aiohttp-3.13.5-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:fee86b7c4bd29bdaf0d53d14739b08a106fdda809ca5fe032a15f52fae5fe254", size = 512883, upload-time = "2026-03-31T21:59:34.098Z" },
+ { url = "https://files.pythonhosted.org/packages/bb/a6/aa109a33671f7a5d3bd78b46da9d852797c5e665bfda7d6b373f56bff2ec/aiohttp-3.13.5-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:20058e23909b9e65f9da62b396b77dfa95965cbe840f8def6e572538b1d32e36", size = 516668, upload-time = "2026-03-31T21:59:36.497Z" },
+ { url = "https://files.pythonhosted.org/packages/79/b3/ca078f9f2fa9563c36fb8ef89053ea2bb146d6f792c5104574d49d8acb63/aiohttp-3.13.5-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8cf20a8d6868cb15a73cab329ffc07291ba8c22b1b88176026106ae39aa6df0f", size = 1883461, upload-time = "2026-03-31T21:59:38.723Z" },
+ { url = "https://files.pythonhosted.org/packages/b7/e3/a7ad633ca1ca497b852233a3cce6906a56c3225fb6d9217b5e5e60b7419d/aiohttp-3.13.5-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:330f5da04c987f1d5bdb8ae189137c77139f36bd1cb23779ca1a354a4b027800", size = 1747661, upload-time = "2026-03-31T21:59:41.187Z" },
+ { url = "https://files.pythonhosted.org/packages/33/b9/cd6fe579bed34a906d3d783fe60f2fa297ef55b27bb4538438ee49d4dc41/aiohttp-3.13.5-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:6f1cbf0c7926d315c3c26c2da41fd2b5d2fe01ac0e157b78caefc51a782196cf", size = 1863800, upload-time = "2026-03-31T21:59:43.84Z" },
+ { url = "https://files.pythonhosted.org/packages/c0/3f/2c1e2f5144cefa889c8afd5cf431994c32f3b29da9961698ff4e3811b79a/aiohttp-3.13.5-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:53fc049ed6390d05423ba33103ded7281fe897cf97878f369a527070bd95795b", size = 1958382, upload-time = "2026-03-31T21:59:46.187Z" },
+ { url = "https://files.pythonhosted.org/packages/66/1d/f31ec3f1013723b3babe3609e7f119c2c2fb6ef33da90061a705ef3e1bc8/aiohttp-3.13.5-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:898703aa2667e3c5ca4c54ca36cd73f58b7a38ef87a5606414799ebce4d3fd3a", size = 1803724, upload-time = "2026-03-31T21:59:48.656Z" },
+ { url = "https://files.pythonhosted.org/packages/0e/b4/57712dfc6f1542f067daa81eb61da282fab3e6f1966fca25db06c4fc62d5/aiohttp-3.13.5-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0494a01ca9584eea1e5fbd6d748e61ecff218c51b576ee1999c23db7066417d8", size = 1640027, upload-time = "2026-03-31T21:59:51.284Z" },
+ { url = "https://files.pythonhosted.org/packages/25/3c/734c878fb43ec083d8e31bf029daae1beafeae582d1b35da234739e82ee7/aiohttp-3.13.5-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:6cf81fe010b8c17b09495cbd15c1d35afbc8fb405c0c9cf4738e5ae3af1d65be", size = 1806644, upload-time = "2026-03-31T21:59:53.753Z" },
+ { url = "https://files.pythonhosted.org/packages/20/a5/f671e5cbec1c21d044ff3078223f949748f3a7f86b14e34a365d74a5d21f/aiohttp-3.13.5-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:c564dd5f09ddc9d8f2c2d0a301cd30a79a2cc1b46dd1a73bef8f0038863d016b", size = 1791630, upload-time = "2026-03-31T21:59:56.239Z" },
+ { url = "https://files.pythonhosted.org/packages/0b/63/fb8d0ad63a0b8a99be97deac8c04dacf0785721c158bdf23d679a87aa99e/aiohttp-3.13.5-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:2994be9f6e51046c4f864598fd9abeb4fba6e88f0b2152422c9666dcd4aea9c6", size = 1809403, upload-time = "2026-03-31T21:59:59.103Z" },
+ { url = "https://files.pythonhosted.org/packages/59/0c/bfed7f30662fcf12206481c2aac57dedee43fe1c49275e85b3a1e1742294/aiohttp-3.13.5-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:157826e2fa245d2ef46c83ea8a5faf77ca19355d278d425c29fda0beb3318037", size = 1634924, upload-time = "2026-03-31T22:00:02.116Z" },
+ { url = "https://files.pythonhosted.org/packages/17/d6/fd518d668a09fd5a3319ae5e984d4d80b9a4b3df4e21c52f02251ef5a32e/aiohttp-3.13.5-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:a8aca50daa9493e9e13c0f566201a9006f080e7c50e5e90d0b06f53146a54500", size = 1836119, upload-time = "2026-03-31T22:00:04.756Z" },
+ { url = "https://files.pythonhosted.org/packages/78/b7/15fb7a9d52e112a25b621c67b69c167805cb1f2ab8f1708a5c490d1b52fe/aiohttp-3.13.5-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:3b13560160d07e047a93f23aaa30718606493036253d5430887514715b67c9d9", size = 1772072, upload-time = "2026-03-31T22:00:07.494Z" },
+ { url = "https://files.pythonhosted.org/packages/7e/df/57ba7f0c4a553fc2bd8b6321df236870ec6fd64a2a473a8a13d4f733214e/aiohttp-3.13.5-cp314-cp314t-win32.whl", hash = "sha256:9a0f4474b6ea6818b41f82172d799e4b3d29e22c2c520ce4357856fced9af2f8", size = 471819, upload-time = "2026-03-31T22:00:10.277Z" },
+ { url = "https://files.pythonhosted.org/packages/62/29/2f8418269e46454a26171bfdd6a055d74febf32234e474930f2f60a17145/aiohttp-3.13.5-cp314-cp314t-win_amd64.whl", hash = "sha256:18a2f6c1182c51baa1d28d68fea51513cb2a76612f038853c0ad3c145423d3d9", size = 505441, upload-time = "2026-03-31T22:00:12.791Z" },
+ { url = "https://files.pythonhosted.org/packages/e2/a5/630bc484695d4a1342bbae85fb8689bf979106525684fc88f05b397324ad/aiohttp-3.13.5-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:347542f0ea3f95b2a955ee6656461fa1c776e401ac50ebce055a6c38454a0adf", size = 752872, upload-time = "2026-03-31T22:00:15.553Z" },
+ { url = "https://files.pythonhosted.org/packages/cd/b8/6a19dda37fda94a9ebefb3c1ae0ff419ac7fbf4fb40750e992829fc13614/aiohttp-3.13.5-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:178c7b5e62b454c2bc790786e6058c3cc968613b4419251b478c153a4aec32b1", size = 504582, upload-time = "2026-03-31T22:00:18.191Z" },
+ { url = "https://files.pythonhosted.org/packages/d5/34/8413eafee3421ade2d6ce9e7c0da1213e1d7f0049be09dcdc342b03a39ba/aiohttp-3.13.5-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:af545c2cffdb0967a96b6249e6f5f7b0d92cdfd267f9d5238d5b9ca63e8edb10", size = 499094, upload-time = "2026-03-31T22:00:21.118Z" },
+ { url = "https://files.pythonhosted.org/packages/da/cf/c6f97006093d1e8ca40fbab843ff49ec7725ab668f0714dd1cb702c62cbd/aiohttp-3.13.5-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:206b7b3ef96e4ce211754f0cd003feb28b7d81f0ad26b8d077a5d5161436067f", size = 1669505, upload-time = "2026-03-31T22:00:24.01Z" },
+ { url = "https://files.pythonhosted.org/packages/c2/27/3b2288e66dcec8b04771b2bee3909f70e4072bea995cde5ab7e775e73ddc/aiohttp-3.13.5-cp39-cp39-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:ee5e86776273de1795947d17bddd6bb19e0365fd2af4289c0d2c5454b6b1d36b", size = 1648928, upload-time = "2026-03-31T22:00:27.001Z" },
+ { url = "https://files.pythonhosted.org/packages/3a/7f/605d766887594a88dcc27a19663499c7c5e13e7aa87f129b763765a2ee63/aiohttp-3.13.5-cp39-cp39-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:95d14ca7abefde230f7639ec136ade282655431fd5db03c343b19dda72dd1643", size = 1731800, upload-time = "2026-03-31T22:00:29.603Z" },
+ { url = "https://files.pythonhosted.org/packages/71/94/5a878e728e30699d22b118f1a6ad576ab6fff9eb2c6fc8a7faa9376a1c3e/aiohttp-3.13.5-cp39-cp39-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:912d4b6af530ddb1338a66229dac3a25ff11d4448be3ec3d6340583995f56031", size = 1824247, upload-time = "2026-03-31T22:00:32.139Z" },
+ { url = "https://files.pythonhosted.org/packages/37/99/84b448291e9996bb83bf4fad3a71a9786d542f19c50a3ff0531bfaba6fac/aiohttp-3.13.5-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e999f0c88a458c836d5fb521814e92ed2172c649200336a6df514987c1488258", size = 1670742, upload-time = "2026-03-31T22:00:34.788Z" },
+ { url = "https://files.pythonhosted.org/packages/14/a8/d8d5d1ab6d29a4a3bdb9db31f161e338bfdf6638f6574ea8380f1d4a243c/aiohttp-3.13.5-cp39-cp39-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:39380e12bd1f2fdab4285b6e055ad48efbaed5c836433b142ed4f5b9be71036a", size = 1562474, upload-time = "2026-03-31T22:00:37.623Z" },
+ { url = "https://files.pythonhosted.org/packages/92/e8/bd889697916f10b65524422c61b4eeaf919eb35a170290cccb680cbe4eb4/aiohttp-3.13.5-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:9efcc0f11d850cefcafdd9275b9576ad3bfb539bed96807663b32ad99c4d4b88", size = 1642235, upload-time = "2026-03-31T22:00:40.541Z" },
+ { url = "https://files.pythonhosted.org/packages/60/42/3f1928107131f1413a5972ace14ddcd5364968e9bd7b3ad71272defafc9c/aiohttp-3.13.5-cp39-cp39-musllinux_1_2_armv7l.whl", hash = "sha256:147b4f501d0292077f29d5268c16bb7c864a1f054d7001c4c1812c0421ea1ed0", size = 1655397, upload-time = "2026-03-31T22:00:43.167Z" },
+ { url = "https://files.pythonhosted.org/packages/b2/79/c4bbcf4cac3a4715a326e49720ccdc3a4b5e14a367c5029eae7727d06029/aiohttp-3.13.5-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:d147004fede1b12f6013a6dbb2a26a986a671a03c6ea740ddc76500e5f1c399f", size = 1703509, upload-time = "2026-03-31T22:00:45.908Z" },
+ { url = "https://files.pythonhosted.org/packages/d1/e6/32d245876f211a7308a7d5437707f9296b1f9837a2888a407ed04e61321c/aiohttp-3.13.5-cp39-cp39-musllinux_1_2_riscv64.whl", hash = "sha256:9277145d36a01653863899c665243871434694bcc3431922c3b35c978061bdb8", size = 1550098, upload-time = "2026-03-31T22:00:49.48Z" },
+ { url = "https://files.pythonhosted.org/packages/db/62/ab0f1304def56ce2356e6fbb9f0b024d6544010351430070f48f53b89e0a/aiohttp-3.13.5-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:4e704c52438f66fdd89588346183d898bb42167cf88f8b7ff1c0f9fc957c348f", size = 1724326, upload-time = "2026-03-31T22:00:52.165Z" },
+ { url = "https://files.pythonhosted.org/packages/c4/9a/aab4469689024046220ea438aa020ea2ae04cd1dd71aea3057e094f8c357/aiohttp-3.13.5-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:a8a4d3427e8de1312ddf309cc482186466c79895b3a139fed3259fc01dfa9a5b", size = 1658824, upload-time = "2026-03-31T22:00:55.122Z" },
+ { url = "https://files.pythonhosted.org/packages/b0/98/bcc35d4db687acabf06d41f561a99fa88bca145292513388c858d99b72c5/aiohttp-3.13.5-cp39-cp39-win32.whl", hash = "sha256:6f497a6876aa4b1a102b04996ce4c1170c7040d83faa9387dd921c16e30d5c83", size = 440302, upload-time = "2026-03-31T22:00:57.673Z" },
+ { url = "https://files.pythonhosted.org/packages/25/61/b0203c2ef6bd268fca0eda142f0efbba7cbebd7ad38f7bb01dd31c2ff68e/aiohttp-3.13.5-cp39-cp39-win_amd64.whl", hash = "sha256:cb979826071c0986a5f08333a36104153478ce6018c58cba7f9caddaf63d5d67", size = 463076, upload-time = "2026-03-31T22:01:00.264Z" },
+]
+
+[[package]]
+name = "aiohttp"
+version = "3.14.3"
+source = { registry = "https://pypi.org/simple" }
+resolution-markers = [
+ "python_full_version >= '3.10'",
+]
+dependencies = [
+ { name = "aiohappyeyeballs", version = "2.7.1", source = { registry = "https://pypi.org/simple" } },
+ { name = "aiosignal" },
+ { name = "async-timeout", marker = "python_full_version < '3.11'" },
+ { name = "attrs" },
+ { name = "frozenlist" },
+ { name = "multidict" },
+ { name = "propcache", version = "0.5.2", source = { registry = "https://pypi.org/simple" } },
+ { name = "typing-extensions", marker = "python_full_version < '3.13'" },
+ { name = "yarl", version = "1.24.5", source = { registry = "https://pypi.org/simple" } },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/58/d9/22ce5786ac0c1653ae8b6c23bded02c1686d11f0dbb45b31ce128e0df985/aiohttp-3.14.3.tar.gz", hash = "sha256:9491196535a88924a60afd5b5f434b5b203b6cc616250878dbdb223a8f7844bc", size = 7971213, upload-time = "2026-07-23T01:57:27.037Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/2d/4d/4a99fb425c5e0cad715eea7bd190aff46f38b959a0a2dadb993705d34b26/aiohttp-3.14.3-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:eb0495d778817619273c108784292be161a924b9f5ae5cbbc70a2caa6838250b", size = 765848, upload-time = "2026-07-23T01:52:08.217Z" },
+ { url = "https://files.pythonhosted.org/packages/74/e8/43b85dc55b8e950dc644babe762add781319ea881b57b33d2cce12017d12/aiohttp-3.14.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:c3c200cf9757edd785051dc699c7ecbec22110dbfcb3fefc7a9f9695eda8ea7a", size = 517476, upload-time = "2026-07-23T01:52:10.846Z" },
+ { url = "https://files.pythonhosted.org/packages/7f/9e/73b582c4dbbc3c12ef4473822475effaabf1f934b56f14f5b03fe5d3a2af/aiohttp-3.14.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:fd51ebf9d3a00c074df4ede271023f4d2dba289bcc740b88191872716014e3c5", size = 515334, upload-time = "2026-07-23T01:52:12.636Z" },
+ { url = "https://files.pythonhosted.org/packages/79/03/e98c3c9e05a5bdf97defe5ff9169baba4f0ec9a901f2d60e0f060c2f051e/aiohttp-3.14.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:134ac5ddcf61c6fad984b9a5727d83492ada43d63471db20fb73042c13fca62f", size = 1708830, upload-time = "2026-07-23T01:52:14.538Z" },
+ { url = "https://files.pythonhosted.org/packages/d7/2c/26e60b694844dfd2176c57f913a22d0cd6a16f9ff202cbda7580d0328b98/aiohttp-3.14.3-cp310-cp310-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:70c987b27534f9ae1a723f47ae921571d616da21d3208282bf4c52af5164ac43", size = 1674012, upload-time = "2026-07-23T01:52:16.486Z" },
+ { url = "https://files.pythonhosted.org/packages/38/65/672df92e3172cd876aacfa97a952ac560877eb169384b2991ac5b273de4c/aiohttp-3.14.3-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:1b59533861b70a2185c8f4f350f791f39d64358ef6944ce71c5240c9ec0982c9", size = 1767015, upload-time = "2026-07-23T01:52:18.28Z" },
+ { url = "https://files.pythonhosted.org/packages/9e/c5/228dec7bfec1c373cc2217cdeb47d6456dcd7a13a4c55144930a75ae3851/aiohttp-3.14.3-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:1c5281acc88b92396f88c7e1e2748f8466689df22b80170e4f51efa712fb47a8", size = 1858700, upload-time = "2026-07-23T01:52:20.08Z" },
+ { url = "https://files.pythonhosted.org/packages/bd/ff/cb36724e8c8d17f90ada567a9ff3efe1d6e9b549fba697a242aece180f21/aiohttp-3.14.3-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:48d67b87db6279c044760787eb01f6413032c2e6f3ba1cafaa492b1c8e578479", size = 1714075, upload-time = "2026-07-23T01:52:22.071Z" },
+ { url = "https://files.pythonhosted.org/packages/9f/3a/296a4135c6366376263aeef54b15caca1f07676c2ae0c525d7832f2f808a/aiohttp-3.14.3-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f53bcd52f585e1ac3e590d61434eb61f9a88c38df041b4ea126d97144344a77b", size = 1588234, upload-time = "2026-07-23T01:52:23.757Z" },
+ { url = "https://files.pythonhosted.org/packages/7d/81/9d5d853ef892dc066d1eb6db0e87a47348b920c1c879aa554612fdbd9d79/aiohttp-3.14.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:0fdea2281997af69da84c77ffa6f5938a0285f21fb3887c249d67419ca865b3d", size = 1677300, upload-time = "2026-07-23T01:52:25.861Z" },
+ { url = "https://files.pythonhosted.org/packages/68/96/021d386ae32d9b26d4b88df2e794546232ff56bb6be952bf6be227c0bbc7/aiohttp-3.14.3-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:cda5fd5c95ad7a125a2e8464acc78b98b94c475a3780d6aa0aa157c93f470f4d", size = 1691501, upload-time = "2026-07-23T01:52:28Z" },
+ { url = "https://files.pythonhosted.org/packages/29/9f/af66adce26a14af135c003cbd0f44ccaa68cebd30ff8ac99ca47fb4958f7/aiohttp-3.14.3-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:6debfa7312ff9d4c124dc71d72e9a0a4b9e0879e48ba6fcb42bef5c3300289e2", size = 1735113, upload-time = "2026-07-23T01:52:29.995Z" },
+ { url = "https://files.pythonhosted.org/packages/2f/90/28c390d4c9851effe52ac25b5a2e1d92246acd00728b4fc7975dafb67484/aiohttp-3.14.3-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:f4e05329faa0ea1a404b37de4f034fd2c2defcca06a68dc6745e4e56c88e8a48", size = 1577486, upload-time = "2026-07-23T01:52:31.937Z" },
+ { url = "https://files.pythonhosted.org/packages/db/c2/00e23a1bf2abb70dd353f6987db7e7f2491d0261f7363997738c71c98f95/aiohttp-3.14.3-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:a3a8296e7ab5c295f53f1041487cb088e1480775aafbf7fe545d93b770a0f96f", size = 1751353, upload-time = "2026-07-23T01:52:33.688Z" },
+ { url = "https://files.pythonhosted.org/packages/6e/7d/d51a706a8cbfa57f0611127daf61ab3ae02ab8420b0407412079227d1c65/aiohttp-3.14.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:5373dc80ad1aa2fb9ad95c83f24eef418bbda3a61375f128e5b0192e4f3f9b32", size = 1698681, upload-time = "2026-07-23T01:52:38.167Z" },
+ { url = "https://files.pythonhosted.org/packages/ec/b0/90bd5cd9fdd9787cb4211d284d1fb8401339a933cb0227a15b71e789232f/aiohttp-3.14.3-cp310-cp310-win32.whl", hash = "sha256:a3e22975f905b89a55a488c2a08f2fdb2186175349e917d48985cc468a3d4c6e", size = 456733, upload-time = "2026-07-23T01:52:41.823Z" },
+ { url = "https://files.pythonhosted.org/packages/d8/15/fe5b8f6a71ae112bc677163d0b0701bda5dc15005249582258ede0eb88c7/aiohttp-3.14.3-cp310-cp310-win_amd64.whl", hash = "sha256:bdd0e2834dce1a26c1bbe26464861e16bbe217042cbff619247c11594472518c", size = 480460, upload-time = "2026-07-23T01:52:43.905Z" },
+ { url = "https://files.pythonhosted.org/packages/54/00/45e98b6645cd7f00a4b78b749ebd309094b0eaeb2d2e96157eadbc0d0050/aiohttp-3.14.3-cp310-cp310-win_arm64.whl", hash = "sha256:eac645b09bcfdf73df7536331f0678c1086ea250981118ddb5199e17ccef72bb", size = 453479, upload-time = "2026-07-23T01:52:46.075Z" },
+ { url = "https://files.pythonhosted.org/packages/f8/5c/b3e4ff8ad43a8afef9602c5e90285936da1beaea8b029016b793891f03c3/aiohttp-3.14.3-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:e568e14940c09955aa51f4e645b6daa18a581c5dcfcd73744dcc86a856e3ced3", size = 764250, upload-time = "2026-07-23T01:52:48.525Z" },
+ { url = "https://files.pythonhosted.org/packages/0e/da/f1b384465e51449d844056b75070461da03a9a23e6c1747003695bf4172a/aiohttp-3.14.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:54cfcdee2770dac994417cbb0ee1f3eb0e7cb6b30c79bf44f2c02ff79ec5124a", size = 516281, upload-time = "2026-07-23T01:52:51.047Z" },
+ { url = "https://files.pythonhosted.org/packages/b9/3f/01264f820ee2e3712a827892b1cd6ff80f3300c1fcbffbb45714a915d47a/aiohttp-3.14.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:21c016079415ed3fd676963e9793700a566d85dbbd6bfc564b9b2d209147dcc8", size = 514742, upload-time = "2026-07-23T01:52:53.779Z" },
+ { url = "https://files.pythonhosted.org/packages/9e/8d/a71c6f2db52ac1ed142b133f7feddaa6b70539c3f4de24d7e226c95b794c/aiohttp-3.14.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d6088ec9894113802bddb3c09e974929aed2c7b3a8c456219b8aab4481f1a239", size = 1780613, upload-time = "2026-07-23T01:52:56.948Z" },
+ { url = "https://files.pythonhosted.org/packages/a5/11/3dd9b3fb3a170f6ec9011b5291d876a6fab4086714c9e158600edf01b4fd/aiohttp-3.14.3-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:16ea7e24c309fb7c0bbd505d149abe4fe4dccfb8db911db7dbec0921bc889a6f", size = 1737688, upload-time = "2026-07-23T01:52:59.294Z" },
+ { url = "https://files.pythonhosted.org/packages/6d/3e/834c26918be7d88068822b40e0db30fca50b5f4fe79104aa16a93f1d74e6/aiohttp-3.14.3-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:56f355e79f71aef2a85c80305cc915f894b170dba76de5fe84f6351939b83c06", size = 1845742, upload-time = "2026-07-23T01:53:01.641Z" },
+ { url = "https://files.pythonhosted.org/packages/cc/c9/49ab8572df7d66bc13d11e31f781292badb04180dd87ba98733066c6aed7/aiohttp-3.14.3-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:18c441d0a8fca6de8d1f546849b9f0ab20d435993e2c5b59562b2fae6be2f929", size = 1928412, upload-time = "2026-07-23T01:53:04.018Z" },
+ { url = "https://files.pythonhosted.org/packages/a5/b9/2b8f0c0ce09c87a1daf80fd483431b56b1435d3f62789bc86f572e1245de/aiohttp-3.14.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:53e7b4ce82b54a8bcc71b3b67a5cbd177ca1d7f592cbc92cd38b7349f73482db", size = 1786220, upload-time = "2026-07-23T01:53:06.481Z" },
+ { url = "https://files.pythonhosted.org/packages/85/00/9c45f81de11710460edfa1dc81317b6e882703b160926c879a9d20da9fcc/aiohttp-3.14.3-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f55119f7bf25f49ed210f6096090715da24f2943c62102448915fde3c62877ce", size = 1637231, upload-time = "2026-07-23T01:53:10.258Z" },
+ { url = "https://files.pythonhosted.org/packages/19/ce/967d628e910756f3539c6107cb7844a1b69440dcb3029a5ee7871b09ab63/aiohttp-3.14.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:9aa6e61fdf20105c4144e755bd586008ff450791d67b1c8146fdc15959c4d51c", size = 1753161, upload-time = "2026-07-23T01:53:13.817Z" },
+ { url = "https://files.pythonhosted.org/packages/11/b2/0c3d4114f0aee4f580f5b3b4eb71b24d7a23b834ea506a4dfebe76513f35/aiohttp-3.14.3-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:ccd4893707b3e2a13e39c90d43cf80edf2e4d0457935bcc103bf2346214c3f15", size = 1756356, upload-time = "2026-07-23T01:53:16.211Z" },
+ { url = "https://files.pythonhosted.org/packages/63/5d/99e7d91c82f1399d1ae2a854e080bd1493fbc31e5e959dbc4ec33dac3bec/aiohttp-3.14.3-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:b2466434105a4e03113c36ec775cc2ebe6676b62eae326fa670bb607ef788c1c", size = 1819846, upload-time = "2026-07-23T01:53:18.289Z" },
+ { url = "https://files.pythonhosted.org/packages/ad/05/d5e1cb6480eeffd3f901d40a2c5e2d1e7effdc797837da3b490272699f13/aiohttp-3.14.3-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:ba59d59aba08ac02fc03b0c8983ccd5ee39a199d0552ce9e6d2b4845b34d59ae", size = 1628531, upload-time = "2026-07-23T01:53:23.86Z" },
+ { url = "https://files.pythonhosted.org/packages/c9/90/b934682bcaefae18a9e04f3dff5b68522ba810906358ae5029b68110ea3b/aiohttp-3.14.3-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:ed099d105449c4f9e84f24af203cd131349d4761d8813fa7e02c32e7128cd910", size = 1832712, upload-time = "2026-07-23T01:53:27.551Z" },
+ { url = "https://files.pythonhosted.org/packages/21/df/6061679faaf81fac746e7307c7adb71e858071a5d34c27583afefc64f543/aiohttp-3.14.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:152516815ef926786a0b6ae2b8f1fd2e0c71582dee0b435636865316fd4891b7", size = 1775014, upload-time = "2026-07-23T01:53:30.223Z" },
+ { url = "https://files.pythonhosted.org/packages/8a/1d/f854878bbc69b88faefe924b619a34a6f59ec05fd387c77690667eaa75eb/aiohttp-3.14.3-cp311-cp311-win32.whl", hash = "sha256:a4af35c443e0b1a1bd6a8af3f3485d7fda15c142751a00f3ff8090f0b93346fa", size = 456006, upload-time = "2026-07-23T01:53:34.97Z" },
+ { url = "https://files.pythonhosted.org/packages/73/0c/2af9d1674baccd1dbd47282a93d660a22e57ef6167c856deb24b4214fbab/aiohttp-3.14.3-cp311-cp311-win_amd64.whl", hash = "sha256:e1e74298bab6ee0d6e749ed4fd1901c7e604bdda32c03d787a2cc71c46d0433d", size = 481069, upload-time = "2026-07-23T01:53:39.673Z" },
+ { url = "https://files.pythonhosted.org/packages/8e/76/88401ff3fc95e85c5fc38d588f36f55e61ecb64343b2bc8d69326f453cc0/aiohttp-3.14.3-cp311-cp311-win_arm64.whl", hash = "sha256:03cd2bde3d7f085b64e549c985f4bb928cad7e8ecf5323bfca320db548d81b39", size = 453021, upload-time = "2026-07-23T01:53:43.749Z" },
+ { url = "https://files.pythonhosted.org/packages/18/d4/eb96299230e20acf2efae207cb8d69051f1f68e357e5ea5e479bf6fb097a/aiohttp-3.14.3-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:39aded8c7f3b935b54aab1d8d73c70ec0ee2d3ec3b943e0e86611bc150ba47f5", size = 754690, upload-time = "2026-07-23T01:53:47.332Z" },
+ { url = "https://files.pythonhosted.org/packages/88/11/e7a70a209eb9a067c0d3212b518a0134e3484f5178c7533878b6b514d469/aiohttp-3.14.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:5bcb6ff3fdab1258a192679ff1a05d44f59626430aa05cd1a9d2447423599228", size = 509484, upload-time = "2026-07-23T01:53:51.159Z" },
+ { url = "https://files.pythonhosted.org/packages/30/07/4bbc222cc8dbe31d4c3e8a5baad2286e4d42026ac0c570027b89afce6344/aiohttp-3.14.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:617105e2c3018ee38d0c8ce5ee3c84f621a6d8b9f723202aacaff28449ca91ee", size = 511949, upload-time = "2026-07-23T01:53:55.083Z" },
+ { url = "https://files.pythonhosted.org/packages/54/b9/42e74c46b7b7c794b995bbc1f573fb48950c38b19d8600c62a6804ee2d67/aiohttp-3.14.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f631fe87a6f30df5fbe6d79640b25e4cffb38c31c7fb6f10871517b84b0f8c1a", size = 1765282, upload-time = "2026-07-23T01:53:59.662Z" },
+ { url = "https://files.pythonhosted.org/packages/6b/ed/62bc4d74363ad346d518e0720363a949f63e2e23439a79eb5813d4d29bb3/aiohttp-3.14.3-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:a94dbaae5ae27bd849c93570669bff91e0510f33a80805738e3de72a7be0447b", size = 1741511, upload-time = "2026-07-23T01:54:04.063Z" },
+ { url = "https://files.pythonhosted.org/packages/d0/9f/181e8a8bc79e47d13c7fc4540bd7a3b729d9505609c61f392a8dd2fbfe55/aiohttp-3.14.3-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:8f2f1c4c032c7cedd7d8da6f54c97b70266c6570c3108d3fdffee7188bb70529", size = 1810680, upload-time = "2026-07-23T01:54:09.882Z" },
+ { url = "https://files.pythonhosted.org/packages/5c/9a/dec94d6ad694552fe3424e3f1928d7a606a5d9d9433a04e7ecdd9d38ae7f/aiohttp-3.14.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:ea05e1f97ceea523942d9b2a7d7c0359d781d683d6b043f5943a602b14da4787", size = 1905646, upload-time = "2026-07-23T01:54:13.475Z" },
+ { url = "https://files.pythonhosted.org/packages/52/b7/7cd31f29d6055bd711ae6e669367fba6f5ae9de463910a793e30556a8db7/aiohttp-3.14.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:543906c127fb1d929b95076db19b83fa2d46751006ff1e23b093aa5ac4d8db42", size = 1792122, upload-time = "2026-07-23T01:54:15.752Z" },
+ { url = "https://files.pythonhosted.org/packages/66/73/10b1ef93afa61f4963c746257b70ced619cf31a4798671de5fdb2608501d/aiohttp-3.14.3-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0a5ff2dfbb9ce645fa5b8ef3e02c6c0b9cc3f6030ff863d0c51fffc50cb5541b", size = 1591127, upload-time = "2026-07-23T01:54:19.489Z" },
+ { url = "https://files.pythonhosted.org/packages/49/ed/3b203fa6de1b338c14acdc06bf6ca9b043b7944f005966958c2ced932cde/aiohttp-3.14.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:041badb8f84396357c4d3ad26de6afd7a32b112f43d3c63045c0c8278cfd2043", size = 1725210, upload-time = "2026-07-23T01:54:24.129Z" },
+ { url = "https://files.pythonhosted.org/packages/28/b7/1c2aab8c706436dcc28598452488ac9cd7c409da815237c28c27d58993e6/aiohttp-3.14.3-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:530125ee1163c4219af35dc3aa1206e541e7b31b6efc1a3f93b70a136f65d427", size = 1764848, upload-time = "2026-07-23T01:54:27.973Z" },
+ { url = "https://files.pythonhosted.org/packages/54/50/94c28f08b131c4bf10984ea2c7a536c9920608bb2d6e7f95642c30cc87b7/aiohttp-3.14.3-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:c8653fd547c93a61aadc612007790f5555cdd18946fa48cf45e26d8ea4ea473d", size = 1777102, upload-time = "2026-07-23T01:54:31.775Z" },
+ { url = "https://files.pythonhosted.org/packages/13/d4/e7d09ba7d345fb2d74440fd2fa033c5e079fac05552927705986f41a364f/aiohttp-3.14.3-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:89176250f686cb9853c0fb7ead90e639e915b84a6f43eedc2a4e7ec21f1037f0", size = 1580205, upload-time = "2026-07-23T01:54:34.518Z" },
+ { url = "https://files.pythonhosted.org/packages/a3/84/072a91d68e1e1eb587985b54baab94221277f877e8ef274fc213a0ceae28/aiohttp-3.14.3-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:3a26434dafe408229ff3403458ca58de24fb51936504decac49ce6755f77e59d", size = 1797219, upload-time = "2026-07-23T01:54:36.995Z" },
+ { url = "https://files.pythonhosted.org/packages/e0/eb/aad34e897e668424d6e995da5dff8a4a09af93363d3392488772957a63aa/aiohttp-3.14.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:d1558173930a5a8d3069cee5c92fc91c87c4dbcb099debbb3622053717145a19", size = 1768629, upload-time = "2026-07-23T01:54:40.103Z" },
+ { url = "https://files.pythonhosted.org/packages/b6/2b/6bb88ddba0fecd9122aa3ebcad25996cf6c083a4a7040dbb3a4f97972af6/aiohttp-3.14.3-cp312-cp312-win32.whl", hash = "sha256:16100ad3ab8d649fdfbee87602d9d2dcdca9df0b9eda8a1b5fdc0d41f96da559", size = 451481, upload-time = "2026-07-23T01:54:42.547Z" },
+ { url = "https://files.pythonhosted.org/packages/76/9b/f2f8f108da17ecef2cc3efc424e8b7ad3782b1a8360f7b8eae8ced84f6ea/aiohttp-3.14.3-cp312-cp312-win_amd64.whl", hash = "sha256:33a2d7c28d33797a2e99923dffa63f83d908a19b6bf26cfe80fa790aa5e1a75a", size = 476845, upload-time = "2026-07-23T01:54:44.853Z" },
+ { url = "https://files.pythonhosted.org/packages/3e/44/28dac80a8941b604f4da10ce21097614ca1bf905ce93dca28d8d7de9c1e7/aiohttp-3.14.3-cp312-cp312-win_arm64.whl", hash = "sha256:362a3fd481769cac1a824514bcd86fda51c65e8fe6e051099e008fddde6db17c", size = 448050, upload-time = "2026-07-23T01:54:47.087Z" },
+ { url = "https://files.pythonhosted.org/packages/57/be/5afd201cc0ab139029aadb75392efe85a293403d9dd3a3226161c21ce00c/aiohttp-3.14.3-cp313-cp313-android_21_arm64_v8a.whl", hash = "sha256:2e9878ae68e4a5f1c0abe4dd497dbc3d51946f5837b56759e2a02e78fa90ef86", size = 506269, upload-time = "2026-07-23T01:54:49.075Z" },
+ { url = "https://files.pythonhosted.org/packages/22/09/dec8189d62b45ade009f6792a2264b942a90cb88aeaf181239933cd72c3c/aiohttp-3.14.3-cp313-cp313-android_21_x86_64.whl", hash = "sha256:f3d2669fe7dec7fc359ecdb5984b29b50d85d5d00f8c1cb61de4f4a24ee42627", size = 515166, upload-time = "2026-07-23T01:54:51.894Z" },
+ { url = "https://files.pythonhosted.org/packages/28/24/2854869d29ed8a8b19d74f9ec6629515f7e04d02dd329d9d179201e58e47/aiohttp-3.14.3-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:cc7cb243a68167172f48c1fd43cee91ec4b1d40cefd190edd43369d1a6bc9c82", size = 486263, upload-time = "2026-07-23T01:54:54.223Z" },
+ { url = "https://files.pythonhosted.org/packages/d4/dd/57187c8be2a35aea65eaee3bd2c3dcbbcf0204f5106c89637e3610380cd1/aiohttp-3.14.3-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:78253b573e6ffab5028924fc98bc281aae05445969982a10864bc360dea2016c", size = 492299, upload-time = "2026-07-23T01:54:56.236Z" },
+ { url = "https://files.pythonhosted.org/packages/b9/11/06ae6ed8f0d414edf4068861e233d8fe23ee699bfd4b3ceb8663db948a62/aiohttp-3.14.3-cp313-cp313-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:7041d52c3a7fa20c9e8c182b534704abb19502c8bdcbde7ab23bfda6f642394f", size = 502235, upload-time = "2026-07-23T01:54:58.377Z" },
+ { url = "https://files.pythonhosted.org/packages/7e/a3/559639c34a345d2cf7c52dff6838119f2eaf29eb508227b5b83f573af813/aiohttp-3.14.3-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ac74facc01463f138b0da5580329cfcc82818dea5656e83ddcd11268fc12ff80", size = 750883, upload-time = "2026-07-23T01:55:00.65Z" },
+ { url = "https://files.pythonhosted.org/packages/91/cd/41e131f13afd1e7b0172a9d9eda085ef90eb8439f41f0d279db81ed3ae60/aiohttp-3.14.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:d6218d92e450824e9b4881f44e8c09f1853b490f9a64130801024a4793b1b3b0", size = 508473, upload-time = "2026-07-23T01:55:02.945Z" },
+ { url = "https://files.pythonhosted.org/packages/bc/6b/e7f13410d391c6e55b4c007a8de024355389d7d459e3d64c42b2d33617e5/aiohttp-3.14.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:11fb37ef075669eee52ab1928fbf6e1741fada40409fa309ebde9607a962aebf", size = 509190, upload-time = "2026-07-23T01:55:05.173Z" },
+ { url = "https://files.pythonhosted.org/packages/97/21/6464573e53d69672cc1eada3e5c5cb2d2efa82701e8305a0f2047a576967/aiohttp-3.14.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:55bdcc472aafe2de4a253045cc128007a64f1e0264fb675791e132ea5edaa3bd", size = 1761478, upload-time = "2026-07-23T01:55:07.383Z" },
+ { url = "https://files.pythonhosted.org/packages/1a/81/d217043a4c17fbce360905e3b2bdd20139ebc9a2de836d035d179c4da006/aiohttp-3.14.3-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:c39846c3aad97a8530c89d7a3869a8f8e9e3762c6ac0504481e5c80948f7e807", size = 1735092, upload-time = "2026-07-23T01:55:09.803Z" },
+ { url = "https://files.pythonhosted.org/packages/a1/66/e13a02d0eeb1a9a502402a977abb4e4abff9fe4051c26f80558c57a7c975/aiohttp-3.14.3-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:5895ef58c4620afe02fa16044f023dc4dafec08158f9d08874a46a7dbc0341b8", size = 1800546, upload-time = "2026-07-23T01:55:12.012Z" },
+ { url = "https://files.pythonhosted.org/packages/26/5e/57d42fca1d18cb5acc1cad945d017fabc5d6ae71d8a08ad66be8dc3ee544/aiohttp-3.14.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:fa9467a8113aa69d3d7c55a70ef0b7c636010a40993f3df9d9d0d73b3eb7ef24", size = 1895250, upload-time = "2026-07-23T01:55:14.357Z" },
+ { url = "https://files.pythonhosted.org/packages/ca/1c/7da8d08e74d56f00070822f9638ff3f1c563f8ad87d1efa996c87bfc8644/aiohttp-3.14.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d7d2deec16eeedf55f2c7cf75b521ea3856a5177e123844f8fd0f114ce252cb5", size = 1789289, upload-time = "2026-07-23T01:55:16.668Z" },
+ { url = "https://files.pythonhosted.org/packages/cd/0f/cf16bcf56896981c1a0319f5d5db9337994b5165730c48a8fa07e9b34be6/aiohttp-3.14.3-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:dd54d0e8717de95939766febac482ac0474d8ac3b048115f9f2b1d23a16e7db4", size = 1586706, upload-time = "2026-07-23T01:55:18.913Z" },
+ { url = "https://files.pythonhosted.org/packages/fe/6f/76eac12a7f2480e1e304f842efdb07db33256b0d9165b866b6ef0806c202/aiohttp-3.14.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:df82f3787c940c94986b34222d59c9e38843fba85139f36e85255a82ad5355a9", size = 1724652, upload-time = "2026-07-23T01:55:21.296Z" },
+ { url = "https://files.pythonhosted.org/packages/39/b6/19c8c592baeeb94b75f966547d40c02ac7590902306ec5863d5c027cf506/aiohttp-3.14.3-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:42a67efc36300d052fb4508a53e8b6901b9284b599ae63945c377569c5fcc1e1", size = 1756239, upload-time = "2026-07-23T01:55:23.705Z" },
+ { url = "https://files.pythonhosted.org/packages/dc/c9/4e9383150296f97f873b680c4de8fb2cd88608fb9f48c79edcb111611abc/aiohttp-3.14.3-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:7a75aa63cbf9b21cfaf60dc2657e19df2c2867d91707d653fee171ffeedd1371", size = 1769161, upload-time = "2026-07-23T01:55:26.082Z" },
+ { url = "https://files.pythonhosted.org/packages/aa/1e/147bdc6cc5de5f3ab011be8bf5d6e786633249f22c20bae06f85e45f5387/aiohttp-3.14.3-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:e92eb8acc45eb6a9f4935071a77edf5b85cc6f8dfad5cd99e97653c26593cdde", size = 1578759, upload-time = "2026-07-23T01:55:28.846Z" },
+ { url = "https://files.pythonhosted.org/packages/fd/31/78388a9d6040ece2e11df62ea229a822cf5e52d238374b220ae9975b2623/aiohttp-3.14.3-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:b014a6ed7cf912e787149fdc529166d3ceabac23f26efeea3158c9aba2354e7e", size = 1792025, upload-time = "2026-07-23T01:55:31.457Z" },
+ { url = "https://files.pythonhosted.org/packages/03/51/a3d29fdf2c25d796746af8ad6fe56a45d6256c38b0a8a2ed752e1160b3a2/aiohttp-3.14.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:3d4f72af88ac2474bb5bca640030320e3d38a0163a1d7533500e87be458eef71", size = 1768477, upload-time = "2026-07-23T01:55:33.87Z" },
+ { url = "https://files.pythonhosted.org/packages/29/a6/442e18b5afeade534d877a2dc3c3e392aff8d49787890b0cf84790410267/aiohttp-3.14.3-cp313-cp313-win32.whl", hash = "sha256:5f08ec777f35ee70720233b8b9811d3bb5d728137f30ac91b7457709c3261ac0", size = 451069, upload-time = "2026-07-23T01:55:36.121Z" },
+ { url = "https://files.pythonhosted.org/packages/9d/69/3d876ac02659f271cf7f6769f14a8e3de5b6e888ed8b5a7e998086a4cec8/aiohttp-3.14.3-cp313-cp313-win_amd64.whl", hash = "sha256:dff9461ec275f22135650d5ba4b4931a11f3958df7dfbb8db630000d4dee0883", size = 476518, upload-time = "2026-07-23T01:55:38.303Z" },
+ { url = "https://files.pythonhosted.org/packages/b2/0e/50d6e6471cd31edce8b282bdec59375a3a69124d8a989a0b1313355cae52/aiohttp-3.14.3-cp313-cp313-win_arm64.whl", hash = "sha256:ddcac3c6b382e81f1dd0499199d4136b877beb4cb5ef770bbbfba56c4b8f55d2", size = 447676, upload-time = "2026-07-23T01:55:40.451Z" },
+ { url = "https://files.pythonhosted.org/packages/c8/20/887fdcf832326571b370ffc347b3e70abe101096f3720126aac161b1d872/aiohttp-3.14.3-cp314-cp314-android_24_arm64_v8a.whl", hash = "sha256:49f7325beb0f85ef4aef5f48f490269575f83e6e2acad00a1d80b807eb027062", size = 509067, upload-time = "2026-07-23T01:55:42.618Z" },
+ { url = "https://files.pythonhosted.org/packages/ad/a3/92cec936f78cc4bf0fa5554ebe593b73459d94e3c62303e1902a4cccb6f7/aiohttp-3.14.3-cp314-cp314-android_24_x86_64.whl", hash = "sha256:e3be98a7c30b8c25d573dafba7171d66dfb05ee6a9070fc46535464ff97700a6", size = 514774, upload-time = "2026-07-23T01:55:44.937Z" },
+ { url = "https://files.pythonhosted.org/packages/29/ba/2a0c38df3fc557620b6a5acd98364af050053b6285b4dc7ee74100c63c18/aiohttp-3.14.3-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:614c61d478b83953e261d02bb2df750f17227cd33ef8002945bf5aebbde21919", size = 488134, upload-time = "2026-07-23T01:55:47.135Z" },
+ { url = "https://files.pythonhosted.org/packages/48/d6/d51b7d4bf309af3693940d8ffd2b9ed0b682434ef85959b7c9c137f60cf8/aiohttp-3.14.3-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:1caa7b0d05f3e3a36f87788c59e970a7ee1cefcfcbb924a9f138c4a6551c9cb7", size = 494201, upload-time = "2026-07-23T01:55:49.451Z" },
+ { url = "https://files.pythonhosted.org/packages/3f/5a/8f624384e5f1efabb5229b94157eb966b021e97bdb188c62860c2ae243c2/aiohttp-3.14.3-cp314-cp314-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:dfa68deb2a443bdaa3ea5297b0699c1464f08aef3812b486d1348eee61b07dc0", size = 502766, upload-time = "2026-07-23T01:55:51.656Z" },
+ { url = "https://files.pythonhosted.org/packages/a6/26/4ff0164370deec18fb19254ee4ab10b7a73304ac0c860b13f5f84663759b/aiohttp-3.14.3-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:e72ee89e28d907a18f46959b4eb0bb06701cc7f8cf4366e00029e2ccfaaf5924", size = 756557, upload-time = "2026-07-23T01:55:53.964Z" },
+ { url = "https://files.pythonhosted.org/packages/97/a3/7056b86dc0d9ec709ea9777eae3b0161428f943372f8b98c01c11593b682/aiohttp-3.14.3-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:ad4c8b7488d745d2ca4838ebd8ae5ba9b56341d30b1da43640e4ce87f9f49646", size = 510168, upload-time = "2026-07-23T01:55:56.22Z" },
+ { url = "https://files.pythonhosted.org/packages/85/ed/0357a015892fd68058bf2d39d3fd1958e459b997a7db30aaa6aaa434ae96/aiohttp-3.14.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:db332af25642007330fca8be5c4d194caf2bea7a7fc84415aff3497af5dfee6b", size = 512957, upload-time = "2026-07-23T01:55:58.437Z" },
+ { url = "https://files.pythonhosted.org/packages/47/d1/8aba53f15ccb2238405f5e9d30e2a8ca44f93878c26e7165ade00d374b1c/aiohttp-3.14.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:25bd2708db6bdf6a6630dd37bdcdfcb47c4434d22ac69c64665b802910140b30", size = 1750149, upload-time = "2026-07-23T01:56:00.856Z" },
+ { url = "https://files.pythonhosted.org/packages/49/bd/40c3fee327529284375c6701cbb0fa4600cc2e8432af1378f897e2ef7d3a/aiohttp-3.14.3-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:cef89a58e628c4efcac3275c2d68083f82426dcdc89c1492a6f654f9f7ea6ab9", size = 1707685, upload-time = "2026-07-23T01:56:03.371Z" },
+ { url = "https://files.pythonhosted.org/packages/2a/a3/ca0cc6724cca8114b05694abd916060758c79894c3aa5b012cdadc1bc28e/aiohttp-3.14.3-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c23ec8ee9d5ab2f5421f9c7fffce208435607af27fd46d4a44e031954352838f", size = 1803911, upload-time = "2026-07-23T01:56:05.817Z" },
+ { url = "https://files.pythonhosted.org/packages/95/b5/85b099c299c3ffd38ad9b3e43694c8a346934e4a30c88c4fd5a841234f77/aiohttp-3.14.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:e2667f0bbe7eb6c74eae5e9691441ad186e5845ca3cff63230fc09c4e7514f5d", size = 1876929, upload-time = "2026-07-23T01:56:08.413Z" },
+ { url = "https://files.pythonhosted.org/packages/d5/b7/1da684a04175473fa4cddbf9a2f572e79514c3fd27a74597f43057d4f3da/aiohttp-3.14.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:18cb43369747b2ae007bd2655fb8e63a099c2ff1d207962943636dac989b3147", size = 1761112, upload-time = "2026-07-23T01:56:10.918Z" },
+ { url = "https://files.pythonhosted.org/packages/d1/16/bc4b55e3e5cb175fd69c53c90d60d2f47797cb343da5106e23863dc4dba4/aiohttp-3.14.3-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:d77640cc618c1d99fc4f8589c0f24a730adfa54eb1e57ef7bf0c8dfb78da898c", size = 1583500, upload-time = "2026-07-23T01:56:13.613Z" },
+ { url = "https://files.pythonhosted.org/packages/2a/e8/13a9d957a1ee40837f46aa30f0f4c657e673ad86a2e6362a9f9be20d26d9/aiohttp-3.14.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:53e5179d8abb5710f8e83ba207c41c8d1261fcffd4616500e15ca2b7a33be10a", size = 1713940, upload-time = "2026-07-23T01:56:15.969Z" },
+ { url = "https://files.pythonhosted.org/packages/38/05/d33c680c1bcf1c7e130f9cbfc1fc02fe8bb0c4af2a94a53dd5fb56131e5c/aiohttp-3.14.3-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:cd817772b2fcf2b8c0905795318485f9ec16eae60b29feb7f4c77085311637f0", size = 1724413, upload-time = "2026-07-23T01:56:18.591Z" },
+ { url = "https://files.pythonhosted.org/packages/85/1d/af798d306f7a74b6a632dbcabcf62a4c91391b7582d2a8c6d7712e2cc54e/aiohttp-3.14.3-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:4e3ac92d90e92773b2362d506068e9a948192bd553e743c5b2429e28527c8661", size = 1770748, upload-time = "2026-07-23T01:56:21.074Z" },
+ { url = "https://files.pythonhosted.org/packages/a8/92/ad720d472556a995049206867765e9410969684f86ee09423ff9969044c1/aiohttp-3.14.3-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:3f42e9b78301f11c8f861746175d8b9c1ccef713fcad9eab396e2f6db8ed4a22", size = 1577564, upload-time = "2026-07-23T01:56:23.475Z" },
+ { url = "https://files.pythonhosted.org/packages/60/ad/0ed7586cbef7a884e23a752fa2bb987a122e6a5dd50dab109258d0a95193/aiohttp-3.14.3-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:9d9edccfe496b476db5f398d97b865e9a6752bcf8aec4eef8390ce20fb64bb41", size = 1782080, upload-time = "2026-07-23T01:56:25.994Z" },
+ { url = "https://files.pythonhosted.org/packages/97/ea/dbaed0d73e8a69aad653b045dab451c67c2454bb731a37b45a86593e9422/aiohttp-3.14.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:1c5ec8fb1bcc31a8466f74aaf26c345d5c386fa4bd08a3f0eb9c7a4a3fe8b5bf", size = 1745813, upload-time = "2026-07-23T01:56:28.604Z" },
+ { url = "https://files.pythonhosted.org/packages/81/1b/6893d4bc57e434fc93a6c9217c637d967a0b651d989f6e3265179375754a/aiohttp-3.14.3-cp314-cp314-win32.whl", hash = "sha256:38901a84da3ce22249f6e860bf8f90d141bcab7da090cc398f8bb58c0e44b7da", size = 455872, upload-time = "2026-07-23T01:56:31.031Z" },
+ { url = "https://files.pythonhosted.org/packages/f5/8b/c7baa1ba1eda4db6989baefe5de6d99834921b84ebd7918624febcb9f290/aiohttp-3.14.3-cp314-cp314-win_amd64.whl", hash = "sha256:8b3b60de05f3dcb6f6a00f818bb2ec781cee4de0645f59ccaf99b1d1823b6100", size = 481030, upload-time = "2026-07-23T01:56:33.365Z" },
+ { url = "https://files.pythonhosted.org/packages/22/8c/c29d067df825a2df88ca432db848aa2fe8199598359cc06c12b09320cac9/aiohttp-3.14.3-cp314-cp314-win_arm64.whl", hash = "sha256:1576145bdceeb92382d899751e12743a3a5b8e460a841e3e50543859e54864dc", size = 453669, upload-time = "2026-07-23T01:56:35.731Z" },
+ { url = "https://files.pythonhosted.org/packages/6a/a4/9c033beb355d39b6147980597ec9645e4729243f686ee4dc73945de72030/aiohttp-3.14.3-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:8800c996b01c2772a783e3e46f3e1abd5823029adca0df54231960de9bfefa5b", size = 791403, upload-time = "2026-07-23T01:56:37.972Z" },
+ { url = "https://files.pythonhosted.org/packages/80/ca/87c32a0a7704583cfc49660bd817889bae5b830bf53b5dcb4e92145ac2da/aiohttp-3.14.3-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:ebe8e504f058fe91223351cecd2d9d6946c9d241bb0250d898ffbdf584cc72b0", size = 526413, upload-time = "2026-07-23T01:56:40.523Z" },
+ { url = "https://files.pythonhosted.org/packages/9e/d8/8ec0e471248c500acdce2be3f46db8fb62b5eb60efef072529cc85ee1d26/aiohttp-3.14.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:30402d03a7c0ff52bce290b57e564e9079fd9d0cb545c8aba73f86a103162d2e", size = 532135, upload-time = "2026-07-23T01:56:42.876Z" },
+ { url = "https://files.pythonhosted.org/packages/fe/45/f8919fd936e8b79fcd9bda7b6d8e62613462a713f4f17987fd7c34399142/aiohttp-3.14.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9fc7b5bfec6573f3ae844f457fdde5adeb713f8b8e4a81ad64fc207b49383716", size = 1922742, upload-time = "2026-07-23T01:56:45.528Z" },
+ { url = "https://files.pythonhosted.org/packages/f6/ec/9ca76b28a27525b0cc53e20842e0228b022f301ce1f436b7d814b4aaf2df/aiohttp-3.14.3-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:8a5fd34f7f7410d1730d5c2ba873cacb2eed3fede366feb268a70ba22581ed8f", size = 1787371, upload-time = "2026-07-23T01:56:48.045Z" },
+ { url = "https://files.pythonhosted.org/packages/b1/04/6acdbf17315f7b55f1937e3387acb89a3cddeb4995689553d064af8e92ab/aiohttp-3.14.3-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:270d3dace9ca2f10f0da5d8ebe519b7a310fc6112ed916e32df5866df0888553", size = 1912623, upload-time = "2026-07-23T01:56:50.605Z" },
+ { url = "https://files.pythonhosted.org/packages/86/e6/438b0c79ca6f45eb9fd9817dd4c01a91919a38c0de5ee9e05e2b4dc0ece7/aiohttp-3.14.3-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:3ae5b3a59436d089b5395d910121a390feed4d00578eb95a0fd1a329fe963100", size = 2005515, upload-time = "2026-07-23T01:56:53.153Z" },
+ { url = "https://files.pythonhosted.org/packages/bb/6b/62cbd6577758699525f5c712d1ddef57d9875fbab0ae8d5f5a202fd598f8/aiohttp-3.14.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2498f0fe69ead802f9675beca44a7c21c62fdaa4ec5145ea1c3ad6edbee29f85", size = 1879906, upload-time = "2026-07-23T01:56:55.818Z" },
+ { url = "https://files.pythonhosted.org/packages/00/95/18bcbf830a21dc3aae24d8f6b6feaf3db1d2090242d00a7868db2ffb0b67/aiohttp-3.14.3-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a0dc483c00da8b673abbb367eb6f8d8f4bcec30eb58529ea13cb42e7fd2dfa33", size = 1675849, upload-time = "2026-07-23T01:56:58.861Z" },
+ { url = "https://files.pythonhosted.org/packages/a9/19/47f4968659c5e23606c3790c80fc624e691c153d036148449ee84d31b287/aiohttp-3.14.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:c7d3a97c678d34fc5b59da671ee9cd630096ddc643e7b5a30d54a2a6f3574d3f", size = 1843496, upload-time = "2026-07-23T01:57:01.591Z" },
+ { url = "https://files.pythonhosted.org/packages/64/af/38c33c4dd82fddcb4e56c4653b6f1072a8edbc6b7fa15809f14932c41e2d/aiohttp-3.14.3-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:f8fb78a83c9e5f741ca3a68cfb455c1f5bb83b4e7249a3848b3cd78d0a8563b0", size = 1827746, upload-time = "2026-07-23T01:57:05.131Z" },
+ { url = "https://files.pythonhosted.org/packages/a1/9d/0537cda4885ac8f5b7053d164dd06312f4c483a4edcb8ee5b8aaf2a989bf/aiohttp-3.14.3-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:74ab5b6a9fb13e873e5a90946588baecaf488745e1db1a4a5c433f971f035098", size = 1853810, upload-time = "2026-07-23T01:57:08.043Z" },
+ { url = "https://files.pythonhosted.org/packages/19/fe/26f9c5e6458385aa86497836b0dea6fb2f027827d63f37c7856cce9286ee/aiohttp-3.14.3-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:bd52f811e65f6fb634b1047159657c98f52b407f8efec907bcfc09da9a4c0a25", size = 1668895, upload-time = "2026-07-23T01:57:10.837Z" },
+ { url = "https://files.pythonhosted.org/packages/ec/4c/618b1db9b9ba079b8875d2cdf78e7c4a3bf72903bd5850fee7dd9544600a/aiohttp-3.14.3-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:f0f177d1b195b9e06376cfd7d308d8a1b920909a609d03ac82a8c73bbb16d3b9", size = 1883833, upload-time = "2026-07-23T01:57:13.672Z" },
+ { url = "https://files.pythonhosted.org/packages/94/c6/bd959bd1e4771f9fd944e9e436224c48c77b018b73b519b5aad346335bcc/aiohttp-3.14.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:498c6c623134f8e09a3c4e60bcd607a0b4590dd7dbf08dd40851b27cbb520ccb", size = 1844251, upload-time = "2026-07-23T01:57:16.593Z" },
+ { url = "https://files.pythonhosted.org/packages/5e/19/08d41839658bdd44a0ed2480f3891705ecb487ce28c0dde62c9040c997e0/aiohttp-3.14.3-cp314-cp314t-win32.whl", hash = "sha256:b304db572b4368edd8dda8a2274f73156fe15558fca4a917cb8a09fc47af5963", size = 474180, upload-time = "2026-07-23T01:57:19.306Z" },
+ { url = "https://files.pythonhosted.org/packages/99/5d/3cd6ef0a2b2851f7ab913b5b079334781bd50ff56a323e4454063377a080/aiohttp-3.14.3-cp314-cp314t-win_amd64.whl", hash = "sha256:b20032766aedf6261c7a566585a40867d092ac03a0d81592d5370ef9b054f99b", size = 500528, upload-time = "2026-07-23T01:57:21.762Z" },
+ { url = "https://files.pythonhosted.org/packages/a4/37/cfd1ed540a4d318da025590d96b728e63713c09e9377950fc655dadeb856/aiohttp-3.14.3-cp314-cp314t-win_arm64.whl", hash = "sha256:2e1161602f45a54de2ce0905243a95f58cb42dcd378402f3697f5e0b21e9d2e7", size = 469280, upload-time = "2026-07-23T01:57:24.241Z" },
+]
+
+[[package]]
+name = "aiosignal"
+version = "1.4.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "frozenlist" },
+ { name = "typing-extensions", marker = "python_full_version < '3.13'" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/61/62/06741b579156360248d1ec624842ad0edf697050bbaf7c3e46394e106ad1/aiosignal-1.4.0.tar.gz", hash = "sha256:f47eecd9468083c2029cc99945502cb7708b082c232f9aca65da147157b251c7", size = 25007, upload-time = "2025-07-03T22:54:43.528Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/fb/76/641ae371508676492379f16e2fa48f4e2c11741bd63c48be4b12a6b09cba/aiosignal-1.4.0-py3-none-any.whl", hash = "sha256:053243f8b92b990551949e63930a839ff0cf0b0ebbe0597b0f3fb19e1a0fe82e", size = 7490, upload-time = "2025-07-03T22:54:42.156Z" },
+]
+
+[[package]]
+name = "annotated-types"
+version = "0.7.0"
+source = { registry = "https://pypi.org/simple" }
+resolution-markers = [
+ "python_full_version < '3.10'",
+]
+sdist = { url = "https://files.pythonhosted.org/packages/ee/67/531ea369ba64dcff5ec9c3402f9f51bf748cec26dde048a2f973a4eea7f5/annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89", size = 16081, upload-time = "2024-05-20T21:33:25.928Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", size = 13643, upload-time = "2024-05-20T21:33:24.1Z" },
+]
+
+[[package]]
+name = "annotated-types"
+version = "0.8.0"
+source = { registry = "https://pypi.org/simple" }
+resolution-markers = [
+ "python_full_version >= '3.10'",
+]
+sdist = { url = "https://files.pythonhosted.org/packages/5f/56/a8120250d128bed162cd73c76d45f6ef9991f3e068f62a8ee060afa3104a/annotated_types-0.8.0.tar.gz", hash = "sha256:13b2beaad985e05e2d6407ee4c4f35590b11f8d693a258a561055cac8f64cab7", size = 15893, upload-time = "2026-07-23T20:16:13.995Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/99/91/8acff4f5e50511b911bbccb72b8628a49c68ce14148cd9f6431094859a90/annotated_types-0.8.0-py3-none-any.whl", hash = "sha256:f072f4d804ea359e4eaf198b1af7a8b0943881a87f31bb764f8bf219bb9419e0", size = 13427, upload-time = "2026-07-23T20:16:12.938Z" },
+]
+
+[[package]]
+name = "anyio"
+version = "4.12.1"
+source = { registry = "https://pypi.org/simple" }
+resolution-markers = [
+ "python_full_version < '3.10'",
+]
+dependencies = [
+ { name = "exceptiongroup" },
+ { name = "idna" },
+ { name = "typing-extensions" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/96/f0/5eb65b2bb0d09ac6776f2eb54adee6abe8228ea05b20a5ad0e4945de8aac/anyio-4.12.1.tar.gz", hash = "sha256:41cfcc3a4c85d3f05c932da7c26d0201ac36f72abd4435ba90d0464a3ffed703", size = 228685, upload-time = "2026-01-06T11:45:21.246Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/38/0e/27be9fdef66e72d64c0cdc3cc2823101b80585f8119b5c112c2e8f5f7dab/anyio-4.12.1-py3-none-any.whl", hash = "sha256:d405828884fc140aa80a3c667b8beed277f1dfedec42ba031bd6ac3db606ab6c", size = 113592, upload-time = "2026-01-06T11:45:19.497Z" },
+]
+
+[[package]]
+name = "anyio"
+version = "4.14.2"
+source = { registry = "https://pypi.org/simple" }
+resolution-markers = [
+ "python_full_version >= '3.10'",
+]
+dependencies = [
+ { name = "exceptiongroup", marker = "python_full_version < '3.11'" },
+ { name = "idna" },
+ { name = "typing-extensions", marker = "python_full_version < '3.13'" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/61/cc/a381afa6efea9f496eff839d4a6a1aed3bfafc7b3ab4b0d1b243a12573dd/anyio-4.14.2.tar.gz", hash = "sha256:cfa139f3ed1a23ee8f88a145ddb5ac7605b8bbfd8592baacd7ce3d8bb4313c7f", size = 260176, upload-time = "2026-07-12T20:29:07.082Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/da/35/f2287558c17e29fafc8ef3daf819bb9834061cfa43bff8014f7df7f63bdc/anyio-4.14.2-py3-none-any.whl", hash = "sha256:9f505dda5ac9f0c8309b5e8bd445a8c2bf7246f3ce950121e45ea15bc41d1494", size = 125813, upload-time = "2026-07-12T20:29:05.763Z" },
+]
+
+[[package]]
+name = "async-timeout"
+version = "5.0.1"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/a5/ae/136395dfbfe00dfc94da3f3e136d0b13f394cba8f4841120e34226265780/async_timeout-5.0.1.tar.gz", hash = "sha256:d9321a7a3d5a6a5e187e824d2fa0793ce379a202935782d555d6e9d2735677d3", size = 9274, upload-time = "2024-11-06T16:41:39.6Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/fe/ba/e2081de779ca30d473f21f5b30e0e737c438205440784c7dfc81efc2b029/async_timeout-5.0.1-py3-none-any.whl", hash = "sha256:39e3809566ff85354557ec2398b55e096c8364bacac9405a7a1fa429e77fe76c", size = 6233, upload-time = "2024-11-06T16:41:37.9Z" },
+]
+
+[[package]]
+name = "attrs"
+version = "26.1.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/9a/8e/82a0fe20a541c03148528be8cac2408564a6c9a0cc7e9171802bc1d26985/attrs-26.1.0.tar.gz", hash = "sha256:d03ceb89cb322a8fd706d4fb91940737b6642aa36998fe130a9bc96c985eff32", size = 952055, upload-time = "2026-03-19T14:22:25.026Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/64/b4/17d4b0b2a2dc85a6df63d1157e028ed19f90d4cd97c36717afef2bc2f395/attrs-26.1.0-py3-none-any.whl", hash = "sha256:c647aa4a12dfbad9333ca4e71fe62ddc36f4e63b2d260a37a8b83d2f043ac309", size = 67548, upload-time = "2026-03-19T14:22:23.645Z" },
+]
+
+[[package]]
+name = "certifi"
+version = "2026.7.22"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/a3/c2/24167ea9858356b47a87a50d39908bfdb72ceeefe0041586e704e5376b3a/certifi-2026.7.22.tar.gz", hash = "sha256:741e2c3b351ddf169a738da9f2c048608ff7f2c5cc02f1ebc6b118bb090d5d55", size = 138112, upload-time = "2026-07-22T03:35:12.644Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/0b/a7/71ac2cff56fec219ed242bb11b8efb69fcc4bec75db06fb7bfe35de520e6/certifi-2026.7.22-py3-none-any.whl", hash = "sha256:62f22742b58a1a33014a2b6b706588a8d7e2a88ae7bd1a6ebe8c992928483775", size = 136983, upload-time = "2026-07-22T03:35:11.276Z" },
+]
+
+[[package]]
+name = "distro"
+version = "1.9.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/fc/f8/98eea607f65de6527f8a2e8885fc8015d3e6f5775df186e443e0964a11c3/distro-1.9.0.tar.gz", hash = "sha256:2fa77c6fd8940f116ee1d6b94a2f90b13b5ea8d019b98bc8bafdcabcdd9bdbed", size = 60722, upload-time = "2023-12-24T09:54:32.31Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/12/b3/231ffd4ab1fc9d679809f356cebee130ac7daa00d6d6f3206dd4fd137e9e/distro-1.9.0-py3-none-any.whl", hash = "sha256:7bffd925d65168f85027d8da9af6bddab658135b840670a223589bc0c8ef02b2", size = 20277, upload-time = "2023-12-24T09:54:30.421Z" },
+]
+
+[[package]]
+name = "exceptiongroup"
+version = "1.3.1"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "typing-extensions" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/50/79/66800aadf48771f6b62f7eb014e352e5d06856655206165d775e675a02c9/exceptiongroup-1.3.1.tar.gz", hash = "sha256:8b412432c6055b0b7d14c310000ae93352ed6754f70fa8f7c34141f91c4e3219", size = 30371, upload-time = "2025-11-21T23:01:54.787Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/8a/0e/97c33bf5009bdbac74fd2beace167cab3f978feb69cc36f1ef79360d6c4e/exceptiongroup-1.3.1-py3-none-any.whl", hash = "sha256:a7a39a3bd276781e98394987d3a5701d0c4edffb633bb7a5144577f82c773598", size = 16740, upload-time = "2025-11-21T23:01:53.443Z" },
+]
+
+[[package]]
+name = "frozenlist"
+version = "1.8.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/2d/f5/c831fac6cc817d26fd54c7eaccd04ef7e0288806943f7cc5bbf69f3ac1f0/frozenlist-1.8.0.tar.gz", hash = "sha256:3ede829ed8d842f6cd48fc7081d7a41001a56f1f38603f9d49bf3020d59a31ad", size = 45875, upload-time = "2025-10-06T05:38:17.865Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/83/4a/557715d5047da48d54e659203b9335be7bfaafda2c3f627b7c47e0b3aaf3/frozenlist-1.8.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:b37f6d31b3dcea7deb5e9696e529a6aa4a898adc33db82da12e4c60a7c4d2011", size = 86230, upload-time = "2025-10-06T05:35:23.699Z" },
+ { url = "https://files.pythonhosted.org/packages/a2/fb/c85f9fed3ea8fe8740e5b46a59cc141c23b842eca617da8876cfce5f760e/frozenlist-1.8.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:ef2b7b394f208233e471abc541cc6991f907ffd47dc72584acee3147899d6565", size = 49621, upload-time = "2025-10-06T05:35:25.341Z" },
+ { url = "https://files.pythonhosted.org/packages/63/70/26ca3f06aace16f2352796b08704338d74b6d1a24ca38f2771afbb7ed915/frozenlist-1.8.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a88f062f072d1589b7b46e951698950e7da00442fc1cacbe17e19e025dc327ad", size = 49889, upload-time = "2025-10-06T05:35:26.797Z" },
+ { url = "https://files.pythonhosted.org/packages/5d/ed/c7895fd2fde7f3ee70d248175f9b6cdf792fb741ab92dc59cd9ef3bd241b/frozenlist-1.8.0-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:f57fb59d9f385710aa7060e89410aeb5058b99e62f4d16b08b91986b9a2140c2", size = 219464, upload-time = "2025-10-06T05:35:28.254Z" },
+ { url = "https://files.pythonhosted.org/packages/6b/83/4d587dccbfca74cb8b810472392ad62bfa100bf8108c7223eb4c4fa2f7b3/frozenlist-1.8.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:799345ab092bee59f01a915620b5d014698547afd011e691a208637312db9186", size = 221649, upload-time = "2025-10-06T05:35:29.454Z" },
+ { url = "https://files.pythonhosted.org/packages/6a/c6/fd3b9cd046ec5fff9dab66831083bc2077006a874a2d3d9247dea93ddf7e/frozenlist-1.8.0-cp310-cp310-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:c23c3ff005322a6e16f71bf8692fcf4d5a304aaafe1e262c98c6d4adc7be863e", size = 219188, upload-time = "2025-10-06T05:35:30.951Z" },
+ { url = "https://files.pythonhosted.org/packages/ce/80/6693f55eb2e085fc8afb28cf611448fb5b90e98e068fa1d1b8d8e66e5c7d/frozenlist-1.8.0-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:8a76ea0f0b9dfa06f254ee06053d93a600865b3274358ca48a352ce4f0798450", size = 231748, upload-time = "2025-10-06T05:35:32.101Z" },
+ { url = "https://files.pythonhosted.org/packages/97/d6/e9459f7c5183854abd989ba384fe0cc1a0fb795a83c033f0571ec5933ca4/frozenlist-1.8.0-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:c7366fe1418a6133d5aa824ee53d406550110984de7637d65a178010f759c6ef", size = 236351, upload-time = "2025-10-06T05:35:33.834Z" },
+ { url = "https://files.pythonhosted.org/packages/97/92/24e97474b65c0262e9ecd076e826bfd1d3074adcc165a256e42e7b8a7249/frozenlist-1.8.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:13d23a45c4cebade99340c4165bd90eeb4a56c6d8a9d8aa49568cac19a6d0dc4", size = 218767, upload-time = "2025-10-06T05:35:35.205Z" },
+ { url = "https://files.pythonhosted.org/packages/ee/bf/dc394a097508f15abff383c5108cb8ad880d1f64a725ed3b90d5c2fbf0bb/frozenlist-1.8.0-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:e4a3408834f65da56c83528fb52ce7911484f0d1eaf7b761fc66001db1646eff", size = 235887, upload-time = "2025-10-06T05:35:36.354Z" },
+ { url = "https://files.pythonhosted.org/packages/40/90/25b201b9c015dbc999a5baf475a257010471a1fa8c200c843fd4abbee725/frozenlist-1.8.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:42145cd2748ca39f32801dad54aeea10039da6f86e303659db90db1c4b614c8c", size = 228785, upload-time = "2025-10-06T05:35:37.949Z" },
+ { url = "https://files.pythonhosted.org/packages/84/f4/b5bc148df03082f05d2dd30c089e269acdbe251ac9a9cf4e727b2dbb8a3d/frozenlist-1.8.0-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:e2de870d16a7a53901e41b64ffdf26f2fbb8917b3e6ebf398098d72c5b20bd7f", size = 230312, upload-time = "2025-10-06T05:35:39.178Z" },
+ { url = "https://files.pythonhosted.org/packages/db/4b/87e95b5d15097c302430e647136b7d7ab2398a702390cf4c8601975709e7/frozenlist-1.8.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:20e63c9493d33ee48536600d1a5c95eefc870cd71e7ab037763d1fbb89cc51e7", size = 217650, upload-time = "2025-10-06T05:35:40.377Z" },
+ { url = "https://files.pythonhosted.org/packages/e5/70/78a0315d1fea97120591a83e0acd644da638c872f142fd72a6cebee825f3/frozenlist-1.8.0-cp310-cp310-win32.whl", hash = "sha256:adbeebaebae3526afc3c96fad434367cafbfd1b25d72369a9e5858453b1bb71a", size = 39659, upload-time = "2025-10-06T05:35:41.863Z" },
+ { url = "https://files.pythonhosted.org/packages/66/aa/3f04523fb189a00e147e60c5b2205126118f216b0aa908035c45336e27e4/frozenlist-1.8.0-cp310-cp310-win_amd64.whl", hash = "sha256:667c3777ca571e5dbeb76f331562ff98b957431df140b54c85fd4d52eea8d8f6", size = 43837, upload-time = "2025-10-06T05:35:43.205Z" },
+ { url = "https://files.pythonhosted.org/packages/39/75/1135feecdd7c336938bd55b4dc3b0dfc46d85b9be12ef2628574b28de776/frozenlist-1.8.0-cp310-cp310-win_arm64.whl", hash = "sha256:80f85f0a7cc86e7a54c46d99c9e1318ff01f4687c172ede30fd52d19d1da1c8e", size = 39989, upload-time = "2025-10-06T05:35:44.596Z" },
+ { url = "https://files.pythonhosted.org/packages/bc/03/077f869d540370db12165c0aa51640a873fb661d8b315d1d4d67b284d7ac/frozenlist-1.8.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:09474e9831bc2b2199fad6da3c14c7b0fbdd377cce9d3d77131be28906cb7d84", size = 86912, upload-time = "2025-10-06T05:35:45.98Z" },
+ { url = "https://files.pythonhosted.org/packages/df/b5/7610b6bd13e4ae77b96ba85abea1c8cb249683217ef09ac9e0ae93f25a91/frozenlist-1.8.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:17c883ab0ab67200b5f964d2b9ed6b00971917d5d8a92df149dc2c9779208ee9", size = 50046, upload-time = "2025-10-06T05:35:47.009Z" },
+ { url = "https://files.pythonhosted.org/packages/6e/ef/0e8f1fe32f8a53dd26bdd1f9347efe0778b0fddf62789ea683f4cc7d787d/frozenlist-1.8.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:fa47e444b8ba08fffd1c18e8cdb9a75db1b6a27f17507522834ad13ed5922b93", size = 50119, upload-time = "2025-10-06T05:35:48.38Z" },
+ { url = "https://files.pythonhosted.org/packages/11/b1/71a477adc7c36e5fb628245dfbdea2166feae310757dea848d02bd0689fd/frozenlist-1.8.0-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:2552f44204b744fba866e573be4c1f9048d6a324dfe14475103fd51613eb1d1f", size = 231067, upload-time = "2025-10-06T05:35:49.97Z" },
+ { url = "https://files.pythonhosted.org/packages/45/7e/afe40eca3a2dc19b9904c0f5d7edfe82b5304cb831391edec0ac04af94c2/frozenlist-1.8.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:957e7c38f250991e48a9a73e6423db1bb9dd14e722a10f6b8bb8e16a0f55f695", size = 233160, upload-time = "2025-10-06T05:35:51.729Z" },
+ { url = "https://files.pythonhosted.org/packages/a6/aa/7416eac95603ce428679d273255ffc7c998d4132cfae200103f164b108aa/frozenlist-1.8.0-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:8585e3bb2cdea02fc88ffa245069c36555557ad3609e83be0ec71f54fd4abb52", size = 228544, upload-time = "2025-10-06T05:35:53.246Z" },
+ { url = "https://files.pythonhosted.org/packages/8b/3d/2a2d1f683d55ac7e3875e4263d28410063e738384d3adc294f5ff3d7105e/frozenlist-1.8.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:edee74874ce20a373d62dc28b0b18b93f645633c2943fd90ee9d898550770581", size = 243797, upload-time = "2025-10-06T05:35:54.497Z" },
+ { url = "https://files.pythonhosted.org/packages/78/1e/2d5565b589e580c296d3bb54da08d206e797d941a83a6fdea42af23be79c/frozenlist-1.8.0-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:c9a63152fe95756b85f31186bddf42e4c02c6321207fd6601a1c89ebac4fe567", size = 247923, upload-time = "2025-10-06T05:35:55.861Z" },
+ { url = "https://files.pythonhosted.org/packages/aa/c3/65872fcf1d326a7f101ad4d86285c403c87be7d832b7470b77f6d2ed5ddc/frozenlist-1.8.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:b6db2185db9be0a04fecf2f241c70b63b1a242e2805be291855078f2b404dd6b", size = 230886, upload-time = "2025-10-06T05:35:57.399Z" },
+ { url = "https://files.pythonhosted.org/packages/a0/76/ac9ced601d62f6956f03cc794f9e04c81719509f85255abf96e2510f4265/frozenlist-1.8.0-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:f4be2e3d8bc8aabd566f8d5b8ba7ecc09249d74ba3c9ed52e54dc23a293f0b92", size = 245731, upload-time = "2025-10-06T05:35:58.563Z" },
+ { url = "https://files.pythonhosted.org/packages/b9/49/ecccb5f2598daf0b4a1415497eba4c33c1e8ce07495eb07d2860c731b8d5/frozenlist-1.8.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:c8d1634419f39ea6f5c427ea2f90ca85126b54b50837f31497f3bf38266e853d", size = 241544, upload-time = "2025-10-06T05:35:59.719Z" },
+ { url = "https://files.pythonhosted.org/packages/53/4b/ddf24113323c0bbcc54cb38c8b8916f1da7165e07b8e24a717b4a12cbf10/frozenlist-1.8.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:1a7fa382a4a223773ed64242dbe1c9c326ec09457e6b8428efb4118c685c3dfd", size = 241806, upload-time = "2025-10-06T05:36:00.959Z" },
+ { url = "https://files.pythonhosted.org/packages/a7/fb/9b9a084d73c67175484ba2789a59f8eebebd0827d186a8102005ce41e1ba/frozenlist-1.8.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:11847b53d722050808926e785df837353bd4d75f1d494377e59b23594d834967", size = 229382, upload-time = "2025-10-06T05:36:02.22Z" },
+ { url = "https://files.pythonhosted.org/packages/95/a3/c8fb25aac55bf5e12dae5c5aa6a98f85d436c1dc658f21c3ac73f9fa95e5/frozenlist-1.8.0-cp311-cp311-win32.whl", hash = "sha256:27c6e8077956cf73eadd514be8fb04d77fc946a7fe9f7fe167648b0b9085cc25", size = 39647, upload-time = "2025-10-06T05:36:03.409Z" },
+ { url = "https://files.pythonhosted.org/packages/0a/f5/603d0d6a02cfd4c8f2a095a54672b3cf967ad688a60fb9faf04fc4887f65/frozenlist-1.8.0-cp311-cp311-win_amd64.whl", hash = "sha256:ac913f8403b36a2c8610bbfd25b8013488533e71e62b4b4adce9c86c8cea905b", size = 44064, upload-time = "2025-10-06T05:36:04.368Z" },
+ { url = "https://files.pythonhosted.org/packages/5d/16/c2c9ab44e181f043a86f9a8f84d5124b62dbcb3a02c0977ec72b9ac1d3e0/frozenlist-1.8.0-cp311-cp311-win_arm64.whl", hash = "sha256:d4d3214a0f8394edfa3e303136d0575eece0745ff2b47bd2cb2e66dd92d4351a", size = 39937, upload-time = "2025-10-06T05:36:05.669Z" },
+ { url = "https://files.pythonhosted.org/packages/69/29/948b9aa87e75820a38650af445d2ef2b6b8a6fab1a23b6bb9e4ef0be2d59/frozenlist-1.8.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:78f7b9e5d6f2fdb88cdde9440dc147259b62b9d3b019924def9f6478be254ac1", size = 87782, upload-time = "2025-10-06T05:36:06.649Z" },
+ { url = "https://files.pythonhosted.org/packages/64/80/4f6e318ee2a7c0750ed724fa33a4bdf1eacdc5a39a7a24e818a773cd91af/frozenlist-1.8.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:229bf37d2e4acdaf808fd3f06e854a4a7a3661e871b10dc1f8f1896a3b05f18b", size = 50594, upload-time = "2025-10-06T05:36:07.69Z" },
+ { url = "https://files.pythonhosted.org/packages/2b/94/5c8a2b50a496b11dd519f4a24cb5496cf125681dd99e94c604ccdea9419a/frozenlist-1.8.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f833670942247a14eafbb675458b4e61c82e002a148f49e68257b79296e865c4", size = 50448, upload-time = "2025-10-06T05:36:08.78Z" },
+ { url = "https://files.pythonhosted.org/packages/6a/bd/d91c5e39f490a49df14320f4e8c80161cfcce09f1e2cde1edd16a551abb3/frozenlist-1.8.0-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:494a5952b1c597ba44e0e78113a7266e656b9794eec897b19ead706bd7074383", size = 242411, upload-time = "2025-10-06T05:36:09.801Z" },
+ { url = "https://files.pythonhosted.org/packages/8f/83/f61505a05109ef3293dfb1ff594d13d64a2324ac3482be2cedc2be818256/frozenlist-1.8.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:96f423a119f4777a4a056b66ce11527366a8bb92f54e541ade21f2374433f6d4", size = 243014, upload-time = "2025-10-06T05:36:11.394Z" },
+ { url = "https://files.pythonhosted.org/packages/d8/cb/cb6c7b0f7d4023ddda30cf56b8b17494eb3a79e3fda666bf735f63118b35/frozenlist-1.8.0-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:3462dd9475af2025c31cc61be6652dfa25cbfb56cbbf52f4ccfe029f38decaf8", size = 234909, upload-time = "2025-10-06T05:36:12.598Z" },
+ { url = "https://files.pythonhosted.org/packages/31/c5/cd7a1f3b8b34af009fb17d4123c5a778b44ae2804e3ad6b86204255f9ec5/frozenlist-1.8.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c4c800524c9cd9bac5166cd6f55285957fcfc907db323e193f2afcd4d9abd69b", size = 250049, upload-time = "2025-10-06T05:36:14.065Z" },
+ { url = "https://files.pythonhosted.org/packages/c0/01/2f95d3b416c584a1e7f0e1d6d31998c4a795f7544069ee2e0962a4b60740/frozenlist-1.8.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d6a5df73acd3399d893dafc71663ad22534b5aa4f94e8a2fabfe856c3c1b6a52", size = 256485, upload-time = "2025-10-06T05:36:15.39Z" },
+ { url = "https://files.pythonhosted.org/packages/ce/03/024bf7720b3abaebcff6d0793d73c154237b85bdf67b7ed55e5e9596dc9a/frozenlist-1.8.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:405e8fe955c2280ce66428b3ca55e12b3c4e9c336fb2103a4937e891c69a4a29", size = 237619, upload-time = "2025-10-06T05:36:16.558Z" },
+ { url = "https://files.pythonhosted.org/packages/69/fa/f8abdfe7d76b731f5d8bd217827cf6764d4f1d9763407e42717b4bed50a0/frozenlist-1.8.0-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:908bd3f6439f2fef9e85031b59fd4f1297af54415fb60e4254a95f75b3cab3f3", size = 250320, upload-time = "2025-10-06T05:36:17.821Z" },
+ { url = "https://files.pythonhosted.org/packages/f5/3c/b051329f718b463b22613e269ad72138cc256c540f78a6de89452803a47d/frozenlist-1.8.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:294e487f9ec720bd8ffcebc99d575f7eff3568a08a253d1ee1a0378754b74143", size = 246820, upload-time = "2025-10-06T05:36:19.046Z" },
+ { url = "https://files.pythonhosted.org/packages/0f/ae/58282e8f98e444b3f4dd42448ff36fa38bef29e40d40f330b22e7108f565/frozenlist-1.8.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:74c51543498289c0c43656701be6b077f4b265868fa7f8a8859c197006efb608", size = 250518, upload-time = "2025-10-06T05:36:20.763Z" },
+ { url = "https://files.pythonhosted.org/packages/8f/96/007e5944694d66123183845a106547a15944fbbb7154788cbf7272789536/frozenlist-1.8.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:776f352e8329135506a1d6bf16ac3f87bc25b28e765949282dcc627af36123aa", size = 239096, upload-time = "2025-10-06T05:36:22.129Z" },
+ { url = "https://files.pythonhosted.org/packages/66/bb/852b9d6db2fa40be96f29c0d1205c306288f0684df8fd26ca1951d461a56/frozenlist-1.8.0-cp312-cp312-win32.whl", hash = "sha256:433403ae80709741ce34038da08511d4a77062aa924baf411ef73d1146e74faf", size = 39985, upload-time = "2025-10-06T05:36:23.661Z" },
+ { url = "https://files.pythonhosted.org/packages/b8/af/38e51a553dd66eb064cdf193841f16f077585d4d28394c2fa6235cb41765/frozenlist-1.8.0-cp312-cp312-win_amd64.whl", hash = "sha256:34187385b08f866104f0c0617404c8eb08165ab1272e884abc89c112e9c00746", size = 44591, upload-time = "2025-10-06T05:36:24.958Z" },
+ { url = "https://files.pythonhosted.org/packages/a7/06/1dc65480ab147339fecc70797e9c2f69d9cea9cf38934ce08df070fdb9cb/frozenlist-1.8.0-cp312-cp312-win_arm64.whl", hash = "sha256:fe3c58d2f5db5fbd18c2987cba06d51b0529f52bc3a6cdc33d3f4eab725104bd", size = 40102, upload-time = "2025-10-06T05:36:26.333Z" },
+ { url = "https://files.pythonhosted.org/packages/2d/40/0832c31a37d60f60ed79e9dfb5a92e1e2af4f40a16a29abcc7992af9edff/frozenlist-1.8.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:8d92f1a84bb12d9e56f818b3a746f3efba93c1b63c8387a73dde655e1e42282a", size = 85717, upload-time = "2025-10-06T05:36:27.341Z" },
+ { url = "https://files.pythonhosted.org/packages/30/ba/b0b3de23f40bc55a7057bd38434e25c34fa48e17f20ee273bbde5e0650f3/frozenlist-1.8.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:96153e77a591c8adc2ee805756c61f59fef4cf4073a9275ee86fe8cba41241f7", size = 49651, upload-time = "2025-10-06T05:36:28.855Z" },
+ { url = "https://files.pythonhosted.org/packages/0c/ab/6e5080ee374f875296c4243c381bbdef97a9ac39c6e3ce1d5f7d42cb78d6/frozenlist-1.8.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f21f00a91358803399890ab167098c131ec2ddd5f8f5fd5fe9c9f2c6fcd91e40", size = 49417, upload-time = "2025-10-06T05:36:29.877Z" },
+ { url = "https://files.pythonhosted.org/packages/d5/4e/e4691508f9477ce67da2015d8c00acd751e6287739123113a9fca6f1604e/frozenlist-1.8.0-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:fb30f9626572a76dfe4293c7194a09fb1fe93ba94c7d4f720dfae3b646b45027", size = 234391, upload-time = "2025-10-06T05:36:31.301Z" },
+ { url = "https://files.pythonhosted.org/packages/40/76/c202df58e3acdf12969a7895fd6f3bc016c642e6726aa63bd3025e0fc71c/frozenlist-1.8.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:eaa352d7047a31d87dafcacbabe89df0aa506abb5b1b85a2fb91bc3faa02d822", size = 233048, upload-time = "2025-10-06T05:36:32.531Z" },
+ { url = "https://files.pythonhosted.org/packages/f9/c0/8746afb90f17b73ca5979c7a3958116e105ff796e718575175319b5bb4ce/frozenlist-1.8.0-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:03ae967b4e297f58f8c774c7eabcce57fe3c2434817d4385c50661845a058121", size = 226549, upload-time = "2025-10-06T05:36:33.706Z" },
+ { url = "https://files.pythonhosted.org/packages/7e/eb/4c7eefc718ff72f9b6c4893291abaae5fbc0c82226a32dcd8ef4f7a5dbef/frozenlist-1.8.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f6292f1de555ffcc675941d65fffffb0a5bcd992905015f85d0592201793e0e5", size = 239833, upload-time = "2025-10-06T05:36:34.947Z" },
+ { url = "https://files.pythonhosted.org/packages/c2/4e/e5c02187cf704224f8b21bee886f3d713ca379535f16893233b9d672ea71/frozenlist-1.8.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:29548f9b5b5e3460ce7378144c3010363d8035cea44bc0bf02d57f5a685e084e", size = 245363, upload-time = "2025-10-06T05:36:36.534Z" },
+ { url = "https://files.pythonhosted.org/packages/1f/96/cb85ec608464472e82ad37a17f844889c36100eed57bea094518bf270692/frozenlist-1.8.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ec3cc8c5d4084591b4237c0a272cc4f50a5b03396a47d9caaf76f5d7b38a4f11", size = 229314, upload-time = "2025-10-06T05:36:38.582Z" },
+ { url = "https://files.pythonhosted.org/packages/5d/6f/4ae69c550e4cee66b57887daeebe006fe985917c01d0fff9caab9883f6d0/frozenlist-1.8.0-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:517279f58009d0b1f2e7c1b130b377a349405da3f7621ed6bfae50b10adf20c1", size = 243365, upload-time = "2025-10-06T05:36:40.152Z" },
+ { url = "https://files.pythonhosted.org/packages/7a/58/afd56de246cf11780a40a2c28dc7cbabbf06337cc8ddb1c780a2d97e88d8/frozenlist-1.8.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:db1e72ede2d0d7ccb213f218df6a078a9c09a7de257c2fe8fcef16d5925230b1", size = 237763, upload-time = "2025-10-06T05:36:41.355Z" },
+ { url = "https://files.pythonhosted.org/packages/cb/36/cdfaf6ed42e2644740d4a10452d8e97fa1c062e2a8006e4b09f1b5fd7d63/frozenlist-1.8.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:b4dec9482a65c54a5044486847b8a66bf10c9cb4926d42927ec4e8fd5db7fed8", size = 240110, upload-time = "2025-10-06T05:36:42.716Z" },
+ { url = "https://files.pythonhosted.org/packages/03/a8/9ea226fbefad669f11b52e864c55f0bd57d3c8d7eb07e9f2e9a0b39502e1/frozenlist-1.8.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:21900c48ae04d13d416f0e1e0c4d81f7931f73a9dfa0b7a8746fb2fe7dd970ed", size = 233717, upload-time = "2025-10-06T05:36:44.251Z" },
+ { url = "https://files.pythonhosted.org/packages/1e/0b/1b5531611e83ba7d13ccc9988967ea1b51186af64c42b7a7af465dcc9568/frozenlist-1.8.0-cp313-cp313-win32.whl", hash = "sha256:8b7b94a067d1c504ee0b16def57ad5738701e4ba10cec90529f13fa03c833496", size = 39628, upload-time = "2025-10-06T05:36:45.423Z" },
+ { url = "https://files.pythonhosted.org/packages/d8/cf/174c91dbc9cc49bc7b7aab74d8b734e974d1faa8f191c74af9b7e80848e6/frozenlist-1.8.0-cp313-cp313-win_amd64.whl", hash = "sha256:878be833caa6a3821caf85eb39c5ba92d28e85df26d57afb06b35b2efd937231", size = 43882, upload-time = "2025-10-06T05:36:46.796Z" },
+ { url = "https://files.pythonhosted.org/packages/c1/17/502cd212cbfa96eb1388614fe39a3fc9ab87dbbe042b66f97acb57474834/frozenlist-1.8.0-cp313-cp313-win_arm64.whl", hash = "sha256:44389d135b3ff43ba8cc89ff7f51f5a0bb6b63d829c8300f79a2fe4fe61bcc62", size = 39676, upload-time = "2025-10-06T05:36:47.8Z" },
+ { url = "https://files.pythonhosted.org/packages/d2/5c/3bbfaa920dfab09e76946a5d2833a7cbdf7b9b4a91c714666ac4855b88b4/frozenlist-1.8.0-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:e25ac20a2ef37e91c1b39938b591457666a0fa835c7783c3a8f33ea42870db94", size = 89235, upload-time = "2025-10-06T05:36:48.78Z" },
+ { url = "https://files.pythonhosted.org/packages/d2/d6/f03961ef72166cec1687e84e8925838442b615bd0b8854b54923ce5b7b8a/frozenlist-1.8.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:07cdca25a91a4386d2e76ad992916a85038a9b97561bf7a3fd12d5d9ce31870c", size = 50742, upload-time = "2025-10-06T05:36:49.837Z" },
+ { url = "https://files.pythonhosted.org/packages/1e/bb/a6d12b7ba4c3337667d0e421f7181c82dda448ce4e7ad7ecd249a16fa806/frozenlist-1.8.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:4e0c11f2cc6717e0a741f84a527c52616140741cd812a50422f83dc31749fb52", size = 51725, upload-time = "2025-10-06T05:36:50.851Z" },
+ { url = "https://files.pythonhosted.org/packages/bc/71/d1fed0ffe2c2ccd70b43714c6cab0f4188f09f8a67a7914a6b46ee30f274/frozenlist-1.8.0-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:b3210649ee28062ea6099cfda39e147fa1bc039583c8ee4481cb7811e2448c51", size = 284533, upload-time = "2025-10-06T05:36:51.898Z" },
+ { url = "https://files.pythonhosted.org/packages/c9/1f/fb1685a7b009d89f9bf78a42d94461bc06581f6e718c39344754a5d9bada/frozenlist-1.8.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:581ef5194c48035a7de2aefc72ac6539823bb71508189e5de01d60c9dcd5fa65", size = 292506, upload-time = "2025-10-06T05:36:53.101Z" },
+ { url = "https://files.pythonhosted.org/packages/e6/3b/b991fe1612703f7e0d05c0cf734c1b77aaf7c7d321df4572e8d36e7048c8/frozenlist-1.8.0-cp313-cp313t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:3ef2d026f16a2b1866e1d86fc4e1291e1ed8a387b2c333809419a2f8b3a77b82", size = 274161, upload-time = "2025-10-06T05:36:54.309Z" },
+ { url = "https://files.pythonhosted.org/packages/ca/ec/c5c618767bcdf66e88945ec0157d7f6c4a1322f1473392319b7a2501ded7/frozenlist-1.8.0-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:5500ef82073f599ac84d888e3a8c1f77ac831183244bfd7f11eaa0289fb30714", size = 294676, upload-time = "2025-10-06T05:36:55.566Z" },
+ { url = "https://files.pythonhosted.org/packages/7c/ce/3934758637d8f8a88d11f0585d6495ef54b2044ed6ec84492a91fa3b27aa/frozenlist-1.8.0-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:50066c3997d0091c411a66e710f4e11752251e6d2d73d70d8d5d4c76442a199d", size = 300638, upload-time = "2025-10-06T05:36:56.758Z" },
+ { url = "https://files.pythonhosted.org/packages/fc/4f/a7e4d0d467298f42de4b41cbc7ddaf19d3cfeabaf9ff97c20c6c7ee409f9/frozenlist-1.8.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:5c1c8e78426e59b3f8005e9b19f6ff46e5845895adbde20ece9218319eca6506", size = 283067, upload-time = "2025-10-06T05:36:57.965Z" },
+ { url = "https://files.pythonhosted.org/packages/dc/48/c7b163063d55a83772b268e6d1affb960771b0e203b632cfe09522d67ea5/frozenlist-1.8.0-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:eefdba20de0d938cec6a89bd4d70f346a03108a19b9df4248d3cf0d88f1b0f51", size = 292101, upload-time = "2025-10-06T05:36:59.237Z" },
+ { url = "https://files.pythonhosted.org/packages/9f/d0/2366d3c4ecdc2fd391e0afa6e11500bfba0ea772764d631bbf82f0136c9d/frozenlist-1.8.0-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:cf253e0e1c3ceb4aaff6df637ce033ff6535fb8c70a764a8f46aafd3d6ab798e", size = 289901, upload-time = "2025-10-06T05:37:00.811Z" },
+ { url = "https://files.pythonhosted.org/packages/b8/94/daff920e82c1b70e3618a2ac39fbc01ae3e2ff6124e80739ce5d71c9b920/frozenlist-1.8.0-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:032efa2674356903cd0261c4317a561a6850f3ac864a63fc1583147fb05a79b0", size = 289395, upload-time = "2025-10-06T05:37:02.115Z" },
+ { url = "https://files.pythonhosted.org/packages/e3/20/bba307ab4235a09fdcd3cc5508dbabd17c4634a1af4b96e0f69bfe551ebd/frozenlist-1.8.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:6da155091429aeba16851ecb10a9104a108bcd32f6c1642867eadaee401c1c41", size = 283659, upload-time = "2025-10-06T05:37:03.711Z" },
+ { url = "https://files.pythonhosted.org/packages/fd/00/04ca1c3a7a124b6de4f8a9a17cc2fcad138b4608e7a3fc5877804b8715d7/frozenlist-1.8.0-cp313-cp313t-win32.whl", hash = "sha256:0f96534f8bfebc1a394209427d0f8a63d343c9779cda6fc25e8e121b5fd8555b", size = 43492, upload-time = "2025-10-06T05:37:04.915Z" },
+ { url = "https://files.pythonhosted.org/packages/59/5e/c69f733a86a94ab10f68e496dc6b7e8bc078ebb415281d5698313e3af3a1/frozenlist-1.8.0-cp313-cp313t-win_amd64.whl", hash = "sha256:5d63a068f978fc69421fb0e6eb91a9603187527c86b7cd3f534a5b77a592b888", size = 48034, upload-time = "2025-10-06T05:37:06.343Z" },
+ { url = "https://files.pythonhosted.org/packages/16/6c/be9d79775d8abe79b05fa6d23da99ad6e7763a1d080fbae7290b286093fd/frozenlist-1.8.0-cp313-cp313t-win_arm64.whl", hash = "sha256:bf0a7e10b077bf5fb9380ad3ae8ce20ef919a6ad93b4552896419ac7e1d8e042", size = 41749, upload-time = "2025-10-06T05:37:07.431Z" },
+ { url = "https://files.pythonhosted.org/packages/f1/c8/85da824b7e7b9b6e7f7705b2ecaf9591ba6f79c1177f324c2735e41d36a2/frozenlist-1.8.0-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:cee686f1f4cadeb2136007ddedd0aaf928ab95216e7691c63e50a8ec066336d0", size = 86127, upload-time = "2025-10-06T05:37:08.438Z" },
+ { url = "https://files.pythonhosted.org/packages/8e/e8/a1185e236ec66c20afd72399522f142c3724c785789255202d27ae992818/frozenlist-1.8.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:119fb2a1bd47307e899c2fac7f28e85b9a543864df47aa7ec9d3c1b4545f096f", size = 49698, upload-time = "2025-10-06T05:37:09.48Z" },
+ { url = "https://files.pythonhosted.org/packages/a1/93/72b1736d68f03fda5fdf0f2180fb6caaae3894f1b854d006ac61ecc727ee/frozenlist-1.8.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:4970ece02dbc8c3a92fcc5228e36a3e933a01a999f7094ff7c23fbd2beeaa67c", size = 49749, upload-time = "2025-10-06T05:37:10.569Z" },
+ { url = "https://files.pythonhosted.org/packages/a7/b2/fabede9fafd976b991e9f1b9c8c873ed86f202889b864756f240ce6dd855/frozenlist-1.8.0-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:cba69cb73723c3f329622e34bdbf5ce1f80c21c290ff04256cff1cd3c2036ed2", size = 231298, upload-time = "2025-10-06T05:37:11.993Z" },
+ { url = "https://files.pythonhosted.org/packages/3a/3b/d9b1e0b0eed36e70477ffb8360c49c85c8ca8ef9700a4e6711f39a6e8b45/frozenlist-1.8.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:778a11b15673f6f1df23d9586f83c4846c471a8af693a22e066508b77d201ec8", size = 232015, upload-time = "2025-10-06T05:37:13.194Z" },
+ { url = "https://files.pythonhosted.org/packages/dc/94/be719d2766c1138148564a3960fc2c06eb688da592bdc25adcf856101be7/frozenlist-1.8.0-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:0325024fe97f94c41c08872db482cf8ac4800d80e79222c6b0b7b162d5b13686", size = 225038, upload-time = "2025-10-06T05:37:14.577Z" },
+ { url = "https://files.pythonhosted.org/packages/e4/09/6712b6c5465f083f52f50cf74167b92d4ea2f50e46a9eea0523d658454ae/frozenlist-1.8.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:97260ff46b207a82a7567b581ab4190bd4dfa09f4db8a8b49d1a958f6aa4940e", size = 240130, upload-time = "2025-10-06T05:37:15.781Z" },
+ { url = "https://files.pythonhosted.org/packages/f8/d4/cd065cdcf21550b54f3ce6a22e143ac9e4836ca42a0de1022da8498eac89/frozenlist-1.8.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:54b2077180eb7f83dd52c40b2750d0a9f175e06a42e3213ce047219de902717a", size = 242845, upload-time = "2025-10-06T05:37:17.037Z" },
+ { url = "https://files.pythonhosted.org/packages/62/c3/f57a5c8c70cd1ead3d5d5f776f89d33110b1addae0ab010ad774d9a44fb9/frozenlist-1.8.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:2f05983daecab868a31e1da44462873306d3cbfd76d1f0b5b69c473d21dbb128", size = 229131, upload-time = "2025-10-06T05:37:18.221Z" },
+ { url = "https://files.pythonhosted.org/packages/6c/52/232476fe9cb64f0742f3fde2b7d26c1dac18b6d62071c74d4ded55e0ef94/frozenlist-1.8.0-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:33f48f51a446114bc5d251fb2954ab0164d5be02ad3382abcbfe07e2531d650f", size = 240542, upload-time = "2025-10-06T05:37:19.771Z" },
+ { url = "https://files.pythonhosted.org/packages/5f/85/07bf3f5d0fb5414aee5f47d33c6f5c77bfe49aac680bfece33d4fdf6a246/frozenlist-1.8.0-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:154e55ec0655291b5dd1b8731c637ecdb50975a2ae70c606d100750a540082f7", size = 237308, upload-time = "2025-10-06T05:37:20.969Z" },
+ { url = "https://files.pythonhosted.org/packages/11/99/ae3a33d5befd41ac0ca2cc7fd3aa707c9c324de2e89db0e0f45db9a64c26/frozenlist-1.8.0-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:4314debad13beb564b708b4a496020e5306c7333fa9a3ab90374169a20ffab30", size = 238210, upload-time = "2025-10-06T05:37:22.252Z" },
+ { url = "https://files.pythonhosted.org/packages/b2/60/b1d2da22f4970e7a155f0adde9b1435712ece01b3cd45ba63702aea33938/frozenlist-1.8.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:073f8bf8becba60aa931eb3bc420b217bb7d5b8f4750e6f8b3be7f3da85d38b7", size = 231972, upload-time = "2025-10-06T05:37:23.5Z" },
+ { url = "https://files.pythonhosted.org/packages/3f/ab/945b2f32de889993b9c9133216c068b7fcf257d8595a0ac420ac8677cab0/frozenlist-1.8.0-cp314-cp314-win32.whl", hash = "sha256:bac9c42ba2ac65ddc115d930c78d24ab8d4f465fd3fc473cdedfccadb9429806", size = 40536, upload-time = "2025-10-06T05:37:25.581Z" },
+ { url = "https://files.pythonhosted.org/packages/59/ad/9caa9b9c836d9ad6f067157a531ac48b7d36499f5036d4141ce78c230b1b/frozenlist-1.8.0-cp314-cp314-win_amd64.whl", hash = "sha256:3e0761f4d1a44f1d1a47996511752cf3dcec5bbdd9cc2b4fe595caf97754b7a0", size = 44330, upload-time = "2025-10-06T05:37:26.928Z" },
+ { url = "https://files.pythonhosted.org/packages/82/13/e6950121764f2676f43534c555249f57030150260aee9dcf7d64efda11dd/frozenlist-1.8.0-cp314-cp314-win_arm64.whl", hash = "sha256:d1eaff1d00c7751b7c6662e9c5ba6eb2c17a2306ba5e2a37f24ddf3cc953402b", size = 40627, upload-time = "2025-10-06T05:37:28.075Z" },
+ { url = "https://files.pythonhosted.org/packages/c0/c7/43200656ecc4e02d3f8bc248df68256cd9572b3f0017f0a0c4e93440ae23/frozenlist-1.8.0-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:d3bb933317c52d7ea5004a1c442eef86f426886fba134ef8cf4226ea6ee1821d", size = 89238, upload-time = "2025-10-06T05:37:29.373Z" },
+ { url = "https://files.pythonhosted.org/packages/d1/29/55c5f0689b9c0fb765055629f472c0de484dcaf0acee2f7707266ae3583c/frozenlist-1.8.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:8009897cdef112072f93a0efdce29cd819e717fd2f649ee3016efd3cd885a7ed", size = 50738, upload-time = "2025-10-06T05:37:30.792Z" },
+ { url = "https://files.pythonhosted.org/packages/ba/7d/b7282a445956506fa11da8c2db7d276adcbf2b17d8bb8407a47685263f90/frozenlist-1.8.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:2c5dcbbc55383e5883246d11fd179782a9d07a986c40f49abe89ddf865913930", size = 51739, upload-time = "2025-10-06T05:37:32.127Z" },
+ { url = "https://files.pythonhosted.org/packages/62/1c/3d8622e60d0b767a5510d1d3cf21065b9db874696a51ea6d7a43180a259c/frozenlist-1.8.0-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:39ecbc32f1390387d2aa4f5a995e465e9e2f79ba3adcac92d68e3e0afae6657c", size = 284186, upload-time = "2025-10-06T05:37:33.21Z" },
+ { url = "https://files.pythonhosted.org/packages/2d/14/aa36d5f85a89679a85a1d44cd7a6657e0b1c75f61e7cad987b203d2daca8/frozenlist-1.8.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:92db2bf818d5cc8d9c1f1fc56b897662e24ea5adb36ad1f1d82875bd64e03c24", size = 292196, upload-time = "2025-10-06T05:37:36.107Z" },
+ { url = "https://files.pythonhosted.org/packages/05/23/6bde59eb55abd407d34f77d39a5126fb7b4f109a3f611d3929f14b700c66/frozenlist-1.8.0-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:2dc43a022e555de94c3b68a4ef0b11c4f747d12c024a520c7101709a2144fb37", size = 273830, upload-time = "2025-10-06T05:37:37.663Z" },
+ { url = "https://files.pythonhosted.org/packages/d2/3f/22cff331bfad7a8afa616289000ba793347fcd7bc275f3b28ecea2a27909/frozenlist-1.8.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:cb89a7f2de3602cfed448095bab3f178399646ab7c61454315089787df07733a", size = 294289, upload-time = "2025-10-06T05:37:39.261Z" },
+ { url = "https://files.pythonhosted.org/packages/a4/89/5b057c799de4838b6c69aa82b79705f2027615e01be996d2486a69ca99c4/frozenlist-1.8.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:33139dc858c580ea50e7e60a1b0ea003efa1fd42e6ec7fdbad78fff65fad2fd2", size = 300318, upload-time = "2025-10-06T05:37:43.213Z" },
+ { url = "https://files.pythonhosted.org/packages/30/de/2c22ab3eb2a8af6d69dc799e48455813bab3690c760de58e1bf43b36da3e/frozenlist-1.8.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:168c0969a329b416119507ba30b9ea13688fafffac1b7822802537569a1cb0ef", size = 282814, upload-time = "2025-10-06T05:37:45.337Z" },
+ { url = "https://files.pythonhosted.org/packages/59/f7/970141a6a8dbd7f556d94977858cfb36fa9b66e0892c6dd780d2219d8cd8/frozenlist-1.8.0-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:28bd570e8e189d7f7b001966435f9dac6718324b5be2990ac496cf1ea9ddb7fe", size = 291762, upload-time = "2025-10-06T05:37:46.657Z" },
+ { url = "https://files.pythonhosted.org/packages/c1/15/ca1adae83a719f82df9116d66f5bb28bb95557b3951903d39135620ef157/frozenlist-1.8.0-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:b2a095d45c5d46e5e79ba1e5b9cb787f541a8dee0433836cea4b96a2c439dcd8", size = 289470, upload-time = "2025-10-06T05:37:47.946Z" },
+ { url = "https://files.pythonhosted.org/packages/ac/83/dca6dc53bf657d371fbc88ddeb21b79891e747189c5de990b9dfff2ccba1/frozenlist-1.8.0-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:eab8145831a0d56ec9c4139b6c3e594c7a83c2c8be25d5bcf2d86136a532287a", size = 289042, upload-time = "2025-10-06T05:37:49.499Z" },
+ { url = "https://files.pythonhosted.org/packages/96/52/abddd34ca99be142f354398700536c5bd315880ed0a213812bc491cff5e4/frozenlist-1.8.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:974b28cf63cc99dfb2188d8d222bc6843656188164848c4f679e63dae4b0708e", size = 283148, upload-time = "2025-10-06T05:37:50.745Z" },
+ { url = "https://files.pythonhosted.org/packages/af/d3/76bd4ed4317e7119c2b7f57c3f6934aba26d277acc6309f873341640e21f/frozenlist-1.8.0-cp314-cp314t-win32.whl", hash = "sha256:342c97bf697ac5480c0a7ec73cd700ecfa5a8a40ac923bd035484616efecc2df", size = 44676, upload-time = "2025-10-06T05:37:52.222Z" },
+ { url = "https://files.pythonhosted.org/packages/89/76/c615883b7b521ead2944bb3480398cbb07e12b7b4e4d073d3752eb721558/frozenlist-1.8.0-cp314-cp314t-win_amd64.whl", hash = "sha256:06be8f67f39c8b1dc671f5d83aaefd3358ae5cdcf8314552c57e7ed3e6475bdd", size = 49451, upload-time = "2025-10-06T05:37:53.425Z" },
+ { url = "https://files.pythonhosted.org/packages/e0/a3/5982da14e113d07b325230f95060e2169f5311b1017ea8af2a29b374c289/frozenlist-1.8.0-cp314-cp314t-win_arm64.whl", hash = "sha256:102e6314ca4da683dca92e3b1355490fed5f313b768500084fbe6371fddfdb79", size = 42507, upload-time = "2025-10-06T05:37:54.513Z" },
+ { url = "https://files.pythonhosted.org/packages/c2/59/ae5cdac87a00962122ea37bb346d41b66aec05f9ce328fa2b9e216f8967b/frozenlist-1.8.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:d8b7138e5cd0647e4523d6685b0eac5d4be9a184ae9634492f25c6eb38c12a47", size = 86967, upload-time = "2025-10-06T05:37:55.607Z" },
+ { url = "https://files.pythonhosted.org/packages/8a/10/17059b2db5a032fd9323c41c39e9d1f5f9d0c8f04d1e4e3e788573086e61/frozenlist-1.8.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:a6483e309ca809f1efd154b4d37dc6d9f61037d6c6a81c2dc7a15cb22c8c5dca", size = 49984, upload-time = "2025-10-06T05:37:57.049Z" },
+ { url = "https://files.pythonhosted.org/packages/4b/de/ad9d82ca8e5fa8f0c636e64606553c79e2b859ad253030b62a21fe9986f5/frozenlist-1.8.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:1b9290cf81e95e93fdf90548ce9d3c1211cf574b8e3f4b3b7cb0537cf2227068", size = 50240, upload-time = "2025-10-06T05:37:58.145Z" },
+ { url = "https://files.pythonhosted.org/packages/4e/45/3dfb7767c2a67d123650122b62ce13c731b6c745bc14424eea67678b508c/frozenlist-1.8.0-cp39-cp39-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:59a6a5876ca59d1b63af8cd5e7ffffb024c3dc1e9cf9301b21a2e76286505c95", size = 219472, upload-time = "2025-10-06T05:37:59.239Z" },
+ { url = "https://files.pythonhosted.org/packages/0b/bf/5bf23d913a741b960d5c1dac7c1985d8a2a1d015772b2d18ea168b08e7ff/frozenlist-1.8.0-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6dc4126390929823e2d2d9dc79ab4046ed74680360fc5f38b585c12c66cdf459", size = 221531, upload-time = "2025-10-06T05:38:00.521Z" },
+ { url = "https://files.pythonhosted.org/packages/d0/03/27ec393f3b55860859f4b74cdc8c2a4af3dbf3533305e8eacf48a4fd9a54/frozenlist-1.8.0-cp39-cp39-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:332db6b2563333c5671fecacd085141b5800cb866be16d5e3eb15a2086476675", size = 219211, upload-time = "2025-10-06T05:38:01.842Z" },
+ { url = "https://files.pythonhosted.org/packages/3a/ad/0fd00c404fa73fe9b169429e9a972d5ed807973c40ab6b3cf9365a33d360/frozenlist-1.8.0-cp39-cp39-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:9ff15928d62a0b80bb875655c39bf517938c7d589554cbd2669be42d97c2cb61", size = 231775, upload-time = "2025-10-06T05:38:03.384Z" },
+ { url = "https://files.pythonhosted.org/packages/8a/c3/86962566154cb4d2995358bc8331bfc4ea19d07db1a96f64935a1607f2b6/frozenlist-1.8.0-cp39-cp39-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:7bf6cdf8e07c8151fba6fe85735441240ec7f619f935a5205953d58009aef8c6", size = 236631, upload-time = "2025-10-06T05:38:04.609Z" },
+ { url = "https://files.pythonhosted.org/packages/ea/9e/6ffad161dbd83782d2c66dc4d378a9103b31770cb1e67febf43aea42d202/frozenlist-1.8.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:48e6d3f4ec5c7273dfe83ff27c91083c6c9065af655dc2684d2c200c94308bb5", size = 218632, upload-time = "2025-10-06T05:38:05.917Z" },
+ { url = "https://files.pythonhosted.org/packages/58/b2/4677eee46e0a97f9b30735e6ad0bf6aba3e497986066eb68807ac85cf60f/frozenlist-1.8.0-cp39-cp39-musllinux_1_2_armv7l.whl", hash = "sha256:1a7607e17ad33361677adcd1443edf6f5da0ce5e5377b798fba20fae194825f3", size = 235967, upload-time = "2025-10-06T05:38:07.614Z" },
+ { url = "https://files.pythonhosted.org/packages/05/f3/86e75f8639c5a93745ca7addbbc9de6af56aebb930d233512b17e46f6493/frozenlist-1.8.0-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:5a3a935c3a4e89c733303a2d5a7c257ea44af3a56c8202df486b7f5de40f37e1", size = 228799, upload-time = "2025-10-06T05:38:08.845Z" },
+ { url = "https://files.pythonhosted.org/packages/30/00/39aad3a7f0d98f5eb1d99a3c311215674ed87061aecee7851974b335c050/frozenlist-1.8.0-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:940d4a017dbfed9daf46a3b086e1d2167e7012ee297fef9e1c545c4d022f5178", size = 230566, upload-time = "2025-10-06T05:38:10.52Z" },
+ { url = "https://files.pythonhosted.org/packages/0d/4d/aa144cac44568d137846ddc4d5210fb5d9719eb1d7ec6fa2728a54b5b94a/frozenlist-1.8.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:b9be22a69a014bc47e78072d0ecae716f5eb56c15238acca0f43d6eb8e4a5bda", size = 217715, upload-time = "2025-10-06T05:38:11.832Z" },
+ { url = "https://files.pythonhosted.org/packages/64/4c/8f665921667509d25a0dd72540513bc86b356c95541686f6442a3283019f/frozenlist-1.8.0-cp39-cp39-win32.whl", hash = "sha256:1aa77cb5697069af47472e39612976ed05343ff2e84a3dcf15437b232cbfd087", size = 39933, upload-time = "2025-10-06T05:38:13.061Z" },
+ { url = "https://files.pythonhosted.org/packages/79/bd/bcc926f87027fad5e59926ff12d136e1082a115025d33c032d1cd69ab377/frozenlist-1.8.0-cp39-cp39-win_amd64.whl", hash = "sha256:7398c222d1d405e796970320036b1b563892b65809d9e5261487bb2c7f7b5c6a", size = 44121, upload-time = "2025-10-06T05:38:14.572Z" },
+ { url = "https://files.pythonhosted.org/packages/4c/07/9c2e4eb7584af4b705237b971b89a4155a8e57599c4483a131a39256a9a0/frozenlist-1.8.0-cp39-cp39-win_arm64.whl", hash = "sha256:b4f3b365f31c6cd4af24545ca0a244a53688cad8834e32f56831c4923b50a103", size = 40312, upload-time = "2025-10-06T05:38:15.699Z" },
+ { url = "https://files.pythonhosted.org/packages/9a/9a/e35b4a917281c0b8419d4207f4334c8e8c5dbf4f3f5f9ada73958d937dcc/frozenlist-1.8.0-py3-none-any.whl", hash = "sha256:0c18a16eab41e82c295618a77502e17b195883241c563b00f0aa5106fc4eaa0d", size = 13409, upload-time = "2025-10-06T05:38:16.721Z" },
+]
+
+[[package]]
+name = "h11"
+version = "0.16.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/01/ee/02a2c011bdab74c6fb3c75474d40b3052059d95df7e73351460c8588d963/h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1", size = 101250, upload-time = "2025-04-24T03:35:25.427Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86", size = 37515, upload-time = "2025-04-24T03:35:24.344Z" },
+]
+
+[[package]]
+name = "httpcore"
+version = "1.0.9"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "certifi" },
+ { name = "h11" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/06/94/82699a10bca87a5556c9c59b5963f2d039dbd239f25bc2a63907a05a14cb/httpcore-1.0.9.tar.gz", hash = "sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8", size = 85484, upload-time = "2025-04-24T22:06:22.219Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/7e/f5/f66802a942d491edb555dd61e3a9961140fd64c90bce1eafd741609d334d/httpcore-1.0.9-py3-none-any.whl", hash = "sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55", size = 78784, upload-time = "2025-04-24T22:06:20.566Z" },
+]
+
+[[package]]
+name = "httpx"
+version = "0.28.1"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "anyio", version = "4.12.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" },
+ { name = "anyio", version = "4.14.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" },
+ { name = "certifi" },
+ { name = "httpcore" },
+ { name = "idna" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/b1/df/48c586a5fe32a0f01324ee087459e112ebb7224f646c0b5023f5e79e9956/httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc", size = 141406, upload-time = "2024-12-06T15:37:23.222Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad", size = 73517, upload-time = "2024-12-06T15:37:21.509Z" },
+]
+
+[[package]]
+name = "httpx-aiohttp"
+version = "0.2.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "aiohttp", version = "3.13.5", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" },
+ { name = "aiohttp", version = "3.14.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" },
+ { name = "httpx" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/4c/87/3b2df9732a497403e5f4bbf2ec9f25427d53cec797e83070c503649863ef/httpx_aiohttp-0.2.0.tar.gz", hash = "sha256:d4796b981f04734f1d1db9b4d9326ea16bc994f126460b93b69036262cd4a9d8", size = 195714, upload-time = "2026-07-25T07:34:12.17Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/e8/e2/74b6bad3a6d342aee12d8b8d825456c02d21d72319c924326d17444c5ff7/httpx_aiohttp-0.2.0-py3-none-any.whl", hash = "sha256:ccd6eb19ba18805476096e8ef0b369a6beda3955db145a538979eface2fce7ff", size = 9732, upload-time = "2026-07-25T07:34:10.939Z" },
+]
+
+[[package]]
+name = "idna"
+version = "3.18"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/cd/63/9496c57188a2ee585e0f1db071d75089a11e98aa86eb99d9d7618fc1edce/idna-3.18.tar.gz", hash = "sha256:ffb385a7e039654cef1ab9ef32c6fafe283c0c0467bba1d9029738ce4a14a848", size = 196711, upload-time = "2026-06-02T14:34:07.794Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/1e/5e/d4e9f1a599fb8e573b7b87160658329fbf28d19eac2718f51fc3def3aa5a/idna-3.18-py3-none-any.whl", hash = "sha256:7f952cbe720b688055e3f87de14f5c3e5fdaa8bc3928985c4077ca689de849a2", size = 65455, upload-time = "2026-06-02T14:34:06.319Z" },
+]
+
+[[package]]
+name = "kernel"
+version = "0.86.0"
+source = { editable = "." }
+dependencies = [
+ { name = "anyio", version = "4.12.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" },
+ { name = "anyio", version = "4.14.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" },
+ { name = "distro" },
+ { name = "httpx" },
+ { name = "pydantic" },
+ { name = "sniffio" },
+ { name = "typing-extensions" },
+]
+
+[package.optional-dependencies]
+aiohttp = [
+ { name = "aiohttp", version = "3.13.5", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" },
+ { name = "aiohttp", version = "3.14.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" },
+ { name = "httpx-aiohttp" },
+]
+
+[package.metadata]
+requires-dist = [
+ { name = "aiohttp", marker = "extra == 'aiohttp'" },
+ { name = "anyio", specifier = ">=3.5.0,<5" },
+ { name = "distro", specifier = ">=1.7.0,<2" },
+ { name = "httpx", specifier = ">=0.23.0,<1" },
+ { name = "httpx-aiohttp", marker = "extra == 'aiohttp'", specifier = ">=0.1.9" },
+ { name = "pydantic", specifier = ">=1.9.0,<3" },
+ { name = "sniffio" },
+ { name = "typing-extensions", specifier = ">=4.14,<5" },
+]
+provides-extras = ["aiohttp"]
+
+[[package]]
+name = "multidict"
+version = "6.7.1"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "typing-extensions", marker = "python_full_version < '3.11'" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/1a/c2/c2d94cbe6ac1753f3fc980da97b3d930efe1da3af3c9f5125354436c073d/multidict-6.7.1.tar.gz", hash = "sha256:ec6652a1bee61c53a3e5776b6049172c53b6aaba34f18c9ad04f82712bac623d", size = 102010, upload-time = "2026-01-26T02:46:45.979Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/84/0b/19348d4c98980c4851d2f943f8ebafdece2ae7ef737adcfa5994ce8e5f10/multidict-6.7.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:c93c3db7ea657dd4637d57e74ab73de31bccefe144d3d4ce370052035bc85fb5", size = 77176, upload-time = "2026-01-26T02:42:59.784Z" },
+ { url = "https://files.pythonhosted.org/packages/ef/04/9de3f8077852e3d438215c81e9b691244532d2e05b4270e89ce67b7d103c/multidict-6.7.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:974e72a2474600827abaeda71af0c53d9ebbc3c2eb7da37b37d7829ae31232d8", size = 44996, upload-time = "2026-01-26T02:43:01.674Z" },
+ { url = "https://files.pythonhosted.org/packages/31/5c/08c7f7fe311f32e83f7621cd3f99d805f45519cd06fafb247628b861da7d/multidict-6.7.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:cdea2e7b2456cfb6694fb113066fd0ec7ea4d67e3a35e1f4cbeea0b448bf5872", size = 44631, upload-time = "2026-01-26T02:43:03.169Z" },
+ { url = "https://files.pythonhosted.org/packages/b7/7f/0e3b1390ae772f27501199996b94b52ceeb64fe6f9120a32c6c3f6b781be/multidict-6.7.1-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:17207077e29342fdc2c9a82e4b306f1127bf1ea91f8b71e02d4798a70bb99991", size = 242561, upload-time = "2026-01-26T02:43:04.733Z" },
+ { url = "https://files.pythonhosted.org/packages/dd/f4/8719f4f167586af317b69dd3e90f913416c91ca610cac79a45c53f590312/multidict-6.7.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d4f49cb5661344764e4c7c7973e92a47a59b8fc19b6523649ec9dc4960e58a03", size = 242223, upload-time = "2026-01-26T02:43:06.695Z" },
+ { url = "https://files.pythonhosted.org/packages/47/ab/7c36164cce64a6ad19c6d9a85377b7178ecf3b89f8fd589c73381a5eedfd/multidict-6.7.1-cp310-cp310-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:a9fc4caa29e2e6ae408d1c450ac8bf19892c5fca83ee634ecd88a53332c59981", size = 222322, upload-time = "2026-01-26T02:43:08.472Z" },
+ { url = "https://files.pythonhosted.org/packages/f5/79/a25add6fb38035b5337bc5734f296d9afc99163403bbcf56d4170f97eb62/multidict-6.7.1-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c5f0c21549ab432b57dcc82130f388d84ad8179824cc3f223d5e7cfbfd4143f6", size = 254005, upload-time = "2026-01-26T02:43:10.127Z" },
+ { url = "https://files.pythonhosted.org/packages/4a/7b/64a87cf98e12f756fc8bd444b001232ffff2be37288f018ad0d3f0aae931/multidict-6.7.1-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:7dfb78d966b2c906ae1d28ccf6e6712a3cd04407ee5088cd276fe8cb42186190", size = 251173, upload-time = "2026-01-26T02:43:11.731Z" },
+ { url = "https://files.pythonhosted.org/packages/4b/ac/b605473de2bb404e742f2cc3583d12aedb2352a70e49ae8fce455b50c5aa/multidict-6.7.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9b0d9b91d1aa44db9c1f1ecd0d9d2ae610b2f4f856448664e01a3b35899f3f92", size = 243273, upload-time = "2026-01-26T02:43:13.063Z" },
+ { url = "https://files.pythonhosted.org/packages/03/65/11492d6a0e259783720f3bc1d9ea55579a76f1407e31ed44045c99542004/multidict-6.7.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:dd96c01a9dcd4889dcfcf9eb5544ca0c77603f239e3ffab0524ec17aea9a93ee", size = 238956, upload-time = "2026-01-26T02:43:14.843Z" },
+ { url = "https://files.pythonhosted.org/packages/5f/a7/7ee591302af64e7c196fb63fe856c788993c1372df765102bd0448e7e165/multidict-6.7.1-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:067343c68cd6612d375710f895337b3a98a033c94f14b9a99eff902f205424e2", size = 233477, upload-time = "2026-01-26T02:43:16.025Z" },
+ { url = "https://files.pythonhosted.org/packages/9c/99/c109962d58756c35fd9992fed7f2355303846ea2ff054bb5f5e9d6b888de/multidict-6.7.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:5884a04f4ff56c6120f6ccf703bdeb8b5079d808ba604d4d53aec0d55dc33568", size = 243615, upload-time = "2026-01-26T02:43:17.84Z" },
+ { url = "https://files.pythonhosted.org/packages/d5/5f/1973e7c771c86e93dcfe1c9cc55a5481b610f6614acfc28c0d326fe6bfad/multidict-6.7.1-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:8affcf1c98b82bc901702eb73b6947a1bfa170823c153fe8a47b5f5f02e48e40", size = 249930, upload-time = "2026-01-26T02:43:19.06Z" },
+ { url = "https://files.pythonhosted.org/packages/5d/a5/f170fc2268c3243853580203378cd522446b2df632061e0a5409817854c7/multidict-6.7.1-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:0d17522c37d03e85c8098ec8431636309b2682cf12e58f4dbc76121fb50e4962", size = 243807, upload-time = "2026-01-26T02:43:20.286Z" },
+ { url = "https://files.pythonhosted.org/packages/de/01/73856fab6d125e5bc652c3986b90e8699a95e84b48d72f39ade6c0e74a8c/multidict-6.7.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:24c0cf81544ca5e17cfcb6e482e7a82cd475925242b308b890c9452a074d4505", size = 239103, upload-time = "2026-01-26T02:43:21.508Z" },
+ { url = "https://files.pythonhosted.org/packages/e7/46/f1220bd9944d8aa40d8ccff100eeeee19b505b857b6f603d6078cb5315b0/multidict-6.7.1-cp310-cp310-win32.whl", hash = "sha256:d82dd730a95e6643802f4454b8fdecdf08667881a9c5670db85bc5a56693f122", size = 41416, upload-time = "2026-01-26T02:43:22.703Z" },
+ { url = "https://files.pythonhosted.org/packages/68/00/9b38e272a770303692fc406c36e1a4c740f401522d5787691eb38a8925a8/multidict-6.7.1-cp310-cp310-win_amd64.whl", hash = "sha256:cf37cbe5ced48d417ba045aca1b21bafca67489452debcde94778a576666a1df", size = 46022, upload-time = "2026-01-26T02:43:23.77Z" },
+ { url = "https://files.pythonhosted.org/packages/64/65/d8d42490c02ee07b6bbe00f7190d70bb4738b3cce7629aaf9f213ef730dd/multidict-6.7.1-cp310-cp310-win_arm64.whl", hash = "sha256:59bc83d3f66b41dac1e7460aac1d196edc70c9ba3094965c467715a70ecb46db", size = 43238, upload-time = "2026-01-26T02:43:24.882Z" },
+ { url = "https://files.pythonhosted.org/packages/ce/f1/a90635c4f88fb913fbf4ce660b83b7445b7a02615bda034b2f8eb38fd597/multidict-6.7.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:7ff981b266af91d7b4b3793ca3382e53229088d193a85dfad6f5f4c27fc73e5d", size = 76626, upload-time = "2026-01-26T02:43:26.485Z" },
+ { url = "https://files.pythonhosted.org/packages/a6/9b/267e64eaf6fc637a15b35f5de31a566634a2740f97d8d094a69d34f524a4/multidict-6.7.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:844c5bca0b5444adb44a623fb0a1310c2f4cd41f402126bb269cd44c9b3f3e1e", size = 44706, upload-time = "2026-01-26T02:43:27.607Z" },
+ { url = "https://files.pythonhosted.org/packages/dd/a4/d45caf2b97b035c57267791ecfaafbd59c68212004b3842830954bb4b02e/multidict-6.7.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f2a0a924d4c2e9afcd7ec64f9de35fcd96915149b2216e1cb2c10a56df483855", size = 44356, upload-time = "2026-01-26T02:43:28.661Z" },
+ { url = "https://files.pythonhosted.org/packages/fd/d2/0a36c8473f0cbaeadd5db6c8b72d15bbceeec275807772bfcd059bef487d/multidict-6.7.1-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:8be1802715a8e892c784c0197c2ace276ea52702a0ede98b6310c8f255a5afb3", size = 244355, upload-time = "2026-01-26T02:43:31.165Z" },
+ { url = "https://files.pythonhosted.org/packages/5d/16/8c65be997fd7dd311b7d39c7b6e71a0cb449bad093761481eccbbe4b42a2/multidict-6.7.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2e2d2ed645ea29f31c4c7ea1552fcfd7cb7ba656e1eafd4134a6620c9f5fdd9e", size = 246433, upload-time = "2026-01-26T02:43:32.581Z" },
+ { url = "https://files.pythonhosted.org/packages/01/fb/4dbd7e848d2799c6a026ec88ad39cf2b8416aa167fcc903baa55ecaa045c/multidict-6.7.1-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:95922cee9a778659e91db6497596435777bd25ed116701a4c034f8e46544955a", size = 225376, upload-time = "2026-01-26T02:43:34.417Z" },
+ { url = "https://files.pythonhosted.org/packages/b6/8a/4a3a6341eac3830f6053062f8fbc9a9e54407c80755b3f05bc427295c2d0/multidict-6.7.1-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:6b83cabdc375ffaaa15edd97eb7c0c672ad788e2687004990074d7d6c9b140c8", size = 257365, upload-time = "2026-01-26T02:43:35.741Z" },
+ { url = "https://files.pythonhosted.org/packages/f7/a2/dd575a69c1aa206e12d27d0770cdf9b92434b48a9ef0cd0d1afdecaa93c4/multidict-6.7.1-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:38fb49540705369bab8484db0689d86c0a33a0a9f2c1b197f506b71b4b6c19b0", size = 254747, upload-time = "2026-01-26T02:43:36.976Z" },
+ { url = "https://files.pythonhosted.org/packages/5a/56/21b27c560c13822ed93133f08aa6372c53a8e067f11fbed37b4adcdac922/multidict-6.7.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:439cbebd499f92e9aa6793016a8acaa161dfa749ae86d20960189f5398a19144", size = 246293, upload-time = "2026-01-26T02:43:38.258Z" },
+ { url = "https://files.pythonhosted.org/packages/5a/a4/23466059dc3854763423d0ad6c0f3683a379d97673b1b89ec33826e46728/multidict-6.7.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:6d3bc717b6fe763b8be3f2bee2701d3c8eb1b2a8ae9f60910f1b2860c82b6c49", size = 242962, upload-time = "2026-01-26T02:43:40.034Z" },
+ { url = "https://files.pythonhosted.org/packages/1f/67/51dd754a3524d685958001e8fa20a0f5f90a6a856e0a9dcabff69be3dbb7/multidict-6.7.1-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:619e5a1ac57986dbfec9f0b301d865dddf763696435e2962f6d9cf2fdff2bb71", size = 237360, upload-time = "2026-01-26T02:43:41.752Z" },
+ { url = "https://files.pythonhosted.org/packages/64/3f/036dfc8c174934d4b55d86ff4f978e558b0e585cef70cfc1ad01adc6bf18/multidict-6.7.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:0b38ebffd9be37c1170d33bc0f36f4f262e0a09bc1aac1c34c7aa51a7293f0b3", size = 245940, upload-time = "2026-01-26T02:43:43.042Z" },
+ { url = "https://files.pythonhosted.org/packages/3d/20/6214d3c105928ebc353a1c644a6ef1408bc5794fcb4f170bb524a3c16311/multidict-6.7.1-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:10ae39c9cfe6adedcdb764f5e8411d4a92b055e35573a2eaa88d3323289ef93c", size = 253502, upload-time = "2026-01-26T02:43:44.371Z" },
+ { url = "https://files.pythonhosted.org/packages/b1/e2/c653bc4ae1be70a0f836b82172d643fcf1dade042ba2676ab08ec08bff0f/multidict-6.7.1-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:25167cc263257660290fba06b9318d2026e3c910be240a146e1f66dd114af2b0", size = 247065, upload-time = "2026-01-26T02:43:45.745Z" },
+ { url = "https://files.pythonhosted.org/packages/c8/11/a854b4154cd3bd8b1fd375e8a8ca9d73be37610c361543d56f764109509b/multidict-6.7.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:128441d052254f42989ef98b7b6a6ecb1e6f708aa962c7984235316db59f50fa", size = 241870, upload-time = "2026-01-26T02:43:47.054Z" },
+ { url = "https://files.pythonhosted.org/packages/13/bf/9676c0392309b5fdae322333d22a829715b570edb9baa8016a517b55b558/multidict-6.7.1-cp311-cp311-win32.whl", hash = "sha256:d62b7f64ffde3b99d06b707a280db04fb3855b55f5a06df387236051d0668f4a", size = 41302, upload-time = "2026-01-26T02:43:48.753Z" },
+ { url = "https://files.pythonhosted.org/packages/c9/68/f16a3a8ba6f7b6dc92a1f19669c0810bd2c43fc5a02da13b1cbf8e253845/multidict-6.7.1-cp311-cp311-win_amd64.whl", hash = "sha256:bdbf9f3b332abd0cdb306e7c2113818ab1e922dc84b8f8fd06ec89ed2a19ab8b", size = 45981, upload-time = "2026-01-26T02:43:49.921Z" },
+ { url = "https://files.pythonhosted.org/packages/ac/ad/9dd5305253fa00cd3c7555dbef69d5bf4133debc53b87ab8d6a44d411665/multidict-6.7.1-cp311-cp311-win_arm64.whl", hash = "sha256:b8c990b037d2fff2f4e33d3f21b9b531c5745b33a49a7d6dbe7a177266af44f6", size = 43159, upload-time = "2026-01-26T02:43:51.635Z" },
+ { url = "https://files.pythonhosted.org/packages/8d/9c/f20e0e2cf80e4b2e4b1c365bf5fe104ee633c751a724246262db8f1a0b13/multidict-6.7.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:a90f75c956e32891a4eda3639ce6dd86e87105271f43d43442a3aedf3cddf172", size = 76893, upload-time = "2026-01-26T02:43:52.754Z" },
+ { url = "https://files.pythonhosted.org/packages/fe/cf/18ef143a81610136d3da8193da9d80bfe1cb548a1e2d1c775f26b23d024a/multidict-6.7.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:3fccb473e87eaa1382689053e4a4618e7ba7b9b9b8d6adf2027ee474597128cd", size = 45456, upload-time = "2026-01-26T02:43:53.893Z" },
+ { url = "https://files.pythonhosted.org/packages/a9/65/1caac9d4cd32e8433908683446eebc953e82d22b03d10d41a5f0fefe991b/multidict-6.7.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:b0fa96985700739c4c7853a43c0b3e169360d6855780021bfc6d0f1ce7c123e7", size = 43872, upload-time = "2026-01-26T02:43:55.041Z" },
+ { url = "https://files.pythonhosted.org/packages/cf/3b/d6bd75dc4f3ff7c73766e04e705b00ed6dbbaccf670d9e05a12b006f5a21/multidict-6.7.1-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:cb2a55f408c3043e42b40cc8eecd575afa27b7e0b956dfb190de0f8499a57a53", size = 251018, upload-time = "2026-01-26T02:43:56.198Z" },
+ { url = "https://files.pythonhosted.org/packages/fd/80/c959c5933adedb9ac15152e4067c702a808ea183a8b64cf8f31af8ad3155/multidict-6.7.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:eb0ce7b2a32d09892b3dd6cc44877a0d02a33241fafca5f25c8b6b62374f8b75", size = 258883, upload-time = "2026-01-26T02:43:57.499Z" },
+ { url = "https://files.pythonhosted.org/packages/86/85/7ed40adafea3d4f1c8b916e3b5cc3a8e07dfcdcb9cd72800f4ed3ca1b387/multidict-6.7.1-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:c3a32d23520ee37bf327d1e1a656fec76a2edd5c038bf43eddfa0572ec49c60b", size = 242413, upload-time = "2026-01-26T02:43:58.755Z" },
+ { url = "https://files.pythonhosted.org/packages/d2/57/b8565ff533e48595503c785f8361ff9a4fde4d67de25c207cd0ba3befd03/multidict-6.7.1-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:9c90fed18bffc0189ba814749fdcc102b536e83a9f738a9003e569acd540a733", size = 268404, upload-time = "2026-01-26T02:44:00.216Z" },
+ { url = "https://files.pythonhosted.org/packages/e0/50/9810c5c29350f7258180dfdcb2e52783a0632862eb334c4896ac717cebcb/multidict-6.7.1-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:da62917e6076f512daccfbbde27f46fed1c98fee202f0559adec8ee0de67f71a", size = 269456, upload-time = "2026-01-26T02:44:02.202Z" },
+ { url = "https://files.pythonhosted.org/packages/f3/8d/5e5be3ced1d12966fefb5c4ea3b2a5b480afcea36406559442c6e31d4a48/multidict-6.7.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bfde23ef6ed9db7eaee6c37dcec08524cb43903c60b285b172b6c094711b3961", size = 256322, upload-time = "2026-01-26T02:44:03.56Z" },
+ { url = "https://files.pythonhosted.org/packages/31/6e/d8a26d81ac166a5592782d208dd90dfdc0a7a218adaa52b45a672b46c122/multidict-6.7.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3758692429e4e32f1ba0df23219cd0b4fc0a52f476726fff9337d1a57676a582", size = 253955, upload-time = "2026-01-26T02:44:04.845Z" },
+ { url = "https://files.pythonhosted.org/packages/59/4c/7c672c8aad41534ba619bcd4ade7a0dc87ed6b8b5c06149b85d3dd03f0cd/multidict-6.7.1-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:398c1478926eca669f2fd6a5856b6de9c0acf23a2cb59a14c0ba5844fa38077e", size = 251254, upload-time = "2026-01-26T02:44:06.133Z" },
+ { url = "https://files.pythonhosted.org/packages/7b/bd/84c24de512cbafbdbc39439f74e967f19570ce7924e3007174a29c348916/multidict-6.7.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:c102791b1c4f3ab36ce4101154549105a53dc828f016356b3e3bcae2e3a039d3", size = 252059, upload-time = "2026-01-26T02:44:07.518Z" },
+ { url = "https://files.pythonhosted.org/packages/fa/ba/f5449385510825b73d01c2d4087bf6d2fccc20a2d42ac34df93191d3dd03/multidict-6.7.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:a088b62bd733e2ad12c50dad01b7d0166c30287c166e137433d3b410add807a6", size = 263588, upload-time = "2026-01-26T02:44:09.382Z" },
+ { url = "https://files.pythonhosted.org/packages/d7/11/afc7c677f68f75c84a69fe37184f0f82fce13ce4b92f49f3db280b7e92b3/multidict-6.7.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:3d51ff4785d58d3f6c91bdbffcb5e1f7ddfda557727043aa20d20ec4f65e324a", size = 259642, upload-time = "2026-01-26T02:44:10.73Z" },
+ { url = "https://files.pythonhosted.org/packages/2b/17/ebb9644da78c4ab36403739e0e6e0e30ebb135b9caf3440825001a0bddcb/multidict-6.7.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fc5907494fccf3e7d3f94f95c91d6336b092b5fc83811720fae5e2765890dfba", size = 251377, upload-time = "2026-01-26T02:44:12.042Z" },
+ { url = "https://files.pythonhosted.org/packages/ca/a4/840f5b97339e27846c46307f2530a2805d9d537d8b8bd416af031cad7fa0/multidict-6.7.1-cp312-cp312-win32.whl", hash = "sha256:28ca5ce2fd9716631133d0e9a9b9a745ad7f60bac2bccafb56aa380fc0b6c511", size = 41887, upload-time = "2026-01-26T02:44:14.245Z" },
+ { url = "https://files.pythonhosted.org/packages/80/31/0b2517913687895f5904325c2069d6a3b78f66cc641a86a2baf75a05dcbb/multidict-6.7.1-cp312-cp312-win_amd64.whl", hash = "sha256:fcee94dfbd638784645b066074b338bc9cc155d4b4bffa4adce1615c5a426c19", size = 46053, upload-time = "2026-01-26T02:44:15.371Z" },
+ { url = "https://files.pythonhosted.org/packages/0c/5b/aba28e4ee4006ae4c7df8d327d31025d760ffa992ea23812a601d226e682/multidict-6.7.1-cp312-cp312-win_arm64.whl", hash = "sha256:ba0a9fb644d0c1a2194cf7ffb043bd852cea63a57f66fbd33959f7dae18517bf", size = 43307, upload-time = "2026-01-26T02:44:16.852Z" },
+ { url = "https://files.pythonhosted.org/packages/f2/22/929c141d6c0dba87d3e1d38fbdf1ba8baba86b7776469f2bc2d3227a1e67/multidict-6.7.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:2b41f5fed0ed563624f1c17630cb9941cf2309d4df00e494b551b5f3e3d67a23", size = 76174, upload-time = "2026-01-26T02:44:18.509Z" },
+ { url = "https://files.pythonhosted.org/packages/c7/75/bc704ae15fee974f8fccd871305e254754167dce5f9e42d88a2def741a1d/multidict-6.7.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:84e61e3af5463c19b67ced91f6c634effb89ef8bfc5ca0267f954451ed4bb6a2", size = 45116, upload-time = "2026-01-26T02:44:19.745Z" },
+ { url = "https://files.pythonhosted.org/packages/79/76/55cd7186f498ed080a18440c9013011eb548f77ae1b297206d030eb1180a/multidict-6.7.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:935434b9853c7c112eee7ac891bc4cb86455aa631269ae35442cb316790c1445", size = 43524, upload-time = "2026-01-26T02:44:21.571Z" },
+ { url = "https://files.pythonhosted.org/packages/e9/3c/414842ef8d5a1628d68edee29ba0e5bcf235dbfb3ccd3ea303a7fe8c72ff/multidict-6.7.1-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:432feb25a1cb67fe82a9680b4d65fb542e4635cb3166cd9c01560651ad60f177", size = 249368, upload-time = "2026-01-26T02:44:22.803Z" },
+ { url = "https://files.pythonhosted.org/packages/f6/32/befed7f74c458b4a525e60519fe8d87eef72bb1e99924fa2b0f9d97a221e/multidict-6.7.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e82d14e3c948952a1a85503817e038cba5905a3352de76b9a465075d072fba23", size = 256952, upload-time = "2026-01-26T02:44:24.306Z" },
+ { url = "https://files.pythonhosted.org/packages/03/d6/c878a44ba877f366630c860fdf74bfb203c33778f12b6ac274936853c451/multidict-6.7.1-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:4cfb48c6ea66c83bcaaf7e4dfa7ec1b6bbcf751b7db85a328902796dfde4c060", size = 240317, upload-time = "2026-01-26T02:44:25.772Z" },
+ { url = "https://files.pythonhosted.org/packages/68/49/57421b4d7ad2e9e60e25922b08ceb37e077b90444bde6ead629095327a6f/multidict-6.7.1-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:1d540e51b7e8e170174555edecddbd5538105443754539193e3e1061864d444d", size = 267132, upload-time = "2026-01-26T02:44:27.648Z" },
+ { url = "https://files.pythonhosted.org/packages/b7/fe/ec0edd52ddbcea2a2e89e174f0206444a61440b40f39704e64dc807a70bd/multidict-6.7.1-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:273d23f4b40f3dce4d6c8a821c741a86dec62cded82e1175ba3d99be128147ed", size = 268140, upload-time = "2026-01-26T02:44:29.588Z" },
+ { url = "https://files.pythonhosted.org/packages/b0/73/6e1b01cbeb458807aa0831742232dbdd1fa92bfa33f52a3f176b4ff3dc11/multidict-6.7.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9d624335fd4fa1c08a53f8b4be7676ebde19cd092b3895c421045ca87895b429", size = 254277, upload-time = "2026-01-26T02:44:30.902Z" },
+ { url = "https://files.pythonhosted.org/packages/6a/b2/5fb8c124d7561a4974c342bc8c778b471ebbeb3cc17df696f034a7e9afe7/multidict-6.7.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:12fad252f8b267cc75b66e8fc51b3079604e8d43a75428ffe193cd9e2195dfd6", size = 252291, upload-time = "2026-01-26T02:44:32.31Z" },
+ { url = "https://files.pythonhosted.org/packages/5a/96/51d4e4e06bcce92577fcd488e22600bd38e4fd59c20cb49434d054903bd2/multidict-6.7.1-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:03ede2a6ffbe8ef936b92cb4529f27f42be7f56afcdab5ab739cd5f27fb1cbf9", size = 250156, upload-time = "2026-01-26T02:44:33.734Z" },
+ { url = "https://files.pythonhosted.org/packages/db/6b/420e173eec5fba721a50e2a9f89eda89d9c98fded1124f8d5c675f7a0c0f/multidict-6.7.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:90efbcf47dbe33dcf643a1e400d67d59abeac5db07dc3f27d6bdeae497a2198c", size = 249742, upload-time = "2026-01-26T02:44:35.222Z" },
+ { url = "https://files.pythonhosted.org/packages/44/a3/ec5b5bd98f306bc2aa297b8c6f11a46714a56b1e6ef5ebda50a4f5d7c5fb/multidict-6.7.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:5c4b9bfc148f5a91be9244d6264c53035c8a0dcd2f51f1c3c6e30e30ebaa1c84", size = 262221, upload-time = "2026-01-26T02:44:36.604Z" },
+ { url = "https://files.pythonhosted.org/packages/cd/f7/e8c0d0da0cd1e28d10e624604e1a36bcc3353aaebdfdc3a43c72bc683a12/multidict-6.7.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:401c5a650f3add2472d1d288c26deebc540f99e2fb83e9525007a74cd2116f1d", size = 258664, upload-time = "2026-01-26T02:44:38.008Z" },
+ { url = "https://files.pythonhosted.org/packages/52/da/151a44e8016dd33feed44f730bd856a66257c1ee7aed4f44b649fb7edeb3/multidict-6.7.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:97891f3b1b3ffbded884e2916cacf3c6fc87b66bb0dde46f7357404750559f33", size = 249490, upload-time = "2026-01-26T02:44:39.386Z" },
+ { url = "https://files.pythonhosted.org/packages/87/af/a3b86bf9630b732897f6fc3f4c4714b90aa4361983ccbdcd6c0339b21b0c/multidict-6.7.1-cp313-cp313-win32.whl", hash = "sha256:e1c5988359516095535c4301af38d8a8838534158f649c05dd1050222321bcb3", size = 41695, upload-time = "2026-01-26T02:44:41.318Z" },
+ { url = "https://files.pythonhosted.org/packages/b2/35/e994121b0e90e46134673422dd564623f93304614f5d11886b1b3e06f503/multidict-6.7.1-cp313-cp313-win_amd64.whl", hash = "sha256:960c83bf01a95b12b08fd54324a4eb1d5b52c88932b5cba5d6e712bb3ed12eb5", size = 45884, upload-time = "2026-01-26T02:44:42.488Z" },
+ { url = "https://files.pythonhosted.org/packages/ca/61/42d3e5dbf661242a69c97ea363f2d7b46c567da8eadef8890022be6e2ab0/multidict-6.7.1-cp313-cp313-win_arm64.whl", hash = "sha256:563fe25c678aaba333d5399408f5ec3c383ca5b663e7f774dd179a520b8144df", size = 43122, upload-time = "2026-01-26T02:44:43.664Z" },
+ { url = "https://files.pythonhosted.org/packages/6d/b3/e6b21c6c4f314bb956016b0b3ef2162590a529b84cb831c257519e7fde44/multidict-6.7.1-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:c76c4bec1538375dad9d452d246ca5368ad6e1c9039dadcf007ae59c70619ea1", size = 83175, upload-time = "2026-01-26T02:44:44.894Z" },
+ { url = "https://files.pythonhosted.org/packages/fb/76/23ecd2abfe0957b234f6c960f4ade497f55f2c16aeb684d4ecdbf1c95791/multidict-6.7.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:57b46b24b5d5ebcc978da4ec23a819a9402b4228b8a90d9c656422b4bdd8a963", size = 48460, upload-time = "2026-01-26T02:44:46.106Z" },
+ { url = "https://files.pythonhosted.org/packages/c4/57/a0ed92b23f3a042c36bc4227b72b97eca803f5f1801c1ab77c8a212d455e/multidict-6.7.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:e954b24433c768ce78ab7929e84ccf3422e46deb45a4dc9f93438f8217fa2d34", size = 46930, upload-time = "2026-01-26T02:44:47.278Z" },
+ { url = "https://files.pythonhosted.org/packages/b5/66/02ec7ace29162e447f6382c495dc95826bf931d3818799bbef11e8f7df1a/multidict-6.7.1-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:3bd231490fa7217cc832528e1cd8752a96f0125ddd2b5749390f7c3ec8721b65", size = 242582, upload-time = "2026-01-26T02:44:48.604Z" },
+ { url = "https://files.pythonhosted.org/packages/58/18/64f5a795e7677670e872673aca234162514696274597b3708b2c0d276cce/multidict-6.7.1-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:253282d70d67885a15c8a7716f3a73edf2d635793ceda8173b9ecc21f2fb8292", size = 250031, upload-time = "2026-01-26T02:44:50.544Z" },
+ { url = "https://files.pythonhosted.org/packages/c8/ed/e192291dbbe51a8290c5686f482084d31bcd9d09af24f63358c3d42fd284/multidict-6.7.1-cp313-cp313t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:0b4c48648d7649c9335cf1927a8b87fa692de3dcb15faa676c6a6f1f1aabda43", size = 228596, upload-time = "2026-01-26T02:44:51.951Z" },
+ { url = "https://files.pythonhosted.org/packages/1e/7e/3562a15a60cf747397e7f2180b0a11dc0c38d9175a650e75fa1b4d325e15/multidict-6.7.1-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:98bc624954ec4d2c7cb074b8eefc2b5d0ce7d482e410df446414355d158fe4ca", size = 257492, upload-time = "2026-01-26T02:44:53.902Z" },
+ { url = "https://files.pythonhosted.org/packages/24/02/7d0f9eae92b5249bb50ac1595b295f10e263dd0078ebb55115c31e0eaccd/multidict-6.7.1-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:1b99af4d9eec0b49927b4402bcbb58dea89d3e0db8806a4086117019939ad3dd", size = 255899, upload-time = "2026-01-26T02:44:55.316Z" },
+ { url = "https://files.pythonhosted.org/packages/00/e3/9b60ed9e23e64c73a5cde95269ef1330678e9c6e34dd4eb6b431b85b5a10/multidict-6.7.1-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6aac4f16b472d5b7dc6f66a0d49dd57b0e0902090be16594dc9ebfd3d17c47e7", size = 247970, upload-time = "2026-01-26T02:44:56.783Z" },
+ { url = "https://files.pythonhosted.org/packages/3e/06/538e58a63ed5cfb0bd4517e346b91da32fde409d839720f664e9a4ae4f9d/multidict-6.7.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:21f830fe223215dffd51f538e78c172ed7c7f60c9b96a2bf05c4848ad49921c3", size = 245060, upload-time = "2026-01-26T02:44:58.195Z" },
+ { url = "https://files.pythonhosted.org/packages/b2/2f/d743a3045a97c895d401e9bd29aaa09b94f5cbdf1bd561609e5a6c431c70/multidict-6.7.1-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:f5dd81c45b05518b9aa4da4aa74e1c93d715efa234fd3e8a179df611cc85e5f4", size = 235888, upload-time = "2026-01-26T02:44:59.57Z" },
+ { url = "https://files.pythonhosted.org/packages/38/83/5a325cac191ab28b63c52f14f1131f3b0a55ba3b9aa65a6d0bf2a9b921a0/multidict-6.7.1-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:eb304767bca2bb92fb9c5bd33cedc95baee5bb5f6c88e63706533a1c06ad08c8", size = 243554, upload-time = "2026-01-26T02:45:01.054Z" },
+ { url = "https://files.pythonhosted.org/packages/20/1f/9d2327086bd15da2725ef6aae624208e2ef828ed99892b17f60c344e57ed/multidict-6.7.1-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:c9035dde0f916702850ef66460bc4239d89d08df4d02023a5926e7446724212c", size = 252341, upload-time = "2026-01-26T02:45:02.484Z" },
+ { url = "https://files.pythonhosted.org/packages/e8/2c/2a1aa0280cf579d0f6eed8ee5211c4f1730bd7e06c636ba2ee6aafda302e/multidict-6.7.1-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:af959b9beeb66c822380f222f0e0a1889331597e81f1ded7f374f3ecb0fd6c52", size = 246391, upload-time = "2026-01-26T02:45:03.862Z" },
+ { url = "https://files.pythonhosted.org/packages/e5/03/7ca022ffc36c5a3f6e03b179a5ceb829be9da5783e6fe395f347c0794680/multidict-6.7.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:41f2952231456154ee479651491e94118229844dd7226541788be783be2b5108", size = 243422, upload-time = "2026-01-26T02:45:05.296Z" },
+ { url = "https://files.pythonhosted.org/packages/dc/1d/b31650eab6c5778aceed46ba735bd97f7c7d2f54b319fa916c0f96e7805b/multidict-6.7.1-cp313-cp313t-win32.whl", hash = "sha256:df9f19c28adcb40b6aae30bbaa1478c389efd50c28d541d76760199fc1037c32", size = 47770, upload-time = "2026-01-26T02:45:06.754Z" },
+ { url = "https://files.pythonhosted.org/packages/ac/5b/2d2d1d522e51285bd61b1e20df8f47ae1a9d80839db0b24ea783b3832832/multidict-6.7.1-cp313-cp313t-win_amd64.whl", hash = "sha256:d54ecf9f301853f2c5e802da559604b3e95bb7a3b01a9c295c6ee591b9882de8", size = 53109, upload-time = "2026-01-26T02:45:08.044Z" },
+ { url = "https://files.pythonhosted.org/packages/3d/a3/cc409ba012c83ca024a308516703cf339bdc4b696195644a7215a5164a24/multidict-6.7.1-cp313-cp313t-win_arm64.whl", hash = "sha256:5a37ca18e360377cfda1d62f5f382ff41f2b8c4ccb329ed974cc2e1643440118", size = 45573, upload-time = "2026-01-26T02:45:09.349Z" },
+ { url = "https://files.pythonhosted.org/packages/91/cc/db74228a8be41884a567e88a62fd589a913708fcf180d029898c17a9a371/multidict-6.7.1-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:8f333ec9c5eb1b7105e3b84b53141e66ca05a19a605368c55450b6ba208cb9ee", size = 75190, upload-time = "2026-01-26T02:45:10.651Z" },
+ { url = "https://files.pythonhosted.org/packages/d5/22/492f2246bb5b534abd44804292e81eeaf835388901f0c574bac4eeec73c5/multidict-6.7.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:a407f13c188f804c759fc6a9f88286a565c242a76b27626594c133b82883b5c2", size = 44486, upload-time = "2026-01-26T02:45:11.938Z" },
+ { url = "https://files.pythonhosted.org/packages/f1/4f/733c48f270565d78b4544f2baddc2fb2a245e5a8640254b12c36ac7ac68e/multidict-6.7.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:0e161ddf326db5577c3a4cc2d8648f81456e8a20d40415541587a71620d7a7d1", size = 43219, upload-time = "2026-01-26T02:45:14.346Z" },
+ { url = "https://files.pythonhosted.org/packages/24/bb/2c0c2287963f4259c85e8bcbba9182ced8d7fca65c780c38e99e61629d11/multidict-6.7.1-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:1e3a8bb24342a8201d178c3b4984c26ba81a577c80d4d525727427460a50c22d", size = 245132, upload-time = "2026-01-26T02:45:15.712Z" },
+ { url = "https://files.pythonhosted.org/packages/a7/f9/44d4b3064c65079d2467888794dea218d1601898ac50222ab8a9a8094460/multidict-6.7.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:97231140a50f5d447d3164f994b86a0bed7cd016e2682f8650d6a9158e14fd31", size = 252420, upload-time = "2026-01-26T02:45:17.293Z" },
+ { url = "https://files.pythonhosted.org/packages/8b/13/78f7275e73fa17b24c9a51b0bd9d73ba64bb32d0ed51b02a746eb876abe7/multidict-6.7.1-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:6b10359683bd8806a200fd2909e7c8ca3a7b24ec1d8132e483d58e791d881048", size = 233510, upload-time = "2026-01-26T02:45:19.356Z" },
+ { url = "https://files.pythonhosted.org/packages/4b/25/8167187f62ae3cbd52da7893f58cb036b47ea3fb67138787c76800158982/multidict-6.7.1-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:283ddac99f7ac25a4acadbf004cb5ae34480bbeb063520f70ce397b281859362", size = 264094, upload-time = "2026-01-26T02:45:20.834Z" },
+ { url = "https://files.pythonhosted.org/packages/a1/e7/69a3a83b7b030cf283fb06ce074a05a02322359783424d7edf0f15fe5022/multidict-6.7.1-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:538cec1e18c067d0e6103aa9a74f9e832904c957adc260e61cd9d8cf0c3b3d37", size = 260786, upload-time = "2026-01-26T02:45:22.818Z" },
+ { url = "https://files.pythonhosted.org/packages/fe/3b/8ec5074bcfc450fe84273713b4b0a0dd47c0249358f5d82eb8104ffe2520/multidict-6.7.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7eee46ccb30ff48a1e35bb818cc90846c6be2b68240e42a78599166722cea709", size = 248483, upload-time = "2026-01-26T02:45:24.368Z" },
+ { url = "https://files.pythonhosted.org/packages/48/5a/d5a99e3acbca0e29c5d9cba8f92ceb15dce78bab963b308ae692981e3a5d/multidict-6.7.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:fa263a02f4f2dd2d11a7b1bb4362aa7cb1049f84a9235d31adf63f30143469a0", size = 248403, upload-time = "2026-01-26T02:45:25.982Z" },
+ { url = "https://files.pythonhosted.org/packages/35/48/e58cd31f6c7d5102f2a4bf89f96b9cf7e00b6c6f3d04ecc44417c00a5a3c/multidict-6.7.1-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:2e1425e2f99ec5bd36c15a01b690a1a2456209c5deed58f95469ffb46039ccbb", size = 240315, upload-time = "2026-01-26T02:45:27.487Z" },
+ { url = "https://files.pythonhosted.org/packages/94/33/1cd210229559cb90b6786c30676bb0c58249ff42f942765f88793b41fdce/multidict-6.7.1-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:497394b3239fc6f0e13a78a3e1b61296e72bf1c5f94b4c4eb80b265c37a131cd", size = 245528, upload-time = "2026-01-26T02:45:28.991Z" },
+ { url = "https://files.pythonhosted.org/packages/64/f2/6e1107d226278c876c783056b7db43d800bb64c6131cec9c8dfb6903698e/multidict-6.7.1-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:233b398c29d3f1b9676b4b6f75c518a06fcb2ea0b925119fb2c1bc35c05e1601", size = 258784, upload-time = "2026-01-26T02:45:30.503Z" },
+ { url = "https://files.pythonhosted.org/packages/4d/c1/11f664f14d525e4a1b5327a82d4de61a1db604ab34c6603bb3c2cc63ad34/multidict-6.7.1-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:93b1818e4a6e0930454f0f2af7dfce69307ca03cdcfb3739bf4d91241967b6c1", size = 251980, upload-time = "2026-01-26T02:45:32.603Z" },
+ { url = "https://files.pythonhosted.org/packages/e1/9f/75a9ac888121d0c5bbd4ecf4eead45668b1766f6baabfb3b7f66a410e231/multidict-6.7.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:f33dc2a3abe9249ea5d8360f969ec7f4142e7ac45ee7014d8f8d5acddf178b7b", size = 243602, upload-time = "2026-01-26T02:45:34.043Z" },
+ { url = "https://files.pythonhosted.org/packages/9a/e7/50bf7b004cc8525d80dbbbedfdc7aed3e4c323810890be4413e589074032/multidict-6.7.1-cp314-cp314-win32.whl", hash = "sha256:3ab8b9d8b75aef9df299595d5388b14530839f6422333357af1339443cff777d", size = 40930, upload-time = "2026-01-26T02:45:36.278Z" },
+ { url = "https://files.pythonhosted.org/packages/e0/bf/52f25716bbe93745595800f36fb17b73711f14da59ed0bb2eba141bc9f0f/multidict-6.7.1-cp314-cp314-win_amd64.whl", hash = "sha256:5e01429a929600e7dab7b166062d9bb54a5eed752384c7384c968c2afab8f50f", size = 45074, upload-time = "2026-01-26T02:45:37.546Z" },
+ { url = "https://files.pythonhosted.org/packages/97/ab/22803b03285fa3a525f48217963da3a65ae40f6a1b6f6cf2768879e208f9/multidict-6.7.1-cp314-cp314-win_arm64.whl", hash = "sha256:4885cb0e817aef5d00a2e8451d4665c1808378dc27c2705f1bf4ef8505c0d2e5", size = 42471, upload-time = "2026-01-26T02:45:38.889Z" },
+ { url = "https://files.pythonhosted.org/packages/e0/6d/f9293baa6146ba9507e360ea0292b6422b016907c393e2f63fc40ab7b7b5/multidict-6.7.1-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:0458c978acd8e6ea53c81eefaddbbee9c6c5e591f41b3f5e8e194780fe026581", size = 82401, upload-time = "2026-01-26T02:45:40.254Z" },
+ { url = "https://files.pythonhosted.org/packages/7a/68/53b5494738d83558d87c3c71a486504d8373421c3e0dbb6d0db48ad42ee0/multidict-6.7.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:c0abd12629b0af3cf590982c0b413b1e7395cd4ec026f30986818ab95bfaa94a", size = 48143, upload-time = "2026-01-26T02:45:41.635Z" },
+ { url = "https://files.pythonhosted.org/packages/37/e8/5284c53310dcdc99ce5d66563f6e5773531a9b9fe9ec7a615e9bc306b05f/multidict-6.7.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:14525a5f61d7d0c94b368a42cff4c9a4e7ba2d52e2672a7b23d84dc86fb02b0c", size = 46507, upload-time = "2026-01-26T02:45:42.99Z" },
+ { url = "https://files.pythonhosted.org/packages/e4/fc/6800d0e5b3875568b4083ecf5f310dcf91d86d52573160834fb4bfcf5e4f/multidict-6.7.1-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:17307b22c217b4cf05033dabefe68255a534d637c6c9b0cc8382718f87be4262", size = 239358, upload-time = "2026-01-26T02:45:44.376Z" },
+ { url = "https://files.pythonhosted.org/packages/41/75/4ad0973179361cdf3a113905e6e088173198349131be2b390f9fa4da5fc6/multidict-6.7.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7a7e590ff876a3eaf1c02a4dfe0724b6e69a9e9de6d8f556816f29c496046e59", size = 246884, upload-time = "2026-01-26T02:45:47.167Z" },
+ { url = "https://files.pythonhosted.org/packages/c3/9c/095bb28b5da139bd41fb9a5d5caff412584f377914bd8787c2aa98717130/multidict-6.7.1-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:5fa6a95dfee63893d80a34758cd0e0c118a30b8dcb46372bf75106c591b77889", size = 225878, upload-time = "2026-01-26T02:45:48.698Z" },
+ { url = "https://files.pythonhosted.org/packages/07/d0/c0a72000243756e8f5a277b6b514fa005f2c73d481b7d9e47cd4568aa2e4/multidict-6.7.1-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a0543217a6a017692aa6ae5cc39adb75e587af0f3a82288b1492eb73dd6cc2a4", size = 253542, upload-time = "2026-01-26T02:45:50.164Z" },
+ { url = "https://files.pythonhosted.org/packages/c0/6b/f69da15289e384ecf2a68837ec8b5ad8c33e973aa18b266f50fe55f24b8c/multidict-6.7.1-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f99fe611c312b3c1c0ace793f92464d8cd263cc3b26b5721950d977b006b6c4d", size = 252403, upload-time = "2026-01-26T02:45:51.779Z" },
+ { url = "https://files.pythonhosted.org/packages/a2/76/b9669547afa5a1a25cd93eaca91c0da1c095b06b6d2d8ec25b713588d3a1/multidict-6.7.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9004d8386d133b7e6135679424c91b0b854d2d164af6ea3f289f8f2761064609", size = 244889, upload-time = "2026-01-26T02:45:53.27Z" },
+ { url = "https://files.pythonhosted.org/packages/7e/a9/a50d2669e506dad33cfc45b5d574a205587b7b8a5f426f2fbb2e90882588/multidict-6.7.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e628ef0e6859ffd8273c69412a2465c4be4a9517d07261b33334b5ec6f3c7489", size = 241982, upload-time = "2026-01-26T02:45:54.919Z" },
+ { url = "https://files.pythonhosted.org/packages/c5/bb/1609558ad8b456b4827d3c5a5b775c93b87878fd3117ed3db3423dfbce1b/multidict-6.7.1-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:841189848ba629c3552035a6a7f5bf3b02eb304e9fea7492ca220a8eda6b0e5c", size = 232415, upload-time = "2026-01-26T02:45:56.981Z" },
+ { url = "https://files.pythonhosted.org/packages/d8/59/6f61039d2aa9261871e03ab9dc058a550d240f25859b05b67fd70f80d4b3/multidict-6.7.1-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:ce1bbd7d780bb5a0da032e095c951f7014d6b0a205f8318308140f1a6aba159e", size = 240337, upload-time = "2026-01-26T02:45:58.698Z" },
+ { url = "https://files.pythonhosted.org/packages/a1/29/fdc6a43c203890dc2ae9249971ecd0c41deaedfe00d25cb6564b2edd99eb/multidict-6.7.1-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:b26684587228afed0d50cf804cc71062cc9c1cdf55051c4c6345d372947b268c", size = 248788, upload-time = "2026-01-26T02:46:00.862Z" },
+ { url = "https://files.pythonhosted.org/packages/a9/14/a153a06101323e4cf086ecee3faadba52ff71633d471f9685c42e3736163/multidict-6.7.1-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:9f9af11306994335398293f9958071019e3ab95e9a707dc1383a35613f6abcb9", size = 242842, upload-time = "2026-01-26T02:46:02.824Z" },
+ { url = "https://files.pythonhosted.org/packages/41/5f/604ae839e64a4a6efc80db94465348d3b328ee955e37acb24badbcd24d83/multidict-6.7.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:b4938326284c4f1224178a560987b6cf8b4d38458b113d9b8c1db1a836e640a2", size = 240237, upload-time = "2026-01-26T02:46:05.898Z" },
+ { url = "https://files.pythonhosted.org/packages/5f/60/c3a5187bf66f6fb546ff4ab8fb5a077cbdd832d7b1908d4365c7f74a1917/multidict-6.7.1-cp314-cp314t-win32.whl", hash = "sha256:98655c737850c064a65e006a3df7c997cd3b220be4ec8fe26215760b9697d4d7", size = 48008, upload-time = "2026-01-26T02:46:07.468Z" },
+ { url = "https://files.pythonhosted.org/packages/0c/f7/addf1087b860ac60e6f382240f64fb99f8bfb532bb06f7c542b83c29ca61/multidict-6.7.1-cp314-cp314t-win_amd64.whl", hash = "sha256:497bde6223c212ba11d462853cfa4f0ae6ef97465033e7dc9940cdb3ab5b48e5", size = 53542, upload-time = "2026-01-26T02:46:08.809Z" },
+ { url = "https://files.pythonhosted.org/packages/4c/81/4629d0aa32302ef7b2ec65c75a728cc5ff4fa410c50096174c1632e70b3e/multidict-6.7.1-cp314-cp314t-win_arm64.whl", hash = "sha256:2bbd113e0d4af5db41d5ebfe9ccaff89de2120578164f86a5d17d5a576d1e5b2", size = 44719, upload-time = "2026-01-26T02:46:11.146Z" },
+ { url = "https://files.pythonhosted.org/packages/9e/ee/74525ebe3eb5fddcd6735fc03cbea3feeed4122b53bc798ac32d297ac9ae/multidict-6.7.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:65573858d27cdeaca41893185677dc82395159aa28875a8867af66532d413a8f", size = 77107, upload-time = "2026-01-26T02:46:12.608Z" },
+ { url = "https://files.pythonhosted.org/packages/f0/9a/ce8744e777a74b3050b1bf56be3eed1053b3457302ea055f1ea437200a23/multidict-6.7.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:c524c6fb8fc342793708ab111c4dbc90ff9abd568de220432500e47e990c0358", size = 44943, upload-time = "2026-01-26T02:46:14.016Z" },
+ { url = "https://files.pythonhosted.org/packages/83/9c/1d2a283d9c6f31e260cb6c2fccadc3edcf6c4c14ee0929cd2af4d2606dd7/multidict-6.7.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:aa23b001d968faef416ff70dc0f1ab045517b9b42a90edd3e9bcdb06479e31d5", size = 44603, upload-time = "2026-01-26T02:46:15.391Z" },
+ { url = "https://files.pythonhosted.org/packages/87/9d/3b186201671583d8e8d6d79c07481a5aafd0ba7575e3d8566baec80c1e82/multidict-6.7.1-cp39-cp39-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:6704fa2b7453b2fb121740555fa1ee20cd98c4d011120caf4d2b8d4e7c76eec0", size = 240573, upload-time = "2026-01-26T02:46:16.783Z" },
+ { url = "https://files.pythonhosted.org/packages/42/7d/a52f5d4d0754311d1ac78478e34dff88de71259a8585e05ee14e5f877caf/multidict-6.7.1-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:121a34e5bfa410cdf2c8c49716de160de3b1dbcd86b49656f5681e4543bcd1a8", size = 240106, upload-time = "2026-01-26T02:46:18.432Z" },
+ { url = "https://files.pythonhosted.org/packages/84/9f/d80118e6c30ff55b7d171bdc5520aad4b9626e657520b8d7c8ca8c2fad12/multidict-6.7.1-cp39-cp39-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:026d264228bcd637d4e060844e39cdc60f86c479e463d49075dedc21b18fbbe0", size = 219418, upload-time = "2026-01-26T02:46:20.526Z" },
+ { url = "https://files.pythonhosted.org/packages/c7/bd/896e60b3457f194de77c7de64f9acce9f75da0518a5230ce1df534f6747b/multidict-6.7.1-cp39-cp39-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:0e697826df7eb63418ee190fd06ce9f1803593bb4b9517d08c60d9b9a7f69d8f", size = 252124, upload-time = "2026-01-26T02:46:22.157Z" },
+ { url = "https://files.pythonhosted.org/packages/f4/de/ba6b30447c36a37078d0ba604aa12c1a52887af0c355236ca6e0a9d5286f/multidict-6.7.1-cp39-cp39-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:bb08271280173720e9fea9ede98e5231defcbad90f1624bea26f32ec8a956e2f", size = 249402, upload-time = "2026-01-26T02:46:23.718Z" },
+ { url = "https://files.pythonhosted.org/packages/c2/b2/50a383c96230e432895a2fd3bcfe1b65785899598259d871d5de6b93180c/multidict-6.7.1-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c6b3228e1d80af737b72925ce5fb4daf5a335e49cd7ab77ed7b9fdfbf58c526e", size = 240346, upload-time = "2026-01-26T02:46:25.393Z" },
+ { url = "https://files.pythonhosted.org/packages/89/37/16d391fd8da544b1489306e38a46785fa41dd0f0ef766837ed7d4676dde0/multidict-6.7.1-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:3943debf0fbb57bdde5901695c11094a9a36723e5c03875f87718ee15ca2f4d2", size = 237010, upload-time = "2026-01-26T02:46:27.408Z" },
+ { url = "https://files.pythonhosted.org/packages/b0/24/3152ee026eda86d5d3e3685182911e6951af7a016579da931080ce6ac9ad/multidict-6.7.1-cp39-cp39-musllinux_1_2_armv7l.whl", hash = "sha256:98c5787b0a0d9a41d9311eae44c3b76e6753def8d8870ab501320efe75a6a5f8", size = 232018, upload-time = "2026-01-26T02:46:29.941Z" },
+ { url = "https://files.pythonhosted.org/packages/9c/1f/48d3c27a72be7fd23a55d8847193c459959bf35a5bb5844530dab00b739b/multidict-6.7.1-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:08ccb2a6dc72009093ebe7f3f073e5ec5964cba9a706fa94b1a1484039b87941", size = 241498, upload-time = "2026-01-26T02:46:32.052Z" },
+ { url = "https://files.pythonhosted.org/packages/1a/45/413643ae2952d0decdf6c1250f86d08a43e143271441e81027e38d598bd7/multidict-6.7.1-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:eb351f72c26dc9abe338ca7294661aa22969ad8ffe7ef7d5541d19f368dc854a", size = 247957, upload-time = "2026-01-26T02:46:33.666Z" },
+ { url = "https://files.pythonhosted.org/packages/50/f8/f1d0ac23df15e0470776388bdb261506f63af1f81d28bacb5e262d6e12b6/multidict-6.7.1-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:ac1c665bad8b5d762f5f85ebe4d94130c26965f11de70c708c75671297c776de", size = 241651, upload-time = "2026-01-26T02:46:35.7Z" },
+ { url = "https://files.pythonhosted.org/packages/2c/c9/1a2a18f383cf129add66b6c36b75c3911a7ba95cf26cb141482de085cc12/multidict-6.7.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:1fa6609d0364f4f6f58351b4659a1f3e0e898ba2a8c5cac04cb2c7bc556b0bc5", size = 236371, upload-time = "2026-01-26T02:46:37.37Z" },
+ { url = "https://files.pythonhosted.org/packages/bb/aa/77d87e3fca31325b87e0eb72d5fe9a7472dcb51391a42df7ac1f3842f6c0/multidict-6.7.1-cp39-cp39-win32.whl", hash = "sha256:6f77ce314a29263e67adadc7e7c1bc699fcb3a305059ab973d038f87caa42ed0", size = 41426, upload-time = "2026-01-26T02:46:39.026Z" },
+ { url = "https://files.pythonhosted.org/packages/e3/b3/e8863e6a2da15a9d7e98976ff402e871b7352c76566df6c18d0378e0d9cf/multidict-6.7.1-cp39-cp39-win_amd64.whl", hash = "sha256:f537b55778cd3cbee430abe3131255d3a78202e0f9ea7ffc6ada893a4bcaeea4", size = 46180, upload-time = "2026-01-26T02:46:40.422Z" },
+ { url = "https://files.pythonhosted.org/packages/93/d3/dd4fa951ad5b5fa216bf30054d705683d13405eea7459833d78f31b74c9c/multidict-6.7.1-cp39-cp39-win_arm64.whl", hash = "sha256:749aa54f578f2e5f439538706a475aa844bfa8ef75854b1401e6e528e4937cf9", size = 43231, upload-time = "2026-01-26T02:46:41.945Z" },
+ { url = "https://files.pythonhosted.org/packages/81/08/7036c080d7117f28a4af526d794aab6a84463126db031b007717c1a6676e/multidict-6.7.1-py3-none-any.whl", hash = "sha256:55d97cc6dae627efa6a6e548885712d4864b81110ac76fa4e534c03819fa4a56", size = 12319, upload-time = "2026-01-26T02:46:44.004Z" },
+]
+
+[[package]]
+name = "propcache"
+version = "0.4.1"
+source = { registry = "https://pypi.org/simple" }
+resolution-markers = [
+ "python_full_version < '3.10'",
+]
+sdist = { url = "https://files.pythonhosted.org/packages/9e/da/e9fc233cf63743258bff22b3dfa7ea5baef7b5bc324af47a0ad89b8ffc6f/propcache-0.4.1.tar.gz", hash = "sha256:f48107a8c637e80362555f37ecf49abe20370e557cc4ab374f04ec4423c97c3d", size = 46442, upload-time = "2025-10-08T19:49:02.291Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/3c/0e/934b541323035566a9af292dba85a195f7b78179114f2c6ebb24551118a9/propcache-0.4.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:7c2d1fa3201efaf55d730400d945b5b3ab6e672e100ba0f9a409d950ab25d7db", size = 79534, upload-time = "2025-10-08T19:46:02.083Z" },
+ { url = "https://files.pythonhosted.org/packages/a1/6b/db0d03d96726d995dc7171286c6ba9d8d14251f37433890f88368951a44e/propcache-0.4.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:1eb2994229cc8ce7fe9b3db88f5465f5fd8651672840b2e426b88cdb1a30aac8", size = 45526, upload-time = "2025-10-08T19:46:03.884Z" },
+ { url = "https://files.pythonhosted.org/packages/e4/c3/82728404aea669e1600f304f2609cde9e665c18df5a11cdd57ed73c1dceb/propcache-0.4.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:66c1f011f45a3b33d7bcb22daed4b29c0c9e2224758b6be00686731e1b46f925", size = 47263, upload-time = "2025-10-08T19:46:05.405Z" },
+ { url = "https://files.pythonhosted.org/packages/df/1b/39313ddad2bf9187a1432654c38249bab4562ef535ef07f5eb6eb04d0b1b/propcache-0.4.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9a52009f2adffe195d0b605c25ec929d26b36ef986ba85244891dee3b294df21", size = 201012, upload-time = "2025-10-08T19:46:07.165Z" },
+ { url = "https://files.pythonhosted.org/packages/5b/01/f1d0b57d136f294a142acf97f4ed58c8e5b974c21e543000968357115011/propcache-0.4.1-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:5d4e2366a9c7b837555cf02fb9be2e3167d333aff716332ef1b7c3a142ec40c5", size = 209491, upload-time = "2025-10-08T19:46:08.909Z" },
+ { url = "https://files.pythonhosted.org/packages/a1/c8/038d909c61c5bb039070b3fb02ad5cccdb1dde0d714792e251cdb17c9c05/propcache-0.4.1-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:9d2b6caef873b4f09e26ea7e33d65f42b944837563a47a94719cc3544319a0db", size = 215319, upload-time = "2025-10-08T19:46:10.7Z" },
+ { url = "https://files.pythonhosted.org/packages/08/57/8c87e93142b2c1fa2408e45695205a7ba05fb5db458c0bf5c06ba0e09ea6/propcache-0.4.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2b16ec437a8c8a965ecf95739448dd938b5c7f56e67ea009f4300d8df05f32b7", size = 196856, upload-time = "2025-10-08T19:46:12.003Z" },
+ { url = "https://files.pythonhosted.org/packages/42/df/5615fec76aa561987a534759b3686008a288e73107faa49a8ae5795a9f7a/propcache-0.4.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:296f4c8ed03ca7476813fe666c9ea97869a8d7aec972618671b33a38a5182ef4", size = 193241, upload-time = "2025-10-08T19:46:13.495Z" },
+ { url = "https://files.pythonhosted.org/packages/d5/21/62949eb3a7a54afe8327011c90aca7e03547787a88fb8bd9726806482fea/propcache-0.4.1-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:1f0978529a418ebd1f49dad413a2b68af33f85d5c5ca5c6ca2a3bed375a7ac60", size = 190552, upload-time = "2025-10-08T19:46:14.938Z" },
+ { url = "https://files.pythonhosted.org/packages/30/ee/ab4d727dd70806e5b4de96a798ae7ac6e4d42516f030ee60522474b6b332/propcache-0.4.1-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:fd138803047fb4c062b1c1dd95462f5209456bfab55c734458f15d11da288f8f", size = 200113, upload-time = "2025-10-08T19:46:16.695Z" },
+ { url = "https://files.pythonhosted.org/packages/8a/0b/38b46208e6711b016aa8966a3ac793eee0d05c7159d8342aa27fc0bc365e/propcache-0.4.1-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:8c9b3cbe4584636d72ff556d9036e0c9317fa27b3ac1f0f558e7e84d1c9c5900", size = 200778, upload-time = "2025-10-08T19:46:18.023Z" },
+ { url = "https://files.pythonhosted.org/packages/cf/81/5abec54355ed344476bee711e9f04815d4b00a311ab0535599204eecc257/propcache-0.4.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:f93243fdc5657247533273ac4f86ae106cc6445a0efacb9a1bfe982fcfefd90c", size = 193047, upload-time = "2025-10-08T19:46:19.449Z" },
+ { url = "https://files.pythonhosted.org/packages/ec/b6/1f237c04e32063cb034acd5f6ef34ef3a394f75502e72703545631ab1ef6/propcache-0.4.1-cp310-cp310-win32.whl", hash = "sha256:a0ee98db9c5f80785b266eb805016e36058ac72c51a064040f2bc43b61101cdb", size = 38093, upload-time = "2025-10-08T19:46:20.643Z" },
+ { url = "https://files.pythonhosted.org/packages/a6/67/354aac4e0603a15f76439caf0427781bcd6797f370377f75a642133bc954/propcache-0.4.1-cp310-cp310-win_amd64.whl", hash = "sha256:1cdb7988c4e5ac7f6d175a28a9aa0c94cb6f2ebe52756a3c0cda98d2809a9e37", size = 41638, upload-time = "2025-10-08T19:46:21.935Z" },
+ { url = "https://files.pythonhosted.org/packages/e0/e1/74e55b9fd1a4c209ff1a9a824bf6c8b3d1fc5a1ac3eabe23462637466785/propcache-0.4.1-cp310-cp310-win_arm64.whl", hash = "sha256:d82ad62b19645419fe79dd63b3f9253e15b30e955c0170e5cebc350c1844e581", size = 38229, upload-time = "2025-10-08T19:46:23.368Z" },
+ { url = "https://files.pythonhosted.org/packages/8c/d4/4e2c9aaf7ac2242b9358f98dccd8f90f2605402f5afeff6c578682c2c491/propcache-0.4.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:60a8fda9644b7dfd5dece8c61d8a85e271cb958075bfc4e01083c148b61a7caf", size = 80208, upload-time = "2025-10-08T19:46:24.597Z" },
+ { url = "https://files.pythonhosted.org/packages/c2/21/d7b68e911f9c8e18e4ae43bdbc1e1e9bbd971f8866eb81608947b6f585ff/propcache-0.4.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c30b53e7e6bda1d547cabb47c825f3843a0a1a42b0496087bb58d8fedf9f41b5", size = 45777, upload-time = "2025-10-08T19:46:25.733Z" },
+ { url = "https://files.pythonhosted.org/packages/d3/1d/11605e99ac8ea9435651ee71ab4cb4bf03f0949586246476a25aadfec54a/propcache-0.4.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:6918ecbd897443087a3b7cd978d56546a812517dcaaca51b49526720571fa93e", size = 47647, upload-time = "2025-10-08T19:46:27.304Z" },
+ { url = "https://files.pythonhosted.org/packages/58/1a/3c62c127a8466c9c843bccb503d40a273e5cc69838805f322e2826509e0d/propcache-0.4.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3d902a36df4e5989763425a8ab9e98cd8ad5c52c823b34ee7ef307fd50582566", size = 214929, upload-time = "2025-10-08T19:46:28.62Z" },
+ { url = "https://files.pythonhosted.org/packages/56/b9/8fa98f850960b367c4b8fe0592e7fc341daa7a9462e925228f10a60cf74f/propcache-0.4.1-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a9695397f85973bb40427dedddf70d8dc4a44b22f1650dd4af9eedf443d45165", size = 221778, upload-time = "2025-10-08T19:46:30.358Z" },
+ { url = "https://files.pythonhosted.org/packages/46/a6/0ab4f660eb59649d14b3d3d65c439421cf2f87fe5dd68591cbe3c1e78a89/propcache-0.4.1-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:2bb07ffd7eaad486576430c89f9b215f9e4be68c4866a96e97db9e97fead85dc", size = 228144, upload-time = "2025-10-08T19:46:32.607Z" },
+ { url = "https://files.pythonhosted.org/packages/52/6a/57f43e054fb3d3a56ac9fc532bc684fc6169a26c75c353e65425b3e56eef/propcache-0.4.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fd6f30fdcf9ae2a70abd34da54f18da086160e4d7d9251f81f3da0ff84fc5a48", size = 210030, upload-time = "2025-10-08T19:46:33.969Z" },
+ { url = "https://files.pythonhosted.org/packages/40/e2/27e6feebb5f6b8408fa29f5efbb765cd54c153ac77314d27e457a3e993b7/propcache-0.4.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:fc38cba02d1acba4e2869eef1a57a43dfbd3d49a59bf90dda7444ec2be6a5570", size = 208252, upload-time = "2025-10-08T19:46:35.309Z" },
+ { url = "https://files.pythonhosted.org/packages/9e/f8/91c27b22ccda1dbc7967f921c42825564fa5336a01ecd72eb78a9f4f53c2/propcache-0.4.1-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:67fad6162281e80e882fb3ec355398cf72864a54069d060321f6cd0ade95fe85", size = 202064, upload-time = "2025-10-08T19:46:36.993Z" },
+ { url = "https://files.pythonhosted.org/packages/f2/26/7f00bd6bd1adba5aafe5f4a66390f243acab58eab24ff1a08bebb2ef9d40/propcache-0.4.1-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:f10207adf04d08bec185bae14d9606a1444715bc99180f9331c9c02093e1959e", size = 212429, upload-time = "2025-10-08T19:46:38.398Z" },
+ { url = "https://files.pythonhosted.org/packages/84/89/fd108ba7815c1117ddca79c228f3f8a15fc82a73bca8b142eb5de13b2785/propcache-0.4.1-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:e9b0d8d0845bbc4cfcdcbcdbf5086886bc8157aa963c31c777ceff7846c77757", size = 216727, upload-time = "2025-10-08T19:46:39.732Z" },
+ { url = "https://files.pythonhosted.org/packages/79/37/3ec3f7e3173e73f1d600495d8b545b53802cbf35506e5732dd8578db3724/propcache-0.4.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:981333cb2f4c1896a12f4ab92a9cc8f09ea664e9b7dbdc4eff74627af3a11c0f", size = 205097, upload-time = "2025-10-08T19:46:41.025Z" },
+ { url = "https://files.pythonhosted.org/packages/61/b0/b2631c19793f869d35f47d5a3a56fb19e9160d3c119f15ac7344fc3ccae7/propcache-0.4.1-cp311-cp311-win32.whl", hash = "sha256:f1d2f90aeec838a52f1c1a32fe9a619fefd5e411721a9117fbf82aea638fe8a1", size = 38084, upload-time = "2025-10-08T19:46:42.693Z" },
+ { url = "https://files.pythonhosted.org/packages/f4/78/6cce448e2098e9f3bfc91bb877f06aa24b6ccace872e39c53b2f707c4648/propcache-0.4.1-cp311-cp311-win_amd64.whl", hash = "sha256:364426a62660f3f699949ac8c621aad6977be7126c5807ce48c0aeb8e7333ea6", size = 41637, upload-time = "2025-10-08T19:46:43.778Z" },
+ { url = "https://files.pythonhosted.org/packages/9c/e9/754f180cccd7f51a39913782c74717c581b9cc8177ad0e949f4d51812383/propcache-0.4.1-cp311-cp311-win_arm64.whl", hash = "sha256:e53f3a38d3510c11953f3e6a33f205c6d1b001129f972805ca9b42fc308bc239", size = 38064, upload-time = "2025-10-08T19:46:44.872Z" },
+ { url = "https://files.pythonhosted.org/packages/a2/0f/f17b1b2b221d5ca28b4b876e8bb046ac40466513960646bda8e1853cdfa2/propcache-0.4.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:e153e9cd40cc8945138822807139367f256f89c6810c2634a4f6902b52d3b4e2", size = 80061, upload-time = "2025-10-08T19:46:46.075Z" },
+ { url = "https://files.pythonhosted.org/packages/76/47/8ccf75935f51448ba9a16a71b783eb7ef6b9ee60f5d14c7f8a8a79fbeed7/propcache-0.4.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:cd547953428f7abb73c5ad82cbb32109566204260d98e41e5dfdc682eb7f8403", size = 46037, upload-time = "2025-10-08T19:46:47.23Z" },
+ { url = "https://files.pythonhosted.org/packages/0a/b6/5c9a0e42df4d00bfb4a3cbbe5cf9f54260300c88a0e9af1f47ca5ce17ac0/propcache-0.4.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f048da1b4f243fc44f205dfd320933a951b8d89e0afd4c7cacc762a8b9165207", size = 47324, upload-time = "2025-10-08T19:46:48.384Z" },
+ { url = "https://files.pythonhosted.org/packages/9e/d3/6c7ee328b39a81ee877c962469f1e795f9db87f925251efeb0545e0020d0/propcache-0.4.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ec17c65562a827bba85e3872ead335f95405ea1674860d96483a02f5c698fa72", size = 225505, upload-time = "2025-10-08T19:46:50.055Z" },
+ { url = "https://files.pythonhosted.org/packages/01/5d/1c53f4563490b1d06a684742cc6076ef944bc6457df6051b7d1a877c057b/propcache-0.4.1-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:405aac25c6394ef275dee4c709be43745d36674b223ba4eb7144bf4d691b7367", size = 230242, upload-time = "2025-10-08T19:46:51.815Z" },
+ { url = "https://files.pythonhosted.org/packages/20/e1/ce4620633b0e2422207c3cb774a0ee61cac13abc6217763a7b9e2e3f4a12/propcache-0.4.1-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:0013cb6f8dde4b2a2f66903b8ba740bdfe378c943c4377a200551ceb27f379e4", size = 238474, upload-time = "2025-10-08T19:46:53.208Z" },
+ { url = "https://files.pythonhosted.org/packages/46/4b/3aae6835b8e5f44ea6a68348ad90f78134047b503765087be2f9912140ea/propcache-0.4.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:15932ab57837c3368b024473a525e25d316d8353016e7cc0e5ba9eb343fbb1cf", size = 221575, upload-time = "2025-10-08T19:46:54.511Z" },
+ { url = "https://files.pythonhosted.org/packages/6e/a5/8a5e8678bcc9d3a1a15b9a29165640d64762d424a16af543f00629c87338/propcache-0.4.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:031dce78b9dc099f4c29785d9cf5577a3faf9ebf74ecbd3c856a7b92768c3df3", size = 216736, upload-time = "2025-10-08T19:46:56.212Z" },
+ { url = "https://files.pythonhosted.org/packages/f1/63/b7b215eddeac83ca1c6b934f89d09a625aa9ee4ba158338854c87210cc36/propcache-0.4.1-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:ab08df6c9a035bee56e31af99be621526bd237bea9f32def431c656b29e41778", size = 213019, upload-time = "2025-10-08T19:46:57.595Z" },
+ { url = "https://files.pythonhosted.org/packages/57/74/f580099a58c8af587cac7ba19ee7cb418506342fbbe2d4a4401661cca886/propcache-0.4.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:4d7af63f9f93fe593afbf104c21b3b15868efb2c21d07d8732c0c4287e66b6a6", size = 220376, upload-time = "2025-10-08T19:46:59.067Z" },
+ { url = "https://files.pythonhosted.org/packages/c4/ee/542f1313aff7eaf19c2bb758c5d0560d2683dac001a1c96d0774af799843/propcache-0.4.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:cfc27c945f422e8b5071b6e93169679e4eb5bf73bbcbf1ba3ae3a83d2f78ebd9", size = 226988, upload-time = "2025-10-08T19:47:00.544Z" },
+ { url = "https://files.pythonhosted.org/packages/8f/18/9c6b015dd9c6930f6ce2229e1f02fb35298b847f2087ea2b436a5bfa7287/propcache-0.4.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:35c3277624a080cc6ec6f847cbbbb5b49affa3598c4535a0a4682a697aaa5c75", size = 215615, upload-time = "2025-10-08T19:47:01.968Z" },
+ { url = "https://files.pythonhosted.org/packages/80/9e/e7b85720b98c45a45e1fca6a177024934dc9bc5f4d5dd04207f216fc33ed/propcache-0.4.1-cp312-cp312-win32.whl", hash = "sha256:671538c2262dadb5ba6395e26c1731e1d52534bfe9ae56d0b5573ce539266aa8", size = 38066, upload-time = "2025-10-08T19:47:03.503Z" },
+ { url = "https://files.pythonhosted.org/packages/54/09/d19cff2a5aaac632ec8fc03737b223597b1e347416934c1b3a7df079784c/propcache-0.4.1-cp312-cp312-win_amd64.whl", hash = "sha256:cb2d222e72399fcf5890d1d5cc1060857b9b236adff2792ff48ca2dfd46c81db", size = 41655, upload-time = "2025-10-08T19:47:04.973Z" },
+ { url = "https://files.pythonhosted.org/packages/68/ab/6b5c191bb5de08036a8c697b265d4ca76148efb10fa162f14af14fb5f076/propcache-0.4.1-cp312-cp312-win_arm64.whl", hash = "sha256:204483131fb222bdaaeeea9f9e6c6ed0cac32731f75dfc1d4a567fc1926477c1", size = 37789, upload-time = "2025-10-08T19:47:06.077Z" },
+ { url = "https://files.pythonhosted.org/packages/bf/df/6d9c1b6ac12b003837dde8a10231a7344512186e87b36e855bef32241942/propcache-0.4.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:43eedf29202c08550aac1d14e0ee619b0430aaef78f85864c1a892294fbc28cf", size = 77750, upload-time = "2025-10-08T19:47:07.648Z" },
+ { url = "https://files.pythonhosted.org/packages/8b/e8/677a0025e8a2acf07d3418a2e7ba529c9c33caf09d3c1f25513023c1db56/propcache-0.4.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:d62cdfcfd89ccb8de04e0eda998535c406bf5e060ffd56be6c586cbcc05b3311", size = 44780, upload-time = "2025-10-08T19:47:08.851Z" },
+ { url = "https://files.pythonhosted.org/packages/89/a4/92380f7ca60f99ebae761936bc48a72a639e8a47b29050615eef757cb2a7/propcache-0.4.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:cae65ad55793da34db5f54e4029b89d3b9b9490d8abe1b4c7ab5d4b8ec7ebf74", size = 46308, upload-time = "2025-10-08T19:47:09.982Z" },
+ { url = "https://files.pythonhosted.org/packages/2d/48/c5ac64dee5262044348d1d78a5f85dd1a57464a60d30daee946699963eb3/propcache-0.4.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:333ddb9031d2704a301ee3e506dc46b1fe5f294ec198ed6435ad5b6a085facfe", size = 208182, upload-time = "2025-10-08T19:47:11.319Z" },
+ { url = "https://files.pythonhosted.org/packages/c6/0c/cd762dd011a9287389a6a3eb43aa30207bde253610cca06824aeabfe9653/propcache-0.4.1-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:fd0858c20f078a32cf55f7e81473d96dcf3b93fd2ccdb3d40fdf54b8573df3af", size = 211215, upload-time = "2025-10-08T19:47:13.146Z" },
+ { url = "https://files.pythonhosted.org/packages/30/3e/49861e90233ba36890ae0ca4c660e95df565b2cd15d4a68556ab5865974e/propcache-0.4.1-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:678ae89ebc632c5c204c794f8dab2837c5f159aeb59e6ed0539500400577298c", size = 218112, upload-time = "2025-10-08T19:47:14.913Z" },
+ { url = "https://files.pythonhosted.org/packages/f1/8b/544bc867e24e1bd48f3118cecd3b05c694e160a168478fa28770f22fd094/propcache-0.4.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d472aeb4fbf9865e0c6d622d7f4d54a4e101a89715d8904282bb5f9a2f476c3f", size = 204442, upload-time = "2025-10-08T19:47:16.277Z" },
+ { url = "https://files.pythonhosted.org/packages/50/a6/4282772fd016a76d3e5c0df58380a5ea64900afd836cec2c2f662d1b9bb3/propcache-0.4.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:4d3df5fa7e36b3225954fba85589da77a0fe6a53e3976de39caf04a0db4c36f1", size = 199398, upload-time = "2025-10-08T19:47:17.962Z" },
+ { url = "https://files.pythonhosted.org/packages/3e/ec/d8a7cd406ee1ddb705db2139f8a10a8a427100347bd698e7014351c7af09/propcache-0.4.1-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:ee17f18d2498f2673e432faaa71698032b0127ebf23ae5974eeaf806c279df24", size = 196920, upload-time = "2025-10-08T19:47:19.355Z" },
+ { url = "https://files.pythonhosted.org/packages/f6/6c/f38ab64af3764f431e359f8baf9e0a21013e24329e8b85d2da32e8ed07ca/propcache-0.4.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:580e97762b950f993ae618e167e7be9256b8353c2dcd8b99ec100eb50f5286aa", size = 203748, upload-time = "2025-10-08T19:47:21.338Z" },
+ { url = "https://files.pythonhosted.org/packages/d6/e3/fa846bd70f6534d647886621388f0a265254d30e3ce47e5c8e6e27dbf153/propcache-0.4.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:501d20b891688eb8e7aa903021f0b72d5a55db40ffaab27edefd1027caaafa61", size = 205877, upload-time = "2025-10-08T19:47:23.059Z" },
+ { url = "https://files.pythonhosted.org/packages/e2/39/8163fc6f3133fea7b5f2827e8eba2029a0277ab2c5beee6c1db7b10fc23d/propcache-0.4.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9a0bd56e5b100aef69bd8562b74b46254e7c8812918d3baa700c8a8009b0af66", size = 199437, upload-time = "2025-10-08T19:47:24.445Z" },
+ { url = "https://files.pythonhosted.org/packages/93/89/caa9089970ca49c7c01662bd0eeedfe85494e863e8043565aeb6472ce8fe/propcache-0.4.1-cp313-cp313-win32.whl", hash = "sha256:bcc9aaa5d80322bc2fb24bb7accb4a30f81e90ab8d6ba187aec0744bc302ad81", size = 37586, upload-time = "2025-10-08T19:47:25.736Z" },
+ { url = "https://files.pythonhosted.org/packages/f5/ab/f76ec3c3627c883215b5c8080debb4394ef5a7a29be811f786415fc1e6fd/propcache-0.4.1-cp313-cp313-win_amd64.whl", hash = "sha256:381914df18634f5494334d201e98245c0596067504b9372d8cf93f4bb23e025e", size = 40790, upload-time = "2025-10-08T19:47:26.847Z" },
+ { url = "https://files.pythonhosted.org/packages/59/1b/e71ae98235f8e2ba5004d8cb19765a74877abf189bc53fc0c80d799e56c3/propcache-0.4.1-cp313-cp313-win_arm64.whl", hash = "sha256:8873eb4460fd55333ea49b7d189749ecf6e55bf85080f11b1c4530ed3034cba1", size = 37158, upload-time = "2025-10-08T19:47:27.961Z" },
+ { url = "https://files.pythonhosted.org/packages/83/ce/a31bbdfc24ee0dcbba458c8175ed26089cf109a55bbe7b7640ed2470cfe9/propcache-0.4.1-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:92d1935ee1f8d7442da9c0c4fa7ac20d07e94064184811b685f5c4fada64553b", size = 81451, upload-time = "2025-10-08T19:47:29.445Z" },
+ { url = "https://files.pythonhosted.org/packages/25/9c/442a45a470a68456e710d96cacd3573ef26a1d0a60067e6a7d5e655621ed/propcache-0.4.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:473c61b39e1460d386479b9b2f337da492042447c9b685f28be4f74d3529e566", size = 46374, upload-time = "2025-10-08T19:47:30.579Z" },
+ { url = "https://files.pythonhosted.org/packages/f4/bf/b1d5e21dbc3b2e889ea4327044fb16312a736d97640fb8b6aa3f9c7b3b65/propcache-0.4.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:c0ef0aaafc66fbd87842a3fe3902fd889825646bc21149eafe47be6072725835", size = 48396, upload-time = "2025-10-08T19:47:31.79Z" },
+ { url = "https://files.pythonhosted.org/packages/f4/04/5b4c54a103d480e978d3c8a76073502b18db0c4bc17ab91b3cb5092ad949/propcache-0.4.1-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f95393b4d66bfae908c3ca8d169d5f79cd65636ae15b5e7a4f6e67af675adb0e", size = 275950, upload-time = "2025-10-08T19:47:33.481Z" },
+ { url = "https://files.pythonhosted.org/packages/b4/c1/86f846827fb969c4b78b0af79bba1d1ea2156492e1b83dea8b8a6ae27395/propcache-0.4.1-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c07fda85708bc48578467e85099645167a955ba093be0a2dcba962195676e859", size = 273856, upload-time = "2025-10-08T19:47:34.906Z" },
+ { url = "https://files.pythonhosted.org/packages/36/1d/fc272a63c8d3bbad6878c336c7a7dea15e8f2d23a544bda43205dfa83ada/propcache-0.4.1-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:af223b406d6d000830c6f65f1e6431783fc3f713ba3e6cc8c024d5ee96170a4b", size = 280420, upload-time = "2025-10-08T19:47:36.338Z" },
+ { url = "https://files.pythonhosted.org/packages/07/0c/01f2219d39f7e53d52e5173bcb09c976609ba30209912a0680adfb8c593a/propcache-0.4.1-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a78372c932c90ee474559c5ddfffd718238e8673c340dc21fe45c5b8b54559a0", size = 263254, upload-time = "2025-10-08T19:47:37.692Z" },
+ { url = "https://files.pythonhosted.org/packages/2d/18/cd28081658ce597898f0c4d174d4d0f3c5b6d4dc27ffafeef835c95eb359/propcache-0.4.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:564d9f0d4d9509e1a870c920a89b2fec951b44bf5ba7d537a9e7c1ccec2c18af", size = 261205, upload-time = "2025-10-08T19:47:39.659Z" },
+ { url = "https://files.pythonhosted.org/packages/7a/71/1f9e22eb8b8316701c2a19fa1f388c8a3185082607da8e406a803c9b954e/propcache-0.4.1-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:17612831fda0138059cc5546f4d12a2aacfb9e47068c06af35c400ba58ba7393", size = 247873, upload-time = "2025-10-08T19:47:41.084Z" },
+ { url = "https://files.pythonhosted.org/packages/4a/65/3d4b61f36af2b4eddba9def857959f1016a51066b4f1ce348e0cf7881f58/propcache-0.4.1-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:41a89040cb10bd345b3c1a873b2bf36413d48da1def52f268a055f7398514874", size = 262739, upload-time = "2025-10-08T19:47:42.51Z" },
+ { url = "https://files.pythonhosted.org/packages/2a/42/26746ab087faa77c1c68079b228810436ccd9a5ce9ac85e2b7307195fd06/propcache-0.4.1-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:e35b88984e7fa64aacecea39236cee32dd9bd8c55f57ba8a75cf2399553f9bd7", size = 263514, upload-time = "2025-10-08T19:47:43.927Z" },
+ { url = "https://files.pythonhosted.org/packages/94/13/630690fe201f5502d2403dd3cfd451ed8858fe3c738ee88d095ad2ff407b/propcache-0.4.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:6f8b465489f927b0df505cbe26ffbeed4d6d8a2bbc61ce90eb074ff129ef0ab1", size = 257781, upload-time = "2025-10-08T19:47:45.448Z" },
+ { url = "https://files.pythonhosted.org/packages/92/f7/1d4ec5841505f423469efbfc381d64b7b467438cd5a4bbcbb063f3b73d27/propcache-0.4.1-cp313-cp313t-win32.whl", hash = "sha256:2ad890caa1d928c7c2965b48f3a3815c853180831d0e5503d35cf00c472f4717", size = 41396, upload-time = "2025-10-08T19:47:47.202Z" },
+ { url = "https://files.pythonhosted.org/packages/48/f0/615c30622316496d2cbbc29f5985f7777d3ada70f23370608c1d3e081c1f/propcache-0.4.1-cp313-cp313t-win_amd64.whl", hash = "sha256:f7ee0e597f495cf415bcbd3da3caa3bd7e816b74d0d52b8145954c5e6fd3ff37", size = 44897, upload-time = "2025-10-08T19:47:48.336Z" },
+ { url = "https://files.pythonhosted.org/packages/fd/ca/6002e46eccbe0e33dcd4069ef32f7f1c9e243736e07adca37ae8c4830ec3/propcache-0.4.1-cp313-cp313t-win_arm64.whl", hash = "sha256:929d7cbe1f01bb7baffb33dc14eb5691c95831450a26354cd210a8155170c93a", size = 39789, upload-time = "2025-10-08T19:47:49.876Z" },
+ { url = "https://files.pythonhosted.org/packages/8e/5c/bca52d654a896f831b8256683457ceddd490ec18d9ec50e97dfd8fc726a8/propcache-0.4.1-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:3f7124c9d820ba5548d431afb4632301acf965db49e666aa21c305cbe8c6de12", size = 78152, upload-time = "2025-10-08T19:47:51.051Z" },
+ { url = "https://files.pythonhosted.org/packages/65/9b/03b04e7d82a5f54fb16113d839f5ea1ede58a61e90edf515f6577c66fa8f/propcache-0.4.1-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:c0d4b719b7da33599dfe3b22d3db1ef789210a0597bc650b7cee9c77c2be8c5c", size = 44869, upload-time = "2025-10-08T19:47:52.594Z" },
+ { url = "https://files.pythonhosted.org/packages/b2/fa/89a8ef0468d5833a23fff277b143d0573897cf75bd56670a6d28126c7d68/propcache-0.4.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:9f302f4783709a78240ebc311b793f123328716a60911d667e0c036bc5dcbded", size = 46596, upload-time = "2025-10-08T19:47:54.073Z" },
+ { url = "https://files.pythonhosted.org/packages/86/bd/47816020d337f4a746edc42fe8d53669965138f39ee117414c7d7a340cfe/propcache-0.4.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c80ee5802e3fb9ea37938e7eecc307fb984837091d5fd262bb37238b1ae97641", size = 206981, upload-time = "2025-10-08T19:47:55.715Z" },
+ { url = "https://files.pythonhosted.org/packages/df/f6/c5fa1357cc9748510ee55f37173eb31bfde6d94e98ccd9e6f033f2fc06e1/propcache-0.4.1-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ed5a841e8bb29a55fb8159ed526b26adc5bdd7e8bd7bf793ce647cb08656cdf4", size = 211490, upload-time = "2025-10-08T19:47:57.499Z" },
+ { url = "https://files.pythonhosted.org/packages/80/1e/e5889652a7c4a3846683401a48f0f2e5083ce0ec1a8a5221d8058fbd1adf/propcache-0.4.1-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:55c72fd6ea2da4c318e74ffdf93c4fe4e926051133657459131a95c846d16d44", size = 215371, upload-time = "2025-10-08T19:47:59.317Z" },
+ { url = "https://files.pythonhosted.org/packages/b2/f2/889ad4b2408f72fe1a4f6a19491177b30ea7bf1a0fd5f17050ca08cfc882/propcache-0.4.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8326e144341460402713f91df60ade3c999d601e7eb5ff8f6f7862d54de0610d", size = 201424, upload-time = "2025-10-08T19:48:00.67Z" },
+ { url = "https://files.pythonhosted.org/packages/27/73/033d63069b57b0812c8bd19f311faebeceb6ba31b8f32b73432d12a0b826/propcache-0.4.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:060b16ae65bc098da7f6d25bf359f1f31f688384858204fe5d652979e0015e5b", size = 197566, upload-time = "2025-10-08T19:48:02.604Z" },
+ { url = "https://files.pythonhosted.org/packages/dc/89/ce24f3dc182630b4e07aa6d15f0ff4b14ed4b9955fae95a0b54c58d66c05/propcache-0.4.1-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:89eb3fa9524f7bec9de6e83cf3faed9d79bffa560672c118a96a171a6f55831e", size = 193130, upload-time = "2025-10-08T19:48:04.499Z" },
+ { url = "https://files.pythonhosted.org/packages/a9/24/ef0d5fd1a811fb5c609278d0209c9f10c35f20581fcc16f818da959fc5b4/propcache-0.4.1-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:dee69d7015dc235f526fe80a9c90d65eb0039103fe565776250881731f06349f", size = 202625, upload-time = "2025-10-08T19:48:06.213Z" },
+ { url = "https://files.pythonhosted.org/packages/f5/02/98ec20ff5546f68d673df2f7a69e8c0d076b5abd05ca882dc7ee3a83653d/propcache-0.4.1-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:5558992a00dfd54ccbc64a32726a3357ec93825a418a401f5cc67df0ac5d9e49", size = 204209, upload-time = "2025-10-08T19:48:08.432Z" },
+ { url = "https://files.pythonhosted.org/packages/a0/87/492694f76759b15f0467a2a93ab68d32859672b646aa8a04ce4864e7932d/propcache-0.4.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:c9b822a577f560fbd9554812526831712c1436d2c046cedee4c3796d3543b144", size = 197797, upload-time = "2025-10-08T19:48:09.968Z" },
+ { url = "https://files.pythonhosted.org/packages/ee/36/66367de3575db1d2d3f3d177432bd14ee577a39d3f5d1b3d5df8afe3b6e2/propcache-0.4.1-cp314-cp314-win32.whl", hash = "sha256:ab4c29b49d560fe48b696cdcb127dd36e0bc2472548f3bf56cc5cb3da2b2984f", size = 38140, upload-time = "2025-10-08T19:48:11.232Z" },
+ { url = "https://files.pythonhosted.org/packages/0c/2a/a758b47de253636e1b8aef181c0b4f4f204bf0dd964914fb2af90a95b49b/propcache-0.4.1-cp314-cp314-win_amd64.whl", hash = "sha256:5a103c3eb905fcea0ab98be99c3a9a5ab2de60228aa5aceedc614c0281cf6153", size = 41257, upload-time = "2025-10-08T19:48:12.707Z" },
+ { url = "https://files.pythonhosted.org/packages/34/5e/63bd5896c3fec12edcbd6f12508d4890d23c265df28c74b175e1ef9f4f3b/propcache-0.4.1-cp314-cp314-win_arm64.whl", hash = "sha256:74c1fb26515153e482e00177a1ad654721bf9207da8a494a0c05e797ad27b992", size = 38097, upload-time = "2025-10-08T19:48:13.923Z" },
+ { url = "https://files.pythonhosted.org/packages/99/85/9ff785d787ccf9bbb3f3106f79884a130951436f58392000231b4c737c80/propcache-0.4.1-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:824e908bce90fb2743bd6b59db36eb4f45cd350a39637c9f73b1c1ea66f5b75f", size = 81455, upload-time = "2025-10-08T19:48:15.16Z" },
+ { url = "https://files.pythonhosted.org/packages/90/85/2431c10c8e7ddb1445c1f7c4b54d886e8ad20e3c6307e7218f05922cad67/propcache-0.4.1-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:c2b5e7db5328427c57c8e8831abda175421b709672f6cfc3d630c3b7e2146393", size = 46372, upload-time = "2025-10-08T19:48:16.424Z" },
+ { url = "https://files.pythonhosted.org/packages/01/20/b0972d902472da9bcb683fa595099911f4d2e86e5683bcc45de60dd05dc3/propcache-0.4.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:6f6ff873ed40292cd4969ef5310179afd5db59fdf055897e282485043fc80ad0", size = 48411, upload-time = "2025-10-08T19:48:17.577Z" },
+ { url = "https://files.pythonhosted.org/packages/e2/e3/7dc89f4f21e8f99bad3d5ddb3a3389afcf9da4ac69e3deb2dcdc96e74169/propcache-0.4.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:49a2dc67c154db2c1463013594c458881a069fcf98940e61a0569016a583020a", size = 275712, upload-time = "2025-10-08T19:48:18.901Z" },
+ { url = "https://files.pythonhosted.org/packages/20/67/89800c8352489b21a8047c773067644e3897f02ecbbd610f4d46b7f08612/propcache-0.4.1-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:005f08e6a0529984491e37d8dbc3dd86f84bd78a8ceb5fa9a021f4c48d4984be", size = 273557, upload-time = "2025-10-08T19:48:20.762Z" },
+ { url = "https://files.pythonhosted.org/packages/e2/a1/b52b055c766a54ce6d9c16d9aca0cad8059acd9637cdf8aa0222f4a026ef/propcache-0.4.1-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5c3310452e0d31390da9035c348633b43d7e7feb2e37be252be6da45abd1abcc", size = 280015, upload-time = "2025-10-08T19:48:22.592Z" },
+ { url = "https://files.pythonhosted.org/packages/48/c8/33cee30bd890672c63743049f3c9e4be087e6780906bfc3ec58528be59c1/propcache-0.4.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4c3c70630930447f9ef1caac7728c8ad1c56bc5015338b20fed0d08ea2480b3a", size = 262880, upload-time = "2025-10-08T19:48:23.947Z" },
+ { url = "https://files.pythonhosted.org/packages/0c/b1/8f08a143b204b418285c88b83d00edbd61afbc2c6415ffafc8905da7038b/propcache-0.4.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8e57061305815dfc910a3634dcf584f08168a8836e6999983569f51a8544cd89", size = 260938, upload-time = "2025-10-08T19:48:25.656Z" },
+ { url = "https://files.pythonhosted.org/packages/cf/12/96e4664c82ca2f31e1c8dff86afb867348979eb78d3cb8546a680287a1e9/propcache-0.4.1-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:521a463429ef54143092c11a77e04056dd00636f72e8c45b70aaa3140d639726", size = 247641, upload-time = "2025-10-08T19:48:27.207Z" },
+ { url = "https://files.pythonhosted.org/packages/18/ed/e7a9cfca28133386ba52278136d42209d3125db08d0a6395f0cba0c0285c/propcache-0.4.1-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:120c964da3fdc75e3731aa392527136d4ad35868cc556fd09bb6d09172d9a367", size = 262510, upload-time = "2025-10-08T19:48:28.65Z" },
+ { url = "https://files.pythonhosted.org/packages/f5/76/16d8bf65e8845dd62b4e2b57444ab81f07f40caa5652b8969b87ddcf2ef6/propcache-0.4.1-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:d8f353eb14ee3441ee844ade4277d560cdd68288838673273b978e3d6d2c8f36", size = 263161, upload-time = "2025-10-08T19:48:30.133Z" },
+ { url = "https://files.pythonhosted.org/packages/e7/70/c99e9edb5d91d5ad8a49fa3c1e8285ba64f1476782fed10ab251ff413ba1/propcache-0.4.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:ab2943be7c652f09638800905ee1bab2c544e537edb57d527997a24c13dc1455", size = 257393, upload-time = "2025-10-08T19:48:31.567Z" },
+ { url = "https://files.pythonhosted.org/packages/08/02/87b25304249a35c0915d236575bc3574a323f60b47939a2262b77632a3ee/propcache-0.4.1-cp314-cp314t-win32.whl", hash = "sha256:05674a162469f31358c30bcaa8883cb7829fa3110bf9c0991fe27d7896c42d85", size = 42546, upload-time = "2025-10-08T19:48:32.872Z" },
+ { url = "https://files.pythonhosted.org/packages/cb/ef/3c6ecf8b317aa982f309835e8f96987466123c6e596646d4e6a1dfcd080f/propcache-0.4.1-cp314-cp314t-win_amd64.whl", hash = "sha256:990f6b3e2a27d683cb7602ed6c86f15ee6b43b1194736f9baaeb93d0016633b1", size = 46259, upload-time = "2025-10-08T19:48:34.226Z" },
+ { url = "https://files.pythonhosted.org/packages/c4/2d/346e946d4951f37eca1e4f55be0f0174c52cd70720f84029b02f296f4a38/propcache-0.4.1-cp314-cp314t-win_arm64.whl", hash = "sha256:ecef2343af4cc68e05131e45024ba34f6095821988a9d0a02aa7c73fcc448aa9", size = 40428, upload-time = "2025-10-08T19:48:35.441Z" },
+ { url = "https://files.pythonhosted.org/packages/9b/01/0ebaec9003f5d619a7475165961f8e3083cf8644d704b60395df3601632d/propcache-0.4.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:3d233076ccf9e450c8b3bc6720af226b898ef5d051a2d145f7d765e6e9f9bcff", size = 80277, upload-time = "2025-10-08T19:48:36.647Z" },
+ { url = "https://files.pythonhosted.org/packages/34/58/04af97ac586b4ef6b9026c3fd36ee7798b737a832f5d3440a4280dcebd3a/propcache-0.4.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:357f5bb5c377a82e105e44bd3d52ba22b616f7b9773714bff93573988ef0a5fb", size = 45865, upload-time = "2025-10-08T19:48:37.859Z" },
+ { url = "https://files.pythonhosted.org/packages/7c/19/b65d98ae21384518b291d9939e24a8aeac4fdb5101b732576f8f7540e834/propcache-0.4.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:cbc3b6dfc728105b2a57c06791eb07a94229202ea75c59db644d7d496b698cac", size = 47636, upload-time = "2025-10-08T19:48:39.038Z" },
+ { url = "https://files.pythonhosted.org/packages/b3/0f/317048c6d91c356c7154dca5af019e6effeb7ee15fa6a6db327cc19e12b4/propcache-0.4.1-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:182b51b421f0501952d938dc0b0eb45246a5b5153c50d42b495ad5fb7517c888", size = 201126, upload-time = "2025-10-08T19:48:40.774Z" },
+ { url = "https://files.pythonhosted.org/packages/71/69/0b2a7a5a6ee83292b4b997dbd80549d8ce7d40b6397c1646c0d9495f5a85/propcache-0.4.1-cp39-cp39-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:4b536b39c5199b96fc6245eb5fb796c497381d3942f169e44e8e392b29c9ebcc", size = 209837, upload-time = "2025-10-08T19:48:42.167Z" },
+ { url = "https://files.pythonhosted.org/packages/a5/92/c699ac495a6698df6e497fc2de27af4b6ace10d8e76528357ce153722e45/propcache-0.4.1-cp39-cp39-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:db65d2af507bbfbdcedb254a11149f894169d90488dd3e7190f7cdcb2d6cd57a", size = 215578, upload-time = "2025-10-08T19:48:43.56Z" },
+ { url = "https://files.pythonhosted.org/packages/b3/ee/14de81c5eb02c0ee4f500b4e39c4e1bd0677c06e72379e6ab18923c773fc/propcache-0.4.1-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fd2dbc472da1f772a4dae4fa24be938a6c544671a912e30529984dd80400cd88", size = 197187, upload-time = "2025-10-08T19:48:45.309Z" },
+ { url = "https://files.pythonhosted.org/packages/1d/94/48dce9aaa6d8dd5a0859bad75158ec522546d4ac23f8e2f05fac469477dd/propcache-0.4.1-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:daede9cd44e0f8bdd9e6cc9a607fc81feb80fae7a5fc6cecaff0e0bb32e42d00", size = 193478, upload-time = "2025-10-08T19:48:47.743Z" },
+ { url = "https://files.pythonhosted.org/packages/60/b5/0516b563e801e1ace212afde869a0596a0d7115eec0b12d296d75633fb29/propcache-0.4.1-cp39-cp39-musllinux_1_2_armv7l.whl", hash = "sha256:71b749281b816793678ae7f3d0d84bd36e694953822eaad408d682efc5ca18e0", size = 190650, upload-time = "2025-10-08T19:48:49.373Z" },
+ { url = "https://files.pythonhosted.org/packages/24/89/e0f7d4a5978cd56f8cd67735f74052f257dc471ec901694e430f0d1572fe/propcache-0.4.1-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:0002004213ee1f36cfb3f9a42b5066100c44276b9b72b4e1504cddd3d692e86e", size = 200251, upload-time = "2025-10-08T19:48:51.4Z" },
+ { url = "https://files.pythonhosted.org/packages/06/7d/a1fac863d473876ed4406c914f2e14aa82d2f10dd207c9e16fc383cc5a24/propcache-0.4.1-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:fe49d0a85038f36ba9e3ffafa1103e61170b28e95b16622e11be0a0ea07c6781", size = 200919, upload-time = "2025-10-08T19:48:53.227Z" },
+ { url = "https://files.pythonhosted.org/packages/c3/4e/f86a256ff24944cf5743e4e6c6994e3526f6acfcfb55e21694c2424f758c/propcache-0.4.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:99d43339c83aaf4d32bda60928231848eee470c6bda8d02599cc4cebe872d183", size = 193211, upload-time = "2025-10-08T19:48:55.027Z" },
+ { url = "https://files.pythonhosted.org/packages/6e/3f/3fbad5f4356b068f1b047d300a6ff2c66614d7030f078cd50be3fec04228/propcache-0.4.1-cp39-cp39-win32.whl", hash = "sha256:a129e76735bc792794d5177069691c3217898b9f5cee2b2661471e52ffe13f19", size = 38314, upload-time = "2025-10-08T19:48:56.792Z" },
+ { url = "https://files.pythonhosted.org/packages/a4/45/d78d136c3a3d215677abb886785aae744da2c3005bcb99e58640c56529b1/propcache-0.4.1-cp39-cp39-win_amd64.whl", hash = "sha256:948dab269721ae9a87fd16c514a0a2c2a1bdb23a9a61b969b0f9d9ee2968546f", size = 41912, upload-time = "2025-10-08T19:48:57.995Z" },
+ { url = "https://files.pythonhosted.org/packages/fc/2a/b0632941f25139f4e58450b307242951f7c2717a5704977c6d5323a800af/propcache-0.4.1-cp39-cp39-win_arm64.whl", hash = "sha256:5fd37c406dd6dc85aa743e214cef35dc54bbdd1419baac4f6ae5e5b1a2976938", size = 38450, upload-time = "2025-10-08T19:48:59.349Z" },
+ { url = "https://files.pythonhosted.org/packages/5b/5a/bc7b4a4ef808fa59a816c17b20c4bef6884daebbdf627ff2a161da67da19/propcache-0.4.1-py3-none-any.whl", hash = "sha256:af2a6052aeb6cf17d3e46ee169099044fd8224cbaf75c76a2ef596e8163e2237", size = 13305, upload-time = "2025-10-08T19:49:00.792Z" },
+]
+
+[[package]]
+name = "propcache"
+version = "0.5.2"
+source = { registry = "https://pypi.org/simple" }
+resolution-markers = [
+ "python_full_version >= '3.10'",
+]
+sdist = { url = "https://files.pythonhosted.org/packages/ec/44/c87281c333769159c50594f22610f77398a47ccbfbbf23074e744e86f87c/propcache-0.5.2.tar.gz", hash = "sha256:01c4fc7480cd0598bb4b57022df55b9ca296da7fc5a8760bd8451a7e63a7d427", size = 50208, upload-time = "2026-05-08T21:02:12.199Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/5b/56/030b7b4719d53085722893e0009dffb9236aa10bca1b12121bdc5626ef16/propcache-0.5.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:d5a81be28596d6559f6131ef33e10200de6e17643b3c74ce03f9eb103be6ae8b", size = 93417, upload-time = "2026-05-08T20:59:15.597Z" },
+ { url = "https://files.pythonhosted.org/packages/1a/55/1140a8e067b8ec093a18a4ae7bb0045d9db65da38a08618ddc5e2f1994aa/propcache-0.5.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:29cbaac5ea0212663e6845e04b5e188d5a6ae6dd919810ac835bf1d3b42c3f4c", size = 53847, upload-time = "2026-05-08T20:59:17.096Z" },
+ { url = "https://files.pythonhosted.org/packages/20/42/0e7443c90310498561addf346e7d57fe3c6ba1914e1ba938b5464c7bbfd2/propcache-0.5.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:6bf3be92233808fcd338eba0fb4d0b59ec5772af4f4ecfcec450d1bfc0f8b5eb", size = 53512, upload-time = "2026-05-08T20:59:18.64Z" },
+ { url = "https://files.pythonhosted.org/packages/b7/db/cf51a71bab2009517d1a7f0ee07657e3bd446c4d69f67e6966cf17bcf956/propcache-0.5.2-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2f8ea531c794b9d6274acd4e8d2c2ebcac590a4361d27482edd3010b79f1325e", size = 58068, upload-time = "2026-05-08T20:59:20.683Z" },
+ { url = "https://files.pythonhosted.org/packages/b7/43/39b6bdee9699fa1e1641c519feeb64a67e2a9f93bb465c70776b37a7333f/propcache-0.5.2-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:decfca4c79dd53ebab484b00cc4b6717d8c369f86e74aa4ca395a64ac651495e", size = 61020, upload-time = "2026-05-08T20:59:22.112Z" },
+ { url = "https://files.pythonhosted.org/packages/26/0b/843726fbb0a29a8c5684fdb25971823638399f31e52e9d1f06a02dc9aa6b/propcache-0.5.2-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:4621064bbf28fa77ff64dd5d94367c04684c67d3a5bf1dff25f0cd0d98a38f3b", size = 62732, upload-time = "2026-05-08T20:59:23.805Z" },
+ { url = "https://files.pythonhosted.org/packages/39/6e/899fed76dc1942b8a64193a4f059d7f1a2c7ef65085e8a9366ed8ec0d199/propcache-0.5.2-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b96db7141a592cbc968daf1feea83a118e6ab378af4abbc72b248c895414c22d", size = 60140, upload-time = "2026-05-08T20:59:25.389Z" },
+ { url = "https://files.pythonhosted.org/packages/ab/09/3da4be9b5b879219ad234aa535b3dd4a080ed1ad48d3a73ca07a9e798f22/propcache-0.5.2-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:1ca071adabaab6e9219924bbe00af821f1ee7de113a9eca1cdc292de3d120f4d", size = 60400, upload-time = "2026-05-08T20:59:27.238Z" },
+ { url = "https://files.pythonhosted.org/packages/60/2f/09b72b874a9aa0044faf52a69807a6ed618e267ceaa9ec4a63195fa5b504/propcache-0.5.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:e4294d04a94dcab1b3bccd8b66d962dcad411a1d19414b2a41d1445f1de32ad0", size = 58155, upload-time = "2026-05-08T20:59:28.48Z" },
+ { url = "https://files.pythonhosted.org/packages/8a/37/97489848c54c95578045473954f10956d619ce6a09e7ac137b71cdcb698b/propcache-0.5.2-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:a0e399a2eccb91ed18721f86aa85757727400b6865c89e88934781deb9c8498b", size = 57037, upload-time = "2026-05-08T20:59:30.146Z" },
+ { url = "https://files.pythonhosted.org/packages/22/db/6c695285ccfc49012743ee9c98212b8c5dd0aed7b63cfd816d4a0f7a1601/propcache-0.5.2-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:823581fd5cb08b12a48bfa11fe962a7916766b6170c17b028fbdf762b85eb9bf", size = 61103, upload-time = "2026-05-08T20:59:31.626Z" },
+ { url = "https://files.pythonhosted.org/packages/98/a9/1e500401ca593b0bdb6bf75a70bc2d723835fd53360edff6af70692c7546/propcache-0.5.2-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:949c91d1a990cf3b2e8188dfcfb25005e0b834a06c63fa4ef9f360878ce21ecf", size = 60394, upload-time = "2026-05-08T20:59:32.829Z" },
+ { url = "https://files.pythonhosted.org/packages/1f/87/f638b6e375eae0f30a1a2325d8b34fd85fdc785bb9960cf805f3bf1ec69a/propcache-0.5.2-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:cc1177027eda740fdb152706bd215a3f124e3eea15afc39f2cb9fe351b50619e", size = 63084, upload-time = "2026-05-08T20:59:35.964Z" },
+ { url = "https://files.pythonhosted.org/packages/f6/18/884573f5d97b6d9eba68de759a82c901b7e39d7904d30f7b8d58d42d2a12/propcache-0.5.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:b05d643f944a8c3c4bd86d65ffd87bf3264b617f87791940302bc474d2ff5274", size = 60999, upload-time = "2026-05-08T20:59:38.481Z" },
+ { url = "https://files.pythonhosted.org/packages/8f/1a/c3915eb059ceec9e758a56e4cfd955292bc0f201be2176a46b76d94b303a/propcache-0.5.2-cp310-cp310-win32.whl", hash = "sha256:8114f28879e0904748e831c3a7774261bd9e75f49be089f389a76f959dcd13fe", size = 39036, upload-time = "2026-05-08T20:59:40.323Z" },
+ { url = "https://files.pythonhosted.org/packages/5b/02/1dfd5607501a602d19c1c449d2d193b7d1c611f9246b4059026a1189a80e/propcache-0.5.2-cp310-cp310-win_amd64.whl", hash = "sha256:5fcb98e7598b1ee0addab320d90f65b530297a867dbfe9de52ea838077e16e3d", size = 42190, upload-time = "2026-05-08T20:59:42.232Z" },
+ { url = "https://files.pythonhosted.org/packages/57/93/f71588ad08b3e6f4b555b5ef215808a3c02b042d0151ad82fa6f15be677a/propcache-0.5.2-cp310-cp310-win_arm64.whl", hash = "sha256:04dc2390d9edbbaef7461f33322555976ffddf0b650a038649d026358714e6c5", size = 38545, upload-time = "2026-05-08T20:59:44.087Z" },
+ { url = "https://files.pythonhosted.org/packages/e7/f1/8a8cc1c2c7e7934ab77e0163414f736fadbc0f5e8dd9673b952355ac175b/propcache-0.5.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:74b70780220e2dd89175ca24b81b68b67c83db499ae611e7f2313cb329801c78", size = 90744, upload-time = "2026-05-08T20:59:45.799Z" },
+ { url = "https://files.pythonhosted.org/packages/c2/f4/651b1225e976bd1a2ba5cfba0c29d096581c2636b437e3a9a7ab6276270a/propcache-0.5.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a4840ab0ae0216d952f4b53dc6d0b992bfc2bedbfe360bdd9b548bc184c08959", size = 52033, upload-time = "2026-05-08T20:59:47.408Z" },
+ { url = "https://files.pythonhosted.org/packages/15/a8/8ede85d6aa1f79fc7dc2f8fd2c8d65920b8272c3892903c8a1affde48cfb/propcache-0.5.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:c6844ba6364fb12f403928a82cfd295ab103a2b315c77c747b2dbe4a41894ea7", size = 52754, upload-time = "2026-05-08T20:59:49.202Z" },
+ { url = "https://files.pythonhosted.org/packages/7d/fe/b3551b41bbc2f5b5bb088fc6920567cd43101253e68fbaa261339eb96fe1/propcache-0.5.2-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2293949b855ce597f2826452d17c2d545fb5622379c4ea6fdf525e9b8e8a2511", size = 57573, upload-time = "2026-05-08T20:59:50.778Z" },
+ { url = "https://files.pythonhosted.org/packages/83/27/ab851ebd1b7172e3e161f5f8d39e315d54a91bea246f01f4d872d3376aef/propcache-0.5.2-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:0fd59b5af35f74da48d905dcbad55449ba13be91823cb05a9bd590bbf5b61660", size = 60645, upload-time = "2026-05-08T20:59:52.227Z" },
+ { url = "https://files.pythonhosted.org/packages/95/7d/466b3d18022e9897cbda9c735c493c5bd747d7a4c6f5ea1480b4cec434b6/propcache-0.5.2-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:29f9309a2e42b0d273be006fdb4be2d6c39a47f6f57d8fb1cf9f81481df81b66", size = 61563, upload-time = "2026-05-08T20:59:53.866Z" },
+ { url = "https://files.pythonhosted.org/packages/27/1b/16ab7f2cf2041da2f60d156ba64c2484eadf9168075b4ff43c3ef60045af/propcache-0.5.2-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5aaa2b923c1944ac8febd6609cb373540a5563e7cbcb0fd770f75dace2eb817b", size = 58888, upload-time = "2026-05-08T20:59:55.457Z" },
+ { url = "https://files.pythonhosted.org/packages/0a/67/bb777ffd907633563bf35fd859c4ce97b0512c32f4633cf5d1eb7c33512b/propcache-0.5.2-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:66ea454f095ddf5b6b14f56c064c0941c4788be11e18d2464cf643bf7203ff67", size = 59253, upload-time = "2026-05-08T20:59:57.075Z" },
+ { url = "https://files.pythonhosted.org/packages/b9/42/64f8d90b73fd9cdc1499b48057ff6d9cd2a98a25734c9bb62ecf07e87061/propcache-0.5.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:95f1e3f4760d404b13c9976c0229b2b49a3c8e2c62a9ce92efdd2b11ada75e3f", size = 57558, upload-time = "2026-05-08T20:59:58.602Z" },
+ { url = "https://files.pythonhosted.org/packages/eb/02/dba5bc03c9041f2092ea55a449caf5dfe68352c6654511b29ba0654ddb69/propcache-0.5.2-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:85341b12b9d55bad0bded24cac341bb34289469e03a11f3f583ea1cc1db0326c", size = 55007, upload-time = "2026-05-08T20:59:59.837Z" },
+ { url = "https://files.pythonhosted.org/packages/14/c0/43f649c7aa2a77a3b100d84e9dea3a483120ecb608bfe36ce49eaff517fe/propcache-0.5.2-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:26a4dca084132874e639895c3135dfad5eb20bae209f62d1aeb31b03e601c3c0", size = 60355, upload-time = "2026-05-08T21:00:01.144Z" },
+ { url = "https://files.pythonhosted.org/packages/83/c0/435dafd27f1cb4a495381dae60e25883ccfe4020bb72818e8184c1678092/propcache-0.5.2-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:3b199b9b2b3d6a7edf3183ba8a9a137a22b97f7df525feb5ae1eccf026d2a9c6", size = 59057, upload-time = "2026-05-08T21:00:02.401Z" },
+ { url = "https://files.pythonhosted.org/packages/53/ae/6e292df9135d659944e96cb3389258e4a663e5b2b5f6c217ef0ddc8d2f73/propcache-0.5.2-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:e59bc9e66329185b93dab73f210f1a37f81cb40f321501db8017c9aea15dba27", size = 61938, upload-time = "2026-05-08T21:00:03.638Z" },
+ { url = "https://files.pythonhosted.org/packages/0b/42/314ebc50d8159055411fd6b0bda322ff510e4b1f7d2e4927940ad0f6af20/propcache-0.5.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:552ffadf6ad409844bc5919c42a0a83d88314cedddaea0e41e80a8b8fffe881f", size = 59731, upload-time = "2026-05-08T21:00:04.881Z" },
+ { url = "https://files.pythonhosted.org/packages/b8/9b/2da6dee38871c3c8772fabc2758325a5c9077d6d18c597737dc04dd884cd/propcache-0.5.2-cp311-cp311-win32.whl", hash = "sha256:cd416c1de191973c52ff1a12a57446bfc7642797b282d7caf2162d7d1b8aa9a0", size = 38966, upload-time = "2026-05-08T21:00:06.511Z" },
+ { url = "https://files.pythonhosted.org/packages/42/4e/f17363fb58c0afe05b067361cb6d86ed2d29de6506779a27547c4d183075/propcache-0.5.2-cp311-cp311-win_amd64.whl", hash = "sha256:44e488ef40dbb452700b2b1f8188934121f6648f52c295055662d2191959ff82", size = 42135, upload-time = "2026-05-08T21:00:08.088Z" },
+ { url = "https://files.pythonhosted.org/packages/c6/eb/6af6685077d22e8b33358d3c548e3282706a0b3cd85044ffba4e5dd08e3b/propcache-0.5.2-cp311-cp311-win_arm64.whl", hash = "sha256:54adaa85a22078d1e306304a40984dc5be99d599bf3dc0a24dc98f7daeab89ab", size = 38381, upload-time = "2026-05-08T21:00:09.692Z" },
+ { url = "https://files.pythonhosted.org/packages/4a/cb/e27bc2b2737a0bb49962b275efa051e8f1c35a936df7d5139b6b658b7dc9/propcache-0.5.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:806719138ecd720339a12410fb9614ac9b2b2d3a5fdf8235d56981c36f4039ba", size = 95887, upload-time = "2026-05-08T21:00:11.277Z" },
+ { url = "https://files.pythonhosted.org/packages/e6/13/b8ae04c59392f8d11c6cd9fb4011d1dc7c86b81225c770280300e259ffe1/propcache-0.5.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:db2b80ea58eab4f86b2beec3cc8b39e8ff9276ac20e96b7cce43c8ae84cd6b5a", size = 54654, upload-time = "2026-05-08T21:00:12.604Z" },
+ { url = "https://files.pythonhosted.org/packages/2c/7d/49777a3e20b55863d4794384a38acd460c04157b0a00f8602b0d508b8431/propcache-0.5.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:e5cbfac9f61484f7e9f3597775500cd3ebe8274e9b050c38f9525c77c97520bf", size = 55190, upload-time = "2026-05-08T21:00:13.935Z" },
+ { url = "https://files.pythonhosted.org/packages/44/c7/085d0cd63062e84044e3f05797749c3f8e3938ff3aeb0eb2f69d43fafc91/propcache-0.5.2-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5dbc581d2814337da56222fab8dc5f161cd798a434e49bac27930aaef798e144", size = 59995, upload-time = "2026-05-08T21:00:15.526Z" },
+ { url = "https://files.pythonhosted.org/packages/9c/42/32cf8e3009e92b2645cf1e944f701e8ea4e924dffde1ee26db860bcbf7e4/propcache-0.5.2-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:857187f381f88c8e2fa2fe56ab94879d011b883d5a2ee5a1b60a8cd2a06846d9", size = 63422, upload-time = "2026-05-08T21:00:16.824Z" },
+ { url = "https://files.pythonhosted.org/packages/9e/1b/f112433f99fc979431b87a39ef169e3f8df070d99a72792c56d6937ac48b/propcache-0.5.2-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:178b4a2cdaac1818e2bf1c5a99b94383fa73ea5382e032a48dec07dc5668dc42", size = 64342, upload-time = "2026-05-08T21:00:18.362Z" },
+ { url = "https://files.pythonhosted.org/packages/14/15/5574111ae50dd6e879456888c0eadd4c5a869959775854e18e18a6b345f3/propcache-0.5.2-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6f328175a2cde1f0ff2c4ed8ce968b9dcfb55f3a7153f39e2957ed994da13476", size = 61639, upload-time = "2026-05-08T21:00:19.692Z" },
+ { url = "https://files.pythonhosted.org/packages/cc/da/4d775080b1490c0ae604acda868bd71aabe3a89ed16f2aa4339eb8a283e7/propcache-0.5.2-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:5671d09a36b06d0fd4a3da0fccbcae360e9b1570924171a15e9e0997f0249fba", size = 61588, upload-time = "2026-05-08T21:00:21.155Z" },
+ { url = "https://files.pythonhosted.org/packages/04/ac/f076982cbe2195ee9cf32de5a1e46951d9fb399fc207f390562dd0fd8fb2/propcache-0.5.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:80168e2ebe4d3ec6599d10ad8f520304ae1cad9b6c5a95372aef1b66b7bfb53a", size = 60029, upload-time = "2026-05-08T21:00:22.713Z" },
+ { url = "https://files.pythonhosted.org/packages/70/60/189be62e0dd898dce3b331e1b8c7a543cd3a405ac0c81fe8ee8a9d5d77e1/propcache-0.5.2-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:45f11346f884bc47444f6e6647131055844134c3175b629f84952e2b5cd62b64", size = 56774, upload-time = "2026-05-08T21:00:24.001Z" },
+ { url = "https://files.pythonhosted.org/packages/ea/9e/93377b9c7939c1ffae98f878dee955efadfd638078bc86dbc21f9d52f651/propcache-0.5.2-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:8e778ebd44ef4f66ed60a0416b06b489687db264a9c0b3620362f26489492913", size = 63532, upload-time = "2026-05-08T21:00:25.545Z" },
+ { url = "https://files.pythonhosted.org/packages/14/f9/590ef6cfb9b8028d516d287812ece32bb0bc5f11fbb9c8bf6b2e6313fec8/propcache-0.5.2-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:c0cb9ed24c8964e172768d455a38254c2dd8a552905729ce006cad3d3dda59b1", size = 61592, upload-time = "2026-05-08T21:00:27.186Z" },
+ { url = "https://files.pythonhosted.org/packages/b4/5e/70958b3034c297a630bba2f17ca7abc2d5f39a803ad7e370ab79d1ecd022/propcache-0.5.2-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:1d1ad32d9d4355e2be65574fd0bfd3677e7066b009cd5b9b2dee8aa6a6393b33", size = 64788, upload-time = "2026-05-08T21:00:28.8Z" },
+ { url = "https://files.pythonhosted.org/packages/12/fd/77fe5936d8c3086ca9048f7f415f122ed82e53884a9ec193646b42deef06/propcache-0.5.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:c80f4ba3e8f00189165999a742ee526ebeccedf6c3f7beb0c7df821e9772435a", size = 62514, upload-time = "2026-05-08T21:00:30.098Z" },
+ { url = "https://files.pythonhosted.org/packages/cf/74/66bd798b5b3be70aa1b391f5cc9d6a0a5532d7fd3b19ec0b213e72e6ad9d/propcache-0.5.2-cp312-cp312-win32.whl", hash = "sha256:8c7972d8f193740d9175f0998ab38717e6cd322d5935c5b0fef8c0d323fd9031", size = 39018, upload-time = "2026-05-08T21:00:31.622Z" },
+ { url = "https://files.pythonhosted.org/packages/61/7c/5c0d34aa3024694d6dcb9271cdbdd08c4e47c1c0ad95ec7e7bc74cdea145/propcache-0.5.2-cp312-cp312-win_amd64.whl", hash = "sha256:d9ee8826a7d47863a08ac44e1a5f611a462eefc3a194b492da242128bec75b42", size = 42322, upload-time = "2026-05-08T21:00:32.918Z" },
+ { url = "https://files.pythonhosted.org/packages/4d/91/875812f1a3feb20ceba818ef39fbe4d92f1081e04ac815c822496d0d038b/propcache-0.5.2-cp312-cp312-win_arm64.whl", hash = "sha256:2800a4a8ead6b28cccd1ec54b59346f0def7922ee1c7598e8499c733cfbb7c84", size = 38172, upload-time = "2026-05-08T21:00:35.124Z" },
+ { url = "https://files.pythonhosted.org/packages/c5/09/f049e45385503fe67db75a6b6186a7b9f0c3930366dc960522c312a825b1/propcache-0.5.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:099aaf4b4d1a02265b92a977edf00b5c4f63b3b17ac6de39b0d637c9cac0188a", size = 94457, upload-time = "2026-05-08T21:00:36.355Z" },
+ { url = "https://files.pythonhosted.org/packages/6b/65/83d1d05655baf63113731bd5a1008435e14f8d1e5a06cbe4ec5b23ad7a31/propcache-0.5.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:68ce1c44c7a813a7f71ea04315a8c7b330b63db99d059a797a4651bb6f69f117", size = 53835, upload-time = "2026-05-08T21:00:38.072Z" },
+ { url = "https://files.pythonhosted.org/packages/a9/12/a6ba6482bb5ea3260c000c9b20881c95fa11c6b30173715668259f844ed7/propcache-0.5.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:fc299c129490f55f254cd90be0deca4764e36e9a7c08b4aa588479a3bbed3098", size = 54545, upload-time = "2026-05-08T21:00:39.319Z" },
+ { url = "https://files.pythonhosted.org/packages/a9/19/7fa086f5764c59ec8a8e157cd93aa8497acc00aba9dcdec56bfffb32602d/propcache-0.5.2-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a6ae2198be502c10f09b2516e7b5d019816924bc3183a43ce792a7bd6625e6f4", size = 59886, upload-time = "2026-05-08T21:00:40.621Z" },
+ { url = "https://files.pythonhosted.org/packages/a1/e4/5d7663dc8235956c8f5281698a3af1d351d8820341ddd890f59d9a9127f2/propcache-0.5.2-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:6041d31504dc1779d700e1edcfb08eea334b357620b06681a4eabb57a74e574e", size = 63261, upload-time = "2026-05-08T21:00:41.775Z" },
+ { url = "https://files.pythonhosted.org/packages/4a/4a/15a03adee24d6350da4292caeac44c34c033d2afe5e87eb370f38854560f/propcache-0.5.2-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f7eabc04151c78a9f4d5bbb5f1faf571e4defeb4b585e0fe95b60ff2dbe4d3d7", size = 64184, upload-time = "2026-05-08T21:00:43.018Z" },
+ { url = "https://files.pythonhosted.org/packages/8b/c6/979176efdaa3d239e36d503d5af63a0a773b36662ed8f52e5b6a6d9fd40e/propcache-0.5.2-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4db0ba63d693afd40d249bd93f842b5f144f8fcbb83de05660373bcf30517b1d", size = 61534, upload-time = "2026-05-08T21:00:44.507Z" },
+ { url = "https://files.pythonhosted.org/packages/c8/22/63e8cd1bae4c2d2be6493b6b7d10566ddafad88137cfbc99964a1119853c/propcache-0.5.2-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:1dbcf7675229b35d31abb6547d8ebc8c27a830ac3f9a794edff6254873ec7c0a", size = 61500, upload-time = "2026-05-08T21:00:45.796Z" },
+ { url = "https://files.pythonhosted.org/packages/60/5a/28e5d9acbac1cc9ccb67045e8c1b943aa8d79fdf39c93bd73cacd68008ea/propcache-0.5.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d310c013aad2c72f1c3f2f8dd3279d460a858c551f97aeb8c63e4693cca7b4d2", size = 59994, upload-time = "2026-05-08T21:00:47.093Z" },
+ { url = "https://files.pythonhosted.org/packages/f3/40/db650677f554a95b9c01a7c9d93d629e93a15562f5deb4573c9ee136fed2/propcache-0.5.2-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:06187263ddad280d05b4d8a8b3bb7d164cbebd469236544a42e6d9b28ac6a4fa", size = 56884, upload-time = "2026-05-08T21:00:48.376Z" },
+ { url = "https://files.pythonhosted.org/packages/80/45/70b39b89516ff8b96bf732fa6fded8cef20f293cb1508690101c3c07ec51/propcache-0.5.2-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:3115559b8effafd63b142ea5ed53d63a16ea6469cbc63dce4ee194b42db5d853", size = 63464, upload-time = "2026-05-08T21:00:49.954Z" },
+ { url = "https://files.pythonhosted.org/packages/f9/e2/fa59d3a89eac5534293124af4f1d0d0ada091ce4a0ab4610ce03fd2bdd8d/propcache-0.5.2-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:c60462af8e6dc30c35407c7237ea908d777b22862bbee27bc4699c0d8bcdc45a", size = 61588, upload-time = "2026-05-08T21:00:51.281Z" },
+ { url = "https://files.pythonhosted.org/packages/0b/97/efb547a55c4bc7381cfb202d6a2239ac621045277bc1ea5dfd3a7f0516c0/propcache-0.5.2-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:40314bca9ac559716fe374094fc81c11dcc34b64fd6c585360f5775690505704", size = 64667, upload-time = "2026-05-08T21:00:52.602Z" },
+ { url = "https://files.pythonhosted.org/packages/92/56/f5c7d9b4b7595d5127da38974d791b2153f3d1eae6c674af3583ace92ad3/propcache-0.5.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:cfa21e036ce1e1db2be04ba3b85d2df1bb1702fa01932d984c5464c665228ff4", size = 62463, upload-time = "2026-05-08T21:00:54.303Z" },
+ { url = "https://files.pythonhosted.org/packages/bd/3b/484a3a65fc9f9f60c41dcd17b428bace5389544e2c680994534a20755066/propcache-0.5.2-cp313-cp313-win32.whl", hash = "sha256:f156a3529f38063b6dbaf356e15602a7f95f8055b1295a438433a6386f10463d", size = 38621, upload-time = "2026-05-08T21:00:55.808Z" },
+ { url = "https://files.pythonhosted.org/packages/1c/fd/3f0f10dba4dabad3bf53102be007abf55481067952bde0fdddff439e7c61/propcache-0.5.2-cp313-cp313-win_amd64.whl", hash = "sha256:dfed59d0a5aeb01e242e66ff0300bc4a265a7c05f612d30016f0b60b1017d757", size = 41649, upload-time = "2026-05-08T21:00:57.061Z" },
+ { url = "https://files.pythonhosted.org/packages/90/ec/6ce619cc32bb500a482f811f9cd509368b4e58e638d13f2c68f370d6b475/propcache-0.5.2-cp313-cp313-win_arm64.whl", hash = "sha256:ba338430e87ceb9c8f0cf754de38a9860560261e56c00376debd628698a7364f", size = 37636, upload-time = "2026-05-08T21:00:58.646Z" },
+ { url = "https://files.pythonhosted.org/packages/1b/82/c1d268bbbf2ef981c5bf0fbbe746db617c66e3bcefe431a1aa8943fbe23a/propcache-0.5.2-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:a592f5f3da71c8691c788c13cb6734b6d17663d2e1cb8caddf0673d01ef8847d", size = 98872, upload-time = "2026-05-08T21:00:59.889Z" },
+ { url = "https://files.pythonhosted.org/packages/f4/d4/52c871e73e864e6b34c0e2d58ac1ec5ccd149497ddc7ad2137ae98323a35/propcache-0.5.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:6a997d0489e9668a384fcfd5061b857aa5361de73191cac204d04b889cfbbafa", size = 56257, upload-time = "2026-05-08T21:01:01.195Z" },
+ { url = "https://files.pythonhosted.org/packages/67/f0/9b90ca2a210b3d09bcfcd96ecd0f55545c091535abce2a45de2775cfd357/propcache-0.5.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:10734b5484ea113152ee25a91dccedf81631791805d2c9ccb054958e51842c94", size = 56696, upload-time = "2026-05-08T21:01:02.941Z" },
+ { url = "https://files.pythonhosted.org/packages/9d/0e/6e9d4ba07c8e56e21ddec1e75f12148142b21ca83a51871babce095334f4/propcache-0.5.2-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:cafca7e56c12bb02ae16d283742bef25a61122e9dab2b5b3f2ccbe589ce32164", size = 62378, upload-time = "2026-05-08T21:01:04.475Z" },
+ { url = "https://files.pythonhosted.org/packages/65/19/c10badaa463dde8a27ce884f8ee2ec37e6035b7c9f5ff0c8f74f06f08dac/propcache-0.5.2-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f064f8d2b59177878b7615df1735cd8fe3462ed6be8c7b217d17a276489c2b7f", size = 65283, upload-time = "2026-05-08T21:01:05.959Z" },
+ { url = "https://files.pythonhosted.org/packages/b0/b6/93bea99ca80e19cef6512a8580e5b7857bbe09422d9daa7fd4ef5723306c/propcache-0.5.2-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f78abfa8dfc32376fd1aacf597b2f2fbbe0ea751419aee718af5d4f82537ef8c", size = 66616, upload-time = "2026-05-08T21:01:07.228Z" },
+ { url = "https://files.pythonhosted.org/packages/83/e4/5c7462e50625f051f37fb38b8224f7639f667184bbd34424ec83819bb1b7/propcache-0.5.2-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f7467da8a9822bf1a55336f877340c5bcbd3c482afc43a99771169f74a26dedc", size = 63773, upload-time = "2026-05-08T21:01:08.514Z" },
+ { url = "https://files.pythonhosted.org/packages/ca/b6/99238894047b13c823be25027e736626cd414a52a5e30d2c3347c2733529/propcache-0.5.2-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a6ddc6ac9e25de626c1f129c1b467d7ecd33ce2237d3fd0c4e429feef0a7ee1f", size = 63664, upload-time = "2026-05-08T21:01:09.874Z" },
+ { url = "https://files.pythonhosted.org/packages/85/1e/a3a1a63116a2b8edb415a8bb9a6f0c34bd03830b1e18e8ce2904e1dc1cf4/propcache-0.5.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:2f22cbbac9e26a8e864c0985ff1268d5d939d53d9d9411a9824279097e03a2cb", size = 62643, upload-time = "2026-05-08T21:01:11.132Z" },
+ { url = "https://files.pythonhosted.org/packages/e4/03/893cf147de2fc6543c5eaa07ad833170e7e2a2385725bbebe8c0503723bb/propcache-0.5.2-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:fc76378c62a0f04d0cd82fbb1a2cd2d7e28fcb40d5873f28a6c44e388aaa2751", size = 59595, upload-time = "2026-05-08T21:01:12.387Z" },
+ { url = "https://files.pythonhosted.org/packages/86/3b/04c1a2e12c57766568ba75ba72b3bf2042818d4c1425fab6fc07155c7cff/propcache-0.5.2-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:acd2c8edba48e31e58a363b8cf4e5c7db3b04b3f9e371f601df30d9b0d244836", size = 65711, upload-time = "2026-05-08T21:01:13.676Z" },
+ { url = "https://files.pythonhosted.org/packages/1c/34/80f8d0099f8d6bacc4de1624c85672681c8cd1149ca2da0e38fd120b817f/propcache-0.5.2-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:452b5065457eb9991ec5eb38ff41d6cd4c991c9ac7c531c4d5849ae473a9a13f", size = 64247, upload-time = "2026-05-08T21:01:14.936Z" },
+ { url = "https://files.pythonhosted.org/packages/f3/1a/8b08f3a5f1037e9e370c55883ceeeee0f6dd0416fb2d2d67b8bfc91f2a79/propcache-0.5.2-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:3430bb2bfe1331885c427745a751e774ee679fd4344f80b97bf879815fe8fa55", size = 67102, upload-time = "2026-05-08T21:01:16.281Z" },
+ { url = "https://files.pythonhosted.org/packages/34/68/8bdb7bb7756d76e005490649d10e4a8369e610c74d619f71e1aedf889e9c/propcache-0.5.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:cef6cea3922890dd6c9654971001fa797b526c16ab5e1e46c05fd6f877be7568", size = 64964, upload-time = "2026-05-08T21:01:17.57Z" },
+ { url = "https://files.pythonhosted.org/packages/0a/aa/50fb0b5d3968b61a510926ff8b8465f1d6e976b3ab74496d7a4b9fc42515/propcache-0.5.2-cp313-cp313t-win32.whl", hash = "sha256:72d61e16dd78228b58c5d47be830ff3da7e5f139abdf0aef9d86cde1c5cf2191", size = 42546, upload-time = "2026-05-08T21:01:18.946Z" },
+ { url = "https://files.pythonhosted.org/packages/ae/4c/0ddbae64321bd4a95bcbfc19307238016b5b1fee645c84626c8d539e5b74/propcache-0.5.2-cp313-cp313t-win_amd64.whl", hash = "sha256:0958834041a0166d343b8d2cedcd8bcbaeb4fdbe0cf08320c5379f143c3be6e7", size = 46330, upload-time = "2026-05-08T21:01:20.162Z" },
+ { url = "https://files.pythonhosted.org/packages/00/d9/9cddc8efb78d8af264c5ec9f6d10b62f57c515feda8d321595f56010fb23/propcache-0.5.2-cp313-cp313t-win_arm64.whl", hash = "sha256:6de8bd93ddde9b992cf2b2e0d796d501a19026b5b9fd87356d7d0779531a8d96", size = 40521, upload-time = "2026-05-08T21:01:21.399Z" },
+ { url = "https://files.pythonhosted.org/packages/e2/ea/23ee535d90ce8bcc465a3028eb3cc0ce3bd1005f4bb27710b30587de798d/propcache-0.5.2-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:46088abff4cba581dea21ae0467a480526cb25aa5f3c269e909f800328bc3999", size = 94662, upload-time = "2026-05-08T21:01:22.683Z" },
+ { url = "https://files.pythonhosted.org/packages/b5/06/c5a52f419b5d8972f8d46a7577476090d8e3263ff589ce40b5ca4968d5be/propcache-0.5.2-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:fc88b26f08d634f7bc819a7852e5214f5802641ab8d9fd5326892292eee1993e", size = 53928, upload-time = "2026-05-08T21:01:23.986Z" },
+ { url = "https://files.pythonhosted.org/packages/63/b1/4260d67d6bd85e58a66b72d54ce15d5de789b6f3870cc6bedf8ff9667401/propcache-0.5.2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:97797ebb098e670a2f92dd66f32897e30d7615b14e7f59711de23e30a9072539", size = 54650, upload-time = "2026-05-08T21:01:25.305Z" },
+ { url = "https://files.pythonhosted.org/packages/70/06/2f46c318e3307cd7a6a7481def374ce838c0fe20084b39dd54b0879d0e99/propcache-0.5.2-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ba57fffe4ac99c5d30076161b5866336d97600769bad35cc68f7774b15298a4e", size = 59912, upload-time = "2026-05-08T21:01:26.545Z" },
+ { url = "https://files.pythonhosted.org/packages/4c/29/fe1aebec2ce57ab985a9c382bded1124431f85078113aa222c5d278430d4/propcache-0.5.2-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:583c19759d9eec1e5b69e2fbef36a7d9c326041be9746cb822d335c8cedc2979", size = 63300, upload-time = "2026-05-08T21:01:27.937Z" },
+ { url = "https://files.pythonhosted.org/packages/b4/18/2334b26768b6c82be8c69e83671b767d5ef426aa09b0cba6c2ea47816774/propcache-0.5.2-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d0326e2e5e1f3163fa306c834e48e8d490e5fae607a097a40c0648109b47ba80", size = 64208, upload-time = "2026-05-08T21:01:29.484Z" },
+ { url = "https://files.pythonhosted.org/packages/2b/76/7f1bfd6afff4c5e38e36a3c6d68eb5f4b7311ea80baf693db78d95b603c4/propcache-0.5.2-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e00820e192c8dbebcafb383ebbf99030895f09905e7a0eb2e0340a0bcc2bc825", size = 61633, upload-time = "2026-05-08T21:01:31.068Z" },
+ { url = "https://files.pythonhosted.org/packages/c4/46/b3ff8aba2b4953a3e50de2cf72f1b5748b8eca93b15f3dc2c84339084c09/propcache-0.5.2-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c66afea89b1e43725731d2004732a046fe6fe955d51f952c3e95a7314a284a39", size = 61724, upload-time = "2026-05-08T21:01:32.374Z" },
+ { url = "https://files.pythonhosted.org/packages/c5/01/814cfcafbcff954f94c01cf30e097ddc88a076b5440fbcf4570753437d40/propcache-0.5.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:d4dc37dec6c6cdad0b57881a5658fd14fbf53e333b1a86cf86559f190e1d9ec4", size = 60069, upload-time = "2026-05-08T21:01:33.67Z" },
+ { url = "https://files.pythonhosted.org/packages/da/68/5c6f7622d510cc666a300687e06fd060c1a43361c0c9b20d284f06d8096a/propcache-0.5.2-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:5570dbcc97571c15f68068e529c92715a12f8d54030e272d264b377e22bd17a5", size = 57099, upload-time = "2026-05-08T21:01:34.915Z" },
+ { url = "https://files.pythonhosted.org/packages/55/27/9cb0b4c679124085327957d42521c99dba04c88c90c3e55a6f0b633ebccc/propcache-0.5.2-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:f814362777a9f841adddb200ecdf8f5cb1e5a3c4b7a86378edbd6ccb26edd702", size = 63391, upload-time = "2026-05-08T21:01:36.231Z" },
+ { url = "https://files.pythonhosted.org/packages/f0/9d/7258aaa5bdf60fc6f27591eef6fe52768cb0beda7140be477c8b12c9794a/propcache-0.5.2-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:196913dea116aeb5a2ba95af4ddcb7ea85559ae07d8eee8751688310d09168c3", size = 61626, upload-time = "2026-05-08T21:01:37.545Z" },
+ { url = "https://files.pythonhosted.org/packages/8e/0d/41c602003e8a9b16fe1e7eadf62c7bfba9d5474370b24200bf48b315f45f/propcache-0.5.2-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:6e7b8719005dd1175be4ab1cd25e9b98659a5e0347331506ec6760d2773a7fb5", size = 64781, upload-time = "2026-05-08T21:01:38.83Z" },
+ { url = "https://files.pythonhosted.org/packages/8b/f3/38e66b1856e9bd079deea015bc4a55f7767c0e4db2f7dcf69e7e680ba4ce/propcache-0.5.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:51f96d685ab16e88cab128cd37a52c5da540809c8b879fa047731bfcb4ad35a4", size = 62570, upload-time = "2026-05-08T21:01:40.415Z" },
+ { url = "https://files.pythonhosted.org/packages/95/ca/bbfe9b910ce57dde8bb4876b4520fc02a4e89497c10de26be936758a3aaa/propcache-0.5.2-cp314-cp314-win32.whl", hash = "sha256:cc6fc3cc62e8501d3ed62894425040d2728ecddb1ed072737a5c70bd537aa9f0", size = 39436, upload-time = "2026-05-08T21:01:41.654Z" },
+ { url = "https://files.pythonhosted.org/packages/61/d2/45c9defbaa1ea297035d9d4cce9e8f80daafbf19319c6007f157c6256ea9/propcache-0.5.2-cp314-cp314-win_amd64.whl", hash = "sha256:81e3a30b0bb60caa22033dd0f8a3618d1d67356212514f62c57db75cb0ef410c", size = 42373, upload-time = "2026-05-08T21:01:43.041Z" },
+ { url = "https://files.pythonhosted.org/packages/44/68/9ea5103f41d5217d7d6ec24db90018e23aebec070c3f9a6e54d12b841fd8/propcache-0.5.2-cp314-cp314-win_arm64.whl", hash = "sha256:0d2c9bf8528f135dbb805ce027567e09164f7efa51a2be07458a2c0420f292d0", size = 38554, upload-time = "2026-05-08T21:01:44.336Z" },
+ { url = "https://files.pythonhosted.org/packages/8a/81/fadf555f42d3b762eea8a53950b0489fdc0aa9da5f8ed9e10ce0a4e01b48/propcache-0.5.2-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:4bc8ff1feffc6a61c7002ffe84634c41b822e104990ae009f44a0834430070bb", size = 99395, upload-time = "2026-05-08T21:01:45.883Z" },
+ { url = "https://files.pythonhosted.org/packages/f5/c9/c61e134a686949cf7971af3a390148b1156f7be81c73bc0cd12c873e2d48/propcache-0.5.2-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:79aa3ff0a9b566633b642fa9caf7e21ed1c13d6feca718187873f199e1514078", size = 56653, upload-time = "2026-05-08T21:01:47.307Z" },
+ { url = "https://files.pythonhosted.org/packages/cb/73/daf935ea7048ddd7ec8eec5345b4a40b619d2d178b3c0a0900796bc3c794/propcache-0.5.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:1b31822f4474c4036bae62de9402710051d431a606d6a0f907fec79935a071aa", size = 56914, upload-time = "2026-05-08T21:01:48.573Z" },
+ { url = "https://files.pythonhosted.org/packages/79/9f/aba959b435ea18617edd7cf0a7ad0b9c574b8fc7e3d2cd55fb59cb255d33/propcache-0.5.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:13fef48778b5a2a756523fdb781326b028ca75e32858b04f2cdd19f394564917", size = 62567, upload-time = "2026-05-08T21:01:49.903Z" },
+ { url = "https://files.pythonhosted.org/packages/6c/a1/859942de9a791ff42f6141736f5b37749b8f53e65edfa49638c67dd67e6a/propcache-0.5.2-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:8b73ab70f1a3351fbc71f663b3e645af6dd0329100c353081cf69c37433fc6fe", size = 65542, upload-time = "2026-05-08T21:01:51.204Z" },
+ { url = "https://files.pythonhosted.org/packages/b5/61/315bc0fd6c0fc7f80a528b8afd209e5fc4a875ea79571b91b8f50f442907/propcache-0.5.2-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5538d2c13d93e4698af7e092b57bc7298fd35d1d58e656ae18f23ee0d0378e03", size = 66845, upload-time = "2026-05-08T21:01:52.539Z" },
+ { url = "https://files.pythonhosted.org/packages/47/f7/9f8122e3132e8e354ac41975ef8f1099be7d5a16bc7ae562734e993665c0/propcache-0.5.2-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:cd645f03898405cabe694fb8bc35241e3a9c332ec85627584fe3de201452b335", size = 63985, upload-time = "2026-05-08T21:01:53.847Z" },
+ { url = "https://files.pythonhosted.org/packages/c8/54/c317819ec157cbf6f35df9df9657a6f82daf34d5faf15948b2f639c2192e/propcache-0.5.2-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a473b3440261e0c60706e732b2ed2f517857344fc21bf48fdfe211e2d98eb285", size = 63999, upload-time = "2026-05-08T21:01:55.179Z" },
+ { url = "https://files.pythonhosted.org/packages/5a/56/387e3f7dfce0a9233df41fb888aa1c30222cb4bbbf09537c02dd9bd85fe2/propcache-0.5.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:7afa37062e6650640e932e4cc9297d81f9f42d9944029cc386b8247dea4da837", size = 62779, upload-time = "2026-05-08T21:01:57.489Z" },
+ { url = "https://files.pythonhosted.org/packages/a1/9c/596784cb5824ed61ee960d3f8655a3f0993e107c6e98ab6c818b7fb92ccb/propcache-0.5.2-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:8a90efd5777e996e42d568db9ac740b944d691e565cbfd31b2f7832f9184b2b8", size = 59796, upload-time = "2026-05-08T21:01:58.736Z" },
+ { url = "https://files.pythonhosted.org/packages/c2/3d/1a6cfa1726a48542c1e8784a0761421476a5b68e09b7f36bf95eb954aaba/propcache-0.5.2-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:f19bb891234d72535764d703bfed1153cc34f4214d5bd7150aee1eec9e8f4366", size = 66023, upload-time = "2026-05-08T21:02:00.228Z" },
+ { url = "https://files.pythonhosted.org/packages/e4/0e/05fd6990369477076e4e280bcb970de760fddf0161a46e988bc95f7940ec/propcache-0.5.2-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:32775082acd2d807ee3db715c7770d38767b817870acfa08c29e057f3c4d5b56", size = 64448, upload-time = "2026-05-08T21:02:01.888Z" },
+ { url = "https://files.pythonhosted.org/packages/cd/86/5f8da315a4309c62c10c0b2516b17492d5d3bbe1bb862b96604db67e2a37/propcache-0.5.2-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:9282fb1a3bccd038da9f768b927b24a0c753e466c086b7c4f3c6982851eefb2d", size = 67329, upload-time = "2026-05-08T21:02:03.484Z" },
+ { url = "https://files.pythonhosted.org/packages/da/d3/3368efe79ab21f0cdf86ef49895811c9cc933131d4cde1f28a624e22e712/propcache-0.5.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:cc49723e2f60d6b32a0f0b08a3fd6d13203c07f1cd9566cfce0f12a917c967a2", size = 65172, upload-time = "2026-05-08T21:02:04.745Z" },
+ { url = "https://files.pythonhosted.org/packages/d5/07/127e8b0bacfb325396196f9d976a22453049b89b9b2b08477cc3145faa44/propcache-0.5.2-cp314-cp314t-win32.whl", hash = "sha256:2d7aa89ebca5acc98cba9d1472d976e394782f587bad6661003602a619fd1821", size = 43813, upload-time = "2026-05-08T21:02:06.025Z" },
+ { url = "https://files.pythonhosted.org/packages/88/fb/46dad6c0ae49ed230ab1b16c890c2b6314e2403e6c412976f4a72d64a527/propcache-0.5.2-cp314-cp314t-win_amd64.whl", hash = "sha256:d447bb0b3054be5818458fbb171208b1d9ff11eba14e18ca18b90cbb45767370", size = 47764, upload-time = "2026-05-08T21:02:07.353Z" },
+ { url = "https://files.pythonhosted.org/packages/e7/c4/a47d0a63aa309d10d59ede6e9d4cff03a344a79d1f0f4cd0cd74997b53e0/propcache-0.5.2-cp314-cp314t-win_arm64.whl", hash = "sha256:fe67a3d11cd9b4efabfa45c3d00ffba2b26811442a73a581a94b67c2b5faccf6", size = 41140, upload-time = "2026-05-08T21:02:09.065Z" },
+ { url = "https://files.pythonhosted.org/packages/3a/ed/1cdcab6ba3d6ab7feca11fc14f0eeea80755bb53ef4e892079f31b10a25f/propcache-0.5.2-py3-none-any.whl", hash = "sha256:be1ddfcbb376e3de5d2e2db1d58d6d67463e6b4f9f040c000de8e300295465fe", size = 14036, upload-time = "2026-05-08T21:02:10.673Z" },
+]
+
+[[package]]
+name = "pydantic"
+version = "2.13.4"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "annotated-types", version = "0.7.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" },
+ { name = "annotated-types", version = "0.8.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" },
+ { name = "pydantic-core" },
+ { name = "typing-extensions" },
+ { name = "typing-inspection" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/18/a5/b60d21ac674192f8ab0ba4e9fd860690f9b4a6e51ca5df118733b487d8d6/pydantic-2.13.4.tar.gz", hash = "sha256:c40756b57adaa8b1efeeced5c196f3f3b7c435f90e84ea7f443901bec8099ef6", size = 844775, upload-time = "2026-05-06T13:43:05.343Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/fd/7b/122376b1fd3c62c1ed9dc80c931ace4844b3c55407b6fb2d199377c9736f/pydantic-2.13.4-py3-none-any.whl", hash = "sha256:45a282cde31d808236fd7ea9d919b128653c8b38b393d1c4ab335c62924d9aba", size = 472262, upload-time = "2026-05-06T13:43:02.641Z" },
+]
+
+[[package]]
+name = "pydantic-core"
+version = "2.46.4"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "typing-extensions" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/9d/56/921726b776ace8d8f5db44c4ef961006580d91dc52b803c489fafd1aa249/pydantic_core-2.46.4.tar.gz", hash = "sha256:62f875393d7f270851f20523dd2e29f082bcc82292d66db2b64ea71f64b6e1c1", size = 471464, upload-time = "2026-05-06T13:37:06.98Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/e7/08/f1ba952f1c8ae5581c70fa9c6da89f247b83e3dd8c09c035d5d7931fc23d/pydantic_core-2.46.4-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:a396dcc17e5a0b164dbe026896245a4fa9ff402edca1dff0be3d53a517f74de4", size = 2113146, upload-time = "2026-05-06T13:37:36.537Z" },
+ { url = "https://files.pythonhosted.org/packages/56/c6/65f646c7ff09bd257f660434adb45c4dfcbbcebcc030562fecf6f5bf887d/pydantic_core-2.46.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:da4b951fe36dc7c3a1ccb4e3cd1747c3542b8c9ceede8fc86cae054e764485f5", size = 1949769, upload-time = "2026-05-06T13:37:46.365Z" },
+ { url = "https://files.pythonhosted.org/packages/64/ba/bfb1d928fd5b49e1258935ff104ae356e9fd89384a55bf9f847e9193ad40/pydantic_core-2.46.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bb63e0198ca18aad131c089b9204c23079c3afa95487e561f4c522d519e55aba", size = 1974958, upload-time = "2026-05-06T13:37:28.611Z" },
+ { url = "https://files.pythonhosted.org/packages/4e/74/76223bfb117b64af743c9b6670d1364516f5c0604f96b48f3272f6af6cc6/pydantic_core-2.46.4-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f47286a97f0bc9b8859519809077b91b2cefe4ae47fcbf5e466a009c1c5d742b", size = 2042118, upload-time = "2026-05-06T13:36:55.216Z" },
+ { url = "https://files.pythonhosted.org/packages/cb/7b/848732968bc8f48f3187542f08358b9d842db564147b256669426ebb1652/pydantic_core-2.46.4-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:905a0ed8ea6f2d61c1738835f99b699348d7857379083e5fc497fa0c967a407c", size = 2222876, upload-time = "2026-05-06T13:38:25.455Z" },
+ { url = "https://files.pythonhosted.org/packages/b5/2f/e90b63ee2e14bd8d3db8f705a6d75d64e6ee1b7c2c8833747ce706e1e0ce/pydantic_core-2.46.4-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ea793e075b70290d89d8142074262885d3f7da19634845135751bd6344f73b50", size = 2286703, upload-time = "2026-05-06T13:37:53.304Z" },
+ { url = "https://files.pythonhosted.org/packages/ba/1e/acc4d70f88a0a277e4a1fa77ebb985ceabaf900430f875bf9338e11c9420/pydantic_core-2.46.4-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:395aebd9183f9d112f569aeb5b2214d1a10a33bec8456447f7fbdfa51d38d4cd", size = 2092042, upload-time = "2026-05-06T13:38:46.981Z" },
+ { url = "https://files.pythonhosted.org/packages/a9/da/0a422b57bf8504102bf3c4ccea9c41bab5a5cee6a54650acf8faf67f5a24/pydantic_core-2.46.4-cp310-cp310-manylinux_2_31_riscv64.whl", hash = "sha256:b078afbc25f3a1436c7a1d2cd3e322497ee99615ba97c563566fdf46aff1ee01", size = 2117231, upload-time = "2026-05-06T13:39:23.146Z" },
+ { url = "https://files.pythonhosted.org/packages/bd/2a/2ac13c3af305843e23c5078c53d135656b3f05a2fd78cb7bbbb12e97b473/pydantic_core-2.46.4-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:f747929cf940cddb5b3668a390056ddd5ba2e5010615ea2dcf4f9c4f3ab8791d", size = 2168388, upload-time = "2026-05-06T13:40:08.06Z" },
+ { url = "https://files.pythonhosted.org/packages/72/04/2beacf7e1607e93eefe4aed1b4709f079b905fb77530179d4f7c71745f22/pydantic_core-2.46.4-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:daa27d92c36f24388fe3ad306b174781c747627f134452e4f128ea00ce1fe8c4", size = 2184769, upload-time = "2026-05-06T13:38:13.901Z" },
+ { url = "https://files.pythonhosted.org/packages/9e/29/d2b9fd9f539133548eaf622c06a4ce176cb46ac59f32d0359c4abc0de047/pydantic_core-2.46.4-cp310-cp310-musllinux_1_1_armv7l.whl", hash = "sha256:19e51f073cd3df251856a8a4189fbdf1de4012c3ebacfb1884f94f1eb406079f", size = 2319312, upload-time = "2026-05-06T13:39:08.24Z" },
+ { url = "https://files.pythonhosted.org/packages/7c/af/0f7a5b85fec6075bea96e3ef9187de38fccced0de92c1e7feda8d5cc7bb9/pydantic_core-2.46.4-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:c1747f85cee84c26985853c6f3d9bd3e75da5212912443fa111c113b9c246f39", size = 2361817, upload-time = "2026-05-06T13:38:43.2Z" },
+ { url = "https://files.pythonhosted.org/packages/25/a4/73363fec545fd3ec025490bdda2743c56d0dd5b6266b1a53bbe9e4265375/pydantic_core-2.46.4-cp310-cp310-win32.whl", hash = "sha256:2f84c03c8607173d16b5a854ec68a2f9079ae03237a54fb506d13af47e1d018d", size = 1987085, upload-time = "2026-05-06T13:39:25.497Z" },
+ { url = "https://files.pythonhosted.org/packages/01/aa/62f082da2c91fac1c234bc9ee0066257ce83f0604abd72e4c9d5991f2d84/pydantic_core-2.46.4-cp310-cp310-win_amd64.whl", hash = "sha256:8358a950c8909158e3df31538a7e4edc2d7265a7c54b47f0864d9e5bae9dcebf", size = 2074311, upload-time = "2026-05-06T13:39:59.922Z" },
+ { url = "https://files.pythonhosted.org/packages/5c/fa/6d7708d2cfc1a832acb6aeb0cd16e801902df8a0f583bb3b4b527fde022e/pydantic_core-2.46.4-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:0e96592440881c74a213e5ad528e2b24d3d4f940de2766bed9010ab1d9e51594", size = 2111872, upload-time = "2026-05-06T13:40:27.596Z" },
+ { url = "https://files.pythonhosted.org/packages/ae/6f/aa064a3e74b5745afbdf250594f38e7ead05e2d651bcb35994b9417a0d4d/pydantic_core-2.46.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e0d65b8c354be7fb5f720c3caa8bc940bc2d20ce749c8e06135f07f8ed95dd7c", size = 1948255, upload-time = "2026-05-06T13:39:12.574Z" },
+ { url = "https://files.pythonhosted.org/packages/43/3a/41114a9f7569b84b4d84e7a018c57c56347dac30c0d4a872946ec4e36c46/pydantic_core-2.46.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7bfb192b3f4b9e8a89b6277b6ce787564f62cfd272055f6e685726b111dc7826", size = 1972827, upload-time = "2026-05-06T13:38:19.841Z" },
+ { url = "https://files.pythonhosted.org/packages/ef/25/1ab42e8048fe551934d9884e8d64daa7e990ad386f310a15981aeb6a5b08/pydantic_core-2.46.4-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:9037063db01f09b09e237c282b6792bd4da634b5402c4e7f0c61effed7701a04", size = 2041051, upload-time = "2026-05-06T13:38:10.447Z" },
+ { url = "https://files.pythonhosted.org/packages/94/c2/1a934597ddf08da410385b3b7aae91956a5a76c635effef456074fad7e88/pydantic_core-2.46.4-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fc010ab034c8c7452522748bf937df58020d256ccae0874463d1f4d01758af8e", size = 2221314, upload-time = "2026-05-06T13:40:13.089Z" },
+ { url = "https://files.pythonhosted.org/packages/02/6d/9e8ad178c9c4df27ad3c8f25d1fe2a7ab0d2ba0559fad4aee5d3d1f16771/pydantic_core-2.46.4-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8c5dac79fa1614d1e06ca695109c6105923bd9c7d1d6c918d4e637b7e6b32fd3", size = 2285146, upload-time = "2026-05-06T13:38:59.224Z" },
+ { url = "https://files.pythonhosted.org/packages/80/50/540cd3aeefc041beb111125c4bff779831a2111fc6b15a9138cda277d32c/pydantic_core-2.46.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f9fa868638bf362d3d138ea55829cefb3d5f4b0d7f142234382a15e2485dbec4", size = 2089685, upload-time = "2026-05-06T13:38:17.762Z" },
+ { url = "https://files.pythonhosted.org/packages/6b/a4/b440ad35f05f6a38f89fa0f149accb3f0e02be94ca5e15f3c449a61b4bc9/pydantic_core-2.46.4-cp311-cp311-manylinux_2_31_riscv64.whl", hash = "sha256:17299feefe090f2caa5b8e37222bb5f663e4935a8bfa6931d4102e5df1a9f398", size = 2115420, upload-time = "2026-05-06T13:37:58.195Z" },
+ { url = "https://files.pythonhosted.org/packages/99/61/de4f55db8dfd57bfdfa9a12ec90fe1b57c4f41062f7ca86f08586b3e0ac0/pydantic_core-2.46.4-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:4c63ebc82684aa89d9a3bcbd13d515b3be44250dc68dd3bd81526c1cb31286c3", size = 2165122, upload-time = "2026-05-06T13:37:01.167Z" },
+ { url = "https://files.pythonhosted.org/packages/f7/52/7c529d7bdb2d1068bd52f51fe32572c8301f9a4febf1948f10639f1436f5/pydantic_core-2.46.4-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:aaa2a54443eff1950ba5ddc6b6ccda0d9c84a364276a62f969bdf2a390650848", size = 2182573, upload-time = "2026-05-06T13:38:45.04Z" },
+ { url = "https://files.pythonhosted.org/packages/37/b3/7c40325848ba78247f2812dcf9c7274e38cd801820ca6dd9fe63bcfb0eb4/pydantic_core-2.46.4-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:18e5ceec2ab67e6d5f1a9085e5a24c9c4e2ac4545730bfe668680bca05e555f3", size = 2317139, upload-time = "2026-05-06T13:37:15.539Z" },
+ { url = "https://files.pythonhosted.org/packages/d9/37/f913f81a657c865b75da6c0dbed79876073c2a43b5bd9edbe8da785e4d49/pydantic_core-2.46.4-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:a0f62d0a58f4e7da165457e995725421e0064f2255d8eccebc49f41bbc23b109", size = 2360433, upload-time = "2026-05-06T13:37:30.099Z" },
+ { url = "https://files.pythonhosted.org/packages/c4/67/6acaa1be2567f9256b056d8477158cac7240813956ce86e49deae8e173b4/pydantic_core-2.46.4-cp311-cp311-win32.whl", hash = "sha256:041bde0a48fd37cf71cab1c9d56d3e8625a3793fef1f7dd232b3ff37e978ecda", size = 1985513, upload-time = "2026-05-06T13:38:15.669Z" },
+ { url = "https://files.pythonhosted.org/packages/aa/e6/c505f83dfeda9a2e5c995cfd872949e4d05e12f7feb3dca72f633daefa94/pydantic_core-2.46.4-cp311-cp311-win_amd64.whl", hash = "sha256:6f2eeda33a839975441c86a4119e1383c50b47faf0cbb5176985565c6bb02c33", size = 2071114, upload-time = "2026-05-06T13:40:35.416Z" },
+ { url = "https://files.pythonhosted.org/packages/0f/da/7a263a96d965d9d0df5e8de8a475f33495451117035b09acb110288c381f/pydantic_core-2.46.4-cp311-cp311-win_arm64.whl", hash = "sha256:14f4c5d6db102bd796a627bbb3a17b4cf4574b9ae861d8b7c9a9661c6dd3362d", size = 2044298, upload-time = "2026-05-06T13:38:29.754Z" },
+ { url = "https://files.pythonhosted.org/packages/ce/8c/af022f0af448d7747c5154288d46b5f2bc5f17366eaa0e23e9aa04d59f3b/pydantic_core-2.46.4-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:3245406455a5d98187ec35530fd772b1d799b26667980872c8d4614991e2c4a2", size = 2106158, upload-time = "2026-05-06T13:38:57.215Z" },
+ { url = "https://files.pythonhosted.org/packages/19/95/6195171e385007300f0f5574592e467c568becce2d937a0b6804f218bc49/pydantic_core-2.46.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:962ccbab7b642487b1d8b7df90ef677e03134cf1fd8880bf698649b22a69371f", size = 1951724, upload-time = "2026-05-06T13:37:02.697Z" },
+ { url = "https://files.pythonhosted.org/packages/8e/bc/f47d1ff9cbb1620e1b5b697eef06010035735f07820180e74178226b27b3/pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8233f2947cf85404441fd7e0085f53b10c93e0ee78611099b5c7237e36aacbf7", size = 1975742, upload-time = "2026-05-06T13:37:09.448Z" },
+ { url = "https://files.pythonhosted.org/packages/5b/11/9b9a5b0306345664a2da6410877af6e8082481b5884b3ddd78d47c6013ce/pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3a233125ac121aa3ffba9a2b59edfc4a985a76092dc8279586ab4b71390875e7", size = 2052418, upload-time = "2026-05-06T13:37:38.234Z" },
+ { url = "https://files.pythonhosted.org/packages/f1/b7/a65fec226f5d78fc39f4a13c4cc0c768c22b113438f60c14adc9d2865038/pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5b712b53160b79a5850310b912a5ef8e57e56947c8ad690c227f5c9d7e561712", size = 2232274, upload-time = "2026-05-06T13:38:27.753Z" },
+ { url = "https://files.pythonhosted.org/packages/68/f0/92039db98b907ef49269a8271f67db9cb78ae2fc68062ef7e4e77adb5f61/pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9401557acd873c3a7f3eb9383edef8ac4968f9510e340f4808d427e75667e7b4", size = 2309940, upload-time = "2026-05-06T13:38:05.353Z" },
+ { url = "https://files.pythonhosted.org/packages/5f/97/2aab507d3d00ca626e8e57c1eac6a79e4e5fbcc63eb99733ff55d1717f65/pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:926c9541b14b12b1681dca8a0b75feb510b06c6341b70a8e500c2fdcff837cce", size = 2094516, upload-time = "2026-05-06T13:39:10.577Z" },
+ { url = "https://files.pythonhosted.org/packages/22/37/a8aca44d40d737dde2bc05b3c6c07dff0de07ce6f82e9f3167aeaf4d5dea/pydantic_core-2.46.4-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:56cb4851bcaf3d117eddcef4fe66afd750a50274b0da8e22be256d10e5611987", size = 2136854, upload-time = "2026-05-06T13:40:22.59Z" },
+ { url = "https://files.pythonhosted.org/packages/24/99/fcef1b79238c06a8cbec70819ac722ba76e02bc8ada9b0fd66eba40da01b/pydantic_core-2.46.4-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c68fcd102d71ea85c5b2dfac3f4f8476eff42a9e078fd5faefff6d145063536b", size = 2180306, upload-time = "2026-05-06T13:40:10.666Z" },
+ { url = "https://files.pythonhosted.org/packages/ae/6c/fc44000918855b42779d007ae63b0532794739027b2f417321cddbc44f6a/pydantic_core-2.46.4-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:b2f69dec1725e79a012d920df1707de5caf7ed5e08f3be4435e25803efc47458", size = 2190044, upload-time = "2026-05-06T13:40:43.231Z" },
+ { url = "https://files.pythonhosted.org/packages/6b/65/d9cadc9f1920d7a127ad2edba16c1db7916e59719285cd6c94600b0080ba/pydantic_core-2.46.4-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:8d0820e8192167f80d88d64038e609c31452eeca865b4e1d9950a27a4609b00b", size = 2329133, upload-time = "2026-05-06T13:39:57.365Z" },
+ { url = "https://files.pythonhosted.org/packages/d0/cf/c873d91679f3a30bcf5e7ac280ce5573483e72295307685120d0d5ad3416/pydantic_core-2.46.4-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:fbdb89b3e1c94a30cc5edfce477c6e6a5dc4d8f84665b455c27582f211a1c72c", size = 2374464, upload-time = "2026-05-06T13:38:06.976Z" },
+ { url = "https://files.pythonhosted.org/packages/47/bd/6f2fc8188f31bf10590f1e98e7b306336161fac930a8c514cd7bd828c7dc/pydantic_core-2.46.4-cp312-cp312-win32.whl", hash = "sha256:9aa768456404a8bf48a4406685ac2bec8e72b62c69313734fa3b73cf33b3a894", size = 1974823, upload-time = "2026-05-06T13:40:47.985Z" },
+ { url = "https://files.pythonhosted.org/packages/40/8c/985c1d41ea1107c2534abd9870e4ed5c8e7669b5c308297835c001e7a1c4/pydantic_core-2.46.4-cp312-cp312-win_amd64.whl", hash = "sha256:e9c26f834c65f5752f3f06cb08cb86a913ceb7274d0db6e267808a708b46bc89", size = 2072919, upload-time = "2026-05-06T13:39:21.153Z" },
+ { url = "https://files.pythonhosted.org/packages/c4/ba/f463d006e0c47373ca7ec5e1a261c59dc01ef4d62b2657af925fb0deee3a/pydantic_core-2.46.4-cp312-cp312-win_arm64.whl", hash = "sha256:4fc73cb559bdb54b1134a706a2802a4cddd27a0633f5abb7e53056268751ac6a", size = 2027604, upload-time = "2026-05-06T13:39:03.753Z" },
+ { url = "https://files.pythonhosted.org/packages/51/a2/5d30b469c5267a17b39dec53208222f76a8d351dfac4af661888c5aee77d/pydantic_core-2.46.4-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:5d5902252db0d3cedf8d4a1bc68f70eeb430f7e4c7104c8c476753519b423008", size = 2106306, upload-time = "2026-05-06T13:37:48.029Z" },
+ { url = "https://files.pythonhosted.org/packages/c1/81/4fa520eaffa8bd7d1525e644cd6d39e7d60b1592bc5b516693c7340b50f1/pydantic_core-2.46.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:c94f0688e7b8d0a67abf40e57a7eaaecd17cc9586706a31b76c031f63df052b4", size = 1951906, upload-time = "2026-05-06T13:37:17.012Z" },
+ { url = "https://files.pythonhosted.org/packages/03/d5/fd02da45b659668b05923b17ba3a0100a0a3d5541e3bd8fcc4ecb711309e/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f027324c56cd5406ca49c124b0db10e56c69064fec039acc571c29020cc87c76", size = 1976802, upload-time = "2026-05-06T13:37:35.113Z" },
+ { url = "https://files.pythonhosted.org/packages/21/f2/95727e1368be3d3ed485eaab7adbd7dda408f33f7a36e8b48e0144002b91/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e739fee756ba1010f8bcccb534252e85a35fe45ae92c295a06059ce58b74ccd3", size = 2052446, upload-time = "2026-05-06T13:37:12.313Z" },
+ { url = "https://files.pythonhosted.org/packages/9c/86/5d99feea3f77c7234b8718075b23db11532773c1a0dbd9b9490215dc2eeb/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9d56801be94b86a9da183e5f3766e6310752b99ff647e38b09a9500d88e46e76", size = 2232757, upload-time = "2026-05-06T13:39:01.149Z" },
+ { url = "https://files.pythonhosted.org/packages/d2/3a/508ac615935ef7588cf6d9e9b91309fdc2da751af865e02a9098de88258c/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2412e734dcb48da14d4e4006b82b46b74f2518b8a26ee7e58c6844a6cd6d03c4", size = 2309275, upload-time = "2026-05-06T13:37:41.406Z" },
+ { url = "https://files.pythonhosted.org/packages/07/f8/41db9de19d7987d6b04715a02b3b40aea467000275d9d758ffaa31af7d50/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9551187363ffc0de2a00b2e47c25aeaeb1020b69b668762966df15fc5659dd5a", size = 2094467, upload-time = "2026-05-06T13:39:18.847Z" },
+ { url = "https://files.pythonhosted.org/packages/2c/e2/f35033184cb11d0052daf4416e8e10a502ea2ac006fc4f459aee872727d1/pydantic_core-2.46.4-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:0186750b482eefa11d7f435892b09c5c606193ef3375bcf94aa00ae6bfb66262", size = 2134417, upload-time = "2026-05-06T13:40:17.944Z" },
+ { url = "https://files.pythonhosted.org/packages/7e/7b/6ceeb1cc90e193862f444ebe373d8fdf613f0a82572dde03fb10734c6c71/pydantic_core-2.46.4-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:5855698a4856556d86e8e6cd8434bc3ac0314ee8e12089ae0e143f64c6256e4e", size = 2179782, upload-time = "2026-05-06T13:40:32.618Z" },
+ { url = "https://files.pythonhosted.org/packages/5a/f2/c8d7773ede6af08036423a00ae0ceffce266c3c52a096c435d68c896083f/pydantic_core-2.46.4-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:cbaf13819775b7f769bf4a1f066cb6df7a28d4480081a589828ef190226881cd", size = 2188782, upload-time = "2026-05-06T13:36:51.018Z" },
+ { url = "https://files.pythonhosted.org/packages/59/31/0c864784e31f09f05cdd87606f08923b9c9e7f6e51dd27f20f62f975ce9f/pydantic_core-2.46.4-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:633147d34cf4550417f12e2b1a0383973bdf5cdfde212cb09e9a581cf10820be", size = 2328334, upload-time = "2026-05-06T13:40:37.764Z" },
+ { url = "https://files.pythonhosted.org/packages/c2/eb/4f6c8a41efa30baa755590f4141abf3a8c370fab610915733e74134a7270/pydantic_core-2.46.4-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:82cf5301172168103724d49a1444d3378cb20cdee30b116a1bd6031236298a5d", size = 2372986, upload-time = "2026-05-06T13:39:34.152Z" },
+ { url = "https://files.pythonhosted.org/packages/5b/24/b375a480d53113860c299764bfe9f349a3dc9108b3adc0d7f0d786492ebf/pydantic_core-2.46.4-cp313-cp313-win32.whl", hash = "sha256:9fa8ae11da9e2b3126c6426f147e0fba88d96d65921799bb30c6abd1cb2c97fb", size = 1973693, upload-time = "2026-05-06T13:37:55.072Z" },
+ { url = "https://files.pythonhosted.org/packages/7e/e8/cff247591966f2d22ec8c003cd7587e27b7ba7b81ab2fb888e3ab75dc285/pydantic_core-2.46.4-cp313-cp313-win_amd64.whl", hash = "sha256:6b3ace8194b0e5204818c92802dcdca7fc6d88aabbb799d7c795540d9cd6d292", size = 2071819, upload-time = "2026-05-06T13:38:49.139Z" },
+ { url = "https://files.pythonhosted.org/packages/c6/1a/f4aee670d5670e9e148e0c82c7db98d780be566c6e6a97ee8035528ca0b3/pydantic_core-2.46.4-cp313-cp313-win_arm64.whl", hash = "sha256:184c081504d17f1c1066e430e117142b2c77d9448a97f7b65c6ac9fd9aee238d", size = 2027411, upload-time = "2026-05-06T13:40:45.796Z" },
+ { url = "https://files.pythonhosted.org/packages/8d/74/228a26ddad29c6672b805d9fd78e8d251cd04004fa7eed0e622096cd0250/pydantic_core-2.46.4-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:428e04521a40150c85216fc8b85e8d39fece235a9cf5e383761238c7fa9b96fb", size = 2102079, upload-time = "2026-05-06T13:38:41.019Z" },
+ { url = "https://files.pythonhosted.org/packages/ad/1f/8970b150a4b4365623ae00fc88603491f763c627311ae8031e3111356d6e/pydantic_core-2.46.4-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:23ace664830ee0bfe014a0c7bc248b1f7f25ed7ad103852c317624a1083af462", size = 1952179, upload-time = "2026-05-06T13:36:59.812Z" },
+ { url = "https://files.pythonhosted.org/packages/95/30/5211a831ae054928054b2f79731661087a2bc5c01e825c672b3a4a8f1b3e/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ce5c1d2a8b27468f433ca974829c44060b8097eedc39933e3c206a90ee49c4a9", size = 1978926, upload-time = "2026-05-06T13:37:39.933Z" },
+ { url = "https://files.pythonhosted.org/packages/57/e9/689668733b1eb67adeef047db3c2e8788fcf65a7fd9c9e2b46b7744fe245/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:7283d57845ecf5a163403eb0702dfc220cc4fbdd18919cb5ccea4f95ee1cdab4", size = 2046785, upload-time = "2026-05-06T13:38:01.995Z" },
+ { url = "https://files.pythonhosted.org/packages/60/d9/6715260422ff50a2109878fd24d948a6c3446bb2664f34ee78cd972b3acd/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8daafc69c93ee8a0204506a3b6b30f586ef54028f52aeeeb5c4cfc5184fd5914", size = 2228733, upload-time = "2026-05-06T13:40:50.371Z" },
+ { url = "https://files.pythonhosted.org/packages/18/ae/fdb2f64316afca925640f8e70bb1a564b0ec2721c1389e25b8eb4bf9a299/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cd2213145bcc2ba85884d0ac63d222fece9209678f77b9b4d76f054c561adb28", size = 2307534, upload-time = "2026-05-06T13:37:21.531Z" },
+ { url = "https://files.pythonhosted.org/packages/89/1d/8eff589b45bb8190a9d12c49cfad0f176a5cbd1534908a6b5125e2886239/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7a5f930472650a82629163023e630d160863fce524c616f4e5186e5de9d9a49b", size = 2099732, upload-time = "2026-05-06T13:39:31.942Z" },
+ { url = "https://files.pythonhosted.org/packages/06/d5/ee5a3366637fee41dee51a1fc91562dcf12ddbc68fda34e6b253da2324bb/pydantic_core-2.46.4-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:c1b3f518abeca3aa13c712fd202306e145abf59a18b094a6bafb2d2bbf59192c", size = 2129627, upload-time = "2026-05-06T13:37:25.033Z" },
+ { url = "https://files.pythonhosted.org/packages/94/33/2414be571d2c6a6c4d08be21f9292b6d3fdb08949a97b6dfe985017821db/pydantic_core-2.46.4-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1a7dd0b3ee80d90150e3495a3a13ac34dbcbfd4f012996a6a1d8900e91b5c0fb", size = 2179141, upload-time = "2026-05-06T13:37:14.046Z" },
+ { url = "https://files.pythonhosted.org/packages/7b/79/7daa95be995be0eecc4cf75064cb33f9bbbfe3fe0158caf2f0d4a996a5c7/pydantic_core-2.46.4-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:3fb702cd90b0446a3a1c5e470bfa0dd23c0233b676a9099ddcc964fa6ca13898", size = 2184325, upload-time = "2026-05-06T13:36:53.615Z" },
+ { url = "https://files.pythonhosted.org/packages/9f/cb/d0a382f5c0de8a222dc61c65348e0ce831b1f68e0a018450d31c2cace3a5/pydantic_core-2.46.4-cp314-cp314-musllinux_1_1_armv7l.whl", hash = "sha256:b8458003118a712e66286df6a707db01c52c0f52f7db8e4a38f0da1d3b94fc4e", size = 2323990, upload-time = "2026-05-06T13:40:29.971Z" },
+ { url = "https://files.pythonhosted.org/packages/05/db/d9ba624cc4a5aced1598e88c04fdbd8310c8a69b9d38b9a3d39ce3a61ed7/pydantic_core-2.46.4-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:372429a130e469c9cd698925ce5fc50940b7a1336b0d82038e63d5bbc4edc519", size = 2369978, upload-time = "2026-05-06T13:37:23.027Z" },
+ { url = "https://files.pythonhosted.org/packages/f2/20/d15df15ba918c423461905802bfd2981c3af0bfa0e40d05e13edbfa48bc3/pydantic_core-2.46.4-cp314-cp314-win32.whl", hash = "sha256:85bb3611ff1802f3ee7fdd7dbff26b56f343fb432d57a4728fdd49b6ef35e2f4", size = 1966354, upload-time = "2026-05-06T13:38:03.499Z" },
+ { url = "https://files.pythonhosted.org/packages/fc/b6/6b8de4c0a7d7ab3004c439c80c5c1e0a3e8d78bbae19379b01960383d9e5/pydantic_core-2.46.4-cp314-cp314-win_amd64.whl", hash = "sha256:811ff8e9c313ab425368bcbb36e5c4ebd7108c2bbf4e4089cfbb0b01eff63fac", size = 2072238, upload-time = "2026-05-06T13:39:40.807Z" },
+ { url = "https://files.pythonhosted.org/packages/32/36/51eb763beec1f4cf59b1db243a7dcc39cbb41230f050a09b9d69faaf0a48/pydantic_core-2.46.4-cp314-cp314-win_arm64.whl", hash = "sha256:bfec22eab3c8cc2ceec0248aec886624116dc079afa027ecc8ad4a7e62010f8a", size = 2018251, upload-time = "2026-05-06T13:37:26.72Z" },
+ { url = "https://files.pythonhosted.org/packages/e8/91/855af51d625b23aa987116a19e231d2aaef9c4a415273ddc189b79a45fee/pydantic_core-2.46.4-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:af8244b2bef6aaad6d92cda81372de7f8c8d36c9f0c3ea36e827c60e7d9467a0", size = 2099593, upload-time = "2026-05-06T13:39:47.682Z" },
+ { url = "https://files.pythonhosted.org/packages/fb/1b/8784a54c65edb5f49f0a14d6977cf1b209bba85a4c77445b255c2de58ab3/pydantic_core-2.46.4-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:5a4330cdbc57162e4b3aa303f588ba752257694c9c9be3e7ebb11b4aca659b5d", size = 1935226, upload-time = "2026-05-06T13:40:40.428Z" },
+ { url = "https://files.pythonhosted.org/packages/e8/e7/1955d28d1afc56dd4b3ad7cc0cf39df1b9852964cf16e5d13912756d6d6b/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:29c61fc04a3d840155ff08e475a04809278972fe6aef51e2720554e96367e34b", size = 1974605, upload-time = "2026-05-06T13:37:32.029Z" },
+ { url = "https://files.pythonhosted.org/packages/93/e2/3fedbf0ba7a22850e6e9fd78117f1c0f10f950182344d8a6c535d468fdd8/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c50f2528cf200c5eed56faf3f4e22fcd5f38c157a8b78576e6ba3168ec35f000", size = 2030777, upload-time = "2026-05-06T13:38:55.239Z" },
+ { url = "https://files.pythonhosted.org/packages/f8/61/46be275fcaaba0b4f5b9669dd852267ce1ff616592dccf7a7845588df091/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0cbe8b01f948de4286c74cdd6c667aceb38f5c1e26f0693b3983d9d74887c65e", size = 2236641, upload-time = "2026-05-06T13:37:08.096Z" },
+ { url = "https://files.pythonhosted.org/packages/60/db/12e93e46a8bac9988be3c016860f83293daea8c716c029c9ace279036f2f/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:617d7e2ca7dcb8c5cf6bcb8c59b8832c94b36196bbf1cbd1bfb56ed341905edd", size = 2286404, upload-time = "2026-05-06T13:40:20.221Z" },
+ { url = "https://files.pythonhosted.org/packages/e2/4a/4d8b19008f38d31c53b8219cfedc2e3d5de5fe99d90076b7e767de29274f/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7027560ee92211647d0d34e3f7cd6f50da56399d26a9c8ad0da286d3869a53f3", size = 2109219, upload-time = "2026-05-06T13:38:12.153Z" },
+ { url = "https://files.pythonhosted.org/packages/88/70/3cbc40978fefb7bb09c6708d40d4ad1a5d70fd7213c3d17f971de868ec1f/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:f99626688942fb746e545232e7726926f3be91b5975f8b55327665fafda991c7", size = 2110594, upload-time = "2026-05-06T13:40:02.971Z" },
+ { url = "https://files.pythonhosted.org/packages/9d/20/b8d36736216e29491125531685b2f9e61aa5b4b2599893f8268551da3338/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:fc3e9034a63de20e15e8ade85358bc6efc614008cab72898b4b4952bea0509ff", size = 2159542, upload-time = "2026-05-06T13:39:27.506Z" },
+ { url = "https://files.pythonhosted.org/packages/1d/a2/367df868eb584dacf6bf82a389272406d7178e301c4ac82545ab98bc2dd9/pydantic_core-2.46.4-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:97e7cf2be5c77b7d1a9713a05605d49460d02c6078d38d8bef3cbe323c548424", size = 2168146, upload-time = "2026-05-06T13:38:31.93Z" },
+ { url = "https://files.pythonhosted.org/packages/c1/b8/4460f77f7e201893f649a29ab355dddd3beee8a97bcb1a320db414f9a06e/pydantic_core-2.46.4-cp314-cp314t-musllinux_1_1_armv7l.whl", hash = "sha256:3bf92c5d0e00fefaab325a4d27828fe6b6e2a21848686b5b60d2d9eeb09d76c6", size = 2306309, upload-time = "2026-05-06T13:37:44.717Z" },
+ { url = "https://files.pythonhosted.org/packages/64/c4/be2639293acd87dc8ddbcec41a73cee9b2ebf996fe6d892a1a74e88ad3f7/pydantic_core-2.46.4-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:3ecbc122d18468d06ca279dc26a8c2e2d5acb10943bb35e36ae92096dc3b5565", size = 2369736, upload-time = "2026-05-06T13:37:05.645Z" },
+ { url = "https://files.pythonhosted.org/packages/30/a6/9f9f380dbb301f67023bf8f707aaa75daadf84f7152d95c410fd7e81d994/pydantic_core-2.46.4-cp314-cp314t-win32.whl", hash = "sha256:e846ae7835bf0703ae43f534ab79a867146dadd59dc9ca5c8b53d5c8f7c9ef02", size = 1955575, upload-time = "2026-05-06T13:38:51.116Z" },
+ { url = "https://files.pythonhosted.org/packages/40/1f/f1eb9eb350e795d1af8586289746f5c5677d16043040d63710e22abc43c9/pydantic_core-2.46.4-cp314-cp314t-win_amd64.whl", hash = "sha256:2108ba5c1c1eca18030634489dc544844144ee36357f2f9f780b93e7ddbb44b5", size = 2051624, upload-time = "2026-05-06T13:38:21.672Z" },
+ { url = "https://files.pythonhosted.org/packages/f6/d2/42dd53d0a85c27606f316d3aa5d2869c4e8470a5ed6dec30e4a1abe19192/pydantic_core-2.46.4-cp314-cp314t-win_arm64.whl", hash = "sha256:4fcbe087dbc2068af7eda3aa87634eba216dbda64d1ae73c8684b621d33f6596", size = 2017325, upload-time = "2026-05-06T13:40:52.723Z" },
+ { url = "https://files.pythonhosted.org/packages/5d/00/13a0c039569d1e583779ee1b8d7df6bfe275a0db83fcae14f01d6856c16e/pydantic_core-2.46.4-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:fd8b3d9fd264be37976686c7f65cd52a83f5e84f4bfd2adf9c1d469676bbb6ae", size = 2115337, upload-time = "2026-05-06T13:38:37.741Z" },
+ { url = "https://files.pythonhosted.org/packages/41/60/e70fa1ee03e243bdfd4b1fddf1e1f2a8fba681df3034b51b9376c0fb5bf5/pydantic_core-2.46.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:9f444c499b3eefd3a92e348059471ea0c3a6e303d9c1cec09fa748fd9f895201", size = 1957976, upload-time = "2026-05-06T13:37:33.478Z" },
+ { url = "https://files.pythonhosted.org/packages/11/9a/78fb5f2ea849f767ea802de8b4e8f5a0c4a48ddbe4bc66bd19ac2f55a01c/pydantic_core-2.46.4-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3447661d99f75a3683a4cf5c87da72f2161964611864dbbeac7fbb118bb4bfc0", size = 1979390, upload-time = "2026-05-06T13:36:52.419Z" },
+ { url = "https://files.pythonhosted.org/packages/f5/7d/3acfdcd000bad9735de0430a88355948469781f62cb841fd63e8a307e80e/pydantic_core-2.46.4-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:8b9bab013d1c7a79d3501ff86d0bc9c31bf587db4551677b96bec07df78c6b15", size = 2043263, upload-time = "2026-05-06T13:39:54.798Z" },
+ { url = "https://files.pythonhosted.org/packages/35/60/1325e5a8d7f9697416481c7f7c1c304738d6b961a7fd1ea0f054ce0f14fb/pydantic_core-2.46.4-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d995260fdf4e1db774581b4900e0f832abe3c7c84996726bbc161b19c8f29e76", size = 2225708, upload-time = "2026-05-06T13:40:24.887Z" },
+ { url = "https://files.pythonhosted.org/packages/6a/b0/9ec8c38f33b26db0b612cb7fd165bb0a370773710432a2a74fa31287b430/pydantic_core-2.46.4-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f13a646d65d09fbf1bc6b3a9635d30095c8e7e5cc419ff35ecc563c5fd04cd49", size = 2288494, upload-time = "2026-05-06T13:38:00.091Z" },
+ { url = "https://files.pythonhosted.org/packages/65/05/497446a9586d1b2d24ee25ebe208beb15388f1875d783e1e014055d150ac/pydantic_core-2.46.4-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:432c179df7874eeb73307aad2df0755e1ae0efa61ff0ea89b93e194411ae3928", size = 2095629, upload-time = "2026-05-06T13:38:23.632Z" },
+ { url = "https://files.pythonhosted.org/packages/93/d9/cd5fa98f9d94f9294c15459396c8a2383c164469e679ac178d6d42cfee6b/pydantic_core-2.46.4-cp39-cp39-manylinux_2_31_riscv64.whl", hash = "sha256:e68b7a074f65a2fd746c52a7ce6142ab7006074ac269ace0c25cd8ba171f8066", size = 2119309, upload-time = "2026-05-06T13:39:50.144Z" },
+ { url = "https://files.pythonhosted.org/packages/20/1b/64cec655451ddbf3976df5dc9706b240df4fdaebdeebeadd4f59a8dab926/pydantic_core-2.46.4-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:4a05d69cba51d852c5c3e92758653245a50c0b646ced0cf05bd793ed592839d6", size = 2170216, upload-time = "2026-05-06T13:39:14.561Z" },
+ { url = "https://files.pythonhosted.org/packages/2a/21/fe9f039138c9ea3be10ccdb6ec490acb54dcbef5a5e96dbdf1411f82b929/pydantic_core-2.46.4-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:228ee9bae8bef5b1e97ec58302f80357c37199e0d0a99174e138d28e6957b9d9", size = 2186726, upload-time = "2026-05-06T13:37:51.597Z" },
+ { url = "https://files.pythonhosted.org/packages/44/cb/19ca0da64821d1aefcef65f253aa9ecbdd0dde360f607d0f9b3d95db2b4e/pydantic_core-2.46.4-cp39-cp39-musllinux_1_1_armv7l.whl", hash = "sha256:10e17cbb10a330363733efc4d7c4d0dd827ac0909b8f6a6542298fed1ea62f29", size = 2320400, upload-time = "2026-05-06T13:39:36.29Z" },
+ { url = "https://files.pythonhosted.org/packages/cd/14/fe3fbf6e845bf2080dc2f282d75085ddf79d037b35634ecde68f33c217b4/pydantic_core-2.46.4-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:91a06d2e259ecfbd8c901d70c3c507900458498142b3026a296b7de4d1322cc9", size = 2363318, upload-time = "2026-05-06T13:38:53.039Z" },
+ { url = "https://files.pythonhosted.org/packages/62/88/60b110889507a426eecf626f7536566cb290ada71147eff49b6e2724ca62/pydantic_core-2.46.4-cp39-cp39-win32.whl", hash = "sha256:d80ee3d731373b24cebbc10d689ca4ee1875caf0d5703a245db18efd4dd37fc1", size = 1988880, upload-time = "2026-05-06T13:39:16.572Z" },
+ { url = "https://files.pythonhosted.org/packages/0b/d6/8ede2f98f17e1e4e127d37be0eced4eee931a511c62cd68af50e1b25bfa9/pydantic_core-2.46.4-cp39-cp39-win_amd64.whl", hash = "sha256:3be77f45df024d789a672ae34f8b06fb346c4f9f46ea714956660ea4862e89ac", size = 2079257, upload-time = "2026-05-06T13:39:38.498Z" },
+ { url = "https://files.pythonhosted.org/packages/ee/a4/73995fd4ebbb46ba0ee51e6fa049b8f02c40daebb762208feda8a6b7894d/pydantic_core-2.46.4-graalpy311-graalpy242_311_native-macosx_10_12_x86_64.whl", hash = "sha256:14d4edf427bdcf950a8a02d7cb44a08614388dd6e1bdcbf4f67504fa7887da9c", size = 2111589, upload-time = "2026-05-06T13:37:10.817Z" },
+ { url = "https://files.pythonhosted.org/packages/fb/7f/f37d3a5e8bfcc2e403f5c57a730f2d815693fb42119e8ea48b3789335af1/pydantic_core-2.46.4-graalpy311-graalpy242_311_native-macosx_11_0_arm64.whl", hash = "sha256:0ce40cd7b21210e99342afafbd4d0f76d784eb5b1d60f3bdc566be4983c6c73b", size = 1944552, upload-time = "2026-05-06T13:36:56.717Z" },
+ { url = "https://files.pythonhosted.org/packages/15/3c/d7eb777b3ff43e8433a4efb39a17aa8fd98a4ee8561a24a67ef5db07b2d6/pydantic_core-2.46.4-graalpy311-graalpy242_311_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:90884113d8b48f760e9587002789ddd741e76ab9f89518cd1e43b1f1a52ec44b", size = 1982984, upload-time = "2026-05-06T13:39:06.207Z" },
+ { url = "https://files.pythonhosted.org/packages/63/87/70b9f40170a81afd55ca26c9b2acb25c20d64bcfbf888fafecb3ba077d4c/pydantic_core-2.46.4-graalpy311-graalpy242_311_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:66ce7632c22d837c95301830e111ad0128a32b8207533b60896a96c4915192ea", size = 2138417, upload-time = "2026-05-06T13:39:45.476Z" },
+ { url = "https://files.pythonhosted.org/packages/9d/1d/8987ad40f65ae1432753072f214fb5c74fe47ffbd0698bb9cbbb585664f8/pydantic_core-2.46.4-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:1d8ba486450b14f3b1d63bc521d410ec7565e52f887b9fb671791886436a42f7", size = 2095527, upload-time = "2026-05-06T13:39:52.283Z" },
+ { url = "https://files.pythonhosted.org/packages/64/d3/84c282a7eee1d3ac4c0377546ef5a1ea436ce26840d9ac3b7ed54a377507/pydantic_core-2.46.4-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:3009f12e4e90b7f88b4f9adb1b0c4a3d58fe7820f3238c190047209d148026df", size = 1936024, upload-time = "2026-05-06T13:40:15.671Z" },
+ { url = "https://files.pythonhosted.org/packages/d7/ca/eac61596cdeb4d7e174d3dc0bd8a6238f14f75f97a24e7b7db4c7e7340a0/pydantic_core-2.46.4-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ad785e92e6dc634c21555edc8bd6b64957ab844541bcb96a1366c202951ae526", size = 1990696, upload-time = "2026-05-06T13:38:34.717Z" },
+ { url = "https://files.pythonhosted.org/packages/fa/c3/7c8b240552251faf6b3a957db200fcfbbcec36763c050428b601e0c9b83b/pydantic_core-2.46.4-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:00c603d540afdd6b80eb39f078f33ebd46211f02f33e34a32d9f053bba711de0", size = 2147590, upload-time = "2026-05-06T13:39:29.883Z" },
+ { url = "https://files.pythonhosted.org/packages/11/cb/428de0385b6c8d44b716feba566abfacfbd23ee3c4439faa789a1456242f/pydantic_core-2.46.4-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:0c563b08bca408dc7f65f700633d8442fffb2421fc47b8101377e9fd65051ff0", size = 2112782, upload-time = "2026-05-06T13:37:04.016Z" },
+ { url = "https://files.pythonhosted.org/packages/0b/b5/6a17bdadd0fc1f170adfd05a20d37c832f52b117b4d9131da1f41bb097ce/pydantic_core-2.46.4-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:db06ffe51636ffe9ca531fe9023dd64bdd794be8754cb5df57c5498ae5b518a7", size = 1952146, upload-time = "2026-05-06T13:39:43.092Z" },
+ { url = "https://files.pythonhosted.org/packages/2a/dc/03734d80e362cd43ef65428e9de77c730ce7f2f11c60d2b1e1b39f0fbf99/pydantic_core-2.46.4-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:133878133d271ade3d41d1bfb2a45ec38dbdbda40bc065921c6b04e4630127e2", size = 2134492, upload-time = "2026-05-06T13:36:58.124Z" },
+ { url = "https://files.pythonhosted.org/packages/de/df/5e5ffc085ed07cc22d298134d3d911c63e91f6a0eb91fe646750a3209910/pydantic_core-2.46.4-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:9bc519fbf2b7578398853d815009ae5e4d4603d12f4e3f91da8c06852d3da3e9", size = 2156604, upload-time = "2026-05-06T13:37:49.88Z" },
+ { url = "https://files.pythonhosted.org/packages/81/44/6e112a4253e56f5705467cbab7ab5e91ee7398ba3d56d358635958893d3e/pydantic_core-2.46.4-pp311-pypy311_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:c7a7bd4e39e8e4c12c39cd480356842b6a8a06e41b23a55a5e3e191718838ddf", size = 2183828, upload-time = "2026-05-06T13:37:43.053Z" },
+ { url = "https://files.pythonhosted.org/packages/ac/ad/5565071e937d8e752842ac241463944c9eb14c87e2d269f2658a5bd05e98/pydantic_core-2.46.4-pp311-pypy311_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:d396ec2b979760aaf3218e76c24e65bd0aca24983298653b3a9d7a45f9e47b30", size = 2310000, upload-time = "2026-05-06T13:37:56.694Z" },
+ { url = "https://files.pythonhosted.org/packages/4f/c3/66883a5cec183e7fba4d024b4cbbe61851a63750ef606b0afecc46d1f2bf/pydantic_core-2.46.4-pp311-pypy311_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:86e1a4418c6cd97d60c95c71164158eaf7324fae7b0923264016baa993eba6fc", size = 2361286, upload-time = "2026-05-06T13:40:05.667Z" },
+ { url = "https://files.pythonhosted.org/packages/4b/2d/69abac8f838090bbecd5df894befb2c2619e7996a98ddb949db9f3b93225/pydantic_core-2.46.4-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:d51026d73fcfd93610abc7b27789c26b313920fcfb20e27462d74a7f8b06e983", size = 2193071, upload-time = "2026-05-06T13:38:08.682Z" },
+]
+
+[[package]]
+name = "sniffio"
+version = "1.3.1"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/a2/87/a6771e1546d97e7e041b6ae58d80074f81b7d5121207425c964ddf5cfdbd/sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc", size = 20372, upload-time = "2024-02-25T23:20:04.057Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/e9/44/75a9c9421471a6c4805dbf2356f7c181a29c1879239abab1ea2cc8f38b40/sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2", size = 10235, upload-time = "2024-02-25T23:20:01.196Z" },
+]
+
+[[package]]
+name = "typing-extensions"
+version = "4.16.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/f6/cc/6253133b5bb138fc3306cebfbda2c520f545d36b5be2c7255cc528bb45d6/typing_extensions-4.16.0.tar.gz", hash = "sha256:dc983d19a509c94dba722ee6abd33940f7c05a89e243c47e907eb4db6f1a43e5", size = 113555, upload-time = "2026-07-02T08:40:05.92Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/49/d3/b8441a820a491ddfc024b0b0cf0393375b75ea13866d9c66727e54c2fc80/typing_extensions-4.16.0-py3-none-any.whl", hash = "sha256:481caa481374e813c1b176ada14e97f1f67a4539ce9cfeb3f350d78d6370c2e8", size = 45571, upload-time = "2026-07-02T08:40:04.659Z" },
+]
+
+[[package]]
+name = "typing-inspection"
+version = "0.4.2"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "typing-extensions" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/55/e3/70399cb7dd41c10ac53367ae42139cf4b1ca5f36bb3dc6c9d33acdb43655/typing_inspection-0.4.2.tar.gz", hash = "sha256:ba561c48a67c5958007083d386c3295464928b01faa735ab8547c5692e87f464", size = 75949, upload-time = "2025-10-01T02:14:41.687Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/dc/9b/47798a6c91d8bdb567fe2698fe81e0c6b7cb7ef4d13da4114b41d239f65d/typing_inspection-0.4.2-py3-none-any.whl", hash = "sha256:4ed1cacbdc298c220f1bd249ed5287caa16f34d44ef4e9c3d0cbad5b521545e7", size = 14611, upload-time = "2025-10-01T02:14:40.154Z" },
+]
+
+[[package]]
+name = "yarl"
+version = "1.22.0"
+source = { registry = "https://pypi.org/simple" }
+resolution-markers = [
+ "python_full_version < '3.10'",
+]
+dependencies = [
+ { name = "idna" },
+ { name = "multidict" },
+ { name = "propcache", version = "0.4.1", source = { registry = "https://pypi.org/simple" } },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/57/63/0c6ebca57330cd313f6102b16dd57ffaf3ec4c83403dcb45dbd15c6f3ea1/yarl-1.22.0.tar.gz", hash = "sha256:bebf8557577d4401ba8bd9ff33906f1376c877aa78d1fe216ad01b4d6745af71", size = 187169, upload-time = "2025-10-06T14:12:55.963Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/d1/43/a2204825342f37c337f5edb6637040fa14e365b2fcc2346960201d457579/yarl-1.22.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:c7bd6683587567e5a49ee6e336e0612bec8329be1b7d4c8af5687dcdeb67ee1e", size = 140517, upload-time = "2025-10-06T14:08:42.494Z" },
+ { url = "https://files.pythonhosted.org/packages/44/6f/674f3e6f02266428c56f704cd2501c22f78e8b2eeb23f153117cc86fb28a/yarl-1.22.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:5cdac20da754f3a723cceea5b3448e1a2074866406adeb4ef35b469d089adb8f", size = 93495, upload-time = "2025-10-06T14:08:46.2Z" },
+ { url = "https://files.pythonhosted.org/packages/b8/12/5b274d8a0f30c07b91b2f02cba69152600b47830fcfb465c108880fcee9c/yarl-1.22.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:07a524d84df0c10f41e3ee918846e1974aba4ec017f990dc735aad487a0bdfdf", size = 94400, upload-time = "2025-10-06T14:08:47.855Z" },
+ { url = "https://files.pythonhosted.org/packages/e2/7f/df1b6949b1fa1aa9ff6de6e2631876ad4b73c4437822026e85d8acb56bb1/yarl-1.22.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e1b329cb8146d7b736677a2440e422eadd775d1806a81db2d4cded80a48efc1a", size = 347545, upload-time = "2025-10-06T14:08:49.683Z" },
+ { url = "https://files.pythonhosted.org/packages/84/09/f92ed93bd6cd77872ab6c3462df45ca45cd058d8f1d0c9b4f54c1704429f/yarl-1.22.0-cp310-cp310-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:75976c6945d85dbb9ee6308cd7ff7b1fb9409380c82d6119bd778d8fcfe2931c", size = 319598, upload-time = "2025-10-06T14:08:51.215Z" },
+ { url = "https://files.pythonhosted.org/packages/c3/97/ac3f3feae7d522cf7ccec3d340bb0b2b61c56cb9767923df62a135092c6b/yarl-1.22.0-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:80ddf7a5f8c86cb3eb4bc9028b07bbbf1f08a96c5c0bc1244be5e8fefcb94147", size = 363893, upload-time = "2025-10-06T14:08:53.144Z" },
+ { url = "https://files.pythonhosted.org/packages/06/49/f3219097403b9c84a4d079b1d7bda62dd9b86d0d6e4428c02d46ab2c77fc/yarl-1.22.0-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d332fc2e3c94dad927f2112395772a4e4fedbcf8f80efc21ed7cdfae4d574fdb", size = 371240, upload-time = "2025-10-06T14:08:55.036Z" },
+ { url = "https://files.pythonhosted.org/packages/35/9f/06b765d45c0e44e8ecf0fe15c9eacbbde342bb5b7561c46944f107bfb6c3/yarl-1.22.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0cf71bf877efeac18b38d3930594c0948c82b64547c1cf420ba48722fe5509f6", size = 346965, upload-time = "2025-10-06T14:08:56.722Z" },
+ { url = "https://files.pythonhosted.org/packages/c5/69/599e7cea8d0fcb1694323b0db0dda317fa3162f7b90166faddecf532166f/yarl-1.22.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:663e1cadaddae26be034a6ab6072449a8426ddb03d500f43daf952b74553bba0", size = 342026, upload-time = "2025-10-06T14:08:58.563Z" },
+ { url = "https://files.pythonhosted.org/packages/95/6f/9dfd12c8bc90fea9eab39832ee32ea48f8e53d1256252a77b710c065c89f/yarl-1.22.0-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:6dcbb0829c671f305be48a7227918cfcd11276c2d637a8033a99a02b67bf9eda", size = 335637, upload-time = "2025-10-06T14:09:00.506Z" },
+ { url = "https://files.pythonhosted.org/packages/57/2e/34c5b4eb9b07e16e873db5b182c71e5f06f9b5af388cdaa97736d79dd9a6/yarl-1.22.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:f0d97c18dfd9a9af4490631905a3f131a8e4c9e80a39353919e2cfed8f00aedc", size = 359082, upload-time = "2025-10-06T14:09:01.936Z" },
+ { url = "https://files.pythonhosted.org/packages/31/71/fa7e10fb772d273aa1f096ecb8ab8594117822f683bab7d2c5a89914c92a/yarl-1.22.0-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:437840083abe022c978470b942ff832c3940b2ad3734d424b7eaffcd07f76737", size = 357811, upload-time = "2025-10-06T14:09:03.445Z" },
+ { url = "https://files.pythonhosted.org/packages/26/da/11374c04e8e1184a6a03cf9c8f5688d3e5cec83ed6f31ad3481b3207f709/yarl-1.22.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:a899cbd98dce6f5d8de1aad31cb712ec0a530abc0a86bd6edaa47c1090138467", size = 351223, upload-time = "2025-10-06T14:09:05.401Z" },
+ { url = "https://files.pythonhosted.org/packages/82/8f/e2d01f161b0c034a30410e375e191a5d27608c1f8693bab1a08b089ca096/yarl-1.22.0-cp310-cp310-win32.whl", hash = "sha256:595697f68bd1f0c1c159fcb97b661fc9c3f5db46498043555d04805430e79bea", size = 82118, upload-time = "2025-10-06T14:09:11.148Z" },
+ { url = "https://files.pythonhosted.org/packages/62/46/94c76196642dbeae634c7a61ba3da88cd77bed875bf6e4a8bed037505aa6/yarl-1.22.0-cp310-cp310-win_amd64.whl", hash = "sha256:cb95a9b1adaa48e41815a55ae740cfda005758104049a640a398120bf02515ca", size = 86852, upload-time = "2025-10-06T14:09:12.958Z" },
+ { url = "https://files.pythonhosted.org/packages/af/af/7df4f179d3b1a6dcb9a4bd2ffbc67642746fcafdb62580e66876ce83fff4/yarl-1.22.0-cp310-cp310-win_arm64.whl", hash = "sha256:b85b982afde6df99ecc996990d4ad7ccbdbb70e2a4ba4de0aecde5922ba98a0b", size = 82012, upload-time = "2025-10-06T14:09:14.664Z" },
+ { url = "https://files.pythonhosted.org/packages/4d/27/5ab13fc84c76a0250afd3d26d5936349a35be56ce5785447d6c423b26d92/yarl-1.22.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:1ab72135b1f2db3fed3997d7e7dc1b80573c67138023852b6efb336a5eae6511", size = 141607, upload-time = "2025-10-06T14:09:16.298Z" },
+ { url = "https://files.pythonhosted.org/packages/6a/a1/d065d51d02dc02ce81501d476b9ed2229d9a990818332242a882d5d60340/yarl-1.22.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:669930400e375570189492dc8d8341301578e8493aec04aebc20d4717f899dd6", size = 94027, upload-time = "2025-10-06T14:09:17.786Z" },
+ { url = "https://files.pythonhosted.org/packages/c1/da/8da9f6a53f67b5106ffe902c6fa0164e10398d4e150d85838b82f424072a/yarl-1.22.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:792a2af6d58177ef7c19cbf0097aba92ca1b9cb3ffdd9c7470e156c8f9b5e028", size = 94963, upload-time = "2025-10-06T14:09:19.662Z" },
+ { url = "https://files.pythonhosted.org/packages/68/fe/2c1f674960c376e29cb0bec1249b117d11738db92a6ccc4a530b972648db/yarl-1.22.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3ea66b1c11c9150f1372f69afb6b8116f2dd7286f38e14ea71a44eee9ec51b9d", size = 368406, upload-time = "2025-10-06T14:09:21.402Z" },
+ { url = "https://files.pythonhosted.org/packages/95/26/812a540e1c3c6418fec60e9bbd38e871eaba9545e94fa5eff8f4a8e28e1e/yarl-1.22.0-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:3e2daa88dc91870215961e96a039ec73e4937da13cf77ce17f9cad0c18df3503", size = 336581, upload-time = "2025-10-06T14:09:22.98Z" },
+ { url = "https://files.pythonhosted.org/packages/0b/f5/5777b19e26fdf98563985e481f8be3d8a39f8734147a6ebf459d0dab5a6b/yarl-1.22.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ba440ae430c00eee41509353628600212112cd5018d5def7e9b05ea7ac34eb65", size = 388924, upload-time = "2025-10-06T14:09:24.655Z" },
+ { url = "https://files.pythonhosted.org/packages/86/08/24bd2477bd59c0bbd994fe1d93b126e0472e4e3df5a96a277b0a55309e89/yarl-1.22.0-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:e6438cc8f23a9c1478633d216b16104a586b9761db62bfacb6425bac0a36679e", size = 392890, upload-time = "2025-10-06T14:09:26.617Z" },
+ { url = "https://files.pythonhosted.org/packages/46/00/71b90ed48e895667ecfb1eaab27c1523ee2fa217433ed77a73b13205ca4b/yarl-1.22.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4c52a6e78aef5cf47a98ef8e934755abf53953379b7d53e68b15ff4420e6683d", size = 365819, upload-time = "2025-10-06T14:09:28.544Z" },
+ { url = "https://files.pythonhosted.org/packages/30/2d/f715501cae832651d3282387c6a9236cd26bd00d0ff1e404b3dc52447884/yarl-1.22.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:3b06bcadaac49c70f4c88af4ffcfbe3dc155aab3163e75777818092478bcbbe7", size = 363601, upload-time = "2025-10-06T14:09:30.568Z" },
+ { url = "https://files.pythonhosted.org/packages/f8/f9/a678c992d78e394e7126ee0b0e4e71bd2775e4334d00a9278c06a6cce96a/yarl-1.22.0-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:6944b2dc72c4d7f7052683487e3677456050ff77fcf5e6204e98caf785ad1967", size = 358072, upload-time = "2025-10-06T14:09:32.528Z" },
+ { url = "https://files.pythonhosted.org/packages/2c/d1/b49454411a60edb6fefdcad4f8e6dbba7d8019e3a508a1c5836cba6d0781/yarl-1.22.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:d5372ca1df0f91a86b047d1277c2aaf1edb32d78bbcefffc81b40ffd18f027ed", size = 385311, upload-time = "2025-10-06T14:09:34.634Z" },
+ { url = "https://files.pythonhosted.org/packages/87/e5/40d7a94debb8448c7771a916d1861d6609dddf7958dc381117e7ba36d9e8/yarl-1.22.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:51af598701f5299012b8416486b40fceef8c26fc87dc6d7d1f6fc30609ea0aa6", size = 381094, upload-time = "2025-10-06T14:09:36.268Z" },
+ { url = "https://files.pythonhosted.org/packages/35/d8/611cc282502381ad855448643e1ad0538957fc82ae83dfe7762c14069e14/yarl-1.22.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:b266bd01fedeffeeac01a79ae181719ff848a5a13ce10075adbefc8f1daee70e", size = 370944, upload-time = "2025-10-06T14:09:37.872Z" },
+ { url = "https://files.pythonhosted.org/packages/2d/df/fadd00fb1c90e1a5a8bd731fa3d3de2e165e5a3666a095b04e31b04d9cb6/yarl-1.22.0-cp311-cp311-win32.whl", hash = "sha256:a9b1ba5610a4e20f655258d5a1fdc7ebe3d837bb0e45b581398b99eb98b1f5ca", size = 81804, upload-time = "2025-10-06T14:09:39.359Z" },
+ { url = "https://files.pythonhosted.org/packages/b5/f7/149bb6f45f267cb5c074ac40c01c6b3ea6d8a620d34b337f6321928a1b4d/yarl-1.22.0-cp311-cp311-win_amd64.whl", hash = "sha256:078278b9b0b11568937d9509b589ee83ef98ed6d561dfe2020e24a9fd08eaa2b", size = 86858, upload-time = "2025-10-06T14:09:41.068Z" },
+ { url = "https://files.pythonhosted.org/packages/2b/13/88b78b93ad3f2f0b78e13bfaaa24d11cbc746e93fe76d8c06bf139615646/yarl-1.22.0-cp311-cp311-win_arm64.whl", hash = "sha256:b6a6f620cfe13ccec221fa312139135166e47ae169f8253f72a0abc0dae94376", size = 81637, upload-time = "2025-10-06T14:09:42.712Z" },
+ { url = "https://files.pythonhosted.org/packages/75/ff/46736024fee3429b80a165a732e38e5d5a238721e634ab41b040d49f8738/yarl-1.22.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:e340382d1afa5d32b892b3ff062436d592ec3d692aeea3bef3a5cfe11bbf8c6f", size = 142000, upload-time = "2025-10-06T14:09:44.631Z" },
+ { url = "https://files.pythonhosted.org/packages/5a/9a/b312ed670df903145598914770eb12de1bac44599549b3360acc96878df8/yarl-1.22.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:f1e09112a2c31ffe8d80be1b0988fa6a18c5d5cad92a9ffbb1c04c91bfe52ad2", size = 94338, upload-time = "2025-10-06T14:09:46.372Z" },
+ { url = "https://files.pythonhosted.org/packages/ba/f5/0601483296f09c3c65e303d60c070a5c19fcdbc72daa061e96170785bc7d/yarl-1.22.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:939fe60db294c786f6b7c2d2e121576628468f65453d86b0fe36cb52f987bd74", size = 94909, upload-time = "2025-10-06T14:09:48.648Z" },
+ { url = "https://files.pythonhosted.org/packages/60/41/9a1fe0b73dbcefce72e46cf149b0e0a67612d60bfc90fb59c2b2efdfbd86/yarl-1.22.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e1651bf8e0398574646744c1885a41198eba53dc8a9312b954073f845c90a8df", size = 372940, upload-time = "2025-10-06T14:09:50.089Z" },
+ { url = "https://files.pythonhosted.org/packages/17/7a/795cb6dfee561961c30b800f0ed616b923a2ec6258b5def2a00bf8231334/yarl-1.22.0-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:b8a0588521a26bf92a57a1705b77b8b59044cdceccac7151bd8d229e66b8dedb", size = 345825, upload-time = "2025-10-06T14:09:52.142Z" },
+ { url = "https://files.pythonhosted.org/packages/d7/93/a58f4d596d2be2ae7bab1a5846c4d270b894958845753b2c606d666744d3/yarl-1.22.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:42188e6a615c1a75bcaa6e150c3fe8f3e8680471a6b10150c5f7e83f47cc34d2", size = 386705, upload-time = "2025-10-06T14:09:54.128Z" },
+ { url = "https://files.pythonhosted.org/packages/61/92/682279d0e099d0e14d7fd2e176bd04f48de1484f56546a3e1313cd6c8e7c/yarl-1.22.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f6d2cb59377d99718913ad9a151030d6f83ef420a2b8f521d94609ecc106ee82", size = 396518, upload-time = "2025-10-06T14:09:55.762Z" },
+ { url = "https://files.pythonhosted.org/packages/db/0f/0d52c98b8a885aeda831224b78f3be7ec2e1aa4a62091f9f9188c3c65b56/yarl-1.22.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:50678a3b71c751d58d7908edc96d332af328839eea883bb554a43f539101277a", size = 377267, upload-time = "2025-10-06T14:09:57.958Z" },
+ { url = "https://files.pythonhosted.org/packages/22/42/d2685e35908cbeaa6532c1fc73e89e7f2efb5d8a7df3959ea8e37177c5a3/yarl-1.22.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:1e8fbaa7cec507aa24ea27a01456e8dd4b6fab829059b69844bd348f2d467124", size = 365797, upload-time = "2025-10-06T14:09:59.527Z" },
+ { url = "https://files.pythonhosted.org/packages/a2/83/cf8c7bcc6355631762f7d8bdab920ad09b82efa6b722999dfb05afa6cfac/yarl-1.22.0-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:433885ab5431bc3d3d4f2f9bd15bfa1614c522b0f1405d62c4f926ccd69d04fa", size = 365535, upload-time = "2025-10-06T14:10:01.139Z" },
+ { url = "https://files.pythonhosted.org/packages/25/e1/5302ff9b28f0c59cac913b91fe3f16c59a033887e57ce9ca5d41a3a94737/yarl-1.22.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:b790b39c7e9a4192dc2e201a282109ed2985a1ddbd5ac08dc56d0e121400a8f7", size = 382324, upload-time = "2025-10-06T14:10:02.756Z" },
+ { url = "https://files.pythonhosted.org/packages/bf/cd/4617eb60f032f19ae3a688dc990d8f0d89ee0ea378b61cac81ede3e52fae/yarl-1.22.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:31f0b53913220599446872d757257be5898019c85e7971599065bc55065dc99d", size = 383803, upload-time = "2025-10-06T14:10:04.552Z" },
+ { url = "https://files.pythonhosted.org/packages/59/65/afc6e62bb506a319ea67b694551dab4a7e6fb7bf604e9bd9f3e11d575fec/yarl-1.22.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:a49370e8f711daec68d09b821a34e1167792ee2d24d405cbc2387be4f158b520", size = 374220, upload-time = "2025-10-06T14:10:06.489Z" },
+ { url = "https://files.pythonhosted.org/packages/e7/3d/68bf18d50dc674b942daec86a9ba922d3113d8399b0e52b9897530442da2/yarl-1.22.0-cp312-cp312-win32.whl", hash = "sha256:70dfd4f241c04bd9239d53b17f11e6ab672b9f1420364af63e8531198e3f5fe8", size = 81589, upload-time = "2025-10-06T14:10:09.254Z" },
+ { url = "https://files.pythonhosted.org/packages/c8/9a/6ad1a9b37c2f72874f93e691b2e7ecb6137fb2b899983125db4204e47575/yarl-1.22.0-cp312-cp312-win_amd64.whl", hash = "sha256:8884d8b332a5e9b88e23f60bb166890009429391864c685e17bd73a9eda9105c", size = 87213, upload-time = "2025-10-06T14:10:11.369Z" },
+ { url = "https://files.pythonhosted.org/packages/44/c5/c21b562d1680a77634d748e30c653c3ca918beb35555cff24986fff54598/yarl-1.22.0-cp312-cp312-win_arm64.whl", hash = "sha256:ea70f61a47f3cc93bdf8b2f368ed359ef02a01ca6393916bc8ff877427181e74", size = 81330, upload-time = "2025-10-06T14:10:13.112Z" },
+ { url = "https://files.pythonhosted.org/packages/ea/f3/d67de7260456ee105dc1d162d43a019ecad6b91e2f51809d6cddaa56690e/yarl-1.22.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:8dee9c25c74997f6a750cd317b8ca63545169c098faee42c84aa5e506c819b53", size = 139980, upload-time = "2025-10-06T14:10:14.601Z" },
+ { url = "https://files.pythonhosted.org/packages/01/88/04d98af0b47e0ef42597b9b28863b9060bb515524da0a65d5f4db160b2d5/yarl-1.22.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:01e73b85a5434f89fc4fe27dcda2aff08ddf35e4d47bbbea3bdcd25321af538a", size = 93424, upload-time = "2025-10-06T14:10:16.115Z" },
+ { url = "https://files.pythonhosted.org/packages/18/91/3274b215fd8442a03975ce6bee5fe6aa57a8326b29b9d3d56234a1dca244/yarl-1.22.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:22965c2af250d20c873cdbee8ff958fb809940aeb2e74ba5f20aaf6b7ac8c70c", size = 93821, upload-time = "2025-10-06T14:10:17.993Z" },
+ { url = "https://files.pythonhosted.org/packages/61/3a/caf4e25036db0f2da4ca22a353dfeb3c9d3c95d2761ebe9b14df8fc16eb0/yarl-1.22.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b4f15793aa49793ec8d1c708ab7f9eded1aa72edc5174cae703651555ed1b601", size = 373243, upload-time = "2025-10-06T14:10:19.44Z" },
+ { url = "https://files.pythonhosted.org/packages/6e/9e/51a77ac7516e8e7803b06e01f74e78649c24ee1021eca3d6a739cb6ea49c/yarl-1.22.0-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:e5542339dcf2747135c5c85f68680353d5cb9ffd741c0f2e8d832d054d41f35a", size = 342361, upload-time = "2025-10-06T14:10:21.124Z" },
+ { url = "https://files.pythonhosted.org/packages/d4/f8/33b92454789dde8407f156c00303e9a891f1f51a0330b0fad7c909f87692/yarl-1.22.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:5c401e05ad47a75869c3ab3e35137f8468b846770587e70d71e11de797d113df", size = 387036, upload-time = "2025-10-06T14:10:22.902Z" },
+ { url = "https://files.pythonhosted.org/packages/d9/9a/c5db84ea024f76838220280f732970aa4ee154015d7f5c1bfb60a267af6f/yarl-1.22.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:243dda95d901c733f5b59214d28b0120893d91777cb8aa043e6ef059d3cddfe2", size = 397671, upload-time = "2025-10-06T14:10:24.523Z" },
+ { url = "https://files.pythonhosted.org/packages/11/c9/cd8538dc2e7727095e0c1d867bad1e40c98f37763e6d995c1939f5fdc7b1/yarl-1.22.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bec03d0d388060058f5d291a813f21c011041938a441c593374da6077fe21b1b", size = 377059, upload-time = "2025-10-06T14:10:26.406Z" },
+ { url = "https://files.pythonhosted.org/packages/a1/b9/ab437b261702ced75122ed78a876a6dec0a1b0f5e17a4ac7a9a2482d8abe/yarl-1.22.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:b0748275abb8c1e1e09301ee3cf90c8a99678a4e92e4373705f2a2570d581273", size = 365356, upload-time = "2025-10-06T14:10:28.461Z" },
+ { url = "https://files.pythonhosted.org/packages/b2/9d/8e1ae6d1d008a9567877b08f0ce4077a29974c04c062dabdb923ed98e6fe/yarl-1.22.0-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:47fdb18187e2a4e18fda2c25c05d8251a9e4a521edaed757fef033e7d8498d9a", size = 361331, upload-time = "2025-10-06T14:10:30.541Z" },
+ { url = "https://files.pythonhosted.org/packages/ca/5a/09b7be3905962f145b73beb468cdd53db8aa171cf18c80400a54c5b82846/yarl-1.22.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:c7044802eec4524fde550afc28edda0dd5784c4c45f0be151a2d3ba017daca7d", size = 382590, upload-time = "2025-10-06T14:10:33.352Z" },
+ { url = "https://files.pythonhosted.org/packages/aa/7f/59ec509abf90eda5048b0bc3e2d7b5099dffdb3e6b127019895ab9d5ef44/yarl-1.22.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:139718f35149ff544caba20fce6e8a2f71f1e39b92c700d8438a0b1d2a631a02", size = 385316, upload-time = "2025-10-06T14:10:35.034Z" },
+ { url = "https://files.pythonhosted.org/packages/e5/84/891158426bc8036bfdfd862fabd0e0fa25df4176ec793e447f4b85cf1be4/yarl-1.22.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e1b51bebd221006d3d2f95fbe124b22b247136647ae5dcc8c7acafba66e5ee67", size = 374431, upload-time = "2025-10-06T14:10:37.76Z" },
+ { url = "https://files.pythonhosted.org/packages/bb/49/03da1580665baa8bef5e8ed34c6df2c2aca0a2f28bf397ed238cc1bbc6f2/yarl-1.22.0-cp313-cp313-win32.whl", hash = "sha256:d3e32536234a95f513bd374e93d717cf6b2231a791758de6c509e3653f234c95", size = 81555, upload-time = "2025-10-06T14:10:39.649Z" },
+ { url = "https://files.pythonhosted.org/packages/9a/ee/450914ae11b419eadd067c6183ae08381cfdfcb9798b90b2b713bbebddda/yarl-1.22.0-cp313-cp313-win_amd64.whl", hash = "sha256:47743b82b76d89a1d20b83e60d5c20314cbd5ba2befc9cda8f28300c4a08ed4d", size = 86965, upload-time = "2025-10-06T14:10:41.313Z" },
+ { url = "https://files.pythonhosted.org/packages/98/4d/264a01eae03b6cf629ad69bae94e3b0e5344741e929073678e84bf7a3e3b/yarl-1.22.0-cp313-cp313-win_arm64.whl", hash = "sha256:5d0fcda9608875f7d052eff120c7a5da474a6796fe4d83e152e0e4d42f6d1a9b", size = 81205, upload-time = "2025-10-06T14:10:43.167Z" },
+ { url = "https://files.pythonhosted.org/packages/88/fc/6908f062a2f77b5f9f6d69cecb1747260831ff206adcbc5b510aff88df91/yarl-1.22.0-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:719ae08b6972befcba4310e49edb1161a88cdd331e3a694b84466bd938a6ab10", size = 146209, upload-time = "2025-10-06T14:10:44.643Z" },
+ { url = "https://files.pythonhosted.org/packages/65/47/76594ae8eab26210b4867be6f49129861ad33da1f1ebdf7051e98492bf62/yarl-1.22.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:47d8a5c446df1c4db9d21b49619ffdba90e77c89ec6e283f453856c74b50b9e3", size = 95966, upload-time = "2025-10-06T14:10:46.554Z" },
+ { url = "https://files.pythonhosted.org/packages/ab/ce/05e9828a49271ba6b5b038b15b3934e996980dd78abdfeb52a04cfb9467e/yarl-1.22.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:cfebc0ac8333520d2d0423cbbe43ae43c8838862ddb898f5ca68565e395516e9", size = 97312, upload-time = "2025-10-06T14:10:48.007Z" },
+ { url = "https://files.pythonhosted.org/packages/d1/c5/7dffad5e4f2265b29c9d7ec869c369e4223166e4f9206fc2243ee9eea727/yarl-1.22.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4398557cbf484207df000309235979c79c4356518fd5c99158c7d38203c4da4f", size = 361967, upload-time = "2025-10-06T14:10:49.997Z" },
+ { url = "https://files.pythonhosted.org/packages/50/b2/375b933c93a54bff7fc041e1a6ad2c0f6f733ffb0c6e642ce56ee3b39970/yarl-1.22.0-cp313-cp313t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:2ca6fd72a8cd803be290d42f2dec5cdcd5299eeb93c2d929bf060ad9efaf5de0", size = 323949, upload-time = "2025-10-06T14:10:52.004Z" },
+ { url = "https://files.pythonhosted.org/packages/66/50/bfc2a29a1d78644c5a7220ce2f304f38248dc94124a326794e677634b6cf/yarl-1.22.0-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ca1f59c4e1ab6e72f0a23c13fca5430f889634166be85dbf1013683e49e3278e", size = 361818, upload-time = "2025-10-06T14:10:54.078Z" },
+ { url = "https://files.pythonhosted.org/packages/46/96/f3941a46af7d5d0f0498f86d71275696800ddcdd20426298e572b19b91ff/yarl-1.22.0-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:6c5010a52015e7c70f86eb967db0f37f3c8bd503a695a49f8d45700144667708", size = 372626, upload-time = "2025-10-06T14:10:55.767Z" },
+ { url = "https://files.pythonhosted.org/packages/c1/42/8b27c83bb875cd89448e42cd627e0fb971fa1675c9ec546393d18826cb50/yarl-1.22.0-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9d7672ecf7557476642c88497c2f8d8542f8e36596e928e9bcba0e42e1e7d71f", size = 341129, upload-time = "2025-10-06T14:10:57.985Z" },
+ { url = "https://files.pythonhosted.org/packages/49/36/99ca3122201b382a3cf7cc937b95235b0ac944f7e9f2d5331d50821ed352/yarl-1.22.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:3b7c88eeef021579d600e50363e0b6ee4f7f6f728cd3486b9d0f3ee7b946398d", size = 346776, upload-time = "2025-10-06T14:10:59.633Z" },
+ { url = "https://files.pythonhosted.org/packages/85/b4/47328bf996acd01a4c16ef9dcd2f59c969f495073616586f78cd5f2efb99/yarl-1.22.0-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:f4afb5c34f2c6fecdcc182dfcfc6af6cccf1aa923eed4d6a12e9d96904e1a0d8", size = 334879, upload-time = "2025-10-06T14:11:01.454Z" },
+ { url = "https://files.pythonhosted.org/packages/c2/ad/b77d7b3f14a4283bffb8e92c6026496f6de49751c2f97d4352242bba3990/yarl-1.22.0-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:59c189e3e99a59cf8d83cbb31d4db02d66cda5a1a4374e8a012b51255341abf5", size = 350996, upload-time = "2025-10-06T14:11:03.452Z" },
+ { url = "https://files.pythonhosted.org/packages/81/c8/06e1d69295792ba54d556f06686cbd6a7ce39c22307100e3fb4a2c0b0a1d/yarl-1.22.0-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:5a3bf7f62a289fa90f1990422dc8dff5a458469ea71d1624585ec3a4c8d6960f", size = 356047, upload-time = "2025-10-06T14:11:05.115Z" },
+ { url = "https://files.pythonhosted.org/packages/4b/b8/4c0e9e9f597074b208d18cef227d83aac36184bfbc6eab204ea55783dbc5/yarl-1.22.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:de6b9a04c606978fdfe72666fa216ffcf2d1a9f6a381058d4378f8d7b1e5de62", size = 342947, upload-time = "2025-10-06T14:11:08.137Z" },
+ { url = "https://files.pythonhosted.org/packages/e0/e5/11f140a58bf4c6ad7aca69a892bff0ee638c31bea4206748fc0df4ebcb3a/yarl-1.22.0-cp313-cp313t-win32.whl", hash = "sha256:1834bb90991cc2999f10f97f5f01317f99b143284766d197e43cd5b45eb18d03", size = 86943, upload-time = "2025-10-06T14:11:10.284Z" },
+ { url = "https://files.pythonhosted.org/packages/31/74/8b74bae38ed7fe6793d0c15a0c8207bbb819cf287788459e5ed230996cdd/yarl-1.22.0-cp313-cp313t-win_amd64.whl", hash = "sha256:ff86011bd159a9d2dfc89c34cfd8aff12875980e3bd6a39ff097887520e60249", size = 93715, upload-time = "2025-10-06T14:11:11.739Z" },
+ { url = "https://files.pythonhosted.org/packages/69/66/991858aa4b5892d57aef7ee1ba6b4d01ec3b7eb3060795d34090a3ca3278/yarl-1.22.0-cp313-cp313t-win_arm64.whl", hash = "sha256:7861058d0582b847bc4e3a4a4c46828a410bca738673f35a29ba3ca5db0b473b", size = 83857, upload-time = "2025-10-06T14:11:13.586Z" },
+ { url = "https://files.pythonhosted.org/packages/46/b3/e20ef504049f1a1c54a814b4b9bed96d1ac0e0610c3b4da178f87209db05/yarl-1.22.0-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:34b36c2c57124530884d89d50ed2c1478697ad7473efd59cfd479945c95650e4", size = 140520, upload-time = "2025-10-06T14:11:15.465Z" },
+ { url = "https://files.pythonhosted.org/packages/e4/04/3532d990fdbab02e5ede063676b5c4260e7f3abea2151099c2aa745acc4c/yarl-1.22.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:0dd9a702591ca2e543631c2a017e4a547e38a5c0f29eece37d9097e04a7ac683", size = 93504, upload-time = "2025-10-06T14:11:17.106Z" },
+ { url = "https://files.pythonhosted.org/packages/11/63/ff458113c5c2dac9a9719ac68ee7c947cb621432bcf28c9972b1c0e83938/yarl-1.22.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:594fcab1032e2d2cc3321bb2e51271e7cd2b516c7d9aee780ece81b07ff8244b", size = 94282, upload-time = "2025-10-06T14:11:19.064Z" },
+ { url = "https://files.pythonhosted.org/packages/a7/bc/315a56aca762d44a6aaaf7ad253f04d996cb6b27bad34410f82d76ea8038/yarl-1.22.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f3d7a87a78d46a2e3d5b72587ac14b4c16952dd0887dbb051451eceac774411e", size = 372080, upload-time = "2025-10-06T14:11:20.996Z" },
+ { url = "https://files.pythonhosted.org/packages/3f/3f/08e9b826ec2e099ea6e7c69a61272f4f6da62cb5b1b63590bb80ca2e4a40/yarl-1.22.0-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:852863707010316c973162e703bddabec35e8757e67fcb8ad58829de1ebc8590", size = 338696, upload-time = "2025-10-06T14:11:22.847Z" },
+ { url = "https://files.pythonhosted.org/packages/e3/9f/90360108e3b32bd76789088e99538febfea24a102380ae73827f62073543/yarl-1.22.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:131a085a53bfe839a477c0845acf21efc77457ba2bcf5899618136d64f3303a2", size = 387121, upload-time = "2025-10-06T14:11:24.889Z" },
+ { url = "https://files.pythonhosted.org/packages/98/92/ab8d4657bd5b46a38094cfaea498f18bb70ce6b63508fd7e909bd1f93066/yarl-1.22.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:078a8aefd263f4d4f923a9677b942b445a2be970ca24548a8102689a3a8ab8da", size = 394080, upload-time = "2025-10-06T14:11:27.307Z" },
+ { url = "https://files.pythonhosted.org/packages/f5/e7/d8c5a7752fef68205296201f8ec2bf718f5c805a7a7e9880576c67600658/yarl-1.22.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bca03b91c323036913993ff5c738d0842fc9c60c4648e5c8d98331526df89784", size = 372661, upload-time = "2025-10-06T14:11:29.387Z" },
+ { url = "https://files.pythonhosted.org/packages/b6/2e/f4d26183c8db0bb82d491b072f3127fb8c381a6206a3a56332714b79b751/yarl-1.22.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:68986a61557d37bb90d3051a45b91fa3d5c516d177dfc6dd6f2f436a07ff2b6b", size = 364645, upload-time = "2025-10-06T14:11:31.423Z" },
+ { url = "https://files.pythonhosted.org/packages/80/7c/428e5812e6b87cd00ee8e898328a62c95825bf37c7fa87f0b6bb2ad31304/yarl-1.22.0-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:4792b262d585ff0dff6bcb787f8492e40698443ec982a3568c2096433660c694", size = 355361, upload-time = "2025-10-06T14:11:33.055Z" },
+ { url = "https://files.pythonhosted.org/packages/ec/2a/249405fd26776f8b13c067378ef4d7dd49c9098d1b6457cdd152a99e96a9/yarl-1.22.0-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:ebd4549b108d732dba1d4ace67614b9545b21ece30937a63a65dd34efa19732d", size = 381451, upload-time = "2025-10-06T14:11:35.136Z" },
+ { url = "https://files.pythonhosted.org/packages/67/a8/fb6b1adbe98cf1e2dd9fad71003d3a63a1bc22459c6e15f5714eb9323b93/yarl-1.22.0-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:f87ac53513d22240c7d59203f25cc3beac1e574c6cd681bbfd321987b69f95fd", size = 383814, upload-time = "2025-10-06T14:11:37.094Z" },
+ { url = "https://files.pythonhosted.org/packages/d9/f9/3aa2c0e480fb73e872ae2814c43bc1e734740bb0d54e8cb2a95925f98131/yarl-1.22.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:22b029f2881599e2f1b06f8f1db2ee63bd309e2293ba2d566e008ba12778b8da", size = 370799, upload-time = "2025-10-06T14:11:38.83Z" },
+ { url = "https://files.pythonhosted.org/packages/50/3c/af9dba3b8b5eeb302f36f16f92791f3ea62e3f47763406abf6d5a4a3333b/yarl-1.22.0-cp314-cp314-win32.whl", hash = "sha256:6a635ea45ba4ea8238463b4f7d0e721bad669f80878b7bfd1f89266e2ae63da2", size = 82990, upload-time = "2025-10-06T14:11:40.624Z" },
+ { url = "https://files.pythonhosted.org/packages/ac/30/ac3a0c5bdc1d6efd1b41fa24d4897a4329b3b1e98de9449679dd327af4f0/yarl-1.22.0-cp314-cp314-win_amd64.whl", hash = "sha256:0d6e6885777af0f110b0e5d7e5dda8b704efed3894da26220b7f3d887b839a79", size = 88292, upload-time = "2025-10-06T14:11:42.578Z" },
+ { url = "https://files.pythonhosted.org/packages/df/0a/227ab4ff5b998a1b7410abc7b46c9b7a26b0ca9e86c34ba4b8d8bc7c63d5/yarl-1.22.0-cp314-cp314-win_arm64.whl", hash = "sha256:8218f4e98d3c10d683584cb40f0424f4b9fd6e95610232dd75e13743b070ee33", size = 82888, upload-time = "2025-10-06T14:11:44.863Z" },
+ { url = "https://files.pythonhosted.org/packages/06/5e/a15eb13db90abd87dfbefb9760c0f3f257ac42a5cac7e75dbc23bed97a9f/yarl-1.22.0-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:45c2842ff0e0d1b35a6bf1cd6c690939dacb617a70827f715232b2e0494d55d1", size = 146223, upload-time = "2025-10-06T14:11:46.796Z" },
+ { url = "https://files.pythonhosted.org/packages/18/82/9665c61910d4d84f41a5bf6837597c89e665fa88aa4941080704645932a9/yarl-1.22.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:d947071e6ebcf2e2bee8fce76e10faca8f7a14808ca36a910263acaacef08eca", size = 95981, upload-time = "2025-10-06T14:11:48.845Z" },
+ { url = "https://files.pythonhosted.org/packages/5d/9a/2f65743589809af4d0a6d3aa749343c4b5f4c380cc24a8e94a3c6625a808/yarl-1.22.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:334b8721303e61b00019474cc103bdac3d7b1f65e91f0bfedeec2d56dfe74b53", size = 97303, upload-time = "2025-10-06T14:11:50.897Z" },
+ { url = "https://files.pythonhosted.org/packages/b0/ab/5b13d3e157505c43c3b43b5a776cbf7b24a02bc4cccc40314771197e3508/yarl-1.22.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1e7ce67c34138a058fd092f67d07a72b8e31ff0c9236e751957465a24b28910c", size = 361820, upload-time = "2025-10-06T14:11:52.549Z" },
+ { url = "https://files.pythonhosted.org/packages/fb/76/242a5ef4677615cf95330cfc1b4610e78184400699bdda0acb897ef5e49a/yarl-1.22.0-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:d77e1b2c6d04711478cb1c4ab90db07f1609ccf06a287d5607fcd90dc9863acf", size = 323203, upload-time = "2025-10-06T14:11:54.225Z" },
+ { url = "https://files.pythonhosted.org/packages/8c/96/475509110d3f0153b43d06164cf4195c64d16999e0c7e2d8a099adcd6907/yarl-1.22.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c4647674b6150d2cae088fc07de2738a84b8bcedebef29802cf0b0a82ab6face", size = 363173, upload-time = "2025-10-06T14:11:56.069Z" },
+ { url = "https://files.pythonhosted.org/packages/c9/66/59db471aecfbd559a1fd48aedd954435558cd98c7d0da8b03cc6c140a32c/yarl-1.22.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:efb07073be061c8f79d03d04139a80ba33cbd390ca8f0297aae9cce6411e4c6b", size = 373562, upload-time = "2025-10-06T14:11:58.783Z" },
+ { url = "https://files.pythonhosted.org/packages/03/1f/c5d94abc91557384719da10ff166b916107c1b45e4d0423a88457071dd88/yarl-1.22.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e51ac5435758ba97ad69617e13233da53908beccc6cfcd6c34bbed8dcbede486", size = 339828, upload-time = "2025-10-06T14:12:00.686Z" },
+ { url = "https://files.pythonhosted.org/packages/5f/97/aa6a143d3afba17b6465733681c70cf175af89f76ec8d9286e08437a7454/yarl-1.22.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:33e32a0dd0c8205efa8e83d04fc9f19313772b78522d1bdc7d9aed706bfd6138", size = 347551, upload-time = "2025-10-06T14:12:02.628Z" },
+ { url = "https://files.pythonhosted.org/packages/43/3c/45a2b6d80195959239a7b2a8810506d4eea5487dce61c2a3393e7fc3c52e/yarl-1.22.0-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:bf4a21e58b9cde0e401e683ebd00f6ed30a06d14e93f7c8fd059f8b6e8f87b6a", size = 334512, upload-time = "2025-10-06T14:12:04.871Z" },
+ { url = "https://files.pythonhosted.org/packages/86/a0/c2ab48d74599c7c84cb104ebd799c5813de252bea0f360ffc29d270c2caa/yarl-1.22.0-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:e4b582bab49ac33c8deb97e058cd67c2c50dac0dd134874106d9c774fd272529", size = 352400, upload-time = "2025-10-06T14:12:06.624Z" },
+ { url = "https://files.pythonhosted.org/packages/32/75/f8919b2eafc929567d3d8411f72bdb1a2109c01caaab4ebfa5f8ffadc15b/yarl-1.22.0-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:0b5bcc1a9c4839e7e30b7b30dd47fe5e7e44fb7054ec29b5bb8d526aa1041093", size = 357140, upload-time = "2025-10-06T14:12:08.362Z" },
+ { url = "https://files.pythonhosted.org/packages/cf/72/6a85bba382f22cf78add705d8c3731748397d986e197e53ecc7835e76de7/yarl-1.22.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:c0232bce2170103ec23c454e54a57008a9a72b5d1c3105dc2496750da8cfa47c", size = 341473, upload-time = "2025-10-06T14:12:10.994Z" },
+ { url = "https://files.pythonhosted.org/packages/35/18/55e6011f7c044dc80b98893060773cefcfdbf60dfefb8cb2f58b9bacbd83/yarl-1.22.0-cp314-cp314t-win32.whl", hash = "sha256:8009b3173bcd637be650922ac455946197d858b3630b6d8787aa9e5c4564533e", size = 89056, upload-time = "2025-10-06T14:12:13.317Z" },
+ { url = "https://files.pythonhosted.org/packages/f9/86/0f0dccb6e59a9e7f122c5afd43568b1d31b8ab7dda5f1b01fb5c7025c9a9/yarl-1.22.0-cp314-cp314t-win_amd64.whl", hash = "sha256:9fb17ea16e972c63d25d4a97f016d235c78dd2344820eb35bc034bc32012ee27", size = 96292, upload-time = "2025-10-06T14:12:15.398Z" },
+ { url = "https://files.pythonhosted.org/packages/48/b7/503c98092fb3b344a179579f55814b613c1fbb1c23b3ec14a7b008a66a6e/yarl-1.22.0-cp314-cp314t-win_arm64.whl", hash = "sha256:9f6d73c1436b934e3f01df1e1b21ff765cd1d28c77dfb9ace207f746d4610ee1", size = 85171, upload-time = "2025-10-06T14:12:16.935Z" },
+ { url = "https://files.pythonhosted.org/packages/94/fd/6480106702a79bcceda5fd9c63cb19a04a6506bd5ce7fd8d9b63742f0021/yarl-1.22.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:3aa27acb6de7a23785d81557577491f6c38a5209a254d1191519d07d8fe51748", size = 141301, upload-time = "2025-10-06T14:12:19.01Z" },
+ { url = "https://files.pythonhosted.org/packages/42/e1/6d95d21b17a93e793e4ec420a925fe1f6a9342338ca7a563ed21129c0990/yarl-1.22.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:af74f05666a5e531289cb1cc9c883d1de2088b8e5b4de48004e5ca8a830ac859", size = 93864, upload-time = "2025-10-06T14:12:21.05Z" },
+ { url = "https://files.pythonhosted.org/packages/32/58/b8055273c203968e89808413ea4c984988b6649baabf10f4522e67c22d2f/yarl-1.22.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:62441e55958977b8167b2709c164c91a6363e25da322d87ae6dd9c6019ceecf9", size = 94706, upload-time = "2025-10-06T14:12:23.287Z" },
+ { url = "https://files.pythonhosted.org/packages/18/91/d7bfbc28a88c2895ecd0da6a874def0c147de78afc52c773c28e1aa233a3/yarl-1.22.0-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b580e71cac3f8113d3135888770903eaf2f507e9421e5697d6ee6d8cd1c7f054", size = 347100, upload-time = "2025-10-06T14:12:28.527Z" },
+ { url = "https://files.pythonhosted.org/packages/bd/e8/37a1e7b99721c0564b1fc7b0a4d1f595ef6fb8060d82ca61775b644185f7/yarl-1.22.0-cp39-cp39-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:e81fda2fb4a07eda1a2252b216aa0df23ebcd4d584894e9612e80999a78fd95b", size = 318902, upload-time = "2025-10-06T14:12:30.528Z" },
+ { url = "https://files.pythonhosted.org/packages/1c/ef/34724449d7ef2db4f22df644f2dac0b8a275d20f585e526937b3ae47b02d/yarl-1.22.0-cp39-cp39-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:99b6fc1d55782461b78221e95fc357b47ad98b041e8e20f47c1411d0aacddc60", size = 363302, upload-time = "2025-10-06T14:12:32.295Z" },
+ { url = "https://files.pythonhosted.org/packages/8a/04/88a39a5dad39889f192cce8d66cc4c58dbeca983e83f9b6bf23822a7ed91/yarl-1.22.0-cp39-cp39-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:088e4e08f033db4be2ccd1f34cf29fe994772fb54cfe004bbf54db320af56890", size = 370816, upload-time = "2025-10-06T14:12:34.01Z" },
+ { url = "https://files.pythonhosted.org/packages/6b/1f/5e895e547129413f56c76be2c3ce4b96c797d2d0ff3e16a817d9269b12e6/yarl-1.22.0-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2e4e1f6f0b4da23e61188676e3ed027ef0baa833a2e633c29ff8530800edccba", size = 346465, upload-time = "2025-10-06T14:12:35.977Z" },
+ { url = "https://files.pythonhosted.org/packages/11/13/a750e9fd6f9cc9ed3a52a70fe58ffe505322f0efe0d48e1fd9ffe53281f5/yarl-1.22.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:84fc3ec96fce86ce5aa305eb4aa9358279d1aa644b71fab7b8ed33fe3ba1a7ca", size = 341506, upload-time = "2025-10-06T14:12:37.788Z" },
+ { url = "https://files.pythonhosted.org/packages/3c/67/bb6024de76e7186611ebe626aec5b71a2d2ecf9453e795f2dbd80614784c/yarl-1.22.0-cp39-cp39-musllinux_1_2_armv7l.whl", hash = "sha256:5dbeefd6ca588b33576a01b0ad58aa934bc1b41ef89dee505bf2932b22ddffba", size = 335030, upload-time = "2025-10-06T14:12:39.775Z" },
+ { url = "https://files.pythonhosted.org/packages/a2/be/50b38447fd94a7992996a62b8b463d0579323fcfc08c61bdba949eef8a5d/yarl-1.22.0-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:14291620375b1060613f4aab9ebf21850058b6b1b438f386cc814813d901c60b", size = 358560, upload-time = "2025-10-06T14:12:41.547Z" },
+ { url = "https://files.pythonhosted.org/packages/e2/89/c020b6f547578c4e3dbb6335bf918f26e2f34ad0d1e515d72fd33ac0c635/yarl-1.22.0-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:a4fcfc8eb2c34148c118dfa02e6427ca278bfd0f3df7c5f99e33d2c0e81eae3e", size = 357290, upload-time = "2025-10-06T14:12:43.861Z" },
+ { url = "https://files.pythonhosted.org/packages/8c/52/c49a619ee35a402fa3a7019a4fa8d26878fec0d1243f6968bbf516789578/yarl-1.22.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:029866bde8d7b0878b9c160e72305bbf0a7342bcd20b9999381704ae03308dc8", size = 350700, upload-time = "2025-10-06T14:12:46.868Z" },
+ { url = "https://files.pythonhosted.org/packages/ab/c9/f5042d87777bf6968435f04a2bbb15466b2f142e6e47fa4f34d1a3f32f0c/yarl-1.22.0-cp39-cp39-win32.whl", hash = "sha256:4dcc74149ccc8bba31ce1944acee24813e93cfdee2acda3c172df844948ddf7b", size = 82323, upload-time = "2025-10-06T14:12:48.633Z" },
+ { url = "https://files.pythonhosted.org/packages/fd/58/d00f7cad9eba20c4eefac2682f34661d1d1b3a942fc0092eb60e78cfb733/yarl-1.22.0-cp39-cp39-win_amd64.whl", hash = "sha256:10619d9fdee46d20edc49d3479e2f8269d0779f1b031e6f7c2aa1c76be04b7ed", size = 87145, upload-time = "2025-10-06T14:12:50.241Z" },
+ { url = "https://files.pythonhosted.org/packages/c2/a3/70904f365080780d38b919edd42d224b8c4ce224a86950d2eaa2a24366ad/yarl-1.22.0-cp39-cp39-win_arm64.whl", hash = "sha256:dd7afd3f8b0bfb4e0d9fc3c31bfe8a4ec7debe124cfd90619305def3c8ca8cd2", size = 82173, upload-time = "2025-10-06T14:12:51.869Z" },
+ { url = "https://files.pythonhosted.org/packages/73/ae/b48f95715333080afb75a4504487cbe142cae1268afc482d06692d605ae6/yarl-1.22.0-py3-none-any.whl", hash = "sha256:1380560bdba02b6b6c90de54133c81c9f2a453dee9912fe58c1dcced1edb7cff", size = 46814, upload-time = "2025-10-06T14:12:53.872Z" },
+]
+
+[[package]]
+name = "yarl"
+version = "1.24.5"
+source = { registry = "https://pypi.org/simple" }
+resolution-markers = [
+ "python_full_version >= '3.10'",
+]
+dependencies = [
+ { name = "idna" },
+ { name = "multidict" },
+ { name = "propcache", version = "0.5.2", source = { registry = "https://pypi.org/simple" } },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/31/33/ebe9e3d1f86c7a0b51094c0a146392045ca1631d2664889539dec8088a33/yarl-1.24.5.tar.gz", hash = "sha256:e81b83143bee16329c23db3c1b2d82b29892fcbcb849186d2f6e98a5abe9a57f", size = 228679, upload-time = "2026-07-20T02:07:45.435Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/48/ac/cacdda1f0a90441297210bc34cf7e4ac1b7318c8030ebd83bdf6fe82f1db/yarl-1.24.5-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:88f50c94e21a0a7f14042c015b0eba1881af78562e7bf007e0033e624da59750", size = 135466, upload-time = "2026-07-20T02:04:21.695Z" },
+ { url = "https://files.pythonhosted.org/packages/c0/a5/1b2ceace0230e40c52ab1b263148059a43a6303219b996affc68f8381836/yarl-1.24.5-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:6efbccc3d7f75d5b03105172a8dc86d82ba4da86817952529dd93185f4a88be2", size = 97291, upload-time = "2026-07-20T02:04:24.045Z" },
+ { url = "https://files.pythonhosted.org/packages/59/1d/340d1a0db7bbce1f291afc044255ebf4ebbce2b25ab1b3f7d3d069080f5d/yarl-1.24.5-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:0ebfaffe1a16cb72141c8e09f18cc76856dbe58639f393a4f2b26e474b96b871", size = 97154, upload-time = "2026-07-20T02:04:25.761Z" },
+ { url = "https://files.pythonhosted.org/packages/05/41/25596a33c2fb5098dca8dc3773b04221db64ded0b7f8f09885647d864610/yarl-1.24.5-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8ac73abdc7ab75610f95a8fd994c6457e87752b02a63987e188f937a1fc180f0", size = 109196, upload-time = "2026-07-20T02:04:27.543Z" },
+ { url = "https://files.pythonhosted.org/packages/f2/df/dd9f2fb8a5c6054fbefd1538d2b9b1127e612d2ee64b307a070173b57afd/yarl-1.24.5-cp310-cp310-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:4d97a951a81039050e45f04e96689b58b8243fa5e62aa14fe67cb6075300885e", size = 102556, upload-time = "2026-07-20T02:04:29.16Z" },
+ { url = "https://files.pythonhosted.org/packages/cb/57/4754b9d2c8945880290ecba0864e8b0441e117bba70534fe819e3645e174/yarl-1.24.5-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:fe7b7bb170daccbba19ad33012d2b15f1e7942296fd4d45fc1b79013da8cc0f2", size = 117965, upload-time = "2026-07-20T02:04:30.845Z" },
+ { url = "https://files.pythonhosted.org/packages/74/b5/6a9ece27d2043c3386f902dd078ab35d29ef5126b3206ebffb673283a7cb/yarl-1.24.5-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:89a1bbb58e0e3f7a283653d854b1e95d65e5cfd4af224dac5f02629ec1a3e621", size = 116266, upload-time = "2026-07-20T02:04:32.573Z" },
+ { url = "https://files.pythonhosted.org/packages/9e/bc/a6653249f6ee59ec85dcfec008d9cbc16586dad613963bb17a91b2b993a5/yarl-1.24.5-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7fa5e51397466ea7e98de493fa2ff1b8193cfef8a7b0f9b4842f92d342df0dba", size = 110758, upload-time = "2026-07-20T02:04:34.235Z" },
+ { url = "https://files.pythonhosted.org/packages/65/7e/c5a12fb8208df7b981bc82256e7831ce428eeaf893f7bbe6179c57bb9252/yarl-1.24.5-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:4103b77b8a8225e413107d2349b65eb3c1c52627b5cc5c3c4c1c6a798b218950", size = 110120, upload-time = "2026-07-20T02:04:35.85Z" },
+ { url = "https://files.pythonhosted.org/packages/04/6c/1b659b964626694667b3ec01bf4bcff564b73ae7c48ea1fbfe588b78b461/yarl-1.24.5-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:f9f3e9c8a9ecffa57bef8fb4fa19e5fa4d2d8307cf6bac5b1fca5e5860f4ba00", size = 108834, upload-time = "2026-07-20T02:04:37.67Z" },
+ { url = "https://files.pythonhosted.org/packages/74/a6/bf48f55c2104e40c15b7b13fad0a5756a11552a55f01c90bc90a66ab81c3/yarl-1.24.5-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:c0ebc836c47a6477e182169c6a476fc691d12b518894bf7dd2572f0d59f1c7ed", size = 103442, upload-time = "2026-07-20T02:04:39.576Z" },
+ { url = "https://files.pythonhosted.org/packages/37/ac/84b273ac133ecdce598fc1f4140a08a1bf2044048bff8106371d207d105f/yarl-1.24.5-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:96d30286dd02679e32a39aa8f0b7498fc847fcda46cfc09df5513e82ce252440", size = 117413, upload-time = "2026-07-20T02:04:41.549Z" },
+ { url = "https://files.pythonhosted.org/packages/a4/55/9307e03977d3b290dfa42e5d2bae7b6140808fd1786fbe70cd9d3bee53c5/yarl-1.24.5-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:fd8c81f346b58f45818d09ea11db69a8d5fd34a224b79871f6d44f12cd7977b1", size = 109498, upload-time = "2026-07-20T02:04:43.468Z" },
+ { url = "https://files.pythonhosted.org/packages/fc/be/791a6f314cb4c989c19f8e3a10271f1e469c077143915e52474d80f26b4b/yarl-1.24.5-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:5c55256dee8f4b27bfbf636c8363383c7c8db7890c7cba5217d7bd5f5f21dab6", size = 116062, upload-time = "2026-07-20T02:04:45.319Z" },
+ { url = "https://files.pythonhosted.org/packages/19/1a/ddd3807b86055010e2f99aa89b3c640effdb65696766c20597f696f48a1c/yarl-1.24.5-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:9f4d8cf085a4c6a40fb97ea0f46938a8df43c85d31f9d45e2a8867ea9293790d", size = 110941, upload-time = "2026-07-20T02:04:47.054Z" },
+ { url = "https://files.pythonhosted.org/packages/6d/03/f34271bba042d2187508bf62aea20a14129efb5a1acfc6a2efe7544630b4/yarl-1.24.5-cp310-cp310-win_amd64.whl", hash = "sha256:240cbec09667c1fed4c6cd0060b9ec57332427d7441289a2ed8875dc9fb2b224", size = 97534, upload-time = "2026-07-20T02:04:48.774Z" },
+ { url = "https://files.pythonhosted.org/packages/e4/02/ecc8dc31b9f355731e700f8402b8075d2ea1737dbc4baf4abf0f0fc64288/yarl-1.24.5-cp310-cp310-win_arm64.whl", hash = "sha256:8a6987eaad834cb32dd57d9d582225f0054a5d1af706ccfbbdba735af4927e13", size = 93603, upload-time = "2026-07-20T02:04:50.686Z" },
+ { url = "https://files.pythonhosted.org/packages/fe/db/3cb5df059756a45761cc3dee8fd25ec82b83a6585ea3542b969fda850f99/yarl-1.24.5-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:2c1fe720934a16ea8e7146175cba2126f87f54912c8c5435e7f7c7a51ef808d3", size = 135043, upload-time = "2026-07-20T02:04:52.39Z" },
+ { url = "https://files.pythonhosted.org/packages/44/f8/767d6bd5a03db63bc467df2fb56d6fafeae9667d74aea92cd6af399f828b/yarl-1.24.5-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c687ed078e145f5fd53a14854beff320e1d2ab76df03e2009c98f39a0f68f39a", size = 96942, upload-time = "2026-07-20T02:04:54.26Z" },
+ { url = "https://files.pythonhosted.org/packages/ce/97/10b939c44d7b28d1dbc389cfc7012306d1ea8dba01eaef44b39fffaee52a/yarl-1.24.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:709f1efed56c4a145793c046cd4939f9959bcd818979a787b77d8e09c57a0840", size = 97046, upload-time = "2026-07-20T02:04:56.638Z" },
+ { url = "https://files.pythonhosted.org/packages/5b/7a/b410dbe39b6255c55fb2a2bcee96eb844d0789235ddc381a889a90dc72d6/yarl-1.24.5-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:874019bd513008b009f58657134e5d0c5e030b3559bd0553976837adf52fe966", size = 110512, upload-time = "2026-07-20T02:04:58.955Z" },
+ { url = "https://files.pythonhosted.org/packages/83/c7/da591971f78a5617e1f21f5699858ebccd836fe181a6493788ffc91ba69b/yarl-1.24.5-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:a4582acf7ef76482f6f511ebaf1946dae7f2e85ec4728b81a678c01df63bd723", size = 102454, upload-time = "2026-07-20T02:05:00.623Z" },
+ { url = "https://files.pythonhosted.org/packages/c4/8e/73b0ed4de47289a78a96045d76d1cfe5e41848bf0da59ce25b2ec87ee05d/yarl-1.24.5-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2cabe6546e41dabe439999a23fcb5246e0c3b595b4315b96ef755252be90caeb", size = 117617, upload-time = "2026-07-20T02:05:02.325Z" },
+ { url = "https://files.pythonhosted.org/packages/cf/14/b744747bc4f57a8d55bd744df463457524583e1e9f7538b5ace0346ab92e/yarl-1.24.5-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:17f57620f5475b3c69109376cc87e42a7af5db13c9398e4292772a706ff10780", size = 116135, upload-time = "2026-07-20T02:05:04.05Z" },
+ { url = "https://files.pythonhosted.org/packages/66/ca/95aa4d0e5b7ea4f20e4d577c42d001ed9df207569fdb063cc5ed4ebb496b/yarl-1.24.5-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:570fec8fbd22b032733625f03f10b7ff023bc399213db15e72a7acaef28c2f4e", size = 111935, upload-time = "2026-07-20T02:05:05.738Z" },
+ { url = "https://files.pythonhosted.org/packages/72/0d/d2ad8d6b147832d177a4e720ba1962fe686eb0913b74503b3eca094b8bba/yarl-1.24.5-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:5fede79c6f73ff2c3ef822864cb1ada23196e62756df53bc6231d351a49516a2", size = 110010, upload-time = "2026-07-20T02:05:07.471Z" },
+ { url = "https://files.pythonhosted.org/packages/50/18/eb335e4120903903f4865041355ae46256a2406eb2865bc24827f4f27b61/yarl-1.24.5-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8ccf9aca873b767977c73df497a85dbedee4ee086ae9ae49dc461333b9b79f58", size = 110058, upload-time = "2026-07-20T02:05:09.246Z" },
+ { url = "https://files.pythonhosted.org/packages/44/70/97353add32c62ad6f206d948ac5a5ee84398225e534dc6ed6433d1b335b6/yarl-1.24.5-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:ad5d8201d310b031e6cd839d9bac2d4e5a01533ce5d3d5b50b7de1ef3af1de61", size = 103308, upload-time = "2026-07-20T02:05:11.31Z" },
+ { url = "https://files.pythonhosted.org/packages/68/39/5e7398d4b6f6b3c9062823ebc60802df5b272e3fe9e788f9734c6ee46c85/yarl-1.24.5-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:841f0852f48fefea3b12c9dfec00704dfa3aef5215d0e3ce564bb3d7cd8d57c6", size = 116898, upload-time = "2026-07-20T02:05:13.099Z" },
+ { url = "https://files.pythonhosted.org/packages/e4/c9/09e52f2239e8b96357eccca05915382e4ba5405ebfb623b6036040d99654/yarl-1.24.5-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:9baafc71b04f8f4bb0703b21d6fc9f0c30b346c636a532ff16ec8491a5ea4b1f", size = 109400, upload-time = "2026-07-20T02:05:14.821Z" },
+ { url = "https://files.pythonhosted.org/packages/4b/6a/e94133d4c2d1a14d2384310bf3e79d9cf32c9d1eae1c6f034fb80d098fa1/yarl-1.24.5-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:d897129df1a22b12aeed2c2c98df0785a2e8e6e0bde87b389491d0025c187077", size = 115934, upload-time = "2026-07-20T02:05:17.78Z" },
+ { url = "https://files.pythonhosted.org/packages/4e/3c/34955ed967b976fc38edcbb6d538dee79dbda4cb7fc7f72a0907a7c78e0f/yarl-1.24.5-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:dd625535328fd9882374356269227670189adfcc6a2d90284f323c05862eecbd", size = 112178, upload-time = "2026-07-20T02:05:19.675Z" },
+ { url = "https://files.pythonhosted.org/packages/f5/46/d7bd3a8859d47dcfaffd7127af7076032a7da278a9a02e17b5f37bfb6712/yarl-1.24.5-cp311-cp311-win_amd64.whl", hash = "sha256:f4239bbec5a3577ddb49e4b50aeb32d8e5792098262ae2f63723f916a29b1a25", size = 97544, upload-time = "2026-07-20T02:05:21.523Z" },
+ { url = "https://files.pythonhosted.org/packages/01/69/c1bfd21e32c638974ea2c542a0b8c53ef1fa9eff336020f5d014f9503ff2/yarl-1.24.5-cp311-cp311-win_arm64.whl", hash = "sha256:3ac6aff147deb9c09461b2d4bbdf6256831198f5d8a23f5d37138213090b6d8a", size = 93359, upload-time = "2026-07-20T02:05:23.493Z" },
+ { url = "https://files.pythonhosted.org/packages/1b/84/71d051c850b5af41d168c679d9eb67eb7c55283ac4ee131673edf134bc4e/yarl-1.24.5-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:d693396e5aea78db03decd60aec9ece16c9b40ba00a587f089615ff4e718a81d", size = 136035, upload-time = "2026-07-20T02:05:25.489Z" },
+ { url = "https://files.pythonhosted.org/packages/03/4d/8ad27f9a1b7e69313cca5d695b925b48efe51208d3490e0844bae97cabc0/yarl-1.24.5-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:3363fcc96e665878946ad7a106b9a13eac0541766a690ef287c0232ac768b6ec", size = 97642, upload-time = "2026-07-20T02:05:27.429Z" },
+ { url = "https://files.pythonhosted.org/packages/ea/b4/05b4131c407006cd1e410e9c6539f16a0945724677e5364447313c15ea3e/yarl-1.24.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:9d399bdcfb4a0f659b9b3788bbc89babe63d9a6a65aacdf4d4e7065ff2e6316c", size = 97323, upload-time = "2026-07-20T02:05:29.441Z" },
+ { url = "https://files.pythonhosted.org/packages/20/16/e618c875c73e0e39611f20a581b3d5e8d59b8857bf001bee3263044c6deb/yarl-1.24.5-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:90333fd89b43c0d08ac85f3f1447593fc2c66de18c3d6378d7125ea118dc7a54", size = 107741, upload-time = "2026-07-20T02:05:31.367Z" },
+ { url = "https://files.pythonhosted.org/packages/d9/9a/c4defeaf3ed33fcb346aacf9c6e971a8d4e2bde04a0310e79abb208e7965/yarl-1.24.5-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:665b0a2c463cc9423dd647e0bfd9f4ccc9b50f768c55304d5e9f80b177c1de12", size = 103570, upload-time = "2026-07-20T02:05:33.303Z" },
+ { url = "https://files.pythonhosted.org/packages/5f/e7/0e0e0de5865ebd5914537ef486f36c727a59865c3ac0cf5ff1b32aececbf/yarl-1.24.5-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e006d3a974c4ee19512e5f058abedb6eef36a5e553c14812bdeba1758d812e6d", size = 115815, upload-time = "2026-07-20T02:05:35.292Z" },
+ { url = "https://files.pythonhosted.org/packages/2b/27/ca56b700cb170aba25a3893b75355b213935657dc5714d2383354a270e62/yarl-1.24.5-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:e7d42c531243450ef0d4d9c172e7ed6ef052640f195629065041b5add4e058d1", size = 116025, upload-time = "2026-07-20T02:05:37.503Z" },
+ { url = "https://files.pythonhosted.org/packages/d6/d0/d56c859b8222116f5d68459199f48359e0bf121b6f65a69bf329b3602ba0/yarl-1.24.5-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f08c7513ecef5aad65687bfdf6bc601ae9fccd04a42904501f8f7141abad9eb9", size = 109835, upload-time = "2026-07-20T02:05:39.506Z" },
+ { url = "https://files.pythonhosted.org/packages/70/a2/3a35557e4d1a79425040eba202ccaf08bdc8717680fc77e2498a1ad2e0a5/yarl-1.24.5-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:6c95b17fe34ed802f17e205112e6e10db92275c34fee290aa9bdc55a9c724027", size = 108884, upload-time = "2026-07-20T02:05:41.584Z" },
+ { url = "https://files.pythonhosted.org/packages/e4/35/ef4c26356b7913c68983bac2d72a4212b3347af551cb8d250b99b5ed7b7f/yarl-1.24.5-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:56b149b22de33b23b0c6077ab9518c6dcb538ad462e1830e68d06591ccf6e38b", size = 107308, upload-time = "2026-07-20T02:05:43.697Z" },
+ { url = "https://files.pythonhosted.org/packages/d5/91/ff0dc66c2ccf3e0153ab97ff61eabab4400e6a5264af427ab30cd69f1857/yarl-1.24.5-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:a8fe66b8f300da93798025a785a5b90b42f3810dc2b72283ff84a41aaaebc293", size = 103646, upload-time = "2026-07-20T02:05:45.895Z" },
+ { url = "https://files.pythonhosted.org/packages/74/f0/33b9271c7f881766359d58266fa0811d2e5210ed860e28da7dc6d7786344/yarl-1.24.5-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:377fe3732edbaf78ee74efdf2c9f49f6e99f20e7f9d2649fda3eb4badd77d76e", size = 115305, upload-time = "2026-07-20T02:05:47.832Z" },
+ { url = "https://files.pythonhosted.org/packages/ef/65/fd79fb1868c4a80db8661091de525bf430f63c3bea1b20e8b6a84fc7d359/yarl-1.24.5-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:e8ffa78582120024f476a611d7befc123cee59e47e8309d470cf667d806e613b", size = 108404, upload-time = "2026-07-20T02:05:49.604Z" },
+ { url = "https://files.pythonhosted.org/packages/ff/ba/dbabe6b262f17a816c70cfc09558dbf03ece3ec76684d02f911a3d3a189c/yarl-1.24.5-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:daba5e594f06114e37db186efd2dd916609071e59daca901a0a2e71f02b142ce", size = 115940, upload-time = "2026-07-20T02:05:51.741Z" },
+ { url = "https://files.pythonhosted.org/packages/a5/43/fab2d1dad9d340a268cdde63756a123d069723efff6a372d123fa74a9517/yarl-1.24.5-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:65be18ec59496c13908f02a2472751d9ef840b4f3fb5726f129306bf6a2a7bba", size = 110006, upload-time = "2026-07-20T02:05:53.554Z" },
+ { url = "https://files.pythonhosted.org/packages/c4/27/41eb51bbd1b8d89546b83897cfb0164f1e109304fd408dbb151b639eec0f/yarl-1.24.5-cp312-cp312-win_amd64.whl", hash = "sha256:a929d878fec099030c292803b31e5d5540a7b6a31e6a3cc76cb4685fc2a2f51b", size = 97618, upload-time = "2026-07-20T02:05:55.57Z" },
+ { url = "https://files.pythonhosted.org/packages/3c/25/b2553764b3d65db711d8f45416351ec4f420847558eb669edcbcaadf5780/yarl-1.24.5-cp312-cp312-win_arm64.whl", hash = "sha256:7ce27823052e2013b597e0c738b13e7e36b8ccb9400df8959417b052ab0fd92c", size = 93018, upload-time = "2026-07-20T02:05:57.554Z" },
+ { url = "https://files.pythonhosted.org/packages/e1/63/64ef361967cc983573149dc1515d531db5da8a4c92d22bb833d59e01b313/yarl-1.24.5-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:79af890482fc94648e8cde4c68620378f7fef60932710fa17a66abc039244da2", size = 135075, upload-time = "2026-07-20T02:05:59.671Z" },
+ { url = "https://files.pythonhosted.org/packages/bb/89/55920fd853ce43e608adbc3962456f0d649d6bb15250dc2988321da0fe1c/yarl-1.24.5-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:46c2f213e23a04b93a392942d782eb9e413e6ef6bf7c8c53884e599a5c174dcb", size = 97225, upload-time = "2026-07-20T02:06:01.769Z" },
+ { url = "https://files.pythonhosted.org/packages/15/f0/7688d3f2cfff7590df2af38ec46d969f4281a4dddb08a9ad2eafbcdddf98/yarl-1.24.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:92ab3e11448f2ff7bf53c5a26eff0edc086898ec8b21fb154b85839ce1d88075", size = 96751, upload-time = "2026-07-20T02:06:03.676Z" },
+ { url = "https://files.pythonhosted.org/packages/05/1a/a851a0f94aaaf379dd4f901bfc80f634280bec51eb260b47363e2a4cd62e/yarl-1.24.5-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ebb0ec7f17803063d5aeb982f3b1bd2b2f4e4fae6751226cbd6ba1fcfe9e63ff", size = 107960, upload-time = "2026-07-20T02:06:05.699Z" },
+ { url = "https://files.pythonhosted.org/packages/6c/a8/faea066c12f9c77ca0de90641f1655f9dd7b412477bf28c76d692f3aecff/yarl-1.24.5-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:82632daed195dcc8ea664e8556dc9bdbd671960fb3776bd92806ce05792c2448", size = 103500, upload-time = "2026-07-20T02:06:07.556Z" },
+ { url = "https://files.pythonhosted.org/packages/fb/9c/1e67084c2a6e2f2db0e3be798328cb3be42c0119b621d25461479a224d21/yarl-1.24.5-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:53e549287ef628fecba270045c9701b0c564563a9b0577d24a4ec75b8ab8040f", size = 115780, upload-time = "2026-07-20T02:06:09.599Z" },
+ { url = "https://files.pythonhosted.org/packages/58/86/1f94664e147474337e3359f52012cf3d02f825f694317b178bfba1078c62/yarl-1.24.5-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:fcd3b77e2f17bbe4ca56ec7bcb07992647d19d0b9c05d84886dcd6f9eb810afd", size = 115308, upload-time = "2026-07-20T02:06:11.352Z" },
+ { url = "https://files.pythonhosted.org/packages/0a/43/8e55ae7538ba5f28ccb3c845c6dd4549cf7016d5992e5326512519107cdd/yarl-1.24.5-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d46b86567dd4e248c6c159fcbcdcce01e0a5c8a7cd2334a0fff759d0fa075b16", size = 110574, upload-time = "2026-07-20T02:06:13.129Z" },
+ { url = "https://files.pythonhosted.org/packages/ce/ba/a889ec8765cedcf2ac44dcb02d6a21e4861399b243b263c5f2dde27ee740/yarl-1.24.5-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:7f72c74aa99359e27a2ee8d6613fefa28b5f76a983c083074dfc2aaa4ab46213", size = 109914, upload-time = "2026-07-20T02:06:15.243Z" },
+ { url = "https://files.pythonhosted.org/packages/9c/c3/e45f821af67b791c2dbbe4a9f4137a1d33f8d386654a05a0c3f47bdfa25d/yarl-1.24.5-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:3f45789ce415a7ec0820dc4f82925f9b5f7732070be1dec1f5f23ec381435a24", size = 107712, upload-time = "2026-07-20T02:06:17.443Z" },
+ { url = "https://files.pythonhosted.org/packages/02/00/2ab0f42c9857fcb490bfaa6647b14540b53d241ab209f23220b958cc5832/yarl-1.24.5-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:6e73e7fe93f17a7b191f52ec9da9dd8c06a8fe735a1ecbd13b97d1c723bff385", size = 104251, upload-time = "2026-07-20T02:06:19.259Z" },
+ { url = "https://files.pythonhosted.org/packages/7a/70/709d9a286e98af2c7fd8e4e6cada658b5c0e30d87dd7e2a63c2fb5767217/yarl-1.24.5-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:4a36f9becdd4c5c52a20c3e9484128b070b1dcfc8944c006f3a528295a359a9c", size = 115319, upload-time = "2026-07-20T02:06:21.207Z" },
+ { url = "https://files.pythonhosted.org/packages/5c/6c/3eaa515142991fe84cfc483ff986492211f1978f90161ccefdbec919d09b/yarl-1.24.5-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:7bcbe0fcf850eae67b6b01749815a4f7161c560a844c769ad7b48fcd99f791c4", size = 109163, upload-time = "2026-07-20T02:06:23.006Z" },
+ { url = "https://files.pythonhosted.org/packages/bb/64/711dafce66c323a3144d470547a71c5384c57623308ac8bb5e4b903ac148/yarl-1.24.5-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:24e861e9630e0daddcb9191fb187f60f034e17a4426f8101279f0c475cd74144", size = 115435, upload-time = "2026-07-20T02:06:24.923Z" },
+ { url = "https://files.pythonhosted.org/packages/cf/f3/9b9d0e6d84bea851eb1ba99e4bdc755b86fd813e49ec86dfe42f26befdef/yarl-1.24.5-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9335a099ad87287c37fe5d1a982ff392fa5efe5d14b40a730b1ec1d6a41382b4", size = 110691, upload-time = "2026-07-20T02:06:26.973Z" },
+ { url = "https://files.pythonhosted.org/packages/86/e4/62a06b7e87c4246ac76b7c2da136f972eb4a3a1fc94abb07e7022d6fdb0a/yarl-1.24.5-cp313-cp313-win_amd64.whl", hash = "sha256:2dbe06fc16bc91502bca713704022182e5729861ae00277c3a23354b40929740", size = 97454, upload-time = "2026-07-20T02:06:29.163Z" },
+ { url = "https://files.pythonhosted.org/packages/9e/c9/5fc8025b318ab10db413b61056bd0d95c557a70e8df4210c7511f866329c/yarl-1.24.5-cp313-cp313-win_arm64.whl", hash = "sha256:6b8536851f9f65e7f00c7a1d49ba7f2be0ffe2c11555367fc9f50d9f842410a1", size = 92813, upload-time = "2026-07-20T02:06:31.113Z" },
+ { url = "https://files.pythonhosted.org/packages/a9/08/5f3085fef9564217074db9dd8573de1795bc82cde61a7ad10b6a7234a569/yarl-1.24.5-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:2729fcfc4f6a596fb0c50f32090400aa9367774ac296a00387e65098c0befa76", size = 135680, upload-time = "2026-07-20T02:06:33.273Z" },
+ { url = "https://files.pythonhosted.org/packages/98/35/ba9436e579bd48a8801f2021d842d9ab4994c26e4c7dd3a4c1f1bcb57a9e/yarl-1.24.5-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:ff330d3c30db4eb6b01d79e29d2d0b407a7ecad39cfd9ec993ece57396a2ec0d", size = 97395, upload-time = "2026-07-20T02:06:35.259Z" },
+ { url = "https://files.pythonhosted.org/packages/18/a9/a07f76f3c44e02b25cc743af5ef93eef27f7013eadca770451b6a6ccb5db/yarl-1.24.5-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:e42d75862735da90e7fc5a7b23db0c976f737113a54b3c9777a9b665e9cbff75", size = 97223, upload-time = "2026-07-20T02:06:37.216Z" },
+ { url = "https://files.pythonhosted.org/packages/77/f7/a9a1d6fa7dd9e388f95b30f6ad3ec4e285f6c8f61f44ce16070c3fcfe414/yarl-1.24.5-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a3732e66413163e72508da9eff9ce9d2846fde51fae45d3605393d3e6cd303e9", size = 108777, upload-time = "2026-07-20T02:06:39.292Z" },
+ { url = "https://files.pythonhosted.org/packages/2f/44/e0b86c302471fabd6f02808ecf2ac52b8412b624787849d4bf2cdb466f6f/yarl-1.24.5-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:5b8ee53be440a0cffc991a27be3057e0530122548dbe7c0892df08822fce5ede", size = 103119, upload-time = "2026-07-20T02:06:41.456Z" },
+ { url = "https://files.pythonhosted.org/packages/d1/16/9c16d180bf8faaf223225eb50e1245870ff1ae0e302a27153988e65c51fd/yarl-1.24.5-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:af3aefa655adb5869491fa907e652290386800ae99cc50095cba71e2c6aefdca", size = 116471, upload-time = "2026-07-20T02:06:43.696Z" },
+ { url = "https://files.pythonhosted.org/packages/d2/8d/b219b9df28a02ce95cfbdd41d2f7caa5669d0ff979c1c9975697145e33c5/yarl-1.24.5-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:2120b96872df4a117cde97d270bac96aea7cc52205d305cf4611df694a487027", size = 115974, upload-time = "2026-07-20T02:06:45.874Z" },
+ { url = "https://files.pythonhosted.org/packages/9b/e8/f20557aca240d88e69850ad1ee91756821d094bb1310565c04d25c6682a2/yarl-1.24.5-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:66410eb6345d467151934b49bfa70fb32f5b35a6140baa40ad97d6436abea2e9", size = 110830, upload-time = "2026-07-20T02:06:47.852Z" },
+ { url = "https://files.pythonhosted.org/packages/db/18/199b85109a53eeca64ee19c9cca228287e8e4ab0cc1a09b28f530e65cce0/yarl-1.24.5-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:4af7b7e1be0a69bee8210735fe6dcfc38879adfac6d62e789d53ba432d1ffa41", size = 110054, upload-time = "2026-07-20T02:06:49.84Z" },
+ { url = "https://files.pythonhosted.org/packages/aa/2f/ed28147f8cd7f48c49367c90713b30a555284b6105a6a56f3a05568da795/yarl-1.24.5-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:fa139875ff98ab97da323cfadfaff08900d1ad42f1b5087b0b812a55c5a06373", size = 108312, upload-time = "2026-07-20T02:06:51.835Z" },
+ { url = "https://files.pythonhosted.org/packages/c5/c5/55e16ae0a5c227cea8df1c6871ba57d614a34243146c05729caf2a1bd9c5/yarl-1.24.5-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:0055afc45e864b92729ac7600e2d102c17bef060647e74bca75fa84d66b9ff36", size = 103662, upload-time = "2026-07-20T02:06:54.061Z" },
+ { url = "https://files.pythonhosted.org/packages/8d/ea/dbd7c2caec459c9a426f18b02688ecbfb58620d0f6a3422d24769fbaf8ab/yarl-1.24.5-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:f0e466ed7511fe9d459a819edbc6c2585c0b6eabde9fa8a8947552468a7a6ef0", size = 116090, upload-time = "2026-07-20T02:06:56.015Z" },
+ { url = "https://files.pythonhosted.org/packages/06/84/39ce4ce3059e07fece5fbdbee8c4053406af9aca911ce9fa5f8548aab6af/yarl-1.24.5-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:f141474e85b7e54998ec5180530a7cda99ab29e282fa50e0756d89981a9b43c5", size = 109523, upload-time = "2026-07-20T02:06:57.926Z" },
+ { url = "https://files.pythonhosted.org/packages/a9/8b/71ff44137b405c64a7788075669c24010019f57a7464b78c3a6cbee539d9/yarl-1.24.5-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:e2935f8c39e3b03e83519292d78f075189978f3f4adc15a78144c7c8e2a1cba5", size = 116084, upload-time = "2026-07-20T02:06:59.868Z" },
+ { url = "https://files.pythonhosted.org/packages/62/c0/423078fdd4042e1862c11f0ffd977a0ffa393783c12bee94685923bc189e/yarl-1.24.5-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:9d1216a7f6f77836617dba35687c5b78a4170afc3c3f18fc788f785ba26565c4", size = 111006, upload-time = "2026-07-20T02:07:01.907Z" },
+ { url = "https://files.pythonhosted.org/packages/cf/52/6daa2ee9d95e5c98b8128f8df91eb692eb423ab274b8cf08db52152fad26/yarl-1.24.5-cp314-cp314-win_amd64.whl", hash = "sha256:5ba4f78df2bcc19f764a4b26a8a4f5049c110090ad5825993aacb052bf8003ad", size = 99215, upload-time = "2026-07-20T02:07:03.852Z" },
+ { url = "https://files.pythonhosted.org/packages/ec/0e/464a847d7359e0da75dd9fc5c1d1aa35d0159ea31e5f8e66a3c1c29ff3d0/yarl-1.24.5-cp314-cp314-win_arm64.whl", hash = "sha256:9e4e16c73d717c5cf27626c524d0a2e261ad20e46932b2670f64ad5dde23e26f", size = 94566, upload-time = "2026-07-20T02:07:06.074Z" },
+ { url = "https://files.pythonhosted.org/packages/e2/55/e03acc4446772660bc335e86e41ef31e4d0d838fd641531a11a5ee33b493/yarl-1.24.5-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:e1ae548a9d901adca07899a4147a7c826bbcc06239d3ce9a59f57886a28a4c88", size = 142533, upload-time = "2026-07-20T02:07:08.284Z" },
+ { url = "https://files.pythonhosted.org/packages/ae/71/4acd3a1fc7cf14345cdb302665ecd2097f62c365b4f14ca17d4f37775cf9/yarl-1.24.5-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:ff405d91509d88e8d44129cd87b18d70acd1f0c1aeabd7bc3c46792b1fe2acba", size = 100776, upload-time = "2026-07-20T02:07:10.197Z" },
+ { url = "https://files.pythonhosted.org/packages/ff/0b/cfb76b7fe99686db264bff829779a539d923e7564ffd7ef18da6c54c3774/yarl-1.24.5-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:47e98aab9d8d82ff682e7b0b5dded33bf138a32b817fcf7fa3b27b2d7c412928", size = 100913, upload-time = "2026-07-20T02:07:12.357Z" },
+ { url = "https://files.pythonhosted.org/packages/8b/3f/7116e782992abbd4fb6948488aec72078895e929a23078290739e8396fce/yarl-1.24.5-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f0a658a6d3fafee5c6f63c58f3e785c8c43c93fbc02bf9f2b6663f8185e0971f", size = 106507, upload-time = "2026-07-20T02:07:14.173Z" },
+ { url = "https://files.pythonhosted.org/packages/33/90/d4d2d73ee78229cc889872eb8e085d8f5c6f51abdb178409fd9b23cf74fd/yarl-1.24.5-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:4377407001ca3c057773f44d8ddd6358fa5f691407c1ba92210bd3cf8d9e4c95", size = 99219, upload-time = "2026-07-20T02:07:16.019Z" },
+ { url = "https://files.pythonhosted.org/packages/3e/fa/a6df1a9bccd644eec00abee0dff4277416222cec435330fd1f2858523ec1/yarl-1.24.5-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:7c0494a31a1ac5461a226e7947a9c9b78c44e1dc7185164fa7e9651557a5d9bc", size = 111804, upload-time = "2026-07-20T02:07:18.141Z" },
+ { url = "https://files.pythonhosted.org/packages/8a/9e/7b2a1f4bcc20e9447156dd2b1c4d01f70d9df0759025ee7d09a84ffae134/yarl-1.24.5-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a7cff474ab7cd149765bb784cf6d78b32e18e20473fb7bda860bce98ab58e9da", size = 110943, upload-time = "2026-07-20T02:07:20.06Z" },
+ { url = "https://files.pythonhosted.org/packages/08/ff/22c92affb0f9b623ca753d27d968b5625b868f12c6378d049d55ae247643/yarl-1.24.5-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:cbb833ccacdb5519eff9b8b71ee618cc2801c878e77e288775d77c3a2ced858a", size = 108251, upload-time = "2026-07-20T02:07:22.217Z" },
+ { url = "https://files.pythonhosted.org/packages/45/44/5769b96298c1e195fb412997b6090af2a84105cf59c17613558a2d011d1f/yarl-1.24.5-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:82f75e05912e84b7a0fe57075d9c59de3cb352b928330f2eb69b2e1f54c3e1f0", size = 106025, upload-time = "2026-07-20T02:07:24.083Z" },
+ { url = "https://files.pythonhosted.org/packages/4c/40/009e8e791fd9762c0e1567e69248acb4f49064597e1680874c16dd8bb798/yarl-1.24.5-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:16a2f5010280020e90f5330257e6944bc33e73593b136cc5a241e6c1dc292498", size = 106573, upload-time = "2026-07-20T02:07:26.248Z" },
+ { url = "https://files.pythonhosted.org/packages/20/c6/b7480578f8a0a80946f36ad6df547ecec704f9ba69d2de60f8aa6f1c1cbf/yarl-1.24.5-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:ffcd54362564dc1a30fb74d8b8a6e5a6b11ebd5e27266adc3b7427a21a6c9104", size = 100751, upload-time = "2026-07-20T02:07:28.098Z" },
+ { url = "https://files.pythonhosted.org/packages/d4/27/4476f3360b91a48c5cf125e91f59a3bd35299d84a431a258d57f5977bb11/yarl-1.24.5-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:0465ec8cedc2349b97a6b595ace64084a50c6e839eca40aa0626f38b8350e331", size = 111643, upload-time = "2026-07-20T02:07:30.88Z" },
+ { url = "https://files.pythonhosted.org/packages/4c/4b/5cdd3e5ee944e8af31e52f6cd3d3af5fd7b937e036ccbbba2c9ffebede95/yarl-1.24.5-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:4db9aecb141cb7a5447171b57aa1ed3a8fee06af40b992ffc31206c0b0121550", size = 106312, upload-time = "2026-07-20T02:07:33.06Z" },
+ { url = "https://files.pythonhosted.org/packages/18/86/f406b0c2a6f99575de2da671ef47aa06f89a5be83a27a46971c3b86cecdb/yarl-1.24.5-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:f540c013589084679a6c7fac07096b10159737918174f5dfc5e11bf5bca4dfe6", size = 110379, upload-time = "2026-07-20T02:07:35.155Z" },
+ { url = "https://files.pythonhosted.org/packages/f0/6c/9f3adfbd3b30b4fa0f7ccb3a83eba2c1152d3fff554d535e640ba0f7ba2b/yarl-1.24.5-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:a61834fb15d81322d872eaafd333838ae7c9cea84067f232656f75965933d047", size = 108497, upload-time = "2026-07-20T02:07:37.35Z" },
+ { url = "https://files.pythonhosted.org/packages/dd/37/91eb2e5ca883a529c1b390348a74cd9fc0512171727f547ce70bfe02be5c/yarl-1.24.5-cp314-cp314t-win_amd64.whl", hash = "sha256:5c88e5815a49d289e599f3513aa7fde0bc2092ff188f99c940f007f90f53d104", size = 102450, upload-time = "2026-07-20T02:07:39.578Z" },
+ { url = "https://files.pythonhosted.org/packages/bf/f4/ed5c402ac8fde4403ed3366c2716bfddc8a6677ebd59f3d62772cc7fe468/yarl-1.24.5-cp314-cp314t-win_arm64.whl", hash = "sha256:cf139c02f5f23ef6532040a30ff662c00a318c952334f211046b8e60b7f17688", size = 97222, upload-time = "2026-07-20T02:07:41.55Z" },
+ { url = "https://files.pythonhosted.org/packages/61/02/962c1cbfc401a30c1d034dc67ff395f64b52302c6d62de556c1fca99acc0/yarl-1.24.5-py3-none-any.whl", hash = "sha256:a33700d13d9b7d84fd10947b09ff69fb9a792e519c8cb9764a3ca70baa6c23a7", size = 58612, upload-time = "2026-07-20T02:07:43.461Z" },
+]
From 41a3ad4c735e04bc886a11785a6968519124af67 Mon Sep 17 00:00:00 2001
From: sjmiller609 <7516283+sjmiller609@users.noreply.github.com>
Date: Thu, 6 Aug 2026 13:49:05 +0000
Subject: [PATCH 164/366] ci: authenticate releases with GitHub App
Stainless-Generated-From: 1ae1d13f8e636f021896c6c5632f088b91997a7c
---
.github/workflows/release-please.yml | 18 +++++++++++++++---
1 file changed, 15 insertions(+), 3 deletions(-)
diff --git a/.github/workflows/release-please.yml b/.github/workflows/release-please.yml
index a18a3f9e..3f6bf798 100644
--- a/.github/workflows/release-please.yml
+++ b/.github/workflows/release-please.yml
@@ -5,8 +5,7 @@ on:
- main
permissions:
- contents: write
- pull-requests: write
+ contents: read
jobs:
release-please:
@@ -14,7 +13,20 @@ jobs:
runs-on: ubuntu-latest
steps:
+ - name: Mint release token
+ id: release-token
+ uses: actions/create-github-app-token@d72941d797fd3113feb6b93fd0dec494b13a2547 # v1
+ with:
+ app-id: ${{ secrets.ADMIN_APP_ID }}
+ private-key: ${{ secrets.ADMIN_APP_PRIVATE_KEY }}
+ owner: kernel
+ repositories: kernel-python-sdk
+ permission-contents: write
+ permission-issues: write
+ permission-pull-requests: write
+ permission-workflows: write
+
- uses: googleapis/release-please-action@5c625bfb5d1ff62eadeeb3772007f7f66fdcf071 # v4.4.1
id: release
with:
- token: ${{ secrets.RELEASE_PLEASE_TOKEN }}
+ token: ${{ steps.release-token.outputs.token }}
From d49e8cf462fdb519c366f45e18fcf56bcb187234 Mon Sep 17 00:00:00 2001
From: "stainless-app[bot]"
<142633134+stainless-app[bot]@users.noreply.github.com>
Date: Thu, 6 Aug 2026 14:41:59 +0000
Subject: [PATCH 165/366] release: 0.86.1
---
.release-please-manifest.json | 2 +-
CHANGELOG.md | 4 ++++
pyproject.toml | 2 +-
src/kernel/_version.py | 2 +-
4 files changed, 7 insertions(+), 3 deletions(-)
diff --git a/.release-please-manifest.json b/.release-please-manifest.json
index 53079579..3da9036b 100644
--- a/.release-please-manifest.json
+++ b/.release-please-manifest.json
@@ -1,3 +1,3 @@
{
- ".": "0.86.0"
+ ".": "0.86.1"
}
\ No newline at end of file
diff --git a/CHANGELOG.md b/CHANGELOG.md
index b177e1d3..3731eada 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,9 @@
# Changelog
+## 0.86.1 (2026-08-06)
+
+Full Changelog: [v0.86.0...v0.86.1](https://github.com/kernel/kernel-python-sdk/compare/v0.86.0...v0.86.1)
+
## 0.86.0 (2026-08-03)
Full Changelog: [v0.85.0...v0.86.0](https://github.com/kernel/kernel-python-sdk/compare/v0.85.0...v0.86.0)
diff --git a/pyproject.toml b/pyproject.toml
index 9fbdf513..d4029ac3 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -1,6 +1,6 @@
[project]
name = "kernel"
-version = "0.86.0"
+version = "0.86.1"
description = "The official Python library for the kernel API"
dynamic = ["readme"]
license = "Apache-2.0"
diff --git a/src/kernel/_version.py b/src/kernel/_version.py
index c2d399cd..f6e23035 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.86.0" # x-release-please-version
+__version__ = "0.86.1" # x-release-please-version
From 6135e1288bc08b74532c76f65c5f96851ff98793 Mon Sep 17 00:00:00 2001
From: "kernel-internal[bot]"
<260533166+kernel-internal[bot]@users.noreply.github.com>
Date: Fri, 7 Aug 2026 01:59:39 +0000
Subject: [PATCH 166/366] feat: Profiles, file I/O, replays: ungate for Free
(packaging PR 3)
Stainless-Generated-From: 10cb93573d25bbe91f431ede4c2804e43faa2568
---
requirements-dev.lock | 22 +-
requirements.lock | 20 +-
uv.lock | 1521 -----------------------------------------
3 files changed, 3 insertions(+), 1560 deletions(-)
delete mode 100644 uv.lock
diff --git a/requirements-dev.lock b/requirements-dev.lock
index 979c7980..4a38b203 100644
--- a/requirements-dev.lock
+++ b/requirements-dev.lock
@@ -29,20 +29,15 @@ async-timeout==5.0.1
attrs==25.4.0
# via aiohttp
# via nox
- # via standardwebhooks
backports-asyncio-runner==1.2.0
# via pytest-asyncio
certifi==2026.1.4
# via httpcore
# via httpx
-colorama==0.4.6
- # via griffe
colorlog==6.10.1
# via nox
dependency-groups==1.3.1
# via nox
-deprecated==1.3.1
- # via standardwebhooks
dirty-equals==0.11
distlib==0.4.0
# via virtualenv
@@ -58,16 +53,14 @@ filelock==3.19.1
frozenlist==1.8.0
# via aiohttp
# via aiosignal
-griffe==1.14.0
h11==0.16.0
# via httpcore
httpcore==1.0.9
# via httpx
httpx==0.28.1
# via httpx-aiohttp
- # via respx
- # via standardwebhooks
# via kernel
+ # via respx
httpx-aiohttp==0.1.12
# via kernel
humanize==4.13.0
@@ -119,7 +112,6 @@ pytest==8.4.2
pytest-asyncio==1.2.0
pytest-xdist==3.8.0
python-dateutil==2.9.0.post0
- # via standardwebhooks
# via time-machine
respx==0.22.0
rich==14.2.0
@@ -128,39 +120,29 @@ six==1.17.0
# via python-dateutil
sniffio==1.3.1
# via kernel
-standardwebhooks==1.0.1
- # via kernel
time-machine==2.19.0
tomli==2.4.0
# via dependency-groups
# via mypy
# via nox
# via pytest
-types-deprecated==1.3.1.20251101
- # via standardwebhooks
-types-python-dateutil==2.9.0.20251115
- # via standardwebhooks
typing-extensions==4.15.0
# via aiosignal
# via anyio
# via exceptiongroup
+ # via kernel
# via multidict
# via mypy
# via pydantic
# via pydantic-core
# via pyright
# via pytest-asyncio
- # via kernel
# via typing-inspection
# via virtualenv
typing-inspection==0.4.2
# via pydantic
virtualenv==20.36.1
# via nox
-websockets==15.0.1
- # via kernel
-wrapt==2.0.1
- # via deprecated
yarl==1.22.0
# via aiohttp
zipp==3.23.0
diff --git a/requirements.lock b/requirements.lock
index 64675707..5f6c7ff4 100644
--- a/requirements.lock
+++ b/requirements.lock
@@ -26,12 +26,9 @@ async-timeout==5.0.1
# via aiohttp
attrs==25.4.0
# via aiohttp
- # via standardwebhooks
certifi==2026.1.4
# via httpcore
# via httpx
-deprecated==1.3.1
- # via standardwebhooks
distro==1.9.0
# via kernel
exceptiongroup==1.3.1
@@ -45,7 +42,6 @@ httpcore==1.0.9
# via httpx
httpx==0.28.1
# via httpx-aiohttp
- # via standardwebhooks
# via kernel
httpx-aiohttp==0.1.12
# via kernel
@@ -63,32 +59,18 @@ pydantic==2.12.5
# via kernel
pydantic-core==2.41.5
# via pydantic
-python-dateutil==2.9.0.post0
- # via standardwebhooks
-six==1.17.0
- # via python-dateutil
sniffio==1.3.1
# via kernel
-standardwebhooks==1.0.1
- # via kernel
-types-deprecated==1.3.1.20251101
- # via standardwebhooks
-types-python-dateutil==2.9.0.20251115
- # via standardwebhooks
typing-extensions==4.15.0
# via aiosignal
# via anyio
# via exceptiongroup
+ # via kernel
# via multidict
# via pydantic
# via pydantic-core
- # via kernel
# via typing-inspection
typing-inspection==0.4.2
# via pydantic
-websockets==15.0.1
- # via kernel
-wrapt==2.0.1
- # via deprecated
yarl==1.22.0
# via aiohttp
diff --git a/uv.lock b/uv.lock
deleted file mode 100644
index 79935255..00000000
--- a/uv.lock
+++ /dev/null
@@ -1,1521 +0,0 @@
-version = 1
-revision = 3
-requires-python = ">=3.9"
-resolution-markers = [
- "python_full_version >= '3.10'",
- "python_full_version < '3.10'",
-]
-
-[[package]]
-name = "aiohappyeyeballs"
-version = "2.6.1"
-source = { registry = "https://pypi.org/simple" }
-resolution-markers = [
- "python_full_version < '3.10'",
-]
-sdist = { url = "https://files.pythonhosted.org/packages/26/30/f84a107a9c4331c14b2b586036f40965c128aa4fee4dda5d3d51cb14ad54/aiohappyeyeballs-2.6.1.tar.gz", hash = "sha256:c3f9d0113123803ccadfdf3f0faa505bc78e6a72d1cc4806cbd719826e943558", size = 22760, upload-time = "2025-03-12T01:42:48.764Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/0f/15/5bf3b99495fb160b63f95972b81750f18f7f4e02ad051373b669d17d44f2/aiohappyeyeballs-2.6.1-py3-none-any.whl", hash = "sha256:f349ba8f4b75cb25c99c5c2d84e997e485204d2902a9597802b0371f09331fb8", size = 15265, upload-time = "2025-03-12T01:42:47.083Z" },
-]
-
-[[package]]
-name = "aiohappyeyeballs"
-version = "2.7.1"
-source = { registry = "https://pypi.org/simple" }
-resolution-markers = [
- "python_full_version >= '3.10'",
-]
-sdist = { url = "https://files.pythonhosted.org/packages/ce/f4/eec0465c2f67b2664688d0240b3212d5196fd89e741df67ddb81f8d35658/aiohappyeyeballs-2.7.1.tar.gz", hash = "sha256:065665c041c42a5938ed220bdcd7230f22527fbec085e1853d2402c8a3615d9d", size = 24757, upload-time = "2026-07-01T17:11:55.501Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/71/43/1947f06babed6b3f1d7f38b0c767f52df66bfb2bc10b468c4a7de9eceff2/aiohappyeyeballs-2.7.1-py3-none-any.whl", hash = "sha256:9243213661e29250eb41368e5daa826fc017156c3b8a11440826b2e3ed376472", size = 15038, upload-time = "2026-07-01T17:11:54.055Z" },
-]
-
-[[package]]
-name = "aiohttp"
-version = "3.13.5"
-source = { registry = "https://pypi.org/simple" }
-resolution-markers = [
- "python_full_version < '3.10'",
-]
-dependencies = [
- { name = "aiohappyeyeballs", version = "2.6.1", source = { registry = "https://pypi.org/simple" } },
- { name = "aiosignal" },
- { name = "async-timeout" },
- { name = "attrs" },
- { name = "frozenlist" },
- { name = "multidict" },
- { name = "propcache", version = "0.4.1", source = { registry = "https://pypi.org/simple" } },
- { name = "yarl", version = "1.22.0", source = { registry = "https://pypi.org/simple" } },
-]
-sdist = { url = "https://files.pythonhosted.org/packages/77/9a/152096d4808df8e4268befa55fba462f440f14beab85e8ad9bf990516918/aiohttp-3.13.5.tar.gz", hash = "sha256:9d98cc980ecc96be6eb4c1994ce35d28d8b1f5e5208a23b421187d1209dbb7d1", size = 7858271, upload-time = "2026-03-31T22:01:03.343Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/bd/85/cebc47ee74d8b408749073a1a46c6fcba13d170dc8af7e61996c6c9394ac/aiohttp-3.13.5-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:02222e7e233295f40e011c1b00e3b0bd451f22cf853a0304c3595633ee47da4b", size = 750547, upload-time = "2026-03-31T21:56:30.024Z" },
- { url = "https://files.pythonhosted.org/packages/05/98/afd308e35b9d3d8c9ec54c0918f1d722c86dc17ddfec272fcdbcce5a3124/aiohttp-3.13.5-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:bace460460ed20614fa6bc8cb09966c0b8517b8c58ad8046828c6078d25333b5", size = 503535, upload-time = "2026-03-31T21:56:31.935Z" },
- { url = "https://files.pythonhosted.org/packages/6f/4d/926c183e06b09d5270a309eb50fbde7b09782bfd305dec1e800f329834fb/aiohttp-3.13.5-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8f546a4dc1e6a5edbb9fd1fd6ad18134550e096a5a43f4ad74acfbd834fc6670", size = 497830, upload-time = "2026-03-31T21:56:33.654Z" },
- { url = "https://files.pythonhosted.org/packages/e4/d6/f47d1c690f115a5c2a5e8938cce4a232a5be9aac5c5fb2647efcbbbda333/aiohttp-3.13.5-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c86969d012e51b8e415a8c6ce96f7857d6a87d6207303ab02d5d11ef0cad2274", size = 1682474, upload-time = "2026-03-31T21:56:35.513Z" },
- { url = "https://files.pythonhosted.org/packages/01/44/056fd37b1bb52eac760303e5196acc74d9d546631b035704ae5927f7b4ac/aiohttp-3.13.5-cp310-cp310-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:b6f6cd1560c5fa427e3b6074bb24d2c64e225afbb7165008903bd42e4e33e28a", size = 1655259, upload-time = "2026-03-31T21:56:37.843Z" },
- { url = "https://files.pythonhosted.org/packages/91/9f/78eb1a20c1c28ae02f6a3c0f4d7b0dcc66abce5290cadd53d78ce3084175/aiohttp-3.13.5-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:636bc362f0c5bbc7372bc3ae49737f9e3030dbce469f0f422c8f38079780363d", size = 1736204, upload-time = "2026-03-31T21:56:39.822Z" },
- { url = "https://files.pythonhosted.org/packages/de/6c/d20d7de23f0b52b8c1d9e2033b2db1ac4dacbb470bb74c56de0f5f86bb4f/aiohttp-3.13.5-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:6a7cbeb06d1070f1d14895eeeed4dac5913b22d7b456f2eb969f11f4b3993796", size = 1826198, upload-time = "2026-03-31T21:56:41.378Z" },
- { url = "https://files.pythonhosted.org/packages/2f/86/a6f3ff1fd795f49545a7c74b2c92f62729135d73e7e4055bf74da5a26c82/aiohttp-3.13.5-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bca9ef7517fd7874a1a08970ae88f497bf5c984610caa0bf40bd7e8450852b95", size = 1681329, upload-time = "2026-03-31T21:56:43.374Z" },
- { url = "https://files.pythonhosted.org/packages/fb/68/84cd3dab6b7b4f3e6fe9459a961acb142aaab846417f6e8905110d7027e5/aiohttp-3.13.5-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:019a67772e034a0e6b9b17c13d0a8fe56ad9fb150fc724b7f3ffd3724288d9e5", size = 1560023, upload-time = "2026-03-31T21:56:45.031Z" },
- { url = "https://files.pythonhosted.org/packages/41/2c/db61b64b0249e30f954a65ab4cb4970ced57544b1de2e3c98ee5dc24165f/aiohttp-3.13.5-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:f34ecee82858e41dd217734f0c41a532bd066bcaab636ad830f03a30b2a96f2a", size = 1652372, upload-time = "2026-03-31T21:56:47.075Z" },
- { url = "https://files.pythonhosted.org/packages/25/6f/e96988a6c982d047810c772e28c43c64c300c943b0ed5c1c0c4ce1e1027c/aiohttp-3.13.5-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:4eac02d9af4813ee289cd63a361576da36dba57f5a1ab36377bc2600db0cbb73", size = 1662031, upload-time = "2026-03-31T21:56:48.835Z" },
- { url = "https://files.pythonhosted.org/packages/b7/26/a56feace81f3d347b4052403a9d03754a0ab23f7940780dada0849a38c92/aiohttp-3.13.5-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:4beac52e9fe46d6abf98b0176a88154b742e878fdf209d2248e99fcdf73cd297", size = 1708118, upload-time = "2026-03-31T21:56:50.833Z" },
- { url = "https://files.pythonhosted.org/packages/78/6e/b6173a8ff03d01d5e1a694bc06764b5dad1df2d4ed8f0ceec12bb3277936/aiohttp-3.13.5-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:c180f480207a9b2475f2b8d8bd7204e47aec952d084b2a2be58a782ffcf96074", size = 1548667, upload-time = "2026-03-31T21:56:52.81Z" },
- { url = "https://files.pythonhosted.org/packages/16/13/13296ffe2c132d888b3fe2c195c8b9c0c24c89c3fa5cc2c44464dc23b22e/aiohttp-3.13.5-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:2837fb92951564d6339cedae4a7231692aa9f73cbc4fb2e04263b96844e03b4e", size = 1724490, upload-time = "2026-03-31T21:56:54.541Z" },
- { url = "https://files.pythonhosted.org/packages/7a/b4/1f1c287f4a79782ef36e5a6e62954c85343bc30470d862d30bd5f26c9fa2/aiohttp-3.13.5-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:d9010032a0b9710f58012a1e9c222528763d860ba2ee1422c03473eab47703e7", size = 1667109, upload-time = "2026-03-31T21:56:56.21Z" },
- { url = "https://files.pythonhosted.org/packages/ef/42/8461a2aaf60a8f4ea4549a4056be36b904b0eb03d97ca9a8a2604681a500/aiohttp-3.13.5-cp310-cp310-win32.whl", hash = "sha256:7c4b6668b2b2b9027f209ddf647f2a4407784b5d88b8be4efcc72036f365baf9", size = 439478, upload-time = "2026-03-31T21:56:58.292Z" },
- { url = "https://files.pythonhosted.org/packages/e5/71/06956304cb5ee439dfe8d86e1b2e70088bd88ed1ced1f42fb29e5d855f0e/aiohttp-3.13.5-cp310-cp310-win_amd64.whl", hash = "sha256:cd3db5927bf9167d5a6157ddb2f036f6b6b0ad001ac82355d43e97a4bde76d76", size = 462047, upload-time = "2026-03-31T21:57:00.257Z" },
- { url = "https://files.pythonhosted.org/packages/d6/f5/a20c4ac64aeaef1679e25c9983573618ff765d7aa829fa2b84ae7573169e/aiohttp-3.13.5-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:7ab7229b6f9b5c1ba4910d6c41a9eb11f543eadb3f384df1b4c293f4e73d44d6", size = 757513, upload-time = "2026-03-31T21:57:02.146Z" },
- { url = "https://files.pythonhosted.org/packages/75/0a/39fa6c6b179b53fcb3e4b3d2b6d6cad0180854eda17060c7218540102bef/aiohttp-3.13.5-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:8f14c50708bb156b3a3ca7230b3d820199d56a48e3af76fa21c2d6087190fe3d", size = 506748, upload-time = "2026-03-31T21:57:04.275Z" },
- { url = "https://files.pythonhosted.org/packages/87/ec/e38ce072e724fd7add6243613f8d1810da084f54175353d25ccf9f9c7e5a/aiohttp-3.13.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e7d2f8616f0ff60bd332022279011776c3ac0faa0f1b463f7bb12326fbc97a1c", size = 501673, upload-time = "2026-03-31T21:57:06.208Z" },
- { url = "https://files.pythonhosted.org/packages/ba/ba/3bc7525d7e2beaa11b309a70d48b0d3cfc3c2089ec6a7d0820d59c657053/aiohttp-3.13.5-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a2567b72e1ffc3ab25510db43f355b29eeada56c0a622e58dcdb19530eb0a3cb", size = 1763757, upload-time = "2026-03-31T21:57:07.882Z" },
- { url = "https://files.pythonhosted.org/packages/5e/ab/e87744cf18f1bd78263aba24924d4953b41086bd3a31d22452378e9028a0/aiohttp-3.13.5-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:fb0540c854ac9c0c5ad495908fdfd3e332d553ec731698c0e29b1877ba0d2ec6", size = 1720152, upload-time = "2026-03-31T21:57:09.946Z" },
- { url = "https://files.pythonhosted.org/packages/6b/f3/ed17a6f2d742af17b50bae2d152315ed1b164b07a5fd5cc1754d99e4dfa5/aiohttp-3.13.5-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c9883051c6972f58bfc4ebb2116345ee2aa151178e99c3f2b2bbe2af712abd13", size = 1818010, upload-time = "2026-03-31T21:57:12.157Z" },
- { url = "https://files.pythonhosted.org/packages/53/06/ecbc63dc937192e2a5cb46df4d3edb21deb8225535818802f210a6ea5816/aiohttp-3.13.5-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:2294172ce08a82fb7c7273485895de1fa1186cc8294cfeb6aef4af42ad261174", size = 1907251, upload-time = "2026-03-31T21:57:14.023Z" },
- { url = "https://files.pythonhosted.org/packages/7e/a5/0521aa32c1ddf3aa1e71dcc466be0b7db2771907a13f18cddaa45967d97b/aiohttp-3.13.5-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3a807cabd5115fb55af198b98178997a5e0e57dead43eb74a93d9c07d6d4a7dc", size = 1759969, upload-time = "2026-03-31T21:57:16.146Z" },
- { url = "https://files.pythonhosted.org/packages/f6/78/a38f8c9105199dd3b9706745865a8a59d0041b6be0ca0cc4b2ccf1bab374/aiohttp-3.13.5-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:aa6d0d932e0f39c02b80744273cd5c388a2d9bc07760a03164f229c8e02662f6", size = 1616871, upload-time = "2026-03-31T21:57:17.856Z" },
- { url = "https://files.pythonhosted.org/packages/6f/41/27392a61ead8ab38072105c71aa44ff891e71653fe53d576a7067da2b4e8/aiohttp-3.13.5-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:60869c7ac4aaabe7110f26499f3e6e5696eae98144735b12a9c3d9eae2b51a49", size = 1739844, upload-time = "2026-03-31T21:57:19.679Z" },
- { url = "https://files.pythonhosted.org/packages/6e/55/5564e7ae26d94f3214250009a0b1c65a0c6af4bf88924ccb6fdab901de28/aiohttp-3.13.5-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:26d2f8546f1dfa75efa50c3488215a903c0168d253b75fba4210f57ab77a0fb8", size = 1731969, upload-time = "2026-03-31T21:57:22.006Z" },
- { url = "https://files.pythonhosted.org/packages/6d/c5/705a3929149865fc941bcbdd1047b238e4a72bcb215a9b16b9d7a2e8d992/aiohttp-3.13.5-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:f1162a1492032c82f14271e831c8f4b49f2b6078f4f5fc74de2c912fa225d51d", size = 1795193, upload-time = "2026-03-31T21:57:24.256Z" },
- { url = "https://files.pythonhosted.org/packages/a6/19/edabed62f718d02cff7231ca0db4ef1c72504235bc467f7b67adb1679f48/aiohttp-3.13.5-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:8b14eb3262fad0dc2f89c1a43b13727e709504972186ff6a99a3ecaa77102b6c", size = 1606477, upload-time = "2026-03-31T21:57:26.364Z" },
- { url = "https://files.pythonhosted.org/packages/de/fc/76f80ef008675637d88d0b21584596dc27410a990b0918cb1e5776545b5b/aiohttp-3.13.5-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:ca9ac61ac6db4eb6c2a0cd1d0f7e1357647b638ccc92f7e9d8d133e71ed3c6ac", size = 1813198, upload-time = "2026-03-31T21:57:28.316Z" },
- { url = "https://files.pythonhosted.org/packages/e5/67/5b3ac26b80adb20ea541c487f73730dc8fa107d632c998f25bbbab98fcda/aiohttp-3.13.5-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:7996023b2ed59489ae4762256c8516df9820f751cf2c5da8ed2fb20ee50abab3", size = 1752321, upload-time = "2026-03-31T21:57:30.549Z" },
- { url = "https://files.pythonhosted.org/packages/88/06/e4a2e49255ea23fa4feeb5ab092d90240d927c15e47b5b5c48dff5a9ce29/aiohttp-3.13.5-cp311-cp311-win32.whl", hash = "sha256:77dfa48c9f8013271011e51c00f8ada19851f013cde2c48fca1ba5e0caf5bb06", size = 439069, upload-time = "2026-03-31T21:57:32.388Z" },
- { url = "https://files.pythonhosted.org/packages/c0/43/8c7163a596dab4f8be12c190cf467a1e07e4734cf90eebb39f7f5d53fc6a/aiohttp-3.13.5-cp311-cp311-win_amd64.whl", hash = "sha256:d3a4834f221061624b8887090637db9ad4f61752001eae37d56c52fddade2dc8", size = 462859, upload-time = "2026-03-31T21:57:34.455Z" },
- { url = "https://files.pythonhosted.org/packages/be/6f/353954c29e7dcce7cf00280a02c75f30e133c00793c7a2ed3776d7b2f426/aiohttp-3.13.5-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:023ecba036ddd840b0b19bf195bfae970083fd7024ce1ac22e9bba90464620e9", size = 748876, upload-time = "2026-03-31T21:57:36.319Z" },
- { url = "https://files.pythonhosted.org/packages/f5/1b/428a7c64687b3b2e9cd293186695affc0e1e54a445d0361743b231f11066/aiohttp-3.13.5-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:15c933ad7920b7d9a20de151efcd05a6e38302cbf0e10c9b2acb9a42210a2416", size = 499557, upload-time = "2026-03-31T21:57:38.236Z" },
- { url = "https://files.pythonhosted.org/packages/29/47/7be41556bfbb6917069d6a6634bb7dd5e163ba445b783a90d40f5ac7e3a7/aiohttp-3.13.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ab2899f9fa2f9f741896ebb6fa07c4c883bfa5c7f2ddd8cf2aafa86fa981b2d2", size = 500258, upload-time = "2026-03-31T21:57:39.923Z" },
- { url = "https://files.pythonhosted.org/packages/67/84/c9ecc5828cb0b3695856c07c0a6817a99d51e2473400f705275a2b3d9239/aiohttp-3.13.5-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a60eaa2d440cd4707696b52e40ed3e2b0f73f65be07fd0ef23b6b539c9c0b0b4", size = 1749199, upload-time = "2026-03-31T21:57:41.938Z" },
- { url = "https://files.pythonhosted.org/packages/f0/d3/3c6d610e66b495657622edb6ae7c7fd31b2e9086b4ec50b47897ad6042a9/aiohttp-3.13.5-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:55b3bdd3292283295774ab585160c4004f4f2f203946997f49aac032c84649e9", size = 1721013, upload-time = "2026-03-31T21:57:43.904Z" },
- { url = "https://files.pythonhosted.org/packages/49/a0/24409c12217456df0bae7babe3b014e460b0b38a8e60753d6cb339f6556d/aiohttp-3.13.5-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c2b2355dc094e5f7d45a7bb262fe7207aa0460b37a0d87027dcf21b5d890e7d5", size = 1781501, upload-time = "2026-03-31T21:57:46.285Z" },
- { url = "https://files.pythonhosted.org/packages/98/9d/b65ec649adc5bccc008b0957a9a9c691070aeac4e41cea18559fef49958b/aiohttp-3.13.5-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b38765950832f7d728297689ad78f5f2cf79ff82487131c4d26fe6ceecdc5f8e", size = 1878981, upload-time = "2026-03-31T21:57:48.734Z" },
- { url = "https://files.pythonhosted.org/packages/57/d8/8d44036d7eb7b6a8ec4c5494ea0c8c8b94fbc0ed3991c1a7adf230df03bf/aiohttp-3.13.5-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b18f31b80d5a33661e08c89e202edabf1986e9b49c42b4504371daeaa11b47c1", size = 1767934, upload-time = "2026-03-31T21:57:51.171Z" },
- { url = "https://files.pythonhosted.org/packages/31/04/d3f8211f273356f158e3464e9e45484d3fb8c4ce5eb2f6fe9405c3273983/aiohttp-3.13.5-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:33add2463dde55c4f2d9635c6ab33ce154e5ecf322bd26d09af95c5f81cfa286", size = 1566671, upload-time = "2026-03-31T21:57:53.326Z" },
- { url = "https://files.pythonhosted.org/packages/41/db/073e4ebe00b78e2dfcacff734291651729a62953b48933d765dc513bf798/aiohttp-3.13.5-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:327cc432fdf1356fb4fbc6fe833ad4e9f6aacb71a8acaa5f1855e4b25910e4a9", size = 1705219, upload-time = "2026-03-31T21:57:55.385Z" },
- { url = "https://files.pythonhosted.org/packages/48/45/7dfba71a2f9fd97b15c95c06819de7eb38113d2cdb6319669195a7d64270/aiohttp-3.13.5-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:7c35b0bf0b48a70b4cb4fc5d7bed9b932532728e124874355de1a0af8ec4bc88", size = 1743049, upload-time = "2026-03-31T21:57:57.341Z" },
- { url = "https://files.pythonhosted.org/packages/18/71/901db0061e0f717d226386a7f471bb59b19566f2cae5f0d93874b017271f/aiohttp-3.13.5-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:df23d57718f24badef8656c49743e11a89fd6f5358fa8a7b96e728fda2abf7d3", size = 1749557, upload-time = "2026-03-31T21:57:59.626Z" },
- { url = "https://files.pythonhosted.org/packages/08/d5/41eebd16066e59cd43728fe74bce953d7402f2b4ddfdfef2c0e9f17ca274/aiohttp-3.13.5-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:02e048037a6501a5ec1f6fc9736135aec6eb8a004ce48838cb951c515f32c80b", size = 1558931, upload-time = "2026-03-31T21:58:01.972Z" },
- { url = "https://files.pythonhosted.org/packages/30/e6/4a799798bf05740e66c3a1161079bda7a3dd8e22ca392481d7a7f9af82a6/aiohttp-3.13.5-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:31cebae8b26f8a615d2b546fee45d5ffb76852ae6450e2a03f42c9102260d6fe", size = 1774125, upload-time = "2026-03-31T21:58:04.007Z" },
- { url = "https://files.pythonhosted.org/packages/84/63/7749337c90f92bc2cb18f9560d67aa6258c7060d1397d21529b8004fcf6f/aiohttp-3.13.5-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:888e78eb5ca55a615d285c3c09a7a91b42e9dd6fc699b166ebd5dee87c9ccf14", size = 1732427, upload-time = "2026-03-31T21:58:06.337Z" },
- { url = "https://files.pythonhosted.org/packages/98/de/cf2f44ff98d307e72fb97d5f5bbae3bfcb442f0ea9790c0bf5c5c2331404/aiohttp-3.13.5-cp312-cp312-win32.whl", hash = "sha256:8bd3ec6376e68a41f9f95f5ed170e2fcf22d4eb27a1f8cb361d0508f6e0557f3", size = 433534, upload-time = "2026-03-31T21:58:08.712Z" },
- { url = "https://files.pythonhosted.org/packages/aa/ca/eadf6f9c8fa5e31d40993e3db153fb5ed0b11008ad5d9de98a95045bed84/aiohttp-3.13.5-cp312-cp312-win_amd64.whl", hash = "sha256:110e448e02c729bcebb18c60b9214a87ba33bac4a9fa5e9a5f139938b56c6cb1", size = 460446, upload-time = "2026-03-31T21:58:10.945Z" },
- { url = "https://files.pythonhosted.org/packages/78/e9/d76bf503005709e390122d34e15256b88f7008e246c4bdbe915cd4f1adce/aiohttp-3.13.5-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:a5029cc80718bbd545123cd8fe5d15025eccaaaace5d0eeec6bd556ad6163d61", size = 742930, upload-time = "2026-03-31T21:58:13.155Z" },
- { url = "https://files.pythonhosted.org/packages/57/00/4b7b70223deaebd9bb85984d01a764b0d7bd6526fcdc73cca83bcbe7243e/aiohttp-3.13.5-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:4bb6bf5811620003614076bdc807ef3b5e38244f9d25ca5fe888eaccea2a9832", size = 496927, upload-time = "2026-03-31T21:58:15.073Z" },
- { url = "https://files.pythonhosted.org/packages/9c/f5/0fb20fb49f8efdcdce6cd8127604ad2c503e754a8f139f5e02b01626523f/aiohttp-3.13.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a84792f8631bf5a94e52d9cc881c0b824ab42717165a5579c760b830d9392ac9", size = 497141, upload-time = "2026-03-31T21:58:17.009Z" },
- { url = "https://files.pythonhosted.org/packages/3b/86/b7c870053e36a94e8951b803cb5b909bfbc9b90ca941527f5fcafbf6b0fa/aiohttp-3.13.5-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:57653eac22c6a4c13eb22ecf4d673d64a12f266e72785ab1c8b8e5940d0e8090", size = 1732476, upload-time = "2026-03-31T21:58:18.925Z" },
- { url = "https://files.pythonhosted.org/packages/b5/e5/4e161f84f98d80c03a238671b4136e6530453d65262867d989bbe78244d0/aiohttp-3.13.5-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:e5e5f7debc7a57af53fdf5c5009f9391d9f4c12867049d509bf7bb164a6e295b", size = 1706507, upload-time = "2026-03-31T21:58:21.094Z" },
- { url = "https://files.pythonhosted.org/packages/d4/56/ea11a9f01518bd5a2a2fcee869d248c4b8a0cfa0bb13401574fa31adf4d4/aiohttp-3.13.5-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c719f65bebcdf6716f10e9eff80d27567f7892d8988c06de12bbbd39307c6e3a", size = 1773465, upload-time = "2026-03-31T21:58:23.159Z" },
- { url = "https://files.pythonhosted.org/packages/eb/40/333ca27fb74b0383f17c90570c748f7582501507307350a79d9f9f3c6eb1/aiohttp-3.13.5-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d97f93fdae594d886c5a866636397e2bcab146fd7a132fd6bb9ce182224452f8", size = 1873523, upload-time = "2026-03-31T21:58:25.59Z" },
- { url = "https://files.pythonhosted.org/packages/f0/d2/e2f77eef1acb7111405433c707dc735e63f67a56e176e72e9e7a2cd3f493/aiohttp-3.13.5-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3df334e39d4c2f899a914f1dba283c1aadc311790733f705182998c6f7cae665", size = 1754113, upload-time = "2026-03-31T21:58:27.624Z" },
- { url = "https://files.pythonhosted.org/packages/fb/56/3f653d7f53c89669301ec9e42c95233e2a0c0a6dd051269e6e678db4fdb0/aiohttp-3.13.5-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:fe6970addfea9e5e081401bcbadf865d2b6da045472f58af08427e108d618540", size = 1562351, upload-time = "2026-03-31T21:58:29.918Z" },
- { url = "https://files.pythonhosted.org/packages/ec/a6/9b3e91eb8ae791cce4ee736da02211c85c6f835f1bdfac0594a8a3b7018c/aiohttp-3.13.5-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:7becdf835feff2f4f335d7477f121af787e3504b48b449ff737afb35869ba7bb", size = 1693205, upload-time = "2026-03-31T21:58:32.214Z" },
- { url = "https://files.pythonhosted.org/packages/98/fc/bfb437a99a2fcebd6b6eaec609571954de2ed424f01c352f4b5504371dd3/aiohttp-3.13.5-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:676e5651705ad5d8a70aeb8eb6936c436d8ebbd56e63436cb7dd9bb36d2a9a46", size = 1730618, upload-time = "2026-03-31T21:58:34.728Z" },
- { url = "https://files.pythonhosted.org/packages/e4/b6/c8534862126191a034f68153194c389addc285a0f1347d85096d349bbc15/aiohttp-3.13.5-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:9b16c653d38eb1a611cc898c41e76859ca27f119d25b53c12875fd0474ae31a8", size = 1745185, upload-time = "2026-03-31T21:58:36.909Z" },
- { url = "https://files.pythonhosted.org/packages/0b/93/4ca8ee2ef5236e2707e0fd5fecb10ce214aee1ff4ab307af9c558bda3b37/aiohttp-3.13.5-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:999802d5fa0389f58decd24b537c54aa63c01c3219ce17d1214cbda3c2b22d2d", size = 1557311, upload-time = "2026-03-31T21:58:39.38Z" },
- { url = "https://files.pythonhosted.org/packages/57/ae/76177b15f18c5f5d094f19901d284025db28eccc5ae374d1d254181d33f4/aiohttp-3.13.5-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:ec707059ee75732b1ba130ed5f9580fe10ff75180c812bc267ded039db5128c6", size = 1773147, upload-time = "2026-03-31T21:58:41.476Z" },
- { url = "https://files.pythonhosted.org/packages/01/a4/62f05a0a98d88af59d93b7fcac564e5f18f513cb7471696ac286db970d6a/aiohttp-3.13.5-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:2d6d44a5b48132053c2f6cd5c8cb14bc67e99a63594e336b0f2af81e94d5530c", size = 1730356, upload-time = "2026-03-31T21:58:44.049Z" },
- { url = "https://files.pythonhosted.org/packages/e4/85/fc8601f59dfa8c9523808281f2da571f8b4699685f9809a228adcc90838d/aiohttp-3.13.5-cp313-cp313-win32.whl", hash = "sha256:329f292ed14d38a6c4c435e465f48bebb47479fd676a0411936cc371643225cc", size = 432637, upload-time = "2026-03-31T21:58:46.167Z" },
- { url = "https://files.pythonhosted.org/packages/c0/1b/ac685a8882896acf0f6b31d689e3792199cfe7aba37969fa91da63a7fa27/aiohttp-3.13.5-cp313-cp313-win_amd64.whl", hash = "sha256:69f571de7500e0557801c0b51f4780482c0ec5fe2ac851af5a92cfce1af1cb83", size = 458896, upload-time = "2026-03-31T21:58:48.119Z" },
- { url = "https://files.pythonhosted.org/packages/5d/ce/46572759afc859e867a5bc8ec3487315869013f59281ce61764f76d879de/aiohttp-3.13.5-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:eb4639f32fd4a9904ab8fb45bf3383ba71137f3d9d4ba25b3b3f3109977c5b8c", size = 745721, upload-time = "2026-03-31T21:58:50.229Z" },
- { url = "https://files.pythonhosted.org/packages/13/fe/8a2efd7626dbe6049b2ef8ace18ffda8a4dfcbe1bcff3ac30c0c7575c20b/aiohttp-3.13.5-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:7e5dc4311bd5ac493886c63cbf76ab579dbe4641268e7c74e48e774c74b6f2be", size = 497663, upload-time = "2026-03-31T21:58:52.232Z" },
- { url = "https://files.pythonhosted.org/packages/9b/91/cc8cc78a111826c54743d88651e1687008133c37e5ee615fee9b57990fac/aiohttp-3.13.5-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:756c3c304d394977519824449600adaf2be0ccee76d206ee339c5e76b70ded25", size = 499094, upload-time = "2026-03-31T21:58:54.566Z" },
- { url = "https://files.pythonhosted.org/packages/0a/33/a8362cb15cf16a3af7e86ed11962d5cd7d59b449202dc576cdc731310bde/aiohttp-3.13.5-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ecc26751323224cf8186efcf7fbcbc30f4e1d8c7970659daf25ad995e4032a56", size = 1726701, upload-time = "2026-03-31T21:58:56.864Z" },
- { url = "https://files.pythonhosted.org/packages/45/0c/c091ac5c3a17114bd76cbf85d674650969ddf93387876cf67f754204bd77/aiohttp-3.13.5-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:10a75acfcf794edf9d8db50e5a7ec5fc818b2a8d3f591ce93bc7b1210df016d2", size = 1683360, upload-time = "2026-03-31T21:58:59.072Z" },
- { url = "https://files.pythonhosted.org/packages/23/73/bcee1c2b79bc275e964d1446c55c54441a461938e70267c86afaae6fba27/aiohttp-3.13.5-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:0f7a18f258d124cd678c5fe072fe4432a4d5232b0657fca7c1847f599233c83a", size = 1773023, upload-time = "2026-03-31T21:59:01.776Z" },
- { url = "https://files.pythonhosted.org/packages/c7/ef/720e639df03004fee2d869f771799d8c23046dec47d5b81e396c7cda583a/aiohttp-3.13.5-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:df6104c009713d3a89621096f3e3e88cc323fd269dbd7c20afe18535094320be", size = 1853795, upload-time = "2026-03-31T21:59:04.568Z" },
- { url = "https://files.pythonhosted.org/packages/bd/c9/989f4034fb46841208de7aeeac2c6d8300745ab4f28c42f629ba77c2d916/aiohttp-3.13.5-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:241a94f7de7c0c3b616627aaad530fe2cb620084a8b144d3be7b6ecfe95bae3b", size = 1730405, upload-time = "2026-03-31T21:59:07.221Z" },
- { url = "https://files.pythonhosted.org/packages/ce/75/ee1fd286ca7dc599d824b5651dad7b3be7ff8d9a7e7b3fe9820d9180f7db/aiohttp-3.13.5-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c974fb66180e58709b6fc402846f13791240d180b74de81d23913abe48e96d94", size = 1558082, upload-time = "2026-03-31T21:59:09.484Z" },
- { url = "https://files.pythonhosted.org/packages/c3/20/1e9e6650dfc436340116b7aa89ff8cb2bbdf0abc11dfaceaad8f74273a10/aiohttp-3.13.5-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:6e27ea05d184afac78aabbac667450c75e54e35f62238d44463131bd3f96753d", size = 1692346, upload-time = "2026-03-31T21:59:12.068Z" },
- { url = "https://files.pythonhosted.org/packages/d8/40/8ebc6658d48ea630ac7903912fe0dd4e262f0e16825aa4c833c56c9f1f56/aiohttp-3.13.5-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:a79a6d399cef33a11b6f004c67bb07741d91f2be01b8d712d52c75711b1e07c7", size = 1698891, upload-time = "2026-03-31T21:59:14.552Z" },
- { url = "https://files.pythonhosted.org/packages/d8/78/ea0ae5ec8ba7a5c10bdd6e318f1ba5e76fcde17db8275188772afc7917a4/aiohttp-3.13.5-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:c632ce9c0b534fbe25b52c974515ed674937c5b99f549a92127c85f771a78772", size = 1742113, upload-time = "2026-03-31T21:59:17.068Z" },
- { url = "https://files.pythonhosted.org/packages/8a/66/9d308ed71e3f2491be1acb8769d96c6f0c47d92099f3bc9119cada27b357/aiohttp-3.13.5-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:fceedde51fbd67ee2bcc8c0b33d0126cc8b51ef3bbde2f86662bd6d5a6f10ec5", size = 1553088, upload-time = "2026-03-31T21:59:19.541Z" },
- { url = "https://files.pythonhosted.org/packages/da/a6/6cc25ed8dfc6e00c90f5c6d126a98e2cf28957ad06fa1036bd34b6f24a2c/aiohttp-3.13.5-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:f92995dfec9420bb69ae629abf422e516923ba79ba4403bc750d94fb4a6c68c1", size = 1757976, upload-time = "2026-03-31T21:59:22.311Z" },
- { url = "https://files.pythonhosted.org/packages/c1/2b/cce5b0ffe0de99c83e5e36d8f828e4161e415660a9f3e58339d07cce3006/aiohttp-3.13.5-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:20ae0ff08b1f2c8788d6fb85afcb798654ae6ba0b747575f8562de738078457b", size = 1712444, upload-time = "2026-03-31T21:59:24.635Z" },
- { url = "https://files.pythonhosted.org/packages/6c/cf/9e1795b4160c58d29421eafd1a69c6ce351e2f7c8d3c6b7e4ca44aea1a5b/aiohttp-3.13.5-cp314-cp314-win32.whl", hash = "sha256:b20df693de16f42b2472a9c485e1c948ee55524786a0a34345511afdd22246f3", size = 438128, upload-time = "2026-03-31T21:59:27.291Z" },
- { url = "https://files.pythonhosted.org/packages/22/4d/eaedff67fc805aeba4ba746aec891b4b24cebb1a7d078084b6300f79d063/aiohttp-3.13.5-cp314-cp314-win_amd64.whl", hash = "sha256:f85c6f327bf0b8c29da7d93b1cabb6363fb5e4e160a32fa241ed2dce21b73162", size = 464029, upload-time = "2026-03-31T21:59:29.429Z" },
- { url = "https://files.pythonhosted.org/packages/79/11/c27d9332ee20d68dd164dc12a6ecdef2e2e35ecc97ed6cf0d2442844624b/aiohttp-3.13.5-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:1efb06900858bb618ff5cee184ae2de5828896c448403d51fb633f09e109be0a", size = 778758, upload-time = "2026-03-31T21:59:31.547Z" },
- { url = "https://files.pythonhosted.org/packages/04/fb/377aead2e0a3ba5f09b7624f702a964bdf4f08b5b6728a9799830c80041e/aiohttp-3.13.5-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:fee86b7c4bd29bdaf0d53d14739b08a106fdda809ca5fe032a15f52fae5fe254", size = 512883, upload-time = "2026-03-31T21:59:34.098Z" },
- { url = "https://files.pythonhosted.org/packages/bb/a6/aa109a33671f7a5d3bd78b46da9d852797c5e665bfda7d6b373f56bff2ec/aiohttp-3.13.5-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:20058e23909b9e65f9da62b396b77dfa95965cbe840f8def6e572538b1d32e36", size = 516668, upload-time = "2026-03-31T21:59:36.497Z" },
- { url = "https://files.pythonhosted.org/packages/79/b3/ca078f9f2fa9563c36fb8ef89053ea2bb146d6f792c5104574d49d8acb63/aiohttp-3.13.5-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8cf20a8d6868cb15a73cab329ffc07291ba8c22b1b88176026106ae39aa6df0f", size = 1883461, upload-time = "2026-03-31T21:59:38.723Z" },
- { url = "https://files.pythonhosted.org/packages/b7/e3/a7ad633ca1ca497b852233a3cce6906a56c3225fb6d9217b5e5e60b7419d/aiohttp-3.13.5-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:330f5da04c987f1d5bdb8ae189137c77139f36bd1cb23779ca1a354a4b027800", size = 1747661, upload-time = "2026-03-31T21:59:41.187Z" },
- { url = "https://files.pythonhosted.org/packages/33/b9/cd6fe579bed34a906d3d783fe60f2fa297ef55b27bb4538438ee49d4dc41/aiohttp-3.13.5-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:6f1cbf0c7926d315c3c26c2da41fd2b5d2fe01ac0e157b78caefc51a782196cf", size = 1863800, upload-time = "2026-03-31T21:59:43.84Z" },
- { url = "https://files.pythonhosted.org/packages/c0/3f/2c1e2f5144cefa889c8afd5cf431994c32f3b29da9961698ff4e3811b79a/aiohttp-3.13.5-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:53fc049ed6390d05423ba33103ded7281fe897cf97878f369a527070bd95795b", size = 1958382, upload-time = "2026-03-31T21:59:46.187Z" },
- { url = "https://files.pythonhosted.org/packages/66/1d/f31ec3f1013723b3babe3609e7f119c2c2fb6ef33da90061a705ef3e1bc8/aiohttp-3.13.5-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:898703aa2667e3c5ca4c54ca36cd73f58b7a38ef87a5606414799ebce4d3fd3a", size = 1803724, upload-time = "2026-03-31T21:59:48.656Z" },
- { url = "https://files.pythonhosted.org/packages/0e/b4/57712dfc6f1542f067daa81eb61da282fab3e6f1966fca25db06c4fc62d5/aiohttp-3.13.5-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0494a01ca9584eea1e5fbd6d748e61ecff218c51b576ee1999c23db7066417d8", size = 1640027, upload-time = "2026-03-31T21:59:51.284Z" },
- { url = "https://files.pythonhosted.org/packages/25/3c/734c878fb43ec083d8e31bf029daae1beafeae582d1b35da234739e82ee7/aiohttp-3.13.5-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:6cf81fe010b8c17b09495cbd15c1d35afbc8fb405c0c9cf4738e5ae3af1d65be", size = 1806644, upload-time = "2026-03-31T21:59:53.753Z" },
- { url = "https://files.pythonhosted.org/packages/20/a5/f671e5cbec1c21d044ff3078223f949748f3a7f86b14e34a365d74a5d21f/aiohttp-3.13.5-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:c564dd5f09ddc9d8f2c2d0a301cd30a79a2cc1b46dd1a73bef8f0038863d016b", size = 1791630, upload-time = "2026-03-31T21:59:56.239Z" },
- { url = "https://files.pythonhosted.org/packages/0b/63/fb8d0ad63a0b8a99be97deac8c04dacf0785721c158bdf23d679a87aa99e/aiohttp-3.13.5-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:2994be9f6e51046c4f864598fd9abeb4fba6e88f0b2152422c9666dcd4aea9c6", size = 1809403, upload-time = "2026-03-31T21:59:59.103Z" },
- { url = "https://files.pythonhosted.org/packages/59/0c/bfed7f30662fcf12206481c2aac57dedee43fe1c49275e85b3a1e1742294/aiohttp-3.13.5-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:157826e2fa245d2ef46c83ea8a5faf77ca19355d278d425c29fda0beb3318037", size = 1634924, upload-time = "2026-03-31T22:00:02.116Z" },
- { url = "https://files.pythonhosted.org/packages/17/d6/fd518d668a09fd5a3319ae5e984d4d80b9a4b3df4e21c52f02251ef5a32e/aiohttp-3.13.5-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:a8aca50daa9493e9e13c0f566201a9006f080e7c50e5e90d0b06f53146a54500", size = 1836119, upload-time = "2026-03-31T22:00:04.756Z" },
- { url = "https://files.pythonhosted.org/packages/78/b7/15fb7a9d52e112a25b621c67b69c167805cb1f2ab8f1708a5c490d1b52fe/aiohttp-3.13.5-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:3b13560160d07e047a93f23aaa30718606493036253d5430887514715b67c9d9", size = 1772072, upload-time = "2026-03-31T22:00:07.494Z" },
- { url = "https://files.pythonhosted.org/packages/7e/df/57ba7f0c4a553fc2bd8b6321df236870ec6fd64a2a473a8a13d4f733214e/aiohttp-3.13.5-cp314-cp314t-win32.whl", hash = "sha256:9a0f4474b6ea6818b41f82172d799e4b3d29e22c2c520ce4357856fced9af2f8", size = 471819, upload-time = "2026-03-31T22:00:10.277Z" },
- { url = "https://files.pythonhosted.org/packages/62/29/2f8418269e46454a26171bfdd6a055d74febf32234e474930f2f60a17145/aiohttp-3.13.5-cp314-cp314t-win_amd64.whl", hash = "sha256:18a2f6c1182c51baa1d28d68fea51513cb2a76612f038853c0ad3c145423d3d9", size = 505441, upload-time = "2026-03-31T22:00:12.791Z" },
- { url = "https://files.pythonhosted.org/packages/e2/a5/630bc484695d4a1342bbae85fb8689bf979106525684fc88f05b397324ad/aiohttp-3.13.5-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:347542f0ea3f95b2a955ee6656461fa1c776e401ac50ebce055a6c38454a0adf", size = 752872, upload-time = "2026-03-31T22:00:15.553Z" },
- { url = "https://files.pythonhosted.org/packages/cd/b8/6a19dda37fda94a9ebefb3c1ae0ff419ac7fbf4fb40750e992829fc13614/aiohttp-3.13.5-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:178c7b5e62b454c2bc790786e6058c3cc968613b4419251b478c153a4aec32b1", size = 504582, upload-time = "2026-03-31T22:00:18.191Z" },
- { url = "https://files.pythonhosted.org/packages/d5/34/8413eafee3421ade2d6ce9e7c0da1213e1d7f0049be09dcdc342b03a39ba/aiohttp-3.13.5-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:af545c2cffdb0967a96b6249e6f5f7b0d92cdfd267f9d5238d5b9ca63e8edb10", size = 499094, upload-time = "2026-03-31T22:00:21.118Z" },
- { url = "https://files.pythonhosted.org/packages/da/cf/c6f97006093d1e8ca40fbab843ff49ec7725ab668f0714dd1cb702c62cbd/aiohttp-3.13.5-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:206b7b3ef96e4ce211754f0cd003feb28b7d81f0ad26b8d077a5d5161436067f", size = 1669505, upload-time = "2026-03-31T22:00:24.01Z" },
- { url = "https://files.pythonhosted.org/packages/c2/27/3b2288e66dcec8b04771b2bee3909f70e4072bea995cde5ab7e775e73ddc/aiohttp-3.13.5-cp39-cp39-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:ee5e86776273de1795947d17bddd6bb19e0365fd2af4289c0d2c5454b6b1d36b", size = 1648928, upload-time = "2026-03-31T22:00:27.001Z" },
- { url = "https://files.pythonhosted.org/packages/3a/7f/605d766887594a88dcc27a19663499c7c5e13e7aa87f129b763765a2ee63/aiohttp-3.13.5-cp39-cp39-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:95d14ca7abefde230f7639ec136ade282655431fd5db03c343b19dda72dd1643", size = 1731800, upload-time = "2026-03-31T22:00:29.603Z" },
- { url = "https://files.pythonhosted.org/packages/71/94/5a878e728e30699d22b118f1a6ad576ab6fff9eb2c6fc8a7faa9376a1c3e/aiohttp-3.13.5-cp39-cp39-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:912d4b6af530ddb1338a66229dac3a25ff11d4448be3ec3d6340583995f56031", size = 1824247, upload-time = "2026-03-31T22:00:32.139Z" },
- { url = "https://files.pythonhosted.org/packages/37/99/84b448291e9996bb83bf4fad3a71a9786d542f19c50a3ff0531bfaba6fac/aiohttp-3.13.5-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e999f0c88a458c836d5fb521814e92ed2172c649200336a6df514987c1488258", size = 1670742, upload-time = "2026-03-31T22:00:34.788Z" },
- { url = "https://files.pythonhosted.org/packages/14/a8/d8d5d1ab6d29a4a3bdb9db31f161e338bfdf6638f6574ea8380f1d4a243c/aiohttp-3.13.5-cp39-cp39-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:39380e12bd1f2fdab4285b6e055ad48efbaed5c836433b142ed4f5b9be71036a", size = 1562474, upload-time = "2026-03-31T22:00:37.623Z" },
- { url = "https://files.pythonhosted.org/packages/92/e8/bd889697916f10b65524422c61b4eeaf919eb35a170290cccb680cbe4eb4/aiohttp-3.13.5-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:9efcc0f11d850cefcafdd9275b9576ad3bfb539bed96807663b32ad99c4d4b88", size = 1642235, upload-time = "2026-03-31T22:00:40.541Z" },
- { url = "https://files.pythonhosted.org/packages/60/42/3f1928107131f1413a5972ace14ddcd5364968e9bd7b3ad71272defafc9c/aiohttp-3.13.5-cp39-cp39-musllinux_1_2_armv7l.whl", hash = "sha256:147b4f501d0292077f29d5268c16bb7c864a1f054d7001c4c1812c0421ea1ed0", size = 1655397, upload-time = "2026-03-31T22:00:43.167Z" },
- { url = "https://files.pythonhosted.org/packages/b2/79/c4bbcf4cac3a4715a326e49720ccdc3a4b5e14a367c5029eae7727d06029/aiohttp-3.13.5-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:d147004fede1b12f6013a6dbb2a26a986a671a03c6ea740ddc76500e5f1c399f", size = 1703509, upload-time = "2026-03-31T22:00:45.908Z" },
- { url = "https://files.pythonhosted.org/packages/d1/e6/32d245876f211a7308a7d5437707f9296b1f9837a2888a407ed04e61321c/aiohttp-3.13.5-cp39-cp39-musllinux_1_2_riscv64.whl", hash = "sha256:9277145d36a01653863899c665243871434694bcc3431922c3b35c978061bdb8", size = 1550098, upload-time = "2026-03-31T22:00:49.48Z" },
- { url = "https://files.pythonhosted.org/packages/db/62/ab0f1304def56ce2356e6fbb9f0b024d6544010351430070f48f53b89e0a/aiohttp-3.13.5-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:4e704c52438f66fdd89588346183d898bb42167cf88f8b7ff1c0f9fc957c348f", size = 1724326, upload-time = "2026-03-31T22:00:52.165Z" },
- { url = "https://files.pythonhosted.org/packages/c4/9a/aab4469689024046220ea438aa020ea2ae04cd1dd71aea3057e094f8c357/aiohttp-3.13.5-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:a8a4d3427e8de1312ddf309cc482186466c79895b3a139fed3259fc01dfa9a5b", size = 1658824, upload-time = "2026-03-31T22:00:55.122Z" },
- { url = "https://files.pythonhosted.org/packages/b0/98/bcc35d4db687acabf06d41f561a99fa88bca145292513388c858d99b72c5/aiohttp-3.13.5-cp39-cp39-win32.whl", hash = "sha256:6f497a6876aa4b1a102b04996ce4c1170c7040d83faa9387dd921c16e30d5c83", size = 440302, upload-time = "2026-03-31T22:00:57.673Z" },
- { url = "https://files.pythonhosted.org/packages/25/61/b0203c2ef6bd268fca0eda142f0efbba7cbebd7ad38f7bb01dd31c2ff68e/aiohttp-3.13.5-cp39-cp39-win_amd64.whl", hash = "sha256:cb979826071c0986a5f08333a36104153478ce6018c58cba7f9caddaf63d5d67", size = 463076, upload-time = "2026-03-31T22:01:00.264Z" },
-]
-
-[[package]]
-name = "aiohttp"
-version = "3.14.3"
-source = { registry = "https://pypi.org/simple" }
-resolution-markers = [
- "python_full_version >= '3.10'",
-]
-dependencies = [
- { name = "aiohappyeyeballs", version = "2.7.1", source = { registry = "https://pypi.org/simple" } },
- { name = "aiosignal" },
- { name = "async-timeout", marker = "python_full_version < '3.11'" },
- { name = "attrs" },
- { name = "frozenlist" },
- { name = "multidict" },
- { name = "propcache", version = "0.5.2", source = { registry = "https://pypi.org/simple" } },
- { name = "typing-extensions", marker = "python_full_version < '3.13'" },
- { name = "yarl", version = "1.24.5", source = { registry = "https://pypi.org/simple" } },
-]
-sdist = { url = "https://files.pythonhosted.org/packages/58/d9/22ce5786ac0c1653ae8b6c23bded02c1686d11f0dbb45b31ce128e0df985/aiohttp-3.14.3.tar.gz", hash = "sha256:9491196535a88924a60afd5b5f434b5b203b6cc616250878dbdb223a8f7844bc", size = 7971213, upload-time = "2026-07-23T01:57:27.037Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/2d/4d/4a99fb425c5e0cad715eea7bd190aff46f38b959a0a2dadb993705d34b26/aiohttp-3.14.3-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:eb0495d778817619273c108784292be161a924b9f5ae5cbbc70a2caa6838250b", size = 765848, upload-time = "2026-07-23T01:52:08.217Z" },
- { url = "https://files.pythonhosted.org/packages/74/e8/43b85dc55b8e950dc644babe762add781319ea881b57b33d2cce12017d12/aiohttp-3.14.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:c3c200cf9757edd785051dc699c7ecbec22110dbfcb3fefc7a9f9695eda8ea7a", size = 517476, upload-time = "2026-07-23T01:52:10.846Z" },
- { url = "https://files.pythonhosted.org/packages/7f/9e/73b582c4dbbc3c12ef4473822475effaabf1f934b56f14f5b03fe5d3a2af/aiohttp-3.14.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:fd51ebf9d3a00c074df4ede271023f4d2dba289bcc740b88191872716014e3c5", size = 515334, upload-time = "2026-07-23T01:52:12.636Z" },
- { url = "https://files.pythonhosted.org/packages/79/03/e98c3c9e05a5bdf97defe5ff9169baba4f0ec9a901f2d60e0f060c2f051e/aiohttp-3.14.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:134ac5ddcf61c6fad984b9a5727d83492ada43d63471db20fb73042c13fca62f", size = 1708830, upload-time = "2026-07-23T01:52:14.538Z" },
- { url = "https://files.pythonhosted.org/packages/d7/2c/26e60b694844dfd2176c57f913a22d0cd6a16f9ff202cbda7580d0328b98/aiohttp-3.14.3-cp310-cp310-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:70c987b27534f9ae1a723f47ae921571d616da21d3208282bf4c52af5164ac43", size = 1674012, upload-time = "2026-07-23T01:52:16.486Z" },
- { url = "https://files.pythonhosted.org/packages/38/65/672df92e3172cd876aacfa97a952ac560877eb169384b2991ac5b273de4c/aiohttp-3.14.3-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:1b59533861b70a2185c8f4f350f791f39d64358ef6944ce71c5240c9ec0982c9", size = 1767015, upload-time = "2026-07-23T01:52:18.28Z" },
- { url = "https://files.pythonhosted.org/packages/9e/c5/228dec7bfec1c373cc2217cdeb47d6456dcd7a13a4c55144930a75ae3851/aiohttp-3.14.3-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:1c5281acc88b92396f88c7e1e2748f8466689df22b80170e4f51efa712fb47a8", size = 1858700, upload-time = "2026-07-23T01:52:20.08Z" },
- { url = "https://files.pythonhosted.org/packages/bd/ff/cb36724e8c8d17f90ada567a9ff3efe1d6e9b549fba697a242aece180f21/aiohttp-3.14.3-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:48d67b87db6279c044760787eb01f6413032c2e6f3ba1cafaa492b1c8e578479", size = 1714075, upload-time = "2026-07-23T01:52:22.071Z" },
- { url = "https://files.pythonhosted.org/packages/9f/3a/296a4135c6366376263aeef54b15caca1f07676c2ae0c525d7832f2f808a/aiohttp-3.14.3-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f53bcd52f585e1ac3e590d61434eb61f9a88c38df041b4ea126d97144344a77b", size = 1588234, upload-time = "2026-07-23T01:52:23.757Z" },
- { url = "https://files.pythonhosted.org/packages/7d/81/9d5d853ef892dc066d1eb6db0e87a47348b920c1c879aa554612fdbd9d79/aiohttp-3.14.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:0fdea2281997af69da84c77ffa6f5938a0285f21fb3887c249d67419ca865b3d", size = 1677300, upload-time = "2026-07-23T01:52:25.861Z" },
- { url = "https://files.pythonhosted.org/packages/68/96/021d386ae32d9b26d4b88df2e794546232ff56bb6be952bf6be227c0bbc7/aiohttp-3.14.3-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:cda5fd5c95ad7a125a2e8464acc78b98b94c475a3780d6aa0aa157c93f470f4d", size = 1691501, upload-time = "2026-07-23T01:52:28Z" },
- { url = "https://files.pythonhosted.org/packages/29/9f/af66adce26a14af135c003cbd0f44ccaa68cebd30ff8ac99ca47fb4958f7/aiohttp-3.14.3-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:6debfa7312ff9d4c124dc71d72e9a0a4b9e0879e48ba6fcb42bef5c3300289e2", size = 1735113, upload-time = "2026-07-23T01:52:29.995Z" },
- { url = "https://files.pythonhosted.org/packages/2f/90/28c390d4c9851effe52ac25b5a2e1d92246acd00728b4fc7975dafb67484/aiohttp-3.14.3-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:f4e05329faa0ea1a404b37de4f034fd2c2defcca06a68dc6745e4e56c88e8a48", size = 1577486, upload-time = "2026-07-23T01:52:31.937Z" },
- { url = "https://files.pythonhosted.org/packages/db/c2/00e23a1bf2abb70dd353f6987db7e7f2491d0261f7363997738c71c98f95/aiohttp-3.14.3-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:a3a8296e7ab5c295f53f1041487cb088e1480775aafbf7fe545d93b770a0f96f", size = 1751353, upload-time = "2026-07-23T01:52:33.688Z" },
- { url = "https://files.pythonhosted.org/packages/6e/7d/d51a706a8cbfa57f0611127daf61ab3ae02ab8420b0407412079227d1c65/aiohttp-3.14.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:5373dc80ad1aa2fb9ad95c83f24eef418bbda3a61375f128e5b0192e4f3f9b32", size = 1698681, upload-time = "2026-07-23T01:52:38.167Z" },
- { url = "https://files.pythonhosted.org/packages/ec/b0/90bd5cd9fdd9787cb4211d284d1fb8401339a933cb0227a15b71e789232f/aiohttp-3.14.3-cp310-cp310-win32.whl", hash = "sha256:a3e22975f905b89a55a488c2a08f2fdb2186175349e917d48985cc468a3d4c6e", size = 456733, upload-time = "2026-07-23T01:52:41.823Z" },
- { url = "https://files.pythonhosted.org/packages/d8/15/fe5b8f6a71ae112bc677163d0b0701bda5dc15005249582258ede0eb88c7/aiohttp-3.14.3-cp310-cp310-win_amd64.whl", hash = "sha256:bdd0e2834dce1a26c1bbe26464861e16bbe217042cbff619247c11594472518c", size = 480460, upload-time = "2026-07-23T01:52:43.905Z" },
- { url = "https://files.pythonhosted.org/packages/54/00/45e98b6645cd7f00a4b78b749ebd309094b0eaeb2d2e96157eadbc0d0050/aiohttp-3.14.3-cp310-cp310-win_arm64.whl", hash = "sha256:eac645b09bcfdf73df7536331f0678c1086ea250981118ddb5199e17ccef72bb", size = 453479, upload-time = "2026-07-23T01:52:46.075Z" },
- { url = "https://files.pythonhosted.org/packages/f8/5c/b3e4ff8ad43a8afef9602c5e90285936da1beaea8b029016b793891f03c3/aiohttp-3.14.3-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:e568e14940c09955aa51f4e645b6daa18a581c5dcfcd73744dcc86a856e3ced3", size = 764250, upload-time = "2026-07-23T01:52:48.525Z" },
- { url = "https://files.pythonhosted.org/packages/0e/da/f1b384465e51449d844056b75070461da03a9a23e6c1747003695bf4172a/aiohttp-3.14.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:54cfcdee2770dac994417cbb0ee1f3eb0e7cb6b30c79bf44f2c02ff79ec5124a", size = 516281, upload-time = "2026-07-23T01:52:51.047Z" },
- { url = "https://files.pythonhosted.org/packages/b9/3f/01264f820ee2e3712a827892b1cd6ff80f3300c1fcbffbb45714a915d47a/aiohttp-3.14.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:21c016079415ed3fd676963e9793700a566d85dbbd6bfc564b9b2d209147dcc8", size = 514742, upload-time = "2026-07-23T01:52:53.779Z" },
- { url = "https://files.pythonhosted.org/packages/9e/8d/a71c6f2db52ac1ed142b133f7feddaa6b70539c3f4de24d7e226c95b794c/aiohttp-3.14.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d6088ec9894113802bddb3c09e974929aed2c7b3a8c456219b8aab4481f1a239", size = 1780613, upload-time = "2026-07-23T01:52:56.948Z" },
- { url = "https://files.pythonhosted.org/packages/a5/11/3dd9b3fb3a170f6ec9011b5291d876a6fab4086714c9e158600edf01b4fd/aiohttp-3.14.3-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:16ea7e24c309fb7c0bbd505d149abe4fe4dccfb8db911db7dbec0921bc889a6f", size = 1737688, upload-time = "2026-07-23T01:52:59.294Z" },
- { url = "https://files.pythonhosted.org/packages/6d/3e/834c26918be7d88068822b40e0db30fca50b5f4fe79104aa16a93f1d74e6/aiohttp-3.14.3-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:56f355e79f71aef2a85c80305cc915f894b170dba76de5fe84f6351939b83c06", size = 1845742, upload-time = "2026-07-23T01:53:01.641Z" },
- { url = "https://files.pythonhosted.org/packages/cc/c9/49ab8572df7d66bc13d11e31f781292badb04180dd87ba98733066c6aed7/aiohttp-3.14.3-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:18c441d0a8fca6de8d1f546849b9f0ab20d435993e2c5b59562b2fae6be2f929", size = 1928412, upload-time = "2026-07-23T01:53:04.018Z" },
- { url = "https://files.pythonhosted.org/packages/a5/b9/2b8f0c0ce09c87a1daf80fd483431b56b1435d3f62789bc86f572e1245de/aiohttp-3.14.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:53e7b4ce82b54a8bcc71b3b67a5cbd177ca1d7f592cbc92cd38b7349f73482db", size = 1786220, upload-time = "2026-07-23T01:53:06.481Z" },
- { url = "https://files.pythonhosted.org/packages/85/00/9c45f81de11710460edfa1dc81317b6e882703b160926c879a9d20da9fcc/aiohttp-3.14.3-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f55119f7bf25f49ed210f6096090715da24f2943c62102448915fde3c62877ce", size = 1637231, upload-time = "2026-07-23T01:53:10.258Z" },
- { url = "https://files.pythonhosted.org/packages/19/ce/967d628e910756f3539c6107cb7844a1b69440dcb3029a5ee7871b09ab63/aiohttp-3.14.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:9aa6e61fdf20105c4144e755bd586008ff450791d67b1c8146fdc15959c4d51c", size = 1753161, upload-time = "2026-07-23T01:53:13.817Z" },
- { url = "https://files.pythonhosted.org/packages/11/b2/0c3d4114f0aee4f580f5b3b4eb71b24d7a23b834ea506a4dfebe76513f35/aiohttp-3.14.3-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:ccd4893707b3e2a13e39c90d43cf80edf2e4d0457935bcc103bf2346214c3f15", size = 1756356, upload-time = "2026-07-23T01:53:16.211Z" },
- { url = "https://files.pythonhosted.org/packages/63/5d/99e7d91c82f1399d1ae2a854e080bd1493fbc31e5e959dbc4ec33dac3bec/aiohttp-3.14.3-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:b2466434105a4e03113c36ec775cc2ebe6676b62eae326fa670bb607ef788c1c", size = 1819846, upload-time = "2026-07-23T01:53:18.289Z" },
- { url = "https://files.pythonhosted.org/packages/ad/05/d5e1cb6480eeffd3f901d40a2c5e2d1e7effdc797837da3b490272699f13/aiohttp-3.14.3-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:ba59d59aba08ac02fc03b0c8983ccd5ee39a199d0552ce9e6d2b4845b34d59ae", size = 1628531, upload-time = "2026-07-23T01:53:23.86Z" },
- { url = "https://files.pythonhosted.org/packages/c9/90/b934682bcaefae18a9e04f3dff5b68522ba810906358ae5029b68110ea3b/aiohttp-3.14.3-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:ed099d105449c4f9e84f24af203cd131349d4761d8813fa7e02c32e7128cd910", size = 1832712, upload-time = "2026-07-23T01:53:27.551Z" },
- { url = "https://files.pythonhosted.org/packages/21/df/6061679faaf81fac746e7307c7adb71e858071a5d34c27583afefc64f543/aiohttp-3.14.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:152516815ef926786a0b6ae2b8f1fd2e0c71582dee0b435636865316fd4891b7", size = 1775014, upload-time = "2026-07-23T01:53:30.223Z" },
- { url = "https://files.pythonhosted.org/packages/8a/1d/f854878bbc69b88faefe924b619a34a6f59ec05fd387c77690667eaa75eb/aiohttp-3.14.3-cp311-cp311-win32.whl", hash = "sha256:a4af35c443e0b1a1bd6a8af3f3485d7fda15c142751a00f3ff8090f0b93346fa", size = 456006, upload-time = "2026-07-23T01:53:34.97Z" },
- { url = "https://files.pythonhosted.org/packages/73/0c/2af9d1674baccd1dbd47282a93d660a22e57ef6167c856deb24b4214fbab/aiohttp-3.14.3-cp311-cp311-win_amd64.whl", hash = "sha256:e1e74298bab6ee0d6e749ed4fd1901c7e604bdda32c03d787a2cc71c46d0433d", size = 481069, upload-time = "2026-07-23T01:53:39.673Z" },
- { url = "https://files.pythonhosted.org/packages/8e/76/88401ff3fc95e85c5fc38d588f36f55e61ecb64343b2bc8d69326f453cc0/aiohttp-3.14.3-cp311-cp311-win_arm64.whl", hash = "sha256:03cd2bde3d7f085b64e549c985f4bb928cad7e8ecf5323bfca320db548d81b39", size = 453021, upload-time = "2026-07-23T01:53:43.749Z" },
- { url = "https://files.pythonhosted.org/packages/18/d4/eb96299230e20acf2efae207cb8d69051f1f68e357e5ea5e479bf6fb097a/aiohttp-3.14.3-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:39aded8c7f3b935b54aab1d8d73c70ec0ee2d3ec3b943e0e86611bc150ba47f5", size = 754690, upload-time = "2026-07-23T01:53:47.332Z" },
- { url = "https://files.pythonhosted.org/packages/88/11/e7a70a209eb9a067c0d3212b518a0134e3484f5178c7533878b6b514d469/aiohttp-3.14.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:5bcb6ff3fdab1258a192679ff1a05d44f59626430aa05cd1a9d2447423599228", size = 509484, upload-time = "2026-07-23T01:53:51.159Z" },
- { url = "https://files.pythonhosted.org/packages/30/07/4bbc222cc8dbe31d4c3e8a5baad2286e4d42026ac0c570027b89afce6344/aiohttp-3.14.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:617105e2c3018ee38d0c8ce5ee3c84f621a6d8b9f723202aacaff28449ca91ee", size = 511949, upload-time = "2026-07-23T01:53:55.083Z" },
- { url = "https://files.pythonhosted.org/packages/54/b9/42e74c46b7b7c794b995bbc1f573fb48950c38b19d8600c62a6804ee2d67/aiohttp-3.14.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f631fe87a6f30df5fbe6d79640b25e4cffb38c31c7fb6f10871517b84b0f8c1a", size = 1765282, upload-time = "2026-07-23T01:53:59.662Z" },
- { url = "https://files.pythonhosted.org/packages/6b/ed/62bc4d74363ad346d518e0720363a949f63e2e23439a79eb5813d4d29bb3/aiohttp-3.14.3-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:a94dbaae5ae27bd849c93570669bff91e0510f33a80805738e3de72a7be0447b", size = 1741511, upload-time = "2026-07-23T01:54:04.063Z" },
- { url = "https://files.pythonhosted.org/packages/d0/9f/181e8a8bc79e47d13c7fc4540bd7a3b729d9505609c61f392a8dd2fbfe55/aiohttp-3.14.3-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:8f2f1c4c032c7cedd7d8da6f54c97b70266c6570c3108d3fdffee7188bb70529", size = 1810680, upload-time = "2026-07-23T01:54:09.882Z" },
- { url = "https://files.pythonhosted.org/packages/5c/9a/dec94d6ad694552fe3424e3f1928d7a606a5d9d9433a04e7ecdd9d38ae7f/aiohttp-3.14.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:ea05e1f97ceea523942d9b2a7d7c0359d781d683d6b043f5943a602b14da4787", size = 1905646, upload-time = "2026-07-23T01:54:13.475Z" },
- { url = "https://files.pythonhosted.org/packages/52/b7/7cd31f29d6055bd711ae6e669367fba6f5ae9de463910a793e30556a8db7/aiohttp-3.14.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:543906c127fb1d929b95076db19b83fa2d46751006ff1e23b093aa5ac4d8db42", size = 1792122, upload-time = "2026-07-23T01:54:15.752Z" },
- { url = "https://files.pythonhosted.org/packages/66/73/10b1ef93afa61f4963c746257b70ced619cf31a4798671de5fdb2608501d/aiohttp-3.14.3-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0a5ff2dfbb9ce645fa5b8ef3e02c6c0b9cc3f6030ff863d0c51fffc50cb5541b", size = 1591127, upload-time = "2026-07-23T01:54:19.489Z" },
- { url = "https://files.pythonhosted.org/packages/49/ed/3b203fa6de1b338c14acdc06bf6ca9b043b7944f005966958c2ced932cde/aiohttp-3.14.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:041badb8f84396357c4d3ad26de6afd7a32b112f43d3c63045c0c8278cfd2043", size = 1725210, upload-time = "2026-07-23T01:54:24.129Z" },
- { url = "https://files.pythonhosted.org/packages/28/b7/1c2aab8c706436dcc28598452488ac9cd7c409da815237c28c27d58993e6/aiohttp-3.14.3-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:530125ee1163c4219af35dc3aa1206e541e7b31b6efc1a3f93b70a136f65d427", size = 1764848, upload-time = "2026-07-23T01:54:27.973Z" },
- { url = "https://files.pythonhosted.org/packages/54/50/94c28f08b131c4bf10984ea2c7a536c9920608bb2d6e7f95642c30cc87b7/aiohttp-3.14.3-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:c8653fd547c93a61aadc612007790f5555cdd18946fa48cf45e26d8ea4ea473d", size = 1777102, upload-time = "2026-07-23T01:54:31.775Z" },
- { url = "https://files.pythonhosted.org/packages/13/d4/e7d09ba7d345fb2d74440fd2fa033c5e079fac05552927705986f41a364f/aiohttp-3.14.3-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:89176250f686cb9853c0fb7ead90e639e915b84a6f43eedc2a4e7ec21f1037f0", size = 1580205, upload-time = "2026-07-23T01:54:34.518Z" },
- { url = "https://files.pythonhosted.org/packages/a3/84/072a91d68e1e1eb587985b54baab94221277f877e8ef274fc213a0ceae28/aiohttp-3.14.3-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:3a26434dafe408229ff3403458ca58de24fb51936504decac49ce6755f77e59d", size = 1797219, upload-time = "2026-07-23T01:54:36.995Z" },
- { url = "https://files.pythonhosted.org/packages/e0/eb/aad34e897e668424d6e995da5dff8a4a09af93363d3392488772957a63aa/aiohttp-3.14.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:d1558173930a5a8d3069cee5c92fc91c87c4dbcb099debbb3622053717145a19", size = 1768629, upload-time = "2026-07-23T01:54:40.103Z" },
- { url = "https://files.pythonhosted.org/packages/b6/2b/6bb88ddba0fecd9122aa3ebcad25996cf6c083a4a7040dbb3a4f97972af6/aiohttp-3.14.3-cp312-cp312-win32.whl", hash = "sha256:16100ad3ab8d649fdfbee87602d9d2dcdca9df0b9eda8a1b5fdc0d41f96da559", size = 451481, upload-time = "2026-07-23T01:54:42.547Z" },
- { url = "https://files.pythonhosted.org/packages/76/9b/f2f8f108da17ecef2cc3efc424e8b7ad3782b1a8360f7b8eae8ced84f6ea/aiohttp-3.14.3-cp312-cp312-win_amd64.whl", hash = "sha256:33a2d7c28d33797a2e99923dffa63f83d908a19b6bf26cfe80fa790aa5e1a75a", size = 476845, upload-time = "2026-07-23T01:54:44.853Z" },
- { url = "https://files.pythonhosted.org/packages/3e/44/28dac80a8941b604f4da10ce21097614ca1bf905ce93dca28d8d7de9c1e7/aiohttp-3.14.3-cp312-cp312-win_arm64.whl", hash = "sha256:362a3fd481769cac1a824514bcd86fda51c65e8fe6e051099e008fddde6db17c", size = 448050, upload-time = "2026-07-23T01:54:47.087Z" },
- { url = "https://files.pythonhosted.org/packages/57/be/5afd201cc0ab139029aadb75392efe85a293403d9dd3a3226161c21ce00c/aiohttp-3.14.3-cp313-cp313-android_21_arm64_v8a.whl", hash = "sha256:2e9878ae68e4a5f1c0abe4dd497dbc3d51946f5837b56759e2a02e78fa90ef86", size = 506269, upload-time = "2026-07-23T01:54:49.075Z" },
- { url = "https://files.pythonhosted.org/packages/22/09/dec8189d62b45ade009f6792a2264b942a90cb88aeaf181239933cd72c3c/aiohttp-3.14.3-cp313-cp313-android_21_x86_64.whl", hash = "sha256:f3d2669fe7dec7fc359ecdb5984b29b50d85d5d00f8c1cb61de4f4a24ee42627", size = 515166, upload-time = "2026-07-23T01:54:51.894Z" },
- { url = "https://files.pythonhosted.org/packages/28/24/2854869d29ed8a8b19d74f9ec6629515f7e04d02dd329d9d179201e58e47/aiohttp-3.14.3-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:cc7cb243a68167172f48c1fd43cee91ec4b1d40cefd190edd43369d1a6bc9c82", size = 486263, upload-time = "2026-07-23T01:54:54.223Z" },
- { url = "https://files.pythonhosted.org/packages/d4/dd/57187c8be2a35aea65eaee3bd2c3dcbbcf0204f5106c89637e3610380cd1/aiohttp-3.14.3-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:78253b573e6ffab5028924fc98bc281aae05445969982a10864bc360dea2016c", size = 492299, upload-time = "2026-07-23T01:54:56.236Z" },
- { url = "https://files.pythonhosted.org/packages/b9/11/06ae6ed8f0d414edf4068861e233d8fe23ee699bfd4b3ceb8663db948a62/aiohttp-3.14.3-cp313-cp313-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:7041d52c3a7fa20c9e8c182b534704abb19502c8bdcbde7ab23bfda6f642394f", size = 502235, upload-time = "2026-07-23T01:54:58.377Z" },
- { url = "https://files.pythonhosted.org/packages/7e/a3/559639c34a345d2cf7c52dff6838119f2eaf29eb508227b5b83f573af813/aiohttp-3.14.3-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ac74facc01463f138b0da5580329cfcc82818dea5656e83ddcd11268fc12ff80", size = 750883, upload-time = "2026-07-23T01:55:00.65Z" },
- { url = "https://files.pythonhosted.org/packages/91/cd/41e131f13afd1e7b0172a9d9eda085ef90eb8439f41f0d279db81ed3ae60/aiohttp-3.14.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:d6218d92e450824e9b4881f44e8c09f1853b490f9a64130801024a4793b1b3b0", size = 508473, upload-time = "2026-07-23T01:55:02.945Z" },
- { url = "https://files.pythonhosted.org/packages/bc/6b/e7f13410d391c6e55b4c007a8de024355389d7d459e3d64c42b2d33617e5/aiohttp-3.14.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:11fb37ef075669eee52ab1928fbf6e1741fada40409fa309ebde9607a962aebf", size = 509190, upload-time = "2026-07-23T01:55:05.173Z" },
- { url = "https://files.pythonhosted.org/packages/97/21/6464573e53d69672cc1eada3e5c5cb2d2efa82701e8305a0f2047a576967/aiohttp-3.14.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:55bdcc472aafe2de4a253045cc128007a64f1e0264fb675791e132ea5edaa3bd", size = 1761478, upload-time = "2026-07-23T01:55:07.383Z" },
- { url = "https://files.pythonhosted.org/packages/1a/81/d217043a4c17fbce360905e3b2bdd20139ebc9a2de836d035d179c4da006/aiohttp-3.14.3-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:c39846c3aad97a8530c89d7a3869a8f8e9e3762c6ac0504481e5c80948f7e807", size = 1735092, upload-time = "2026-07-23T01:55:09.803Z" },
- { url = "https://files.pythonhosted.org/packages/a1/66/e13a02d0eeb1a9a502402a977abb4e4abff9fe4051c26f80558c57a7c975/aiohttp-3.14.3-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:5895ef58c4620afe02fa16044f023dc4dafec08158f9d08874a46a7dbc0341b8", size = 1800546, upload-time = "2026-07-23T01:55:12.012Z" },
- { url = "https://files.pythonhosted.org/packages/26/5e/57d42fca1d18cb5acc1cad945d017fabc5d6ae71d8a08ad66be8dc3ee544/aiohttp-3.14.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:fa9467a8113aa69d3d7c55a70ef0b7c636010a40993f3df9d9d0d73b3eb7ef24", size = 1895250, upload-time = "2026-07-23T01:55:14.357Z" },
- { url = "https://files.pythonhosted.org/packages/ca/1c/7da8d08e74d56f00070822f9638ff3f1c563f8ad87d1efa996c87bfc8644/aiohttp-3.14.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d7d2deec16eeedf55f2c7cf75b521ea3856a5177e123844f8fd0f114ce252cb5", size = 1789289, upload-time = "2026-07-23T01:55:16.668Z" },
- { url = "https://files.pythonhosted.org/packages/cd/0f/cf16bcf56896981c1a0319f5d5db9337994b5165730c48a8fa07e9b34be6/aiohttp-3.14.3-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:dd54d0e8717de95939766febac482ac0474d8ac3b048115f9f2b1d23a16e7db4", size = 1586706, upload-time = "2026-07-23T01:55:18.913Z" },
- { url = "https://files.pythonhosted.org/packages/fe/6f/76eac12a7f2480e1e304f842efdb07db33256b0d9165b866b6ef0806c202/aiohttp-3.14.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:df82f3787c940c94986b34222d59c9e38843fba85139f36e85255a82ad5355a9", size = 1724652, upload-time = "2026-07-23T01:55:21.296Z" },
- { url = "https://files.pythonhosted.org/packages/39/b6/19c8c592baeeb94b75f966547d40c02ac7590902306ec5863d5c027cf506/aiohttp-3.14.3-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:42a67efc36300d052fb4508a53e8b6901b9284b599ae63945c377569c5fcc1e1", size = 1756239, upload-time = "2026-07-23T01:55:23.705Z" },
- { url = "https://files.pythonhosted.org/packages/dc/c9/4e9383150296f97f873b680c4de8fb2cd88608fb9f48c79edcb111611abc/aiohttp-3.14.3-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:7a75aa63cbf9b21cfaf60dc2657e19df2c2867d91707d653fee171ffeedd1371", size = 1769161, upload-time = "2026-07-23T01:55:26.082Z" },
- { url = "https://files.pythonhosted.org/packages/aa/1e/147bdc6cc5de5f3ab011be8bf5d6e786633249f22c20bae06f85e45f5387/aiohttp-3.14.3-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:e92eb8acc45eb6a9f4935071a77edf5b85cc6f8dfad5cd99e97653c26593cdde", size = 1578759, upload-time = "2026-07-23T01:55:28.846Z" },
- { url = "https://files.pythonhosted.org/packages/fd/31/78388a9d6040ece2e11df62ea229a822cf5e52d238374b220ae9975b2623/aiohttp-3.14.3-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:b014a6ed7cf912e787149fdc529166d3ceabac23f26efeea3158c9aba2354e7e", size = 1792025, upload-time = "2026-07-23T01:55:31.457Z" },
- { url = "https://files.pythonhosted.org/packages/03/51/a3d29fdf2c25d796746af8ad6fe56a45d6256c38b0a8a2ed752e1160b3a2/aiohttp-3.14.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:3d4f72af88ac2474bb5bca640030320e3d38a0163a1d7533500e87be458eef71", size = 1768477, upload-time = "2026-07-23T01:55:33.87Z" },
- { url = "https://files.pythonhosted.org/packages/29/a6/442e18b5afeade534d877a2dc3c3e392aff8d49787890b0cf84790410267/aiohttp-3.14.3-cp313-cp313-win32.whl", hash = "sha256:5f08ec777f35ee70720233b8b9811d3bb5d728137f30ac91b7457709c3261ac0", size = 451069, upload-time = "2026-07-23T01:55:36.121Z" },
- { url = "https://files.pythonhosted.org/packages/9d/69/3d876ac02659f271cf7f6769f14a8e3de5b6e888ed8b5a7e998086a4cec8/aiohttp-3.14.3-cp313-cp313-win_amd64.whl", hash = "sha256:dff9461ec275f22135650d5ba4b4931a11f3958df7dfbb8db630000d4dee0883", size = 476518, upload-time = "2026-07-23T01:55:38.303Z" },
- { url = "https://files.pythonhosted.org/packages/b2/0e/50d6e6471cd31edce8b282bdec59375a3a69124d8a989a0b1313355cae52/aiohttp-3.14.3-cp313-cp313-win_arm64.whl", hash = "sha256:ddcac3c6b382e81f1dd0499199d4136b877beb4cb5ef770bbbfba56c4b8f55d2", size = 447676, upload-time = "2026-07-23T01:55:40.451Z" },
- { url = "https://files.pythonhosted.org/packages/c8/20/887fdcf832326571b370ffc347b3e70abe101096f3720126aac161b1d872/aiohttp-3.14.3-cp314-cp314-android_24_arm64_v8a.whl", hash = "sha256:49f7325beb0f85ef4aef5f48f490269575f83e6e2acad00a1d80b807eb027062", size = 509067, upload-time = "2026-07-23T01:55:42.618Z" },
- { url = "https://files.pythonhosted.org/packages/ad/a3/92cec936f78cc4bf0fa5554ebe593b73459d94e3c62303e1902a4cccb6f7/aiohttp-3.14.3-cp314-cp314-android_24_x86_64.whl", hash = "sha256:e3be98a7c30b8c25d573dafba7171d66dfb05ee6a9070fc46535464ff97700a6", size = 514774, upload-time = "2026-07-23T01:55:44.937Z" },
- { url = "https://files.pythonhosted.org/packages/29/ba/2a0c38df3fc557620b6a5acd98364af050053b6285b4dc7ee74100c63c18/aiohttp-3.14.3-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:614c61d478b83953e261d02bb2df750f17227cd33ef8002945bf5aebbde21919", size = 488134, upload-time = "2026-07-23T01:55:47.135Z" },
- { url = "https://files.pythonhosted.org/packages/48/d6/d51b7d4bf309af3693940d8ffd2b9ed0b682434ef85959b7c9c137f60cf8/aiohttp-3.14.3-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:1caa7b0d05f3e3a36f87788c59e970a7ee1cefcfcbb924a9f138c4a6551c9cb7", size = 494201, upload-time = "2026-07-23T01:55:49.451Z" },
- { url = "https://files.pythonhosted.org/packages/3f/5a/8f624384e5f1efabb5229b94157eb966b021e97bdb188c62860c2ae243c2/aiohttp-3.14.3-cp314-cp314-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:dfa68deb2a443bdaa3ea5297b0699c1464f08aef3812b486d1348eee61b07dc0", size = 502766, upload-time = "2026-07-23T01:55:51.656Z" },
- { url = "https://files.pythonhosted.org/packages/a6/26/4ff0164370deec18fb19254ee4ab10b7a73304ac0c860b13f5f84663759b/aiohttp-3.14.3-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:e72ee89e28d907a18f46959b4eb0bb06701cc7f8cf4366e00029e2ccfaaf5924", size = 756557, upload-time = "2026-07-23T01:55:53.964Z" },
- { url = "https://files.pythonhosted.org/packages/97/a3/7056b86dc0d9ec709ea9777eae3b0161428f943372f8b98c01c11593b682/aiohttp-3.14.3-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:ad4c8b7488d745d2ca4838ebd8ae5ba9b56341d30b1da43640e4ce87f9f49646", size = 510168, upload-time = "2026-07-23T01:55:56.22Z" },
- { url = "https://files.pythonhosted.org/packages/85/ed/0357a015892fd68058bf2d39d3fd1958e459b997a7db30aaa6aaa434ae96/aiohttp-3.14.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:db332af25642007330fca8be5c4d194caf2bea7a7fc84415aff3497af5dfee6b", size = 512957, upload-time = "2026-07-23T01:55:58.437Z" },
- { url = "https://files.pythonhosted.org/packages/47/d1/8aba53f15ccb2238405f5e9d30e2a8ca44f93878c26e7165ade00d374b1c/aiohttp-3.14.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:25bd2708db6bdf6a6630dd37bdcdfcb47c4434d22ac69c64665b802910140b30", size = 1750149, upload-time = "2026-07-23T01:56:00.856Z" },
- { url = "https://files.pythonhosted.org/packages/49/bd/40c3fee327529284375c6701cbb0fa4600cc2e8432af1378f897e2ef7d3a/aiohttp-3.14.3-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:cef89a58e628c4efcac3275c2d68083f82426dcdc89c1492a6f654f9f7ea6ab9", size = 1707685, upload-time = "2026-07-23T01:56:03.371Z" },
- { url = "https://files.pythonhosted.org/packages/2a/a3/ca0cc6724cca8114b05694abd916060758c79894c3aa5b012cdadc1bc28e/aiohttp-3.14.3-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c23ec8ee9d5ab2f5421f9c7fffce208435607af27fd46d4a44e031954352838f", size = 1803911, upload-time = "2026-07-23T01:56:05.817Z" },
- { url = "https://files.pythonhosted.org/packages/95/b5/85b099c299c3ffd38ad9b3e43694c8a346934e4a30c88c4fd5a841234f77/aiohttp-3.14.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:e2667f0bbe7eb6c74eae5e9691441ad186e5845ca3cff63230fc09c4e7514f5d", size = 1876929, upload-time = "2026-07-23T01:56:08.413Z" },
- { url = "https://files.pythonhosted.org/packages/d5/b7/1da684a04175473fa4cddbf9a2f572e79514c3fd27a74597f43057d4f3da/aiohttp-3.14.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:18cb43369747b2ae007bd2655fb8e63a099c2ff1d207962943636dac989b3147", size = 1761112, upload-time = "2026-07-23T01:56:10.918Z" },
- { url = "https://files.pythonhosted.org/packages/d1/16/bc4b55e3e5cb175fd69c53c90d60d2f47797cb343da5106e23863dc4dba4/aiohttp-3.14.3-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:d77640cc618c1d99fc4f8589c0f24a730adfa54eb1e57ef7bf0c8dfb78da898c", size = 1583500, upload-time = "2026-07-23T01:56:13.613Z" },
- { url = "https://files.pythonhosted.org/packages/2a/e8/13a9d957a1ee40837f46aa30f0f4c657e673ad86a2e6362a9f9be20d26d9/aiohttp-3.14.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:53e5179d8abb5710f8e83ba207c41c8d1261fcffd4616500e15ca2b7a33be10a", size = 1713940, upload-time = "2026-07-23T01:56:15.969Z" },
- { url = "https://files.pythonhosted.org/packages/38/05/d33c680c1bcf1c7e130f9cbfc1fc02fe8bb0c4af2a94a53dd5fb56131e5c/aiohttp-3.14.3-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:cd817772b2fcf2b8c0905795318485f9ec16eae60b29feb7f4c77085311637f0", size = 1724413, upload-time = "2026-07-23T01:56:18.591Z" },
- { url = "https://files.pythonhosted.org/packages/85/1d/af798d306f7a74b6a632dbcabcf62a4c91391b7582d2a8c6d7712e2cc54e/aiohttp-3.14.3-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:4e3ac92d90e92773b2362d506068e9a948192bd553e743c5b2429e28527c8661", size = 1770748, upload-time = "2026-07-23T01:56:21.074Z" },
- { url = "https://files.pythonhosted.org/packages/a8/92/ad720d472556a995049206867765e9410969684f86ee09423ff9969044c1/aiohttp-3.14.3-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:3f42e9b78301f11c8f861746175d8b9c1ccef713fcad9eab396e2f6db8ed4a22", size = 1577564, upload-time = "2026-07-23T01:56:23.475Z" },
- { url = "https://files.pythonhosted.org/packages/60/ad/0ed7586cbef7a884e23a752fa2bb987a122e6a5dd50dab109258d0a95193/aiohttp-3.14.3-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:9d9edccfe496b476db5f398d97b865e9a6752bcf8aec4eef8390ce20fb64bb41", size = 1782080, upload-time = "2026-07-23T01:56:25.994Z" },
- { url = "https://files.pythonhosted.org/packages/97/ea/dbaed0d73e8a69aad653b045dab451c67c2454bb731a37b45a86593e9422/aiohttp-3.14.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:1c5ec8fb1bcc31a8466f74aaf26c345d5c386fa4bd08a3f0eb9c7a4a3fe8b5bf", size = 1745813, upload-time = "2026-07-23T01:56:28.604Z" },
- { url = "https://files.pythonhosted.org/packages/81/1b/6893d4bc57e434fc93a6c9217c637d967a0b651d989f6e3265179375754a/aiohttp-3.14.3-cp314-cp314-win32.whl", hash = "sha256:38901a84da3ce22249f6e860bf8f90d141bcab7da090cc398f8bb58c0e44b7da", size = 455872, upload-time = "2026-07-23T01:56:31.031Z" },
- { url = "https://files.pythonhosted.org/packages/f5/8b/c7baa1ba1eda4db6989baefe5de6d99834921b84ebd7918624febcb9f290/aiohttp-3.14.3-cp314-cp314-win_amd64.whl", hash = "sha256:8b3b60de05f3dcb6f6a00f818bb2ec781cee4de0645f59ccaf99b1d1823b6100", size = 481030, upload-time = "2026-07-23T01:56:33.365Z" },
- { url = "https://files.pythonhosted.org/packages/22/8c/c29d067df825a2df88ca432db848aa2fe8199598359cc06c12b09320cac9/aiohttp-3.14.3-cp314-cp314-win_arm64.whl", hash = "sha256:1576145bdceeb92382d899751e12743a3a5b8e460a841e3e50543859e54864dc", size = 453669, upload-time = "2026-07-23T01:56:35.731Z" },
- { url = "https://files.pythonhosted.org/packages/6a/a4/9c033beb355d39b6147980597ec9645e4729243f686ee4dc73945de72030/aiohttp-3.14.3-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:8800c996b01c2772a783e3e46f3e1abd5823029adca0df54231960de9bfefa5b", size = 791403, upload-time = "2026-07-23T01:56:37.972Z" },
- { url = "https://files.pythonhosted.org/packages/80/ca/87c32a0a7704583cfc49660bd817889bae5b830bf53b5dcb4e92145ac2da/aiohttp-3.14.3-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:ebe8e504f058fe91223351cecd2d9d6946c9d241bb0250d898ffbdf584cc72b0", size = 526413, upload-time = "2026-07-23T01:56:40.523Z" },
- { url = "https://files.pythonhosted.org/packages/9e/d8/8ec0e471248c500acdce2be3f46db8fb62b5eb60efef072529cc85ee1d26/aiohttp-3.14.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:30402d03a7c0ff52bce290b57e564e9079fd9d0cb545c8aba73f86a103162d2e", size = 532135, upload-time = "2026-07-23T01:56:42.876Z" },
- { url = "https://files.pythonhosted.org/packages/fe/45/f8919fd936e8b79fcd9bda7b6d8e62613462a713f4f17987fd7c34399142/aiohttp-3.14.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9fc7b5bfec6573f3ae844f457fdde5adeb713f8b8e4a81ad64fc207b49383716", size = 1922742, upload-time = "2026-07-23T01:56:45.528Z" },
- { url = "https://files.pythonhosted.org/packages/f6/ec/9ca76b28a27525b0cc53e20842e0228b022f301ce1f436b7d814b4aaf2df/aiohttp-3.14.3-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:8a5fd34f7f7410d1730d5c2ba873cacb2eed3fede366feb268a70ba22581ed8f", size = 1787371, upload-time = "2026-07-23T01:56:48.045Z" },
- { url = "https://files.pythonhosted.org/packages/b1/04/6acdbf17315f7b55f1937e3387acb89a3cddeb4995689553d064af8e92ab/aiohttp-3.14.3-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:270d3dace9ca2f10f0da5d8ebe519b7a310fc6112ed916e32df5866df0888553", size = 1912623, upload-time = "2026-07-23T01:56:50.605Z" },
- { url = "https://files.pythonhosted.org/packages/86/e6/438b0c79ca6f45eb9fd9817dd4c01a91919a38c0de5ee9e05e2b4dc0ece7/aiohttp-3.14.3-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:3ae5b3a59436d089b5395d910121a390feed4d00578eb95a0fd1a329fe963100", size = 2005515, upload-time = "2026-07-23T01:56:53.153Z" },
- { url = "https://files.pythonhosted.org/packages/bb/6b/62cbd6577758699525f5c712d1ddef57d9875fbab0ae8d5f5a202fd598f8/aiohttp-3.14.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2498f0fe69ead802f9675beca44a7c21c62fdaa4ec5145ea1c3ad6edbee29f85", size = 1879906, upload-time = "2026-07-23T01:56:55.818Z" },
- { url = "https://files.pythonhosted.org/packages/00/95/18bcbf830a21dc3aae24d8f6b6feaf3db1d2090242d00a7868db2ffb0b67/aiohttp-3.14.3-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a0dc483c00da8b673abbb367eb6f8d8f4bcec30eb58529ea13cb42e7fd2dfa33", size = 1675849, upload-time = "2026-07-23T01:56:58.861Z" },
- { url = "https://files.pythonhosted.org/packages/a9/19/47f4968659c5e23606c3790c80fc624e691c153d036148449ee84d31b287/aiohttp-3.14.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:c7d3a97c678d34fc5b59da671ee9cd630096ddc643e7b5a30d54a2a6f3574d3f", size = 1843496, upload-time = "2026-07-23T01:57:01.591Z" },
- { url = "https://files.pythonhosted.org/packages/64/af/38c33c4dd82fddcb4e56c4653b6f1072a8edbc6b7fa15809f14932c41e2d/aiohttp-3.14.3-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:f8fb78a83c9e5f741ca3a68cfb455c1f5bb83b4e7249a3848b3cd78d0a8563b0", size = 1827746, upload-time = "2026-07-23T01:57:05.131Z" },
- { url = "https://files.pythonhosted.org/packages/a1/9d/0537cda4885ac8f5b7053d164dd06312f4c483a4edcb8ee5b8aaf2a989bf/aiohttp-3.14.3-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:74ab5b6a9fb13e873e5a90946588baecaf488745e1db1a4a5c433f971f035098", size = 1853810, upload-time = "2026-07-23T01:57:08.043Z" },
- { url = "https://files.pythonhosted.org/packages/19/fe/26f9c5e6458385aa86497836b0dea6fb2f027827d63f37c7856cce9286ee/aiohttp-3.14.3-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:bd52f811e65f6fb634b1047159657c98f52b407f8efec907bcfc09da9a4c0a25", size = 1668895, upload-time = "2026-07-23T01:57:10.837Z" },
- { url = "https://files.pythonhosted.org/packages/ec/4c/618b1db9b9ba079b8875d2cdf78e7c4a3bf72903bd5850fee7dd9544600a/aiohttp-3.14.3-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:f0f177d1b195b9e06376cfd7d308d8a1b920909a609d03ac82a8c73bbb16d3b9", size = 1883833, upload-time = "2026-07-23T01:57:13.672Z" },
- { url = "https://files.pythonhosted.org/packages/94/c6/bd959bd1e4771f9fd944e9e436224c48c77b018b73b519b5aad346335bcc/aiohttp-3.14.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:498c6c623134f8e09a3c4e60bcd607a0b4590dd7dbf08dd40851b27cbb520ccb", size = 1844251, upload-time = "2026-07-23T01:57:16.593Z" },
- { url = "https://files.pythonhosted.org/packages/5e/19/08d41839658bdd44a0ed2480f3891705ecb487ce28c0dde62c9040c997e0/aiohttp-3.14.3-cp314-cp314t-win32.whl", hash = "sha256:b304db572b4368edd8dda8a2274f73156fe15558fca4a917cb8a09fc47af5963", size = 474180, upload-time = "2026-07-23T01:57:19.306Z" },
- { url = "https://files.pythonhosted.org/packages/99/5d/3cd6ef0a2b2851f7ab913b5b079334781bd50ff56a323e4454063377a080/aiohttp-3.14.3-cp314-cp314t-win_amd64.whl", hash = "sha256:b20032766aedf6261c7a566585a40867d092ac03a0d81592d5370ef9b054f99b", size = 500528, upload-time = "2026-07-23T01:57:21.762Z" },
- { url = "https://files.pythonhosted.org/packages/a4/37/cfd1ed540a4d318da025590d96b728e63713c09e9377950fc655dadeb856/aiohttp-3.14.3-cp314-cp314t-win_arm64.whl", hash = "sha256:2e1161602f45a54de2ce0905243a95f58cb42dcd378402f3697f5e0b21e9d2e7", size = 469280, upload-time = "2026-07-23T01:57:24.241Z" },
-]
-
-[[package]]
-name = "aiosignal"
-version = "1.4.0"
-source = { registry = "https://pypi.org/simple" }
-dependencies = [
- { name = "frozenlist" },
- { name = "typing-extensions", marker = "python_full_version < '3.13'" },
-]
-sdist = { url = "https://files.pythonhosted.org/packages/61/62/06741b579156360248d1ec624842ad0edf697050bbaf7c3e46394e106ad1/aiosignal-1.4.0.tar.gz", hash = "sha256:f47eecd9468083c2029cc99945502cb7708b082c232f9aca65da147157b251c7", size = 25007, upload-time = "2025-07-03T22:54:43.528Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/fb/76/641ae371508676492379f16e2fa48f4e2c11741bd63c48be4b12a6b09cba/aiosignal-1.4.0-py3-none-any.whl", hash = "sha256:053243f8b92b990551949e63930a839ff0cf0b0ebbe0597b0f3fb19e1a0fe82e", size = 7490, upload-time = "2025-07-03T22:54:42.156Z" },
-]
-
-[[package]]
-name = "annotated-types"
-version = "0.7.0"
-source = { registry = "https://pypi.org/simple" }
-resolution-markers = [
- "python_full_version < '3.10'",
-]
-sdist = { url = "https://files.pythonhosted.org/packages/ee/67/531ea369ba64dcff5ec9c3402f9f51bf748cec26dde048a2f973a4eea7f5/annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89", size = 16081, upload-time = "2024-05-20T21:33:25.928Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", size = 13643, upload-time = "2024-05-20T21:33:24.1Z" },
-]
-
-[[package]]
-name = "annotated-types"
-version = "0.8.0"
-source = { registry = "https://pypi.org/simple" }
-resolution-markers = [
- "python_full_version >= '3.10'",
-]
-sdist = { url = "https://files.pythonhosted.org/packages/5f/56/a8120250d128bed162cd73c76d45f6ef9991f3e068f62a8ee060afa3104a/annotated_types-0.8.0.tar.gz", hash = "sha256:13b2beaad985e05e2d6407ee4c4f35590b11f8d693a258a561055cac8f64cab7", size = 15893, upload-time = "2026-07-23T20:16:13.995Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/99/91/8acff4f5e50511b911bbccb72b8628a49c68ce14148cd9f6431094859a90/annotated_types-0.8.0-py3-none-any.whl", hash = "sha256:f072f4d804ea359e4eaf198b1af7a8b0943881a87f31bb764f8bf219bb9419e0", size = 13427, upload-time = "2026-07-23T20:16:12.938Z" },
-]
-
-[[package]]
-name = "anyio"
-version = "4.12.1"
-source = { registry = "https://pypi.org/simple" }
-resolution-markers = [
- "python_full_version < '3.10'",
-]
-dependencies = [
- { name = "exceptiongroup" },
- { name = "idna" },
- { name = "typing-extensions" },
-]
-sdist = { url = "https://files.pythonhosted.org/packages/96/f0/5eb65b2bb0d09ac6776f2eb54adee6abe8228ea05b20a5ad0e4945de8aac/anyio-4.12.1.tar.gz", hash = "sha256:41cfcc3a4c85d3f05c932da7c26d0201ac36f72abd4435ba90d0464a3ffed703", size = 228685, upload-time = "2026-01-06T11:45:21.246Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/38/0e/27be9fdef66e72d64c0cdc3cc2823101b80585f8119b5c112c2e8f5f7dab/anyio-4.12.1-py3-none-any.whl", hash = "sha256:d405828884fc140aa80a3c667b8beed277f1dfedec42ba031bd6ac3db606ab6c", size = 113592, upload-time = "2026-01-06T11:45:19.497Z" },
-]
-
-[[package]]
-name = "anyio"
-version = "4.14.2"
-source = { registry = "https://pypi.org/simple" }
-resolution-markers = [
- "python_full_version >= '3.10'",
-]
-dependencies = [
- { name = "exceptiongroup", marker = "python_full_version < '3.11'" },
- { name = "idna" },
- { name = "typing-extensions", marker = "python_full_version < '3.13'" },
-]
-sdist = { url = "https://files.pythonhosted.org/packages/61/cc/a381afa6efea9f496eff839d4a6a1aed3bfafc7b3ab4b0d1b243a12573dd/anyio-4.14.2.tar.gz", hash = "sha256:cfa139f3ed1a23ee8f88a145ddb5ac7605b8bbfd8592baacd7ce3d8bb4313c7f", size = 260176, upload-time = "2026-07-12T20:29:07.082Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/da/35/f2287558c17e29fafc8ef3daf819bb9834061cfa43bff8014f7df7f63bdc/anyio-4.14.2-py3-none-any.whl", hash = "sha256:9f505dda5ac9f0c8309b5e8bd445a8c2bf7246f3ce950121e45ea15bc41d1494", size = 125813, upload-time = "2026-07-12T20:29:05.763Z" },
-]
-
-[[package]]
-name = "async-timeout"
-version = "5.0.1"
-source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/a5/ae/136395dfbfe00dfc94da3f3e136d0b13f394cba8f4841120e34226265780/async_timeout-5.0.1.tar.gz", hash = "sha256:d9321a7a3d5a6a5e187e824d2fa0793ce379a202935782d555d6e9d2735677d3", size = 9274, upload-time = "2024-11-06T16:41:39.6Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/fe/ba/e2081de779ca30d473f21f5b30e0e737c438205440784c7dfc81efc2b029/async_timeout-5.0.1-py3-none-any.whl", hash = "sha256:39e3809566ff85354557ec2398b55e096c8364bacac9405a7a1fa429e77fe76c", size = 6233, upload-time = "2024-11-06T16:41:37.9Z" },
-]
-
-[[package]]
-name = "attrs"
-version = "26.1.0"
-source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/9a/8e/82a0fe20a541c03148528be8cac2408564a6c9a0cc7e9171802bc1d26985/attrs-26.1.0.tar.gz", hash = "sha256:d03ceb89cb322a8fd706d4fb91940737b6642aa36998fe130a9bc96c985eff32", size = 952055, upload-time = "2026-03-19T14:22:25.026Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/64/b4/17d4b0b2a2dc85a6df63d1157e028ed19f90d4cd97c36717afef2bc2f395/attrs-26.1.0-py3-none-any.whl", hash = "sha256:c647aa4a12dfbad9333ca4e71fe62ddc36f4e63b2d260a37a8b83d2f043ac309", size = 67548, upload-time = "2026-03-19T14:22:23.645Z" },
-]
-
-[[package]]
-name = "certifi"
-version = "2026.7.22"
-source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/a3/c2/24167ea9858356b47a87a50d39908bfdb72ceeefe0041586e704e5376b3a/certifi-2026.7.22.tar.gz", hash = "sha256:741e2c3b351ddf169a738da9f2c048608ff7f2c5cc02f1ebc6b118bb090d5d55", size = 138112, upload-time = "2026-07-22T03:35:12.644Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/0b/a7/71ac2cff56fec219ed242bb11b8efb69fcc4bec75db06fb7bfe35de520e6/certifi-2026.7.22-py3-none-any.whl", hash = "sha256:62f22742b58a1a33014a2b6b706588a8d7e2a88ae7bd1a6ebe8c992928483775", size = 136983, upload-time = "2026-07-22T03:35:11.276Z" },
-]
-
-[[package]]
-name = "distro"
-version = "1.9.0"
-source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/fc/f8/98eea607f65de6527f8a2e8885fc8015d3e6f5775df186e443e0964a11c3/distro-1.9.0.tar.gz", hash = "sha256:2fa77c6fd8940f116ee1d6b94a2f90b13b5ea8d019b98bc8bafdcabcdd9bdbed", size = 60722, upload-time = "2023-12-24T09:54:32.31Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/12/b3/231ffd4ab1fc9d679809f356cebee130ac7daa00d6d6f3206dd4fd137e9e/distro-1.9.0-py3-none-any.whl", hash = "sha256:7bffd925d65168f85027d8da9af6bddab658135b840670a223589bc0c8ef02b2", size = 20277, upload-time = "2023-12-24T09:54:30.421Z" },
-]
-
-[[package]]
-name = "exceptiongroup"
-version = "1.3.1"
-source = { registry = "https://pypi.org/simple" }
-dependencies = [
- { name = "typing-extensions" },
-]
-sdist = { url = "https://files.pythonhosted.org/packages/50/79/66800aadf48771f6b62f7eb014e352e5d06856655206165d775e675a02c9/exceptiongroup-1.3.1.tar.gz", hash = "sha256:8b412432c6055b0b7d14c310000ae93352ed6754f70fa8f7c34141f91c4e3219", size = 30371, upload-time = "2025-11-21T23:01:54.787Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/8a/0e/97c33bf5009bdbac74fd2beace167cab3f978feb69cc36f1ef79360d6c4e/exceptiongroup-1.3.1-py3-none-any.whl", hash = "sha256:a7a39a3bd276781e98394987d3a5701d0c4edffb633bb7a5144577f82c773598", size = 16740, upload-time = "2025-11-21T23:01:53.443Z" },
-]
-
-[[package]]
-name = "frozenlist"
-version = "1.8.0"
-source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/2d/f5/c831fac6cc817d26fd54c7eaccd04ef7e0288806943f7cc5bbf69f3ac1f0/frozenlist-1.8.0.tar.gz", hash = "sha256:3ede829ed8d842f6cd48fc7081d7a41001a56f1f38603f9d49bf3020d59a31ad", size = 45875, upload-time = "2025-10-06T05:38:17.865Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/83/4a/557715d5047da48d54e659203b9335be7bfaafda2c3f627b7c47e0b3aaf3/frozenlist-1.8.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:b37f6d31b3dcea7deb5e9696e529a6aa4a898adc33db82da12e4c60a7c4d2011", size = 86230, upload-time = "2025-10-06T05:35:23.699Z" },
- { url = "https://files.pythonhosted.org/packages/a2/fb/c85f9fed3ea8fe8740e5b46a59cc141c23b842eca617da8876cfce5f760e/frozenlist-1.8.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:ef2b7b394f208233e471abc541cc6991f907ffd47dc72584acee3147899d6565", size = 49621, upload-time = "2025-10-06T05:35:25.341Z" },
- { url = "https://files.pythonhosted.org/packages/63/70/26ca3f06aace16f2352796b08704338d74b6d1a24ca38f2771afbb7ed915/frozenlist-1.8.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a88f062f072d1589b7b46e951698950e7da00442fc1cacbe17e19e025dc327ad", size = 49889, upload-time = "2025-10-06T05:35:26.797Z" },
- { url = "https://files.pythonhosted.org/packages/5d/ed/c7895fd2fde7f3ee70d248175f9b6cdf792fb741ab92dc59cd9ef3bd241b/frozenlist-1.8.0-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:f57fb59d9f385710aa7060e89410aeb5058b99e62f4d16b08b91986b9a2140c2", size = 219464, upload-time = "2025-10-06T05:35:28.254Z" },
- { url = "https://files.pythonhosted.org/packages/6b/83/4d587dccbfca74cb8b810472392ad62bfa100bf8108c7223eb4c4fa2f7b3/frozenlist-1.8.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:799345ab092bee59f01a915620b5d014698547afd011e691a208637312db9186", size = 221649, upload-time = "2025-10-06T05:35:29.454Z" },
- { url = "https://files.pythonhosted.org/packages/6a/c6/fd3b9cd046ec5fff9dab66831083bc2077006a874a2d3d9247dea93ddf7e/frozenlist-1.8.0-cp310-cp310-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:c23c3ff005322a6e16f71bf8692fcf4d5a304aaafe1e262c98c6d4adc7be863e", size = 219188, upload-time = "2025-10-06T05:35:30.951Z" },
- { url = "https://files.pythonhosted.org/packages/ce/80/6693f55eb2e085fc8afb28cf611448fb5b90e98e068fa1d1b8d8e66e5c7d/frozenlist-1.8.0-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:8a76ea0f0b9dfa06f254ee06053d93a600865b3274358ca48a352ce4f0798450", size = 231748, upload-time = "2025-10-06T05:35:32.101Z" },
- { url = "https://files.pythonhosted.org/packages/97/d6/e9459f7c5183854abd989ba384fe0cc1a0fb795a83c033f0571ec5933ca4/frozenlist-1.8.0-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:c7366fe1418a6133d5aa824ee53d406550110984de7637d65a178010f759c6ef", size = 236351, upload-time = "2025-10-06T05:35:33.834Z" },
- { url = "https://files.pythonhosted.org/packages/97/92/24e97474b65c0262e9ecd076e826bfd1d3074adcc165a256e42e7b8a7249/frozenlist-1.8.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:13d23a45c4cebade99340c4165bd90eeb4a56c6d8a9d8aa49568cac19a6d0dc4", size = 218767, upload-time = "2025-10-06T05:35:35.205Z" },
- { url = "https://files.pythonhosted.org/packages/ee/bf/dc394a097508f15abff383c5108cb8ad880d1f64a725ed3b90d5c2fbf0bb/frozenlist-1.8.0-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:e4a3408834f65da56c83528fb52ce7911484f0d1eaf7b761fc66001db1646eff", size = 235887, upload-time = "2025-10-06T05:35:36.354Z" },
- { url = "https://files.pythonhosted.org/packages/40/90/25b201b9c015dbc999a5baf475a257010471a1fa8c200c843fd4abbee725/frozenlist-1.8.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:42145cd2748ca39f32801dad54aeea10039da6f86e303659db90db1c4b614c8c", size = 228785, upload-time = "2025-10-06T05:35:37.949Z" },
- { url = "https://files.pythonhosted.org/packages/84/f4/b5bc148df03082f05d2dd30c089e269acdbe251ac9a9cf4e727b2dbb8a3d/frozenlist-1.8.0-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:e2de870d16a7a53901e41b64ffdf26f2fbb8917b3e6ebf398098d72c5b20bd7f", size = 230312, upload-time = "2025-10-06T05:35:39.178Z" },
- { url = "https://files.pythonhosted.org/packages/db/4b/87e95b5d15097c302430e647136b7d7ab2398a702390cf4c8601975709e7/frozenlist-1.8.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:20e63c9493d33ee48536600d1a5c95eefc870cd71e7ab037763d1fbb89cc51e7", size = 217650, upload-time = "2025-10-06T05:35:40.377Z" },
- { url = "https://files.pythonhosted.org/packages/e5/70/78a0315d1fea97120591a83e0acd644da638c872f142fd72a6cebee825f3/frozenlist-1.8.0-cp310-cp310-win32.whl", hash = "sha256:adbeebaebae3526afc3c96fad434367cafbfd1b25d72369a9e5858453b1bb71a", size = 39659, upload-time = "2025-10-06T05:35:41.863Z" },
- { url = "https://files.pythonhosted.org/packages/66/aa/3f04523fb189a00e147e60c5b2205126118f216b0aa908035c45336e27e4/frozenlist-1.8.0-cp310-cp310-win_amd64.whl", hash = "sha256:667c3777ca571e5dbeb76f331562ff98b957431df140b54c85fd4d52eea8d8f6", size = 43837, upload-time = "2025-10-06T05:35:43.205Z" },
- { url = "https://files.pythonhosted.org/packages/39/75/1135feecdd7c336938bd55b4dc3b0dfc46d85b9be12ef2628574b28de776/frozenlist-1.8.0-cp310-cp310-win_arm64.whl", hash = "sha256:80f85f0a7cc86e7a54c46d99c9e1318ff01f4687c172ede30fd52d19d1da1c8e", size = 39989, upload-time = "2025-10-06T05:35:44.596Z" },
- { url = "https://files.pythonhosted.org/packages/bc/03/077f869d540370db12165c0aa51640a873fb661d8b315d1d4d67b284d7ac/frozenlist-1.8.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:09474e9831bc2b2199fad6da3c14c7b0fbdd377cce9d3d77131be28906cb7d84", size = 86912, upload-time = "2025-10-06T05:35:45.98Z" },
- { url = "https://files.pythonhosted.org/packages/df/b5/7610b6bd13e4ae77b96ba85abea1c8cb249683217ef09ac9e0ae93f25a91/frozenlist-1.8.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:17c883ab0ab67200b5f964d2b9ed6b00971917d5d8a92df149dc2c9779208ee9", size = 50046, upload-time = "2025-10-06T05:35:47.009Z" },
- { url = "https://files.pythonhosted.org/packages/6e/ef/0e8f1fe32f8a53dd26bdd1f9347efe0778b0fddf62789ea683f4cc7d787d/frozenlist-1.8.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:fa47e444b8ba08fffd1c18e8cdb9a75db1b6a27f17507522834ad13ed5922b93", size = 50119, upload-time = "2025-10-06T05:35:48.38Z" },
- { url = "https://files.pythonhosted.org/packages/11/b1/71a477adc7c36e5fb628245dfbdea2166feae310757dea848d02bd0689fd/frozenlist-1.8.0-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:2552f44204b744fba866e573be4c1f9048d6a324dfe14475103fd51613eb1d1f", size = 231067, upload-time = "2025-10-06T05:35:49.97Z" },
- { url = "https://files.pythonhosted.org/packages/45/7e/afe40eca3a2dc19b9904c0f5d7edfe82b5304cb831391edec0ac04af94c2/frozenlist-1.8.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:957e7c38f250991e48a9a73e6423db1bb9dd14e722a10f6b8bb8e16a0f55f695", size = 233160, upload-time = "2025-10-06T05:35:51.729Z" },
- { url = "https://files.pythonhosted.org/packages/a6/aa/7416eac95603ce428679d273255ffc7c998d4132cfae200103f164b108aa/frozenlist-1.8.0-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:8585e3bb2cdea02fc88ffa245069c36555557ad3609e83be0ec71f54fd4abb52", size = 228544, upload-time = "2025-10-06T05:35:53.246Z" },
- { url = "https://files.pythonhosted.org/packages/8b/3d/2a2d1f683d55ac7e3875e4263d28410063e738384d3adc294f5ff3d7105e/frozenlist-1.8.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:edee74874ce20a373d62dc28b0b18b93f645633c2943fd90ee9d898550770581", size = 243797, upload-time = "2025-10-06T05:35:54.497Z" },
- { url = "https://files.pythonhosted.org/packages/78/1e/2d5565b589e580c296d3bb54da08d206e797d941a83a6fdea42af23be79c/frozenlist-1.8.0-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:c9a63152fe95756b85f31186bddf42e4c02c6321207fd6601a1c89ebac4fe567", size = 247923, upload-time = "2025-10-06T05:35:55.861Z" },
- { url = "https://files.pythonhosted.org/packages/aa/c3/65872fcf1d326a7f101ad4d86285c403c87be7d832b7470b77f6d2ed5ddc/frozenlist-1.8.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:b6db2185db9be0a04fecf2f241c70b63b1a242e2805be291855078f2b404dd6b", size = 230886, upload-time = "2025-10-06T05:35:57.399Z" },
- { url = "https://files.pythonhosted.org/packages/a0/76/ac9ced601d62f6956f03cc794f9e04c81719509f85255abf96e2510f4265/frozenlist-1.8.0-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:f4be2e3d8bc8aabd566f8d5b8ba7ecc09249d74ba3c9ed52e54dc23a293f0b92", size = 245731, upload-time = "2025-10-06T05:35:58.563Z" },
- { url = "https://files.pythonhosted.org/packages/b9/49/ecccb5f2598daf0b4a1415497eba4c33c1e8ce07495eb07d2860c731b8d5/frozenlist-1.8.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:c8d1634419f39ea6f5c427ea2f90ca85126b54b50837f31497f3bf38266e853d", size = 241544, upload-time = "2025-10-06T05:35:59.719Z" },
- { url = "https://files.pythonhosted.org/packages/53/4b/ddf24113323c0bbcc54cb38c8b8916f1da7165e07b8e24a717b4a12cbf10/frozenlist-1.8.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:1a7fa382a4a223773ed64242dbe1c9c326ec09457e6b8428efb4118c685c3dfd", size = 241806, upload-time = "2025-10-06T05:36:00.959Z" },
- { url = "https://files.pythonhosted.org/packages/a7/fb/9b9a084d73c67175484ba2789a59f8eebebd0827d186a8102005ce41e1ba/frozenlist-1.8.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:11847b53d722050808926e785df837353bd4d75f1d494377e59b23594d834967", size = 229382, upload-time = "2025-10-06T05:36:02.22Z" },
- { url = "https://files.pythonhosted.org/packages/95/a3/c8fb25aac55bf5e12dae5c5aa6a98f85d436c1dc658f21c3ac73f9fa95e5/frozenlist-1.8.0-cp311-cp311-win32.whl", hash = "sha256:27c6e8077956cf73eadd514be8fb04d77fc946a7fe9f7fe167648b0b9085cc25", size = 39647, upload-time = "2025-10-06T05:36:03.409Z" },
- { url = "https://files.pythonhosted.org/packages/0a/f5/603d0d6a02cfd4c8f2a095a54672b3cf967ad688a60fb9faf04fc4887f65/frozenlist-1.8.0-cp311-cp311-win_amd64.whl", hash = "sha256:ac913f8403b36a2c8610bbfd25b8013488533e71e62b4b4adce9c86c8cea905b", size = 44064, upload-time = "2025-10-06T05:36:04.368Z" },
- { url = "https://files.pythonhosted.org/packages/5d/16/c2c9ab44e181f043a86f9a8f84d5124b62dbcb3a02c0977ec72b9ac1d3e0/frozenlist-1.8.0-cp311-cp311-win_arm64.whl", hash = "sha256:d4d3214a0f8394edfa3e303136d0575eece0745ff2b47bd2cb2e66dd92d4351a", size = 39937, upload-time = "2025-10-06T05:36:05.669Z" },
- { url = "https://files.pythonhosted.org/packages/69/29/948b9aa87e75820a38650af445d2ef2b6b8a6fab1a23b6bb9e4ef0be2d59/frozenlist-1.8.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:78f7b9e5d6f2fdb88cdde9440dc147259b62b9d3b019924def9f6478be254ac1", size = 87782, upload-time = "2025-10-06T05:36:06.649Z" },
- { url = "https://files.pythonhosted.org/packages/64/80/4f6e318ee2a7c0750ed724fa33a4bdf1eacdc5a39a7a24e818a773cd91af/frozenlist-1.8.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:229bf37d2e4acdaf808fd3f06e854a4a7a3661e871b10dc1f8f1896a3b05f18b", size = 50594, upload-time = "2025-10-06T05:36:07.69Z" },
- { url = "https://files.pythonhosted.org/packages/2b/94/5c8a2b50a496b11dd519f4a24cb5496cf125681dd99e94c604ccdea9419a/frozenlist-1.8.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f833670942247a14eafbb675458b4e61c82e002a148f49e68257b79296e865c4", size = 50448, upload-time = "2025-10-06T05:36:08.78Z" },
- { url = "https://files.pythonhosted.org/packages/6a/bd/d91c5e39f490a49df14320f4e8c80161cfcce09f1e2cde1edd16a551abb3/frozenlist-1.8.0-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:494a5952b1c597ba44e0e78113a7266e656b9794eec897b19ead706bd7074383", size = 242411, upload-time = "2025-10-06T05:36:09.801Z" },
- { url = "https://files.pythonhosted.org/packages/8f/83/f61505a05109ef3293dfb1ff594d13d64a2324ac3482be2cedc2be818256/frozenlist-1.8.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:96f423a119f4777a4a056b66ce11527366a8bb92f54e541ade21f2374433f6d4", size = 243014, upload-time = "2025-10-06T05:36:11.394Z" },
- { url = "https://files.pythonhosted.org/packages/d8/cb/cb6c7b0f7d4023ddda30cf56b8b17494eb3a79e3fda666bf735f63118b35/frozenlist-1.8.0-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:3462dd9475af2025c31cc61be6652dfa25cbfb56cbbf52f4ccfe029f38decaf8", size = 234909, upload-time = "2025-10-06T05:36:12.598Z" },
- { url = "https://files.pythonhosted.org/packages/31/c5/cd7a1f3b8b34af009fb17d4123c5a778b44ae2804e3ad6b86204255f9ec5/frozenlist-1.8.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c4c800524c9cd9bac5166cd6f55285957fcfc907db323e193f2afcd4d9abd69b", size = 250049, upload-time = "2025-10-06T05:36:14.065Z" },
- { url = "https://files.pythonhosted.org/packages/c0/01/2f95d3b416c584a1e7f0e1d6d31998c4a795f7544069ee2e0962a4b60740/frozenlist-1.8.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d6a5df73acd3399d893dafc71663ad22534b5aa4f94e8a2fabfe856c3c1b6a52", size = 256485, upload-time = "2025-10-06T05:36:15.39Z" },
- { url = "https://files.pythonhosted.org/packages/ce/03/024bf7720b3abaebcff6d0793d73c154237b85bdf67b7ed55e5e9596dc9a/frozenlist-1.8.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:405e8fe955c2280ce66428b3ca55e12b3c4e9c336fb2103a4937e891c69a4a29", size = 237619, upload-time = "2025-10-06T05:36:16.558Z" },
- { url = "https://files.pythonhosted.org/packages/69/fa/f8abdfe7d76b731f5d8bd217827cf6764d4f1d9763407e42717b4bed50a0/frozenlist-1.8.0-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:908bd3f6439f2fef9e85031b59fd4f1297af54415fb60e4254a95f75b3cab3f3", size = 250320, upload-time = "2025-10-06T05:36:17.821Z" },
- { url = "https://files.pythonhosted.org/packages/f5/3c/b051329f718b463b22613e269ad72138cc256c540f78a6de89452803a47d/frozenlist-1.8.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:294e487f9ec720bd8ffcebc99d575f7eff3568a08a253d1ee1a0378754b74143", size = 246820, upload-time = "2025-10-06T05:36:19.046Z" },
- { url = "https://files.pythonhosted.org/packages/0f/ae/58282e8f98e444b3f4dd42448ff36fa38bef29e40d40f330b22e7108f565/frozenlist-1.8.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:74c51543498289c0c43656701be6b077f4b265868fa7f8a8859c197006efb608", size = 250518, upload-time = "2025-10-06T05:36:20.763Z" },
- { url = "https://files.pythonhosted.org/packages/8f/96/007e5944694d66123183845a106547a15944fbbb7154788cbf7272789536/frozenlist-1.8.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:776f352e8329135506a1d6bf16ac3f87bc25b28e765949282dcc627af36123aa", size = 239096, upload-time = "2025-10-06T05:36:22.129Z" },
- { url = "https://files.pythonhosted.org/packages/66/bb/852b9d6db2fa40be96f29c0d1205c306288f0684df8fd26ca1951d461a56/frozenlist-1.8.0-cp312-cp312-win32.whl", hash = "sha256:433403ae80709741ce34038da08511d4a77062aa924baf411ef73d1146e74faf", size = 39985, upload-time = "2025-10-06T05:36:23.661Z" },
- { url = "https://files.pythonhosted.org/packages/b8/af/38e51a553dd66eb064cdf193841f16f077585d4d28394c2fa6235cb41765/frozenlist-1.8.0-cp312-cp312-win_amd64.whl", hash = "sha256:34187385b08f866104f0c0617404c8eb08165ab1272e884abc89c112e9c00746", size = 44591, upload-time = "2025-10-06T05:36:24.958Z" },
- { url = "https://files.pythonhosted.org/packages/a7/06/1dc65480ab147339fecc70797e9c2f69d9cea9cf38934ce08df070fdb9cb/frozenlist-1.8.0-cp312-cp312-win_arm64.whl", hash = "sha256:fe3c58d2f5db5fbd18c2987cba06d51b0529f52bc3a6cdc33d3f4eab725104bd", size = 40102, upload-time = "2025-10-06T05:36:26.333Z" },
- { url = "https://files.pythonhosted.org/packages/2d/40/0832c31a37d60f60ed79e9dfb5a92e1e2af4f40a16a29abcc7992af9edff/frozenlist-1.8.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:8d92f1a84bb12d9e56f818b3a746f3efba93c1b63c8387a73dde655e1e42282a", size = 85717, upload-time = "2025-10-06T05:36:27.341Z" },
- { url = "https://files.pythonhosted.org/packages/30/ba/b0b3de23f40bc55a7057bd38434e25c34fa48e17f20ee273bbde5e0650f3/frozenlist-1.8.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:96153e77a591c8adc2ee805756c61f59fef4cf4073a9275ee86fe8cba41241f7", size = 49651, upload-time = "2025-10-06T05:36:28.855Z" },
- { url = "https://files.pythonhosted.org/packages/0c/ab/6e5080ee374f875296c4243c381bbdef97a9ac39c6e3ce1d5f7d42cb78d6/frozenlist-1.8.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f21f00a91358803399890ab167098c131ec2ddd5f8f5fd5fe9c9f2c6fcd91e40", size = 49417, upload-time = "2025-10-06T05:36:29.877Z" },
- { url = "https://files.pythonhosted.org/packages/d5/4e/e4691508f9477ce67da2015d8c00acd751e6287739123113a9fca6f1604e/frozenlist-1.8.0-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:fb30f9626572a76dfe4293c7194a09fb1fe93ba94c7d4f720dfae3b646b45027", size = 234391, upload-time = "2025-10-06T05:36:31.301Z" },
- { url = "https://files.pythonhosted.org/packages/40/76/c202df58e3acdf12969a7895fd6f3bc016c642e6726aa63bd3025e0fc71c/frozenlist-1.8.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:eaa352d7047a31d87dafcacbabe89df0aa506abb5b1b85a2fb91bc3faa02d822", size = 233048, upload-time = "2025-10-06T05:36:32.531Z" },
- { url = "https://files.pythonhosted.org/packages/f9/c0/8746afb90f17b73ca5979c7a3958116e105ff796e718575175319b5bb4ce/frozenlist-1.8.0-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:03ae967b4e297f58f8c774c7eabcce57fe3c2434817d4385c50661845a058121", size = 226549, upload-time = "2025-10-06T05:36:33.706Z" },
- { url = "https://files.pythonhosted.org/packages/7e/eb/4c7eefc718ff72f9b6c4893291abaae5fbc0c82226a32dcd8ef4f7a5dbef/frozenlist-1.8.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f6292f1de555ffcc675941d65fffffb0a5bcd992905015f85d0592201793e0e5", size = 239833, upload-time = "2025-10-06T05:36:34.947Z" },
- { url = "https://files.pythonhosted.org/packages/c2/4e/e5c02187cf704224f8b21bee886f3d713ca379535f16893233b9d672ea71/frozenlist-1.8.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:29548f9b5b5e3460ce7378144c3010363d8035cea44bc0bf02d57f5a685e084e", size = 245363, upload-time = "2025-10-06T05:36:36.534Z" },
- { url = "https://files.pythonhosted.org/packages/1f/96/cb85ec608464472e82ad37a17f844889c36100eed57bea094518bf270692/frozenlist-1.8.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ec3cc8c5d4084591b4237c0a272cc4f50a5b03396a47d9caaf76f5d7b38a4f11", size = 229314, upload-time = "2025-10-06T05:36:38.582Z" },
- { url = "https://files.pythonhosted.org/packages/5d/6f/4ae69c550e4cee66b57887daeebe006fe985917c01d0fff9caab9883f6d0/frozenlist-1.8.0-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:517279f58009d0b1f2e7c1b130b377a349405da3f7621ed6bfae50b10adf20c1", size = 243365, upload-time = "2025-10-06T05:36:40.152Z" },
- { url = "https://files.pythonhosted.org/packages/7a/58/afd56de246cf11780a40a2c28dc7cbabbf06337cc8ddb1c780a2d97e88d8/frozenlist-1.8.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:db1e72ede2d0d7ccb213f218df6a078a9c09a7de257c2fe8fcef16d5925230b1", size = 237763, upload-time = "2025-10-06T05:36:41.355Z" },
- { url = "https://files.pythonhosted.org/packages/cb/36/cdfaf6ed42e2644740d4a10452d8e97fa1c062e2a8006e4b09f1b5fd7d63/frozenlist-1.8.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:b4dec9482a65c54a5044486847b8a66bf10c9cb4926d42927ec4e8fd5db7fed8", size = 240110, upload-time = "2025-10-06T05:36:42.716Z" },
- { url = "https://files.pythonhosted.org/packages/03/a8/9ea226fbefad669f11b52e864c55f0bd57d3c8d7eb07e9f2e9a0b39502e1/frozenlist-1.8.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:21900c48ae04d13d416f0e1e0c4d81f7931f73a9dfa0b7a8746fb2fe7dd970ed", size = 233717, upload-time = "2025-10-06T05:36:44.251Z" },
- { url = "https://files.pythonhosted.org/packages/1e/0b/1b5531611e83ba7d13ccc9988967ea1b51186af64c42b7a7af465dcc9568/frozenlist-1.8.0-cp313-cp313-win32.whl", hash = "sha256:8b7b94a067d1c504ee0b16def57ad5738701e4ba10cec90529f13fa03c833496", size = 39628, upload-time = "2025-10-06T05:36:45.423Z" },
- { url = "https://files.pythonhosted.org/packages/d8/cf/174c91dbc9cc49bc7b7aab74d8b734e974d1faa8f191c74af9b7e80848e6/frozenlist-1.8.0-cp313-cp313-win_amd64.whl", hash = "sha256:878be833caa6a3821caf85eb39c5ba92d28e85df26d57afb06b35b2efd937231", size = 43882, upload-time = "2025-10-06T05:36:46.796Z" },
- { url = "https://files.pythonhosted.org/packages/c1/17/502cd212cbfa96eb1388614fe39a3fc9ab87dbbe042b66f97acb57474834/frozenlist-1.8.0-cp313-cp313-win_arm64.whl", hash = "sha256:44389d135b3ff43ba8cc89ff7f51f5a0bb6b63d829c8300f79a2fe4fe61bcc62", size = 39676, upload-time = "2025-10-06T05:36:47.8Z" },
- { url = "https://files.pythonhosted.org/packages/d2/5c/3bbfaa920dfab09e76946a5d2833a7cbdf7b9b4a91c714666ac4855b88b4/frozenlist-1.8.0-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:e25ac20a2ef37e91c1b39938b591457666a0fa835c7783c3a8f33ea42870db94", size = 89235, upload-time = "2025-10-06T05:36:48.78Z" },
- { url = "https://files.pythonhosted.org/packages/d2/d6/f03961ef72166cec1687e84e8925838442b615bd0b8854b54923ce5b7b8a/frozenlist-1.8.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:07cdca25a91a4386d2e76ad992916a85038a9b97561bf7a3fd12d5d9ce31870c", size = 50742, upload-time = "2025-10-06T05:36:49.837Z" },
- { url = "https://files.pythonhosted.org/packages/1e/bb/a6d12b7ba4c3337667d0e421f7181c82dda448ce4e7ad7ecd249a16fa806/frozenlist-1.8.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:4e0c11f2cc6717e0a741f84a527c52616140741cd812a50422f83dc31749fb52", size = 51725, upload-time = "2025-10-06T05:36:50.851Z" },
- { url = "https://files.pythonhosted.org/packages/bc/71/d1fed0ffe2c2ccd70b43714c6cab0f4188f09f8a67a7914a6b46ee30f274/frozenlist-1.8.0-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:b3210649ee28062ea6099cfda39e147fa1bc039583c8ee4481cb7811e2448c51", size = 284533, upload-time = "2025-10-06T05:36:51.898Z" },
- { url = "https://files.pythonhosted.org/packages/c9/1f/fb1685a7b009d89f9bf78a42d94461bc06581f6e718c39344754a5d9bada/frozenlist-1.8.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:581ef5194c48035a7de2aefc72ac6539823bb71508189e5de01d60c9dcd5fa65", size = 292506, upload-time = "2025-10-06T05:36:53.101Z" },
- { url = "https://files.pythonhosted.org/packages/e6/3b/b991fe1612703f7e0d05c0cf734c1b77aaf7c7d321df4572e8d36e7048c8/frozenlist-1.8.0-cp313-cp313t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:3ef2d026f16a2b1866e1d86fc4e1291e1ed8a387b2c333809419a2f8b3a77b82", size = 274161, upload-time = "2025-10-06T05:36:54.309Z" },
- { url = "https://files.pythonhosted.org/packages/ca/ec/c5c618767bcdf66e88945ec0157d7f6c4a1322f1473392319b7a2501ded7/frozenlist-1.8.0-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:5500ef82073f599ac84d888e3a8c1f77ac831183244bfd7f11eaa0289fb30714", size = 294676, upload-time = "2025-10-06T05:36:55.566Z" },
- { url = "https://files.pythonhosted.org/packages/7c/ce/3934758637d8f8a88d11f0585d6495ef54b2044ed6ec84492a91fa3b27aa/frozenlist-1.8.0-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:50066c3997d0091c411a66e710f4e11752251e6d2d73d70d8d5d4c76442a199d", size = 300638, upload-time = "2025-10-06T05:36:56.758Z" },
- { url = "https://files.pythonhosted.org/packages/fc/4f/a7e4d0d467298f42de4b41cbc7ddaf19d3cfeabaf9ff97c20c6c7ee409f9/frozenlist-1.8.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:5c1c8e78426e59b3f8005e9b19f6ff46e5845895adbde20ece9218319eca6506", size = 283067, upload-time = "2025-10-06T05:36:57.965Z" },
- { url = "https://files.pythonhosted.org/packages/dc/48/c7b163063d55a83772b268e6d1affb960771b0e203b632cfe09522d67ea5/frozenlist-1.8.0-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:eefdba20de0d938cec6a89bd4d70f346a03108a19b9df4248d3cf0d88f1b0f51", size = 292101, upload-time = "2025-10-06T05:36:59.237Z" },
- { url = "https://files.pythonhosted.org/packages/9f/d0/2366d3c4ecdc2fd391e0afa6e11500bfba0ea772764d631bbf82f0136c9d/frozenlist-1.8.0-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:cf253e0e1c3ceb4aaff6df637ce033ff6535fb8c70a764a8f46aafd3d6ab798e", size = 289901, upload-time = "2025-10-06T05:37:00.811Z" },
- { url = "https://files.pythonhosted.org/packages/b8/94/daff920e82c1b70e3618a2ac39fbc01ae3e2ff6124e80739ce5d71c9b920/frozenlist-1.8.0-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:032efa2674356903cd0261c4317a561a6850f3ac864a63fc1583147fb05a79b0", size = 289395, upload-time = "2025-10-06T05:37:02.115Z" },
- { url = "https://files.pythonhosted.org/packages/e3/20/bba307ab4235a09fdcd3cc5508dbabd17c4634a1af4b96e0f69bfe551ebd/frozenlist-1.8.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:6da155091429aeba16851ecb10a9104a108bcd32f6c1642867eadaee401c1c41", size = 283659, upload-time = "2025-10-06T05:37:03.711Z" },
- { url = "https://files.pythonhosted.org/packages/fd/00/04ca1c3a7a124b6de4f8a9a17cc2fcad138b4608e7a3fc5877804b8715d7/frozenlist-1.8.0-cp313-cp313t-win32.whl", hash = "sha256:0f96534f8bfebc1a394209427d0f8a63d343c9779cda6fc25e8e121b5fd8555b", size = 43492, upload-time = "2025-10-06T05:37:04.915Z" },
- { url = "https://files.pythonhosted.org/packages/59/5e/c69f733a86a94ab10f68e496dc6b7e8bc078ebb415281d5698313e3af3a1/frozenlist-1.8.0-cp313-cp313t-win_amd64.whl", hash = "sha256:5d63a068f978fc69421fb0e6eb91a9603187527c86b7cd3f534a5b77a592b888", size = 48034, upload-time = "2025-10-06T05:37:06.343Z" },
- { url = "https://files.pythonhosted.org/packages/16/6c/be9d79775d8abe79b05fa6d23da99ad6e7763a1d080fbae7290b286093fd/frozenlist-1.8.0-cp313-cp313t-win_arm64.whl", hash = "sha256:bf0a7e10b077bf5fb9380ad3ae8ce20ef919a6ad93b4552896419ac7e1d8e042", size = 41749, upload-time = "2025-10-06T05:37:07.431Z" },
- { url = "https://files.pythonhosted.org/packages/f1/c8/85da824b7e7b9b6e7f7705b2ecaf9591ba6f79c1177f324c2735e41d36a2/frozenlist-1.8.0-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:cee686f1f4cadeb2136007ddedd0aaf928ab95216e7691c63e50a8ec066336d0", size = 86127, upload-time = "2025-10-06T05:37:08.438Z" },
- { url = "https://files.pythonhosted.org/packages/8e/e8/a1185e236ec66c20afd72399522f142c3724c785789255202d27ae992818/frozenlist-1.8.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:119fb2a1bd47307e899c2fac7f28e85b9a543864df47aa7ec9d3c1b4545f096f", size = 49698, upload-time = "2025-10-06T05:37:09.48Z" },
- { url = "https://files.pythonhosted.org/packages/a1/93/72b1736d68f03fda5fdf0f2180fb6caaae3894f1b854d006ac61ecc727ee/frozenlist-1.8.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:4970ece02dbc8c3a92fcc5228e36a3e933a01a999f7094ff7c23fbd2beeaa67c", size = 49749, upload-time = "2025-10-06T05:37:10.569Z" },
- { url = "https://files.pythonhosted.org/packages/a7/b2/fabede9fafd976b991e9f1b9c8c873ed86f202889b864756f240ce6dd855/frozenlist-1.8.0-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:cba69cb73723c3f329622e34bdbf5ce1f80c21c290ff04256cff1cd3c2036ed2", size = 231298, upload-time = "2025-10-06T05:37:11.993Z" },
- { url = "https://files.pythonhosted.org/packages/3a/3b/d9b1e0b0eed36e70477ffb8360c49c85c8ca8ef9700a4e6711f39a6e8b45/frozenlist-1.8.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:778a11b15673f6f1df23d9586f83c4846c471a8af693a22e066508b77d201ec8", size = 232015, upload-time = "2025-10-06T05:37:13.194Z" },
- { url = "https://files.pythonhosted.org/packages/dc/94/be719d2766c1138148564a3960fc2c06eb688da592bdc25adcf856101be7/frozenlist-1.8.0-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:0325024fe97f94c41c08872db482cf8ac4800d80e79222c6b0b7b162d5b13686", size = 225038, upload-time = "2025-10-06T05:37:14.577Z" },
- { url = "https://files.pythonhosted.org/packages/e4/09/6712b6c5465f083f52f50cf74167b92d4ea2f50e46a9eea0523d658454ae/frozenlist-1.8.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:97260ff46b207a82a7567b581ab4190bd4dfa09f4db8a8b49d1a958f6aa4940e", size = 240130, upload-time = "2025-10-06T05:37:15.781Z" },
- { url = "https://files.pythonhosted.org/packages/f8/d4/cd065cdcf21550b54f3ce6a22e143ac9e4836ca42a0de1022da8498eac89/frozenlist-1.8.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:54b2077180eb7f83dd52c40b2750d0a9f175e06a42e3213ce047219de902717a", size = 242845, upload-time = "2025-10-06T05:37:17.037Z" },
- { url = "https://files.pythonhosted.org/packages/62/c3/f57a5c8c70cd1ead3d5d5f776f89d33110b1addae0ab010ad774d9a44fb9/frozenlist-1.8.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:2f05983daecab868a31e1da44462873306d3cbfd76d1f0b5b69c473d21dbb128", size = 229131, upload-time = "2025-10-06T05:37:18.221Z" },
- { url = "https://files.pythonhosted.org/packages/6c/52/232476fe9cb64f0742f3fde2b7d26c1dac18b6d62071c74d4ded55e0ef94/frozenlist-1.8.0-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:33f48f51a446114bc5d251fb2954ab0164d5be02ad3382abcbfe07e2531d650f", size = 240542, upload-time = "2025-10-06T05:37:19.771Z" },
- { url = "https://files.pythonhosted.org/packages/5f/85/07bf3f5d0fb5414aee5f47d33c6f5c77bfe49aac680bfece33d4fdf6a246/frozenlist-1.8.0-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:154e55ec0655291b5dd1b8731c637ecdb50975a2ae70c606d100750a540082f7", size = 237308, upload-time = "2025-10-06T05:37:20.969Z" },
- { url = "https://files.pythonhosted.org/packages/11/99/ae3a33d5befd41ac0ca2cc7fd3aa707c9c324de2e89db0e0f45db9a64c26/frozenlist-1.8.0-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:4314debad13beb564b708b4a496020e5306c7333fa9a3ab90374169a20ffab30", size = 238210, upload-time = "2025-10-06T05:37:22.252Z" },
- { url = "https://files.pythonhosted.org/packages/b2/60/b1d2da22f4970e7a155f0adde9b1435712ece01b3cd45ba63702aea33938/frozenlist-1.8.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:073f8bf8becba60aa931eb3bc420b217bb7d5b8f4750e6f8b3be7f3da85d38b7", size = 231972, upload-time = "2025-10-06T05:37:23.5Z" },
- { url = "https://files.pythonhosted.org/packages/3f/ab/945b2f32de889993b9c9133216c068b7fcf257d8595a0ac420ac8677cab0/frozenlist-1.8.0-cp314-cp314-win32.whl", hash = "sha256:bac9c42ba2ac65ddc115d930c78d24ab8d4f465fd3fc473cdedfccadb9429806", size = 40536, upload-time = "2025-10-06T05:37:25.581Z" },
- { url = "https://files.pythonhosted.org/packages/59/ad/9caa9b9c836d9ad6f067157a531ac48b7d36499f5036d4141ce78c230b1b/frozenlist-1.8.0-cp314-cp314-win_amd64.whl", hash = "sha256:3e0761f4d1a44f1d1a47996511752cf3dcec5bbdd9cc2b4fe595caf97754b7a0", size = 44330, upload-time = "2025-10-06T05:37:26.928Z" },
- { url = "https://files.pythonhosted.org/packages/82/13/e6950121764f2676f43534c555249f57030150260aee9dcf7d64efda11dd/frozenlist-1.8.0-cp314-cp314-win_arm64.whl", hash = "sha256:d1eaff1d00c7751b7c6662e9c5ba6eb2c17a2306ba5e2a37f24ddf3cc953402b", size = 40627, upload-time = "2025-10-06T05:37:28.075Z" },
- { url = "https://files.pythonhosted.org/packages/c0/c7/43200656ecc4e02d3f8bc248df68256cd9572b3f0017f0a0c4e93440ae23/frozenlist-1.8.0-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:d3bb933317c52d7ea5004a1c442eef86f426886fba134ef8cf4226ea6ee1821d", size = 89238, upload-time = "2025-10-06T05:37:29.373Z" },
- { url = "https://files.pythonhosted.org/packages/d1/29/55c5f0689b9c0fb765055629f472c0de484dcaf0acee2f7707266ae3583c/frozenlist-1.8.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:8009897cdef112072f93a0efdce29cd819e717fd2f649ee3016efd3cd885a7ed", size = 50738, upload-time = "2025-10-06T05:37:30.792Z" },
- { url = "https://files.pythonhosted.org/packages/ba/7d/b7282a445956506fa11da8c2db7d276adcbf2b17d8bb8407a47685263f90/frozenlist-1.8.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:2c5dcbbc55383e5883246d11fd179782a9d07a986c40f49abe89ddf865913930", size = 51739, upload-time = "2025-10-06T05:37:32.127Z" },
- { url = "https://files.pythonhosted.org/packages/62/1c/3d8622e60d0b767a5510d1d3cf21065b9db874696a51ea6d7a43180a259c/frozenlist-1.8.0-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:39ecbc32f1390387d2aa4f5a995e465e9e2f79ba3adcac92d68e3e0afae6657c", size = 284186, upload-time = "2025-10-06T05:37:33.21Z" },
- { url = "https://files.pythonhosted.org/packages/2d/14/aa36d5f85a89679a85a1d44cd7a6657e0b1c75f61e7cad987b203d2daca8/frozenlist-1.8.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:92db2bf818d5cc8d9c1f1fc56b897662e24ea5adb36ad1f1d82875bd64e03c24", size = 292196, upload-time = "2025-10-06T05:37:36.107Z" },
- { url = "https://files.pythonhosted.org/packages/05/23/6bde59eb55abd407d34f77d39a5126fb7b4f109a3f611d3929f14b700c66/frozenlist-1.8.0-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:2dc43a022e555de94c3b68a4ef0b11c4f747d12c024a520c7101709a2144fb37", size = 273830, upload-time = "2025-10-06T05:37:37.663Z" },
- { url = "https://files.pythonhosted.org/packages/d2/3f/22cff331bfad7a8afa616289000ba793347fcd7bc275f3b28ecea2a27909/frozenlist-1.8.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:cb89a7f2de3602cfed448095bab3f178399646ab7c61454315089787df07733a", size = 294289, upload-time = "2025-10-06T05:37:39.261Z" },
- { url = "https://files.pythonhosted.org/packages/a4/89/5b057c799de4838b6c69aa82b79705f2027615e01be996d2486a69ca99c4/frozenlist-1.8.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:33139dc858c580ea50e7e60a1b0ea003efa1fd42e6ec7fdbad78fff65fad2fd2", size = 300318, upload-time = "2025-10-06T05:37:43.213Z" },
- { url = "https://files.pythonhosted.org/packages/30/de/2c22ab3eb2a8af6d69dc799e48455813bab3690c760de58e1bf43b36da3e/frozenlist-1.8.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:168c0969a329b416119507ba30b9ea13688fafffac1b7822802537569a1cb0ef", size = 282814, upload-time = "2025-10-06T05:37:45.337Z" },
- { url = "https://files.pythonhosted.org/packages/59/f7/970141a6a8dbd7f556d94977858cfb36fa9b66e0892c6dd780d2219d8cd8/frozenlist-1.8.0-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:28bd570e8e189d7f7b001966435f9dac6718324b5be2990ac496cf1ea9ddb7fe", size = 291762, upload-time = "2025-10-06T05:37:46.657Z" },
- { url = "https://files.pythonhosted.org/packages/c1/15/ca1adae83a719f82df9116d66f5bb28bb95557b3951903d39135620ef157/frozenlist-1.8.0-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:b2a095d45c5d46e5e79ba1e5b9cb787f541a8dee0433836cea4b96a2c439dcd8", size = 289470, upload-time = "2025-10-06T05:37:47.946Z" },
- { url = "https://files.pythonhosted.org/packages/ac/83/dca6dc53bf657d371fbc88ddeb21b79891e747189c5de990b9dfff2ccba1/frozenlist-1.8.0-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:eab8145831a0d56ec9c4139b6c3e594c7a83c2c8be25d5bcf2d86136a532287a", size = 289042, upload-time = "2025-10-06T05:37:49.499Z" },
- { url = "https://files.pythonhosted.org/packages/96/52/abddd34ca99be142f354398700536c5bd315880ed0a213812bc491cff5e4/frozenlist-1.8.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:974b28cf63cc99dfb2188d8d222bc6843656188164848c4f679e63dae4b0708e", size = 283148, upload-time = "2025-10-06T05:37:50.745Z" },
- { url = "https://files.pythonhosted.org/packages/af/d3/76bd4ed4317e7119c2b7f57c3f6934aba26d277acc6309f873341640e21f/frozenlist-1.8.0-cp314-cp314t-win32.whl", hash = "sha256:342c97bf697ac5480c0a7ec73cd700ecfa5a8a40ac923bd035484616efecc2df", size = 44676, upload-time = "2025-10-06T05:37:52.222Z" },
- { url = "https://files.pythonhosted.org/packages/89/76/c615883b7b521ead2944bb3480398cbb07e12b7b4e4d073d3752eb721558/frozenlist-1.8.0-cp314-cp314t-win_amd64.whl", hash = "sha256:06be8f67f39c8b1dc671f5d83aaefd3358ae5cdcf8314552c57e7ed3e6475bdd", size = 49451, upload-time = "2025-10-06T05:37:53.425Z" },
- { url = "https://files.pythonhosted.org/packages/e0/a3/5982da14e113d07b325230f95060e2169f5311b1017ea8af2a29b374c289/frozenlist-1.8.0-cp314-cp314t-win_arm64.whl", hash = "sha256:102e6314ca4da683dca92e3b1355490fed5f313b768500084fbe6371fddfdb79", size = 42507, upload-time = "2025-10-06T05:37:54.513Z" },
- { url = "https://files.pythonhosted.org/packages/c2/59/ae5cdac87a00962122ea37bb346d41b66aec05f9ce328fa2b9e216f8967b/frozenlist-1.8.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:d8b7138e5cd0647e4523d6685b0eac5d4be9a184ae9634492f25c6eb38c12a47", size = 86967, upload-time = "2025-10-06T05:37:55.607Z" },
- { url = "https://files.pythonhosted.org/packages/8a/10/17059b2db5a032fd9323c41c39e9d1f5f9d0c8f04d1e4e3e788573086e61/frozenlist-1.8.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:a6483e309ca809f1efd154b4d37dc6d9f61037d6c6a81c2dc7a15cb22c8c5dca", size = 49984, upload-time = "2025-10-06T05:37:57.049Z" },
- { url = "https://files.pythonhosted.org/packages/4b/de/ad9d82ca8e5fa8f0c636e64606553c79e2b859ad253030b62a21fe9986f5/frozenlist-1.8.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:1b9290cf81e95e93fdf90548ce9d3c1211cf574b8e3f4b3b7cb0537cf2227068", size = 50240, upload-time = "2025-10-06T05:37:58.145Z" },
- { url = "https://files.pythonhosted.org/packages/4e/45/3dfb7767c2a67d123650122b62ce13c731b6c745bc14424eea67678b508c/frozenlist-1.8.0-cp39-cp39-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:59a6a5876ca59d1b63af8cd5e7ffffb024c3dc1e9cf9301b21a2e76286505c95", size = 219472, upload-time = "2025-10-06T05:37:59.239Z" },
- { url = "https://files.pythonhosted.org/packages/0b/bf/5bf23d913a741b960d5c1dac7c1985d8a2a1d015772b2d18ea168b08e7ff/frozenlist-1.8.0-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6dc4126390929823e2d2d9dc79ab4046ed74680360fc5f38b585c12c66cdf459", size = 221531, upload-time = "2025-10-06T05:38:00.521Z" },
- { url = "https://files.pythonhosted.org/packages/d0/03/27ec393f3b55860859f4b74cdc8c2a4af3dbf3533305e8eacf48a4fd9a54/frozenlist-1.8.0-cp39-cp39-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:332db6b2563333c5671fecacd085141b5800cb866be16d5e3eb15a2086476675", size = 219211, upload-time = "2025-10-06T05:38:01.842Z" },
- { url = "https://files.pythonhosted.org/packages/3a/ad/0fd00c404fa73fe9b169429e9a972d5ed807973c40ab6b3cf9365a33d360/frozenlist-1.8.0-cp39-cp39-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:9ff15928d62a0b80bb875655c39bf517938c7d589554cbd2669be42d97c2cb61", size = 231775, upload-time = "2025-10-06T05:38:03.384Z" },
- { url = "https://files.pythonhosted.org/packages/8a/c3/86962566154cb4d2995358bc8331bfc4ea19d07db1a96f64935a1607f2b6/frozenlist-1.8.0-cp39-cp39-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:7bf6cdf8e07c8151fba6fe85735441240ec7f619f935a5205953d58009aef8c6", size = 236631, upload-time = "2025-10-06T05:38:04.609Z" },
- { url = "https://files.pythonhosted.org/packages/ea/9e/6ffad161dbd83782d2c66dc4d378a9103b31770cb1e67febf43aea42d202/frozenlist-1.8.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:48e6d3f4ec5c7273dfe83ff27c91083c6c9065af655dc2684d2c200c94308bb5", size = 218632, upload-time = "2025-10-06T05:38:05.917Z" },
- { url = "https://files.pythonhosted.org/packages/58/b2/4677eee46e0a97f9b30735e6ad0bf6aba3e497986066eb68807ac85cf60f/frozenlist-1.8.0-cp39-cp39-musllinux_1_2_armv7l.whl", hash = "sha256:1a7607e17ad33361677adcd1443edf6f5da0ce5e5377b798fba20fae194825f3", size = 235967, upload-time = "2025-10-06T05:38:07.614Z" },
- { url = "https://files.pythonhosted.org/packages/05/f3/86e75f8639c5a93745ca7addbbc9de6af56aebb930d233512b17e46f6493/frozenlist-1.8.0-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:5a3a935c3a4e89c733303a2d5a7c257ea44af3a56c8202df486b7f5de40f37e1", size = 228799, upload-time = "2025-10-06T05:38:08.845Z" },
- { url = "https://files.pythonhosted.org/packages/30/00/39aad3a7f0d98f5eb1d99a3c311215674ed87061aecee7851974b335c050/frozenlist-1.8.0-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:940d4a017dbfed9daf46a3b086e1d2167e7012ee297fef9e1c545c4d022f5178", size = 230566, upload-time = "2025-10-06T05:38:10.52Z" },
- { url = "https://files.pythonhosted.org/packages/0d/4d/aa144cac44568d137846ddc4d5210fb5d9719eb1d7ec6fa2728a54b5b94a/frozenlist-1.8.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:b9be22a69a014bc47e78072d0ecae716f5eb56c15238acca0f43d6eb8e4a5bda", size = 217715, upload-time = "2025-10-06T05:38:11.832Z" },
- { url = "https://files.pythonhosted.org/packages/64/4c/8f665921667509d25a0dd72540513bc86b356c95541686f6442a3283019f/frozenlist-1.8.0-cp39-cp39-win32.whl", hash = "sha256:1aa77cb5697069af47472e39612976ed05343ff2e84a3dcf15437b232cbfd087", size = 39933, upload-time = "2025-10-06T05:38:13.061Z" },
- { url = "https://files.pythonhosted.org/packages/79/bd/bcc926f87027fad5e59926ff12d136e1082a115025d33c032d1cd69ab377/frozenlist-1.8.0-cp39-cp39-win_amd64.whl", hash = "sha256:7398c222d1d405e796970320036b1b563892b65809d9e5261487bb2c7f7b5c6a", size = 44121, upload-time = "2025-10-06T05:38:14.572Z" },
- { url = "https://files.pythonhosted.org/packages/4c/07/9c2e4eb7584af4b705237b971b89a4155a8e57599c4483a131a39256a9a0/frozenlist-1.8.0-cp39-cp39-win_arm64.whl", hash = "sha256:b4f3b365f31c6cd4af24545ca0a244a53688cad8834e32f56831c4923b50a103", size = 40312, upload-time = "2025-10-06T05:38:15.699Z" },
- { url = "https://files.pythonhosted.org/packages/9a/9a/e35b4a917281c0b8419d4207f4334c8e8c5dbf4f3f5f9ada73958d937dcc/frozenlist-1.8.0-py3-none-any.whl", hash = "sha256:0c18a16eab41e82c295618a77502e17b195883241c563b00f0aa5106fc4eaa0d", size = 13409, upload-time = "2025-10-06T05:38:16.721Z" },
-]
-
-[[package]]
-name = "h11"
-version = "0.16.0"
-source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/01/ee/02a2c011bdab74c6fb3c75474d40b3052059d95df7e73351460c8588d963/h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1", size = 101250, upload-time = "2025-04-24T03:35:25.427Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86", size = 37515, upload-time = "2025-04-24T03:35:24.344Z" },
-]
-
-[[package]]
-name = "httpcore"
-version = "1.0.9"
-source = { registry = "https://pypi.org/simple" }
-dependencies = [
- { name = "certifi" },
- { name = "h11" },
-]
-sdist = { url = "https://files.pythonhosted.org/packages/06/94/82699a10bca87a5556c9c59b5963f2d039dbd239f25bc2a63907a05a14cb/httpcore-1.0.9.tar.gz", hash = "sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8", size = 85484, upload-time = "2025-04-24T22:06:22.219Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/7e/f5/f66802a942d491edb555dd61e3a9961140fd64c90bce1eafd741609d334d/httpcore-1.0.9-py3-none-any.whl", hash = "sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55", size = 78784, upload-time = "2025-04-24T22:06:20.566Z" },
-]
-
-[[package]]
-name = "httpx"
-version = "0.28.1"
-source = { registry = "https://pypi.org/simple" }
-dependencies = [
- { name = "anyio", version = "4.12.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" },
- { name = "anyio", version = "4.14.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" },
- { name = "certifi" },
- { name = "httpcore" },
- { name = "idna" },
-]
-sdist = { url = "https://files.pythonhosted.org/packages/b1/df/48c586a5fe32a0f01324ee087459e112ebb7224f646c0b5023f5e79e9956/httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc", size = 141406, upload-time = "2024-12-06T15:37:23.222Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad", size = 73517, upload-time = "2024-12-06T15:37:21.509Z" },
-]
-
-[[package]]
-name = "httpx-aiohttp"
-version = "0.2.0"
-source = { registry = "https://pypi.org/simple" }
-dependencies = [
- { name = "aiohttp", version = "3.13.5", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" },
- { name = "aiohttp", version = "3.14.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" },
- { name = "httpx" },
-]
-sdist = { url = "https://files.pythonhosted.org/packages/4c/87/3b2df9732a497403e5f4bbf2ec9f25427d53cec797e83070c503649863ef/httpx_aiohttp-0.2.0.tar.gz", hash = "sha256:d4796b981f04734f1d1db9b4d9326ea16bc994f126460b93b69036262cd4a9d8", size = 195714, upload-time = "2026-07-25T07:34:12.17Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/e8/e2/74b6bad3a6d342aee12d8b8d825456c02d21d72319c924326d17444c5ff7/httpx_aiohttp-0.2.0-py3-none-any.whl", hash = "sha256:ccd6eb19ba18805476096e8ef0b369a6beda3955db145a538979eface2fce7ff", size = 9732, upload-time = "2026-07-25T07:34:10.939Z" },
-]
-
-[[package]]
-name = "idna"
-version = "3.18"
-source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/cd/63/9496c57188a2ee585e0f1db071d75089a11e98aa86eb99d9d7618fc1edce/idna-3.18.tar.gz", hash = "sha256:ffb385a7e039654cef1ab9ef32c6fafe283c0c0467bba1d9029738ce4a14a848", size = 196711, upload-time = "2026-06-02T14:34:07.794Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/1e/5e/d4e9f1a599fb8e573b7b87160658329fbf28d19eac2718f51fc3def3aa5a/idna-3.18-py3-none-any.whl", hash = "sha256:7f952cbe720b688055e3f87de14f5c3e5fdaa8bc3928985c4077ca689de849a2", size = 65455, upload-time = "2026-06-02T14:34:06.319Z" },
-]
-
-[[package]]
-name = "kernel"
-version = "0.86.0"
-source = { editable = "." }
-dependencies = [
- { name = "anyio", version = "4.12.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" },
- { name = "anyio", version = "4.14.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" },
- { name = "distro" },
- { name = "httpx" },
- { name = "pydantic" },
- { name = "sniffio" },
- { name = "typing-extensions" },
-]
-
-[package.optional-dependencies]
-aiohttp = [
- { name = "aiohttp", version = "3.13.5", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" },
- { name = "aiohttp", version = "3.14.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" },
- { name = "httpx-aiohttp" },
-]
-
-[package.metadata]
-requires-dist = [
- { name = "aiohttp", marker = "extra == 'aiohttp'" },
- { name = "anyio", specifier = ">=3.5.0,<5" },
- { name = "distro", specifier = ">=1.7.0,<2" },
- { name = "httpx", specifier = ">=0.23.0,<1" },
- { name = "httpx-aiohttp", marker = "extra == 'aiohttp'", specifier = ">=0.1.9" },
- { name = "pydantic", specifier = ">=1.9.0,<3" },
- { name = "sniffio" },
- { name = "typing-extensions", specifier = ">=4.14,<5" },
-]
-provides-extras = ["aiohttp"]
-
-[[package]]
-name = "multidict"
-version = "6.7.1"
-source = { registry = "https://pypi.org/simple" }
-dependencies = [
- { name = "typing-extensions", marker = "python_full_version < '3.11'" },
-]
-sdist = { url = "https://files.pythonhosted.org/packages/1a/c2/c2d94cbe6ac1753f3fc980da97b3d930efe1da3af3c9f5125354436c073d/multidict-6.7.1.tar.gz", hash = "sha256:ec6652a1bee61c53a3e5776b6049172c53b6aaba34f18c9ad04f82712bac623d", size = 102010, upload-time = "2026-01-26T02:46:45.979Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/84/0b/19348d4c98980c4851d2f943f8ebafdece2ae7ef737adcfa5994ce8e5f10/multidict-6.7.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:c93c3db7ea657dd4637d57e74ab73de31bccefe144d3d4ce370052035bc85fb5", size = 77176, upload-time = "2026-01-26T02:42:59.784Z" },
- { url = "https://files.pythonhosted.org/packages/ef/04/9de3f8077852e3d438215c81e9b691244532d2e05b4270e89ce67b7d103c/multidict-6.7.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:974e72a2474600827abaeda71af0c53d9ebbc3c2eb7da37b37d7829ae31232d8", size = 44996, upload-time = "2026-01-26T02:43:01.674Z" },
- { url = "https://files.pythonhosted.org/packages/31/5c/08c7f7fe311f32e83f7621cd3f99d805f45519cd06fafb247628b861da7d/multidict-6.7.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:cdea2e7b2456cfb6694fb113066fd0ec7ea4d67e3a35e1f4cbeea0b448bf5872", size = 44631, upload-time = "2026-01-26T02:43:03.169Z" },
- { url = "https://files.pythonhosted.org/packages/b7/7f/0e3b1390ae772f27501199996b94b52ceeb64fe6f9120a32c6c3f6b781be/multidict-6.7.1-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:17207077e29342fdc2c9a82e4b306f1127bf1ea91f8b71e02d4798a70bb99991", size = 242561, upload-time = "2026-01-26T02:43:04.733Z" },
- { url = "https://files.pythonhosted.org/packages/dd/f4/8719f4f167586af317b69dd3e90f913416c91ca610cac79a45c53f590312/multidict-6.7.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d4f49cb5661344764e4c7c7973e92a47a59b8fc19b6523649ec9dc4960e58a03", size = 242223, upload-time = "2026-01-26T02:43:06.695Z" },
- { url = "https://files.pythonhosted.org/packages/47/ab/7c36164cce64a6ad19c6d9a85377b7178ecf3b89f8fd589c73381a5eedfd/multidict-6.7.1-cp310-cp310-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:a9fc4caa29e2e6ae408d1c450ac8bf19892c5fca83ee634ecd88a53332c59981", size = 222322, upload-time = "2026-01-26T02:43:08.472Z" },
- { url = "https://files.pythonhosted.org/packages/f5/79/a25add6fb38035b5337bc5734f296d9afc99163403bbcf56d4170f97eb62/multidict-6.7.1-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c5f0c21549ab432b57dcc82130f388d84ad8179824cc3f223d5e7cfbfd4143f6", size = 254005, upload-time = "2026-01-26T02:43:10.127Z" },
- { url = "https://files.pythonhosted.org/packages/4a/7b/64a87cf98e12f756fc8bd444b001232ffff2be37288f018ad0d3f0aae931/multidict-6.7.1-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:7dfb78d966b2c906ae1d28ccf6e6712a3cd04407ee5088cd276fe8cb42186190", size = 251173, upload-time = "2026-01-26T02:43:11.731Z" },
- { url = "https://files.pythonhosted.org/packages/4b/ac/b605473de2bb404e742f2cc3583d12aedb2352a70e49ae8fce455b50c5aa/multidict-6.7.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9b0d9b91d1aa44db9c1f1ecd0d9d2ae610b2f4f856448664e01a3b35899f3f92", size = 243273, upload-time = "2026-01-26T02:43:13.063Z" },
- { url = "https://files.pythonhosted.org/packages/03/65/11492d6a0e259783720f3bc1d9ea55579a76f1407e31ed44045c99542004/multidict-6.7.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:dd96c01a9dcd4889dcfcf9eb5544ca0c77603f239e3ffab0524ec17aea9a93ee", size = 238956, upload-time = "2026-01-26T02:43:14.843Z" },
- { url = "https://files.pythonhosted.org/packages/5f/a7/7ee591302af64e7c196fb63fe856c788993c1372df765102bd0448e7e165/multidict-6.7.1-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:067343c68cd6612d375710f895337b3a98a033c94f14b9a99eff902f205424e2", size = 233477, upload-time = "2026-01-26T02:43:16.025Z" },
- { url = "https://files.pythonhosted.org/packages/9c/99/c109962d58756c35fd9992fed7f2355303846ea2ff054bb5f5e9d6b888de/multidict-6.7.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:5884a04f4ff56c6120f6ccf703bdeb8b5079d808ba604d4d53aec0d55dc33568", size = 243615, upload-time = "2026-01-26T02:43:17.84Z" },
- { url = "https://files.pythonhosted.org/packages/d5/5f/1973e7c771c86e93dcfe1c9cc55a5481b610f6614acfc28c0d326fe6bfad/multidict-6.7.1-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:8affcf1c98b82bc901702eb73b6947a1bfa170823c153fe8a47b5f5f02e48e40", size = 249930, upload-time = "2026-01-26T02:43:19.06Z" },
- { url = "https://files.pythonhosted.org/packages/5d/a5/f170fc2268c3243853580203378cd522446b2df632061e0a5409817854c7/multidict-6.7.1-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:0d17522c37d03e85c8098ec8431636309b2682cf12e58f4dbc76121fb50e4962", size = 243807, upload-time = "2026-01-26T02:43:20.286Z" },
- { url = "https://files.pythonhosted.org/packages/de/01/73856fab6d125e5bc652c3986b90e8699a95e84b48d72f39ade6c0e74a8c/multidict-6.7.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:24c0cf81544ca5e17cfcb6e482e7a82cd475925242b308b890c9452a074d4505", size = 239103, upload-time = "2026-01-26T02:43:21.508Z" },
- { url = "https://files.pythonhosted.org/packages/e7/46/f1220bd9944d8aa40d8ccff100eeeee19b505b857b6f603d6078cb5315b0/multidict-6.7.1-cp310-cp310-win32.whl", hash = "sha256:d82dd730a95e6643802f4454b8fdecdf08667881a9c5670db85bc5a56693f122", size = 41416, upload-time = "2026-01-26T02:43:22.703Z" },
- { url = "https://files.pythonhosted.org/packages/68/00/9b38e272a770303692fc406c36e1a4c740f401522d5787691eb38a8925a8/multidict-6.7.1-cp310-cp310-win_amd64.whl", hash = "sha256:cf37cbe5ced48d417ba045aca1b21bafca67489452debcde94778a576666a1df", size = 46022, upload-time = "2026-01-26T02:43:23.77Z" },
- { url = "https://files.pythonhosted.org/packages/64/65/d8d42490c02ee07b6bbe00f7190d70bb4738b3cce7629aaf9f213ef730dd/multidict-6.7.1-cp310-cp310-win_arm64.whl", hash = "sha256:59bc83d3f66b41dac1e7460aac1d196edc70c9ba3094965c467715a70ecb46db", size = 43238, upload-time = "2026-01-26T02:43:24.882Z" },
- { url = "https://files.pythonhosted.org/packages/ce/f1/a90635c4f88fb913fbf4ce660b83b7445b7a02615bda034b2f8eb38fd597/multidict-6.7.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:7ff981b266af91d7b4b3793ca3382e53229088d193a85dfad6f5f4c27fc73e5d", size = 76626, upload-time = "2026-01-26T02:43:26.485Z" },
- { url = "https://files.pythonhosted.org/packages/a6/9b/267e64eaf6fc637a15b35f5de31a566634a2740f97d8d094a69d34f524a4/multidict-6.7.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:844c5bca0b5444adb44a623fb0a1310c2f4cd41f402126bb269cd44c9b3f3e1e", size = 44706, upload-time = "2026-01-26T02:43:27.607Z" },
- { url = "https://files.pythonhosted.org/packages/dd/a4/d45caf2b97b035c57267791ecfaafbd59c68212004b3842830954bb4b02e/multidict-6.7.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f2a0a924d4c2e9afcd7ec64f9de35fcd96915149b2216e1cb2c10a56df483855", size = 44356, upload-time = "2026-01-26T02:43:28.661Z" },
- { url = "https://files.pythonhosted.org/packages/fd/d2/0a36c8473f0cbaeadd5db6c8b72d15bbceeec275807772bfcd059bef487d/multidict-6.7.1-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:8be1802715a8e892c784c0197c2ace276ea52702a0ede98b6310c8f255a5afb3", size = 244355, upload-time = "2026-01-26T02:43:31.165Z" },
- { url = "https://files.pythonhosted.org/packages/5d/16/8c65be997fd7dd311b7d39c7b6e71a0cb449bad093761481eccbbe4b42a2/multidict-6.7.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2e2d2ed645ea29f31c4c7ea1552fcfd7cb7ba656e1eafd4134a6620c9f5fdd9e", size = 246433, upload-time = "2026-01-26T02:43:32.581Z" },
- { url = "https://files.pythonhosted.org/packages/01/fb/4dbd7e848d2799c6a026ec88ad39cf2b8416aa167fcc903baa55ecaa045c/multidict-6.7.1-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:95922cee9a778659e91db6497596435777bd25ed116701a4c034f8e46544955a", size = 225376, upload-time = "2026-01-26T02:43:34.417Z" },
- { url = "https://files.pythonhosted.org/packages/b6/8a/4a3a6341eac3830f6053062f8fbc9a9e54407c80755b3f05bc427295c2d0/multidict-6.7.1-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:6b83cabdc375ffaaa15edd97eb7c0c672ad788e2687004990074d7d6c9b140c8", size = 257365, upload-time = "2026-01-26T02:43:35.741Z" },
- { url = "https://files.pythonhosted.org/packages/f7/a2/dd575a69c1aa206e12d27d0770cdf9b92434b48a9ef0cd0d1afdecaa93c4/multidict-6.7.1-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:38fb49540705369bab8484db0689d86c0a33a0a9f2c1b197f506b71b4b6c19b0", size = 254747, upload-time = "2026-01-26T02:43:36.976Z" },
- { url = "https://files.pythonhosted.org/packages/5a/56/21b27c560c13822ed93133f08aa6372c53a8e067f11fbed37b4adcdac922/multidict-6.7.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:439cbebd499f92e9aa6793016a8acaa161dfa749ae86d20960189f5398a19144", size = 246293, upload-time = "2026-01-26T02:43:38.258Z" },
- { url = "https://files.pythonhosted.org/packages/5a/a4/23466059dc3854763423d0ad6c0f3683a379d97673b1b89ec33826e46728/multidict-6.7.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:6d3bc717b6fe763b8be3f2bee2701d3c8eb1b2a8ae9f60910f1b2860c82b6c49", size = 242962, upload-time = "2026-01-26T02:43:40.034Z" },
- { url = "https://files.pythonhosted.org/packages/1f/67/51dd754a3524d685958001e8fa20a0f5f90a6a856e0a9dcabff69be3dbb7/multidict-6.7.1-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:619e5a1ac57986dbfec9f0b301d865dddf763696435e2962f6d9cf2fdff2bb71", size = 237360, upload-time = "2026-01-26T02:43:41.752Z" },
- { url = "https://files.pythonhosted.org/packages/64/3f/036dfc8c174934d4b55d86ff4f978e558b0e585cef70cfc1ad01adc6bf18/multidict-6.7.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:0b38ebffd9be37c1170d33bc0f36f4f262e0a09bc1aac1c34c7aa51a7293f0b3", size = 245940, upload-time = "2026-01-26T02:43:43.042Z" },
- { url = "https://files.pythonhosted.org/packages/3d/20/6214d3c105928ebc353a1c644a6ef1408bc5794fcb4f170bb524a3c16311/multidict-6.7.1-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:10ae39c9cfe6adedcdb764f5e8411d4a92b055e35573a2eaa88d3323289ef93c", size = 253502, upload-time = "2026-01-26T02:43:44.371Z" },
- { url = "https://files.pythonhosted.org/packages/b1/e2/c653bc4ae1be70a0f836b82172d643fcf1dade042ba2676ab08ec08bff0f/multidict-6.7.1-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:25167cc263257660290fba06b9318d2026e3c910be240a146e1f66dd114af2b0", size = 247065, upload-time = "2026-01-26T02:43:45.745Z" },
- { url = "https://files.pythonhosted.org/packages/c8/11/a854b4154cd3bd8b1fd375e8a8ca9d73be37610c361543d56f764109509b/multidict-6.7.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:128441d052254f42989ef98b7b6a6ecb1e6f708aa962c7984235316db59f50fa", size = 241870, upload-time = "2026-01-26T02:43:47.054Z" },
- { url = "https://files.pythonhosted.org/packages/13/bf/9676c0392309b5fdae322333d22a829715b570edb9baa8016a517b55b558/multidict-6.7.1-cp311-cp311-win32.whl", hash = "sha256:d62b7f64ffde3b99d06b707a280db04fb3855b55f5a06df387236051d0668f4a", size = 41302, upload-time = "2026-01-26T02:43:48.753Z" },
- { url = "https://files.pythonhosted.org/packages/c9/68/f16a3a8ba6f7b6dc92a1f19669c0810bd2c43fc5a02da13b1cbf8e253845/multidict-6.7.1-cp311-cp311-win_amd64.whl", hash = "sha256:bdbf9f3b332abd0cdb306e7c2113818ab1e922dc84b8f8fd06ec89ed2a19ab8b", size = 45981, upload-time = "2026-01-26T02:43:49.921Z" },
- { url = "https://files.pythonhosted.org/packages/ac/ad/9dd5305253fa00cd3c7555dbef69d5bf4133debc53b87ab8d6a44d411665/multidict-6.7.1-cp311-cp311-win_arm64.whl", hash = "sha256:b8c990b037d2fff2f4e33d3f21b9b531c5745b33a49a7d6dbe7a177266af44f6", size = 43159, upload-time = "2026-01-26T02:43:51.635Z" },
- { url = "https://files.pythonhosted.org/packages/8d/9c/f20e0e2cf80e4b2e4b1c365bf5fe104ee633c751a724246262db8f1a0b13/multidict-6.7.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:a90f75c956e32891a4eda3639ce6dd86e87105271f43d43442a3aedf3cddf172", size = 76893, upload-time = "2026-01-26T02:43:52.754Z" },
- { url = "https://files.pythonhosted.org/packages/fe/cf/18ef143a81610136d3da8193da9d80bfe1cb548a1e2d1c775f26b23d024a/multidict-6.7.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:3fccb473e87eaa1382689053e4a4618e7ba7b9b9b8d6adf2027ee474597128cd", size = 45456, upload-time = "2026-01-26T02:43:53.893Z" },
- { url = "https://files.pythonhosted.org/packages/a9/65/1caac9d4cd32e8433908683446eebc953e82d22b03d10d41a5f0fefe991b/multidict-6.7.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:b0fa96985700739c4c7853a43c0b3e169360d6855780021bfc6d0f1ce7c123e7", size = 43872, upload-time = "2026-01-26T02:43:55.041Z" },
- { url = "https://files.pythonhosted.org/packages/cf/3b/d6bd75dc4f3ff7c73766e04e705b00ed6dbbaccf670d9e05a12b006f5a21/multidict-6.7.1-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:cb2a55f408c3043e42b40cc8eecd575afa27b7e0b956dfb190de0f8499a57a53", size = 251018, upload-time = "2026-01-26T02:43:56.198Z" },
- { url = "https://files.pythonhosted.org/packages/fd/80/c959c5933adedb9ac15152e4067c702a808ea183a8b64cf8f31af8ad3155/multidict-6.7.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:eb0ce7b2a32d09892b3dd6cc44877a0d02a33241fafca5f25c8b6b62374f8b75", size = 258883, upload-time = "2026-01-26T02:43:57.499Z" },
- { url = "https://files.pythonhosted.org/packages/86/85/7ed40adafea3d4f1c8b916e3b5cc3a8e07dfcdcb9cd72800f4ed3ca1b387/multidict-6.7.1-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:c3a32d23520ee37bf327d1e1a656fec76a2edd5c038bf43eddfa0572ec49c60b", size = 242413, upload-time = "2026-01-26T02:43:58.755Z" },
- { url = "https://files.pythonhosted.org/packages/d2/57/b8565ff533e48595503c785f8361ff9a4fde4d67de25c207cd0ba3befd03/multidict-6.7.1-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:9c90fed18bffc0189ba814749fdcc102b536e83a9f738a9003e569acd540a733", size = 268404, upload-time = "2026-01-26T02:44:00.216Z" },
- { url = "https://files.pythonhosted.org/packages/e0/50/9810c5c29350f7258180dfdcb2e52783a0632862eb334c4896ac717cebcb/multidict-6.7.1-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:da62917e6076f512daccfbbde27f46fed1c98fee202f0559adec8ee0de67f71a", size = 269456, upload-time = "2026-01-26T02:44:02.202Z" },
- { url = "https://files.pythonhosted.org/packages/f3/8d/5e5be3ced1d12966fefb5c4ea3b2a5b480afcea36406559442c6e31d4a48/multidict-6.7.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bfde23ef6ed9db7eaee6c37dcec08524cb43903c60b285b172b6c094711b3961", size = 256322, upload-time = "2026-01-26T02:44:03.56Z" },
- { url = "https://files.pythonhosted.org/packages/31/6e/d8a26d81ac166a5592782d208dd90dfdc0a7a218adaa52b45a672b46c122/multidict-6.7.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3758692429e4e32f1ba0df23219cd0b4fc0a52f476726fff9337d1a57676a582", size = 253955, upload-time = "2026-01-26T02:44:04.845Z" },
- { url = "https://files.pythonhosted.org/packages/59/4c/7c672c8aad41534ba619bcd4ade7a0dc87ed6b8b5c06149b85d3dd03f0cd/multidict-6.7.1-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:398c1478926eca669f2fd6a5856b6de9c0acf23a2cb59a14c0ba5844fa38077e", size = 251254, upload-time = "2026-01-26T02:44:06.133Z" },
- { url = "https://files.pythonhosted.org/packages/7b/bd/84c24de512cbafbdbc39439f74e967f19570ce7924e3007174a29c348916/multidict-6.7.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:c102791b1c4f3ab36ce4101154549105a53dc828f016356b3e3bcae2e3a039d3", size = 252059, upload-time = "2026-01-26T02:44:07.518Z" },
- { url = "https://files.pythonhosted.org/packages/fa/ba/f5449385510825b73d01c2d4087bf6d2fccc20a2d42ac34df93191d3dd03/multidict-6.7.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:a088b62bd733e2ad12c50dad01b7d0166c30287c166e137433d3b410add807a6", size = 263588, upload-time = "2026-01-26T02:44:09.382Z" },
- { url = "https://files.pythonhosted.org/packages/d7/11/afc7c677f68f75c84a69fe37184f0f82fce13ce4b92f49f3db280b7e92b3/multidict-6.7.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:3d51ff4785d58d3f6c91bdbffcb5e1f7ddfda557727043aa20d20ec4f65e324a", size = 259642, upload-time = "2026-01-26T02:44:10.73Z" },
- { url = "https://files.pythonhosted.org/packages/2b/17/ebb9644da78c4ab36403739e0e6e0e30ebb135b9caf3440825001a0bddcb/multidict-6.7.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fc5907494fccf3e7d3f94f95c91d6336b092b5fc83811720fae5e2765890dfba", size = 251377, upload-time = "2026-01-26T02:44:12.042Z" },
- { url = "https://files.pythonhosted.org/packages/ca/a4/840f5b97339e27846c46307f2530a2805d9d537d8b8bd416af031cad7fa0/multidict-6.7.1-cp312-cp312-win32.whl", hash = "sha256:28ca5ce2fd9716631133d0e9a9b9a745ad7f60bac2bccafb56aa380fc0b6c511", size = 41887, upload-time = "2026-01-26T02:44:14.245Z" },
- { url = "https://files.pythonhosted.org/packages/80/31/0b2517913687895f5904325c2069d6a3b78f66cc641a86a2baf75a05dcbb/multidict-6.7.1-cp312-cp312-win_amd64.whl", hash = "sha256:fcee94dfbd638784645b066074b338bc9cc155d4b4bffa4adce1615c5a426c19", size = 46053, upload-time = "2026-01-26T02:44:15.371Z" },
- { url = "https://files.pythonhosted.org/packages/0c/5b/aba28e4ee4006ae4c7df8d327d31025d760ffa992ea23812a601d226e682/multidict-6.7.1-cp312-cp312-win_arm64.whl", hash = "sha256:ba0a9fb644d0c1a2194cf7ffb043bd852cea63a57f66fbd33959f7dae18517bf", size = 43307, upload-time = "2026-01-26T02:44:16.852Z" },
- { url = "https://files.pythonhosted.org/packages/f2/22/929c141d6c0dba87d3e1d38fbdf1ba8baba86b7776469f2bc2d3227a1e67/multidict-6.7.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:2b41f5fed0ed563624f1c17630cb9941cf2309d4df00e494b551b5f3e3d67a23", size = 76174, upload-time = "2026-01-26T02:44:18.509Z" },
- { url = "https://files.pythonhosted.org/packages/c7/75/bc704ae15fee974f8fccd871305e254754167dce5f9e42d88a2def741a1d/multidict-6.7.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:84e61e3af5463c19b67ced91f6c634effb89ef8bfc5ca0267f954451ed4bb6a2", size = 45116, upload-time = "2026-01-26T02:44:19.745Z" },
- { url = "https://files.pythonhosted.org/packages/79/76/55cd7186f498ed080a18440c9013011eb548f77ae1b297206d030eb1180a/multidict-6.7.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:935434b9853c7c112eee7ac891bc4cb86455aa631269ae35442cb316790c1445", size = 43524, upload-time = "2026-01-26T02:44:21.571Z" },
- { url = "https://files.pythonhosted.org/packages/e9/3c/414842ef8d5a1628d68edee29ba0e5bcf235dbfb3ccd3ea303a7fe8c72ff/multidict-6.7.1-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:432feb25a1cb67fe82a9680b4d65fb542e4635cb3166cd9c01560651ad60f177", size = 249368, upload-time = "2026-01-26T02:44:22.803Z" },
- { url = "https://files.pythonhosted.org/packages/f6/32/befed7f74c458b4a525e60519fe8d87eef72bb1e99924fa2b0f9d97a221e/multidict-6.7.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e82d14e3c948952a1a85503817e038cba5905a3352de76b9a465075d072fba23", size = 256952, upload-time = "2026-01-26T02:44:24.306Z" },
- { url = "https://files.pythonhosted.org/packages/03/d6/c878a44ba877f366630c860fdf74bfb203c33778f12b6ac274936853c451/multidict-6.7.1-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:4cfb48c6ea66c83bcaaf7e4dfa7ec1b6bbcf751b7db85a328902796dfde4c060", size = 240317, upload-time = "2026-01-26T02:44:25.772Z" },
- { url = "https://files.pythonhosted.org/packages/68/49/57421b4d7ad2e9e60e25922b08ceb37e077b90444bde6ead629095327a6f/multidict-6.7.1-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:1d540e51b7e8e170174555edecddbd5538105443754539193e3e1061864d444d", size = 267132, upload-time = "2026-01-26T02:44:27.648Z" },
- { url = "https://files.pythonhosted.org/packages/b7/fe/ec0edd52ddbcea2a2e89e174f0206444a61440b40f39704e64dc807a70bd/multidict-6.7.1-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:273d23f4b40f3dce4d6c8a821c741a86dec62cded82e1175ba3d99be128147ed", size = 268140, upload-time = "2026-01-26T02:44:29.588Z" },
- { url = "https://files.pythonhosted.org/packages/b0/73/6e1b01cbeb458807aa0831742232dbdd1fa92bfa33f52a3f176b4ff3dc11/multidict-6.7.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9d624335fd4fa1c08a53f8b4be7676ebde19cd092b3895c421045ca87895b429", size = 254277, upload-time = "2026-01-26T02:44:30.902Z" },
- { url = "https://files.pythonhosted.org/packages/6a/b2/5fb8c124d7561a4974c342bc8c778b471ebbeb3cc17df696f034a7e9afe7/multidict-6.7.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:12fad252f8b267cc75b66e8fc51b3079604e8d43a75428ffe193cd9e2195dfd6", size = 252291, upload-time = "2026-01-26T02:44:32.31Z" },
- { url = "https://files.pythonhosted.org/packages/5a/96/51d4e4e06bcce92577fcd488e22600bd38e4fd59c20cb49434d054903bd2/multidict-6.7.1-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:03ede2a6ffbe8ef936b92cb4529f27f42be7f56afcdab5ab739cd5f27fb1cbf9", size = 250156, upload-time = "2026-01-26T02:44:33.734Z" },
- { url = "https://files.pythonhosted.org/packages/db/6b/420e173eec5fba721a50e2a9f89eda89d9c98fded1124f8d5c675f7a0c0f/multidict-6.7.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:90efbcf47dbe33dcf643a1e400d67d59abeac5db07dc3f27d6bdeae497a2198c", size = 249742, upload-time = "2026-01-26T02:44:35.222Z" },
- { url = "https://files.pythonhosted.org/packages/44/a3/ec5b5bd98f306bc2aa297b8c6f11a46714a56b1e6ef5ebda50a4f5d7c5fb/multidict-6.7.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:5c4b9bfc148f5a91be9244d6264c53035c8a0dcd2f51f1c3c6e30e30ebaa1c84", size = 262221, upload-time = "2026-01-26T02:44:36.604Z" },
- { url = "https://files.pythonhosted.org/packages/cd/f7/e8c0d0da0cd1e28d10e624604e1a36bcc3353aaebdfdc3a43c72bc683a12/multidict-6.7.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:401c5a650f3add2472d1d288c26deebc540f99e2fb83e9525007a74cd2116f1d", size = 258664, upload-time = "2026-01-26T02:44:38.008Z" },
- { url = "https://files.pythonhosted.org/packages/52/da/151a44e8016dd33feed44f730bd856a66257c1ee7aed4f44b649fb7edeb3/multidict-6.7.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:97891f3b1b3ffbded884e2916cacf3c6fc87b66bb0dde46f7357404750559f33", size = 249490, upload-time = "2026-01-26T02:44:39.386Z" },
- { url = "https://files.pythonhosted.org/packages/87/af/a3b86bf9630b732897f6fc3f4c4714b90aa4361983ccbdcd6c0339b21b0c/multidict-6.7.1-cp313-cp313-win32.whl", hash = "sha256:e1c5988359516095535c4301af38d8a8838534158f649c05dd1050222321bcb3", size = 41695, upload-time = "2026-01-26T02:44:41.318Z" },
- { url = "https://files.pythonhosted.org/packages/b2/35/e994121b0e90e46134673422dd564623f93304614f5d11886b1b3e06f503/multidict-6.7.1-cp313-cp313-win_amd64.whl", hash = "sha256:960c83bf01a95b12b08fd54324a4eb1d5b52c88932b5cba5d6e712bb3ed12eb5", size = 45884, upload-time = "2026-01-26T02:44:42.488Z" },
- { url = "https://files.pythonhosted.org/packages/ca/61/42d3e5dbf661242a69c97ea363f2d7b46c567da8eadef8890022be6e2ab0/multidict-6.7.1-cp313-cp313-win_arm64.whl", hash = "sha256:563fe25c678aaba333d5399408f5ec3c383ca5b663e7f774dd179a520b8144df", size = 43122, upload-time = "2026-01-26T02:44:43.664Z" },
- { url = "https://files.pythonhosted.org/packages/6d/b3/e6b21c6c4f314bb956016b0b3ef2162590a529b84cb831c257519e7fde44/multidict-6.7.1-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:c76c4bec1538375dad9d452d246ca5368ad6e1c9039dadcf007ae59c70619ea1", size = 83175, upload-time = "2026-01-26T02:44:44.894Z" },
- { url = "https://files.pythonhosted.org/packages/fb/76/23ecd2abfe0957b234f6c960f4ade497f55f2c16aeb684d4ecdbf1c95791/multidict-6.7.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:57b46b24b5d5ebcc978da4ec23a819a9402b4228b8a90d9c656422b4bdd8a963", size = 48460, upload-time = "2026-01-26T02:44:46.106Z" },
- { url = "https://files.pythonhosted.org/packages/c4/57/a0ed92b23f3a042c36bc4227b72b97eca803f5f1801c1ab77c8a212d455e/multidict-6.7.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:e954b24433c768ce78ab7929e84ccf3422e46deb45a4dc9f93438f8217fa2d34", size = 46930, upload-time = "2026-01-26T02:44:47.278Z" },
- { url = "https://files.pythonhosted.org/packages/b5/66/02ec7ace29162e447f6382c495dc95826bf931d3818799bbef11e8f7df1a/multidict-6.7.1-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:3bd231490fa7217cc832528e1cd8752a96f0125ddd2b5749390f7c3ec8721b65", size = 242582, upload-time = "2026-01-26T02:44:48.604Z" },
- { url = "https://files.pythonhosted.org/packages/58/18/64f5a795e7677670e872673aca234162514696274597b3708b2c0d276cce/multidict-6.7.1-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:253282d70d67885a15c8a7716f3a73edf2d635793ceda8173b9ecc21f2fb8292", size = 250031, upload-time = "2026-01-26T02:44:50.544Z" },
- { url = "https://files.pythonhosted.org/packages/c8/ed/e192291dbbe51a8290c5686f482084d31bcd9d09af24f63358c3d42fd284/multidict-6.7.1-cp313-cp313t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:0b4c48648d7649c9335cf1927a8b87fa692de3dcb15faa676c6a6f1f1aabda43", size = 228596, upload-time = "2026-01-26T02:44:51.951Z" },
- { url = "https://files.pythonhosted.org/packages/1e/7e/3562a15a60cf747397e7f2180b0a11dc0c38d9175a650e75fa1b4d325e15/multidict-6.7.1-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:98bc624954ec4d2c7cb074b8eefc2b5d0ce7d482e410df446414355d158fe4ca", size = 257492, upload-time = "2026-01-26T02:44:53.902Z" },
- { url = "https://files.pythonhosted.org/packages/24/02/7d0f9eae92b5249bb50ac1595b295f10e263dd0078ebb55115c31e0eaccd/multidict-6.7.1-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:1b99af4d9eec0b49927b4402bcbb58dea89d3e0db8806a4086117019939ad3dd", size = 255899, upload-time = "2026-01-26T02:44:55.316Z" },
- { url = "https://files.pythonhosted.org/packages/00/e3/9b60ed9e23e64c73a5cde95269ef1330678e9c6e34dd4eb6b431b85b5a10/multidict-6.7.1-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6aac4f16b472d5b7dc6f66a0d49dd57b0e0902090be16594dc9ebfd3d17c47e7", size = 247970, upload-time = "2026-01-26T02:44:56.783Z" },
- { url = "https://files.pythonhosted.org/packages/3e/06/538e58a63ed5cfb0bd4517e346b91da32fde409d839720f664e9a4ae4f9d/multidict-6.7.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:21f830fe223215dffd51f538e78c172ed7c7f60c9b96a2bf05c4848ad49921c3", size = 245060, upload-time = "2026-01-26T02:44:58.195Z" },
- { url = "https://files.pythonhosted.org/packages/b2/2f/d743a3045a97c895d401e9bd29aaa09b94f5cbdf1bd561609e5a6c431c70/multidict-6.7.1-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:f5dd81c45b05518b9aa4da4aa74e1c93d715efa234fd3e8a179df611cc85e5f4", size = 235888, upload-time = "2026-01-26T02:44:59.57Z" },
- { url = "https://files.pythonhosted.org/packages/38/83/5a325cac191ab28b63c52f14f1131f3b0a55ba3b9aa65a6d0bf2a9b921a0/multidict-6.7.1-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:eb304767bca2bb92fb9c5bd33cedc95baee5bb5f6c88e63706533a1c06ad08c8", size = 243554, upload-time = "2026-01-26T02:45:01.054Z" },
- { url = "https://files.pythonhosted.org/packages/20/1f/9d2327086bd15da2725ef6aae624208e2ef828ed99892b17f60c344e57ed/multidict-6.7.1-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:c9035dde0f916702850ef66460bc4239d89d08df4d02023a5926e7446724212c", size = 252341, upload-time = "2026-01-26T02:45:02.484Z" },
- { url = "https://files.pythonhosted.org/packages/e8/2c/2a1aa0280cf579d0f6eed8ee5211c4f1730bd7e06c636ba2ee6aafda302e/multidict-6.7.1-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:af959b9beeb66c822380f222f0e0a1889331597e81f1ded7f374f3ecb0fd6c52", size = 246391, upload-time = "2026-01-26T02:45:03.862Z" },
- { url = "https://files.pythonhosted.org/packages/e5/03/7ca022ffc36c5a3f6e03b179a5ceb829be9da5783e6fe395f347c0794680/multidict-6.7.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:41f2952231456154ee479651491e94118229844dd7226541788be783be2b5108", size = 243422, upload-time = "2026-01-26T02:45:05.296Z" },
- { url = "https://files.pythonhosted.org/packages/dc/1d/b31650eab6c5778aceed46ba735bd97f7c7d2f54b319fa916c0f96e7805b/multidict-6.7.1-cp313-cp313t-win32.whl", hash = "sha256:df9f19c28adcb40b6aae30bbaa1478c389efd50c28d541d76760199fc1037c32", size = 47770, upload-time = "2026-01-26T02:45:06.754Z" },
- { url = "https://files.pythonhosted.org/packages/ac/5b/2d2d1d522e51285bd61b1e20df8f47ae1a9d80839db0b24ea783b3832832/multidict-6.7.1-cp313-cp313t-win_amd64.whl", hash = "sha256:d54ecf9f301853f2c5e802da559604b3e95bb7a3b01a9c295c6ee591b9882de8", size = 53109, upload-time = "2026-01-26T02:45:08.044Z" },
- { url = "https://files.pythonhosted.org/packages/3d/a3/cc409ba012c83ca024a308516703cf339bdc4b696195644a7215a5164a24/multidict-6.7.1-cp313-cp313t-win_arm64.whl", hash = "sha256:5a37ca18e360377cfda1d62f5f382ff41f2b8c4ccb329ed974cc2e1643440118", size = 45573, upload-time = "2026-01-26T02:45:09.349Z" },
- { url = "https://files.pythonhosted.org/packages/91/cc/db74228a8be41884a567e88a62fd589a913708fcf180d029898c17a9a371/multidict-6.7.1-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:8f333ec9c5eb1b7105e3b84b53141e66ca05a19a605368c55450b6ba208cb9ee", size = 75190, upload-time = "2026-01-26T02:45:10.651Z" },
- { url = "https://files.pythonhosted.org/packages/d5/22/492f2246bb5b534abd44804292e81eeaf835388901f0c574bac4eeec73c5/multidict-6.7.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:a407f13c188f804c759fc6a9f88286a565c242a76b27626594c133b82883b5c2", size = 44486, upload-time = "2026-01-26T02:45:11.938Z" },
- { url = "https://files.pythonhosted.org/packages/f1/4f/733c48f270565d78b4544f2baddc2fb2a245e5a8640254b12c36ac7ac68e/multidict-6.7.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:0e161ddf326db5577c3a4cc2d8648f81456e8a20d40415541587a71620d7a7d1", size = 43219, upload-time = "2026-01-26T02:45:14.346Z" },
- { url = "https://files.pythonhosted.org/packages/24/bb/2c0c2287963f4259c85e8bcbba9182ced8d7fca65c780c38e99e61629d11/multidict-6.7.1-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:1e3a8bb24342a8201d178c3b4984c26ba81a577c80d4d525727427460a50c22d", size = 245132, upload-time = "2026-01-26T02:45:15.712Z" },
- { url = "https://files.pythonhosted.org/packages/a7/f9/44d4b3064c65079d2467888794dea218d1601898ac50222ab8a9a8094460/multidict-6.7.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:97231140a50f5d447d3164f994b86a0bed7cd016e2682f8650d6a9158e14fd31", size = 252420, upload-time = "2026-01-26T02:45:17.293Z" },
- { url = "https://files.pythonhosted.org/packages/8b/13/78f7275e73fa17b24c9a51b0bd9d73ba64bb32d0ed51b02a746eb876abe7/multidict-6.7.1-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:6b10359683bd8806a200fd2909e7c8ca3a7b24ec1d8132e483d58e791d881048", size = 233510, upload-time = "2026-01-26T02:45:19.356Z" },
- { url = "https://files.pythonhosted.org/packages/4b/25/8167187f62ae3cbd52da7893f58cb036b47ea3fb67138787c76800158982/multidict-6.7.1-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:283ddac99f7ac25a4acadbf004cb5ae34480bbeb063520f70ce397b281859362", size = 264094, upload-time = "2026-01-26T02:45:20.834Z" },
- { url = "https://files.pythonhosted.org/packages/a1/e7/69a3a83b7b030cf283fb06ce074a05a02322359783424d7edf0f15fe5022/multidict-6.7.1-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:538cec1e18c067d0e6103aa9a74f9e832904c957adc260e61cd9d8cf0c3b3d37", size = 260786, upload-time = "2026-01-26T02:45:22.818Z" },
- { url = "https://files.pythonhosted.org/packages/fe/3b/8ec5074bcfc450fe84273713b4b0a0dd47c0249358f5d82eb8104ffe2520/multidict-6.7.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7eee46ccb30ff48a1e35bb818cc90846c6be2b68240e42a78599166722cea709", size = 248483, upload-time = "2026-01-26T02:45:24.368Z" },
- { url = "https://files.pythonhosted.org/packages/48/5a/d5a99e3acbca0e29c5d9cba8f92ceb15dce78bab963b308ae692981e3a5d/multidict-6.7.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:fa263a02f4f2dd2d11a7b1bb4362aa7cb1049f84a9235d31adf63f30143469a0", size = 248403, upload-time = "2026-01-26T02:45:25.982Z" },
- { url = "https://files.pythonhosted.org/packages/35/48/e58cd31f6c7d5102f2a4bf89f96b9cf7e00b6c6f3d04ecc44417c00a5a3c/multidict-6.7.1-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:2e1425e2f99ec5bd36c15a01b690a1a2456209c5deed58f95469ffb46039ccbb", size = 240315, upload-time = "2026-01-26T02:45:27.487Z" },
- { url = "https://files.pythonhosted.org/packages/94/33/1cd210229559cb90b6786c30676bb0c58249ff42f942765f88793b41fdce/multidict-6.7.1-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:497394b3239fc6f0e13a78a3e1b61296e72bf1c5f94b4c4eb80b265c37a131cd", size = 245528, upload-time = "2026-01-26T02:45:28.991Z" },
- { url = "https://files.pythonhosted.org/packages/64/f2/6e1107d226278c876c783056b7db43d800bb64c6131cec9c8dfb6903698e/multidict-6.7.1-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:233b398c29d3f1b9676b4b6f75c518a06fcb2ea0b925119fb2c1bc35c05e1601", size = 258784, upload-time = "2026-01-26T02:45:30.503Z" },
- { url = "https://files.pythonhosted.org/packages/4d/c1/11f664f14d525e4a1b5327a82d4de61a1db604ab34c6603bb3c2cc63ad34/multidict-6.7.1-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:93b1818e4a6e0930454f0f2af7dfce69307ca03cdcfb3739bf4d91241967b6c1", size = 251980, upload-time = "2026-01-26T02:45:32.603Z" },
- { url = "https://files.pythonhosted.org/packages/e1/9f/75a9ac888121d0c5bbd4ecf4eead45668b1766f6baabfb3b7f66a410e231/multidict-6.7.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:f33dc2a3abe9249ea5d8360f969ec7f4142e7ac45ee7014d8f8d5acddf178b7b", size = 243602, upload-time = "2026-01-26T02:45:34.043Z" },
- { url = "https://files.pythonhosted.org/packages/9a/e7/50bf7b004cc8525d80dbbbedfdc7aed3e4c323810890be4413e589074032/multidict-6.7.1-cp314-cp314-win32.whl", hash = "sha256:3ab8b9d8b75aef9df299595d5388b14530839f6422333357af1339443cff777d", size = 40930, upload-time = "2026-01-26T02:45:36.278Z" },
- { url = "https://files.pythonhosted.org/packages/e0/bf/52f25716bbe93745595800f36fb17b73711f14da59ed0bb2eba141bc9f0f/multidict-6.7.1-cp314-cp314-win_amd64.whl", hash = "sha256:5e01429a929600e7dab7b166062d9bb54a5eed752384c7384c968c2afab8f50f", size = 45074, upload-time = "2026-01-26T02:45:37.546Z" },
- { url = "https://files.pythonhosted.org/packages/97/ab/22803b03285fa3a525f48217963da3a65ae40f6a1b6f6cf2768879e208f9/multidict-6.7.1-cp314-cp314-win_arm64.whl", hash = "sha256:4885cb0e817aef5d00a2e8451d4665c1808378dc27c2705f1bf4ef8505c0d2e5", size = 42471, upload-time = "2026-01-26T02:45:38.889Z" },
- { url = "https://files.pythonhosted.org/packages/e0/6d/f9293baa6146ba9507e360ea0292b6422b016907c393e2f63fc40ab7b7b5/multidict-6.7.1-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:0458c978acd8e6ea53c81eefaddbbee9c6c5e591f41b3f5e8e194780fe026581", size = 82401, upload-time = "2026-01-26T02:45:40.254Z" },
- { url = "https://files.pythonhosted.org/packages/7a/68/53b5494738d83558d87c3c71a486504d8373421c3e0dbb6d0db48ad42ee0/multidict-6.7.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:c0abd12629b0af3cf590982c0b413b1e7395cd4ec026f30986818ab95bfaa94a", size = 48143, upload-time = "2026-01-26T02:45:41.635Z" },
- { url = "https://files.pythonhosted.org/packages/37/e8/5284c53310dcdc99ce5d66563f6e5773531a9b9fe9ec7a615e9bc306b05f/multidict-6.7.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:14525a5f61d7d0c94b368a42cff4c9a4e7ba2d52e2672a7b23d84dc86fb02b0c", size = 46507, upload-time = "2026-01-26T02:45:42.99Z" },
- { url = "https://files.pythonhosted.org/packages/e4/fc/6800d0e5b3875568b4083ecf5f310dcf91d86d52573160834fb4bfcf5e4f/multidict-6.7.1-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:17307b22c217b4cf05033dabefe68255a534d637c6c9b0cc8382718f87be4262", size = 239358, upload-time = "2026-01-26T02:45:44.376Z" },
- { url = "https://files.pythonhosted.org/packages/41/75/4ad0973179361cdf3a113905e6e088173198349131be2b390f9fa4da5fc6/multidict-6.7.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7a7e590ff876a3eaf1c02a4dfe0724b6e69a9e9de6d8f556816f29c496046e59", size = 246884, upload-time = "2026-01-26T02:45:47.167Z" },
- { url = "https://files.pythonhosted.org/packages/c3/9c/095bb28b5da139bd41fb9a5d5caff412584f377914bd8787c2aa98717130/multidict-6.7.1-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:5fa6a95dfee63893d80a34758cd0e0c118a30b8dcb46372bf75106c591b77889", size = 225878, upload-time = "2026-01-26T02:45:48.698Z" },
- { url = "https://files.pythonhosted.org/packages/07/d0/c0a72000243756e8f5a277b6b514fa005f2c73d481b7d9e47cd4568aa2e4/multidict-6.7.1-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a0543217a6a017692aa6ae5cc39adb75e587af0f3a82288b1492eb73dd6cc2a4", size = 253542, upload-time = "2026-01-26T02:45:50.164Z" },
- { url = "https://files.pythonhosted.org/packages/c0/6b/f69da15289e384ecf2a68837ec8b5ad8c33e973aa18b266f50fe55f24b8c/multidict-6.7.1-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f99fe611c312b3c1c0ace793f92464d8cd263cc3b26b5721950d977b006b6c4d", size = 252403, upload-time = "2026-01-26T02:45:51.779Z" },
- { url = "https://files.pythonhosted.org/packages/a2/76/b9669547afa5a1a25cd93eaca91c0da1c095b06b6d2d8ec25b713588d3a1/multidict-6.7.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9004d8386d133b7e6135679424c91b0b854d2d164af6ea3f289f8f2761064609", size = 244889, upload-time = "2026-01-26T02:45:53.27Z" },
- { url = "https://files.pythonhosted.org/packages/7e/a9/a50d2669e506dad33cfc45b5d574a205587b7b8a5f426f2fbb2e90882588/multidict-6.7.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e628ef0e6859ffd8273c69412a2465c4be4a9517d07261b33334b5ec6f3c7489", size = 241982, upload-time = "2026-01-26T02:45:54.919Z" },
- { url = "https://files.pythonhosted.org/packages/c5/bb/1609558ad8b456b4827d3c5a5b775c93b87878fd3117ed3db3423dfbce1b/multidict-6.7.1-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:841189848ba629c3552035a6a7f5bf3b02eb304e9fea7492ca220a8eda6b0e5c", size = 232415, upload-time = "2026-01-26T02:45:56.981Z" },
- { url = "https://files.pythonhosted.org/packages/d8/59/6f61039d2aa9261871e03ab9dc058a550d240f25859b05b67fd70f80d4b3/multidict-6.7.1-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:ce1bbd7d780bb5a0da032e095c951f7014d6b0a205f8318308140f1a6aba159e", size = 240337, upload-time = "2026-01-26T02:45:58.698Z" },
- { url = "https://files.pythonhosted.org/packages/a1/29/fdc6a43c203890dc2ae9249971ecd0c41deaedfe00d25cb6564b2edd99eb/multidict-6.7.1-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:b26684587228afed0d50cf804cc71062cc9c1cdf55051c4c6345d372947b268c", size = 248788, upload-time = "2026-01-26T02:46:00.862Z" },
- { url = "https://files.pythonhosted.org/packages/a9/14/a153a06101323e4cf086ecee3faadba52ff71633d471f9685c42e3736163/multidict-6.7.1-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:9f9af11306994335398293f9958071019e3ab95e9a707dc1383a35613f6abcb9", size = 242842, upload-time = "2026-01-26T02:46:02.824Z" },
- { url = "https://files.pythonhosted.org/packages/41/5f/604ae839e64a4a6efc80db94465348d3b328ee955e37acb24badbcd24d83/multidict-6.7.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:b4938326284c4f1224178a560987b6cf8b4d38458b113d9b8c1db1a836e640a2", size = 240237, upload-time = "2026-01-26T02:46:05.898Z" },
- { url = "https://files.pythonhosted.org/packages/5f/60/c3a5187bf66f6fb546ff4ab8fb5a077cbdd832d7b1908d4365c7f74a1917/multidict-6.7.1-cp314-cp314t-win32.whl", hash = "sha256:98655c737850c064a65e006a3df7c997cd3b220be4ec8fe26215760b9697d4d7", size = 48008, upload-time = "2026-01-26T02:46:07.468Z" },
- { url = "https://files.pythonhosted.org/packages/0c/f7/addf1087b860ac60e6f382240f64fb99f8bfb532bb06f7c542b83c29ca61/multidict-6.7.1-cp314-cp314t-win_amd64.whl", hash = "sha256:497bde6223c212ba11d462853cfa4f0ae6ef97465033e7dc9940cdb3ab5b48e5", size = 53542, upload-time = "2026-01-26T02:46:08.809Z" },
- { url = "https://files.pythonhosted.org/packages/4c/81/4629d0aa32302ef7b2ec65c75a728cc5ff4fa410c50096174c1632e70b3e/multidict-6.7.1-cp314-cp314t-win_arm64.whl", hash = "sha256:2bbd113e0d4af5db41d5ebfe9ccaff89de2120578164f86a5d17d5a576d1e5b2", size = 44719, upload-time = "2026-01-26T02:46:11.146Z" },
- { url = "https://files.pythonhosted.org/packages/9e/ee/74525ebe3eb5fddcd6735fc03cbea3feeed4122b53bc798ac32d297ac9ae/multidict-6.7.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:65573858d27cdeaca41893185677dc82395159aa28875a8867af66532d413a8f", size = 77107, upload-time = "2026-01-26T02:46:12.608Z" },
- { url = "https://files.pythonhosted.org/packages/f0/9a/ce8744e777a74b3050b1bf56be3eed1053b3457302ea055f1ea437200a23/multidict-6.7.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:c524c6fb8fc342793708ab111c4dbc90ff9abd568de220432500e47e990c0358", size = 44943, upload-time = "2026-01-26T02:46:14.016Z" },
- { url = "https://files.pythonhosted.org/packages/83/9c/1d2a283d9c6f31e260cb6c2fccadc3edcf6c4c14ee0929cd2af4d2606dd7/multidict-6.7.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:aa23b001d968faef416ff70dc0f1ab045517b9b42a90edd3e9bcdb06479e31d5", size = 44603, upload-time = "2026-01-26T02:46:15.391Z" },
- { url = "https://files.pythonhosted.org/packages/87/9d/3b186201671583d8e8d6d79c07481a5aafd0ba7575e3d8566baec80c1e82/multidict-6.7.1-cp39-cp39-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:6704fa2b7453b2fb121740555fa1ee20cd98c4d011120caf4d2b8d4e7c76eec0", size = 240573, upload-time = "2026-01-26T02:46:16.783Z" },
- { url = "https://files.pythonhosted.org/packages/42/7d/a52f5d4d0754311d1ac78478e34dff88de71259a8585e05ee14e5f877caf/multidict-6.7.1-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:121a34e5bfa410cdf2c8c49716de160de3b1dbcd86b49656f5681e4543bcd1a8", size = 240106, upload-time = "2026-01-26T02:46:18.432Z" },
- { url = "https://files.pythonhosted.org/packages/84/9f/d80118e6c30ff55b7d171bdc5520aad4b9626e657520b8d7c8ca8c2fad12/multidict-6.7.1-cp39-cp39-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:026d264228bcd637d4e060844e39cdc60f86c479e463d49075dedc21b18fbbe0", size = 219418, upload-time = "2026-01-26T02:46:20.526Z" },
- { url = "https://files.pythonhosted.org/packages/c7/bd/896e60b3457f194de77c7de64f9acce9f75da0518a5230ce1df534f6747b/multidict-6.7.1-cp39-cp39-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:0e697826df7eb63418ee190fd06ce9f1803593bb4b9517d08c60d9b9a7f69d8f", size = 252124, upload-time = "2026-01-26T02:46:22.157Z" },
- { url = "https://files.pythonhosted.org/packages/f4/de/ba6b30447c36a37078d0ba604aa12c1a52887af0c355236ca6e0a9d5286f/multidict-6.7.1-cp39-cp39-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:bb08271280173720e9fea9ede98e5231defcbad90f1624bea26f32ec8a956e2f", size = 249402, upload-time = "2026-01-26T02:46:23.718Z" },
- { url = "https://files.pythonhosted.org/packages/c2/b2/50a383c96230e432895a2fd3bcfe1b65785899598259d871d5de6b93180c/multidict-6.7.1-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c6b3228e1d80af737b72925ce5fb4daf5a335e49cd7ab77ed7b9fdfbf58c526e", size = 240346, upload-time = "2026-01-26T02:46:25.393Z" },
- { url = "https://files.pythonhosted.org/packages/89/37/16d391fd8da544b1489306e38a46785fa41dd0f0ef766837ed7d4676dde0/multidict-6.7.1-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:3943debf0fbb57bdde5901695c11094a9a36723e5c03875f87718ee15ca2f4d2", size = 237010, upload-time = "2026-01-26T02:46:27.408Z" },
- { url = "https://files.pythonhosted.org/packages/b0/24/3152ee026eda86d5d3e3685182911e6951af7a016579da931080ce6ac9ad/multidict-6.7.1-cp39-cp39-musllinux_1_2_armv7l.whl", hash = "sha256:98c5787b0a0d9a41d9311eae44c3b76e6753def8d8870ab501320efe75a6a5f8", size = 232018, upload-time = "2026-01-26T02:46:29.941Z" },
- { url = "https://files.pythonhosted.org/packages/9c/1f/48d3c27a72be7fd23a55d8847193c459959bf35a5bb5844530dab00b739b/multidict-6.7.1-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:08ccb2a6dc72009093ebe7f3f073e5ec5964cba9a706fa94b1a1484039b87941", size = 241498, upload-time = "2026-01-26T02:46:32.052Z" },
- { url = "https://files.pythonhosted.org/packages/1a/45/413643ae2952d0decdf6c1250f86d08a43e143271441e81027e38d598bd7/multidict-6.7.1-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:eb351f72c26dc9abe338ca7294661aa22969ad8ffe7ef7d5541d19f368dc854a", size = 247957, upload-time = "2026-01-26T02:46:33.666Z" },
- { url = "https://files.pythonhosted.org/packages/50/f8/f1d0ac23df15e0470776388bdb261506f63af1f81d28bacb5e262d6e12b6/multidict-6.7.1-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:ac1c665bad8b5d762f5f85ebe4d94130c26965f11de70c708c75671297c776de", size = 241651, upload-time = "2026-01-26T02:46:35.7Z" },
- { url = "https://files.pythonhosted.org/packages/2c/c9/1a2a18f383cf129add66b6c36b75c3911a7ba95cf26cb141482de085cc12/multidict-6.7.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:1fa6609d0364f4f6f58351b4659a1f3e0e898ba2a8c5cac04cb2c7bc556b0bc5", size = 236371, upload-time = "2026-01-26T02:46:37.37Z" },
- { url = "https://files.pythonhosted.org/packages/bb/aa/77d87e3fca31325b87e0eb72d5fe9a7472dcb51391a42df7ac1f3842f6c0/multidict-6.7.1-cp39-cp39-win32.whl", hash = "sha256:6f77ce314a29263e67adadc7e7c1bc699fcb3a305059ab973d038f87caa42ed0", size = 41426, upload-time = "2026-01-26T02:46:39.026Z" },
- { url = "https://files.pythonhosted.org/packages/e3/b3/e8863e6a2da15a9d7e98976ff402e871b7352c76566df6c18d0378e0d9cf/multidict-6.7.1-cp39-cp39-win_amd64.whl", hash = "sha256:f537b55778cd3cbee430abe3131255d3a78202e0f9ea7ffc6ada893a4bcaeea4", size = 46180, upload-time = "2026-01-26T02:46:40.422Z" },
- { url = "https://files.pythonhosted.org/packages/93/d3/dd4fa951ad5b5fa216bf30054d705683d13405eea7459833d78f31b74c9c/multidict-6.7.1-cp39-cp39-win_arm64.whl", hash = "sha256:749aa54f578f2e5f439538706a475aa844bfa8ef75854b1401e6e528e4937cf9", size = 43231, upload-time = "2026-01-26T02:46:41.945Z" },
- { url = "https://files.pythonhosted.org/packages/81/08/7036c080d7117f28a4af526d794aab6a84463126db031b007717c1a6676e/multidict-6.7.1-py3-none-any.whl", hash = "sha256:55d97cc6dae627efa6a6e548885712d4864b81110ac76fa4e534c03819fa4a56", size = 12319, upload-time = "2026-01-26T02:46:44.004Z" },
-]
-
-[[package]]
-name = "propcache"
-version = "0.4.1"
-source = { registry = "https://pypi.org/simple" }
-resolution-markers = [
- "python_full_version < '3.10'",
-]
-sdist = { url = "https://files.pythonhosted.org/packages/9e/da/e9fc233cf63743258bff22b3dfa7ea5baef7b5bc324af47a0ad89b8ffc6f/propcache-0.4.1.tar.gz", hash = "sha256:f48107a8c637e80362555f37ecf49abe20370e557cc4ab374f04ec4423c97c3d", size = 46442, upload-time = "2025-10-08T19:49:02.291Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/3c/0e/934b541323035566a9af292dba85a195f7b78179114f2c6ebb24551118a9/propcache-0.4.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:7c2d1fa3201efaf55d730400d945b5b3ab6e672e100ba0f9a409d950ab25d7db", size = 79534, upload-time = "2025-10-08T19:46:02.083Z" },
- { url = "https://files.pythonhosted.org/packages/a1/6b/db0d03d96726d995dc7171286c6ba9d8d14251f37433890f88368951a44e/propcache-0.4.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:1eb2994229cc8ce7fe9b3db88f5465f5fd8651672840b2e426b88cdb1a30aac8", size = 45526, upload-time = "2025-10-08T19:46:03.884Z" },
- { url = "https://files.pythonhosted.org/packages/e4/c3/82728404aea669e1600f304f2609cde9e665c18df5a11cdd57ed73c1dceb/propcache-0.4.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:66c1f011f45a3b33d7bcb22daed4b29c0c9e2224758b6be00686731e1b46f925", size = 47263, upload-time = "2025-10-08T19:46:05.405Z" },
- { url = "https://files.pythonhosted.org/packages/df/1b/39313ddad2bf9187a1432654c38249bab4562ef535ef07f5eb6eb04d0b1b/propcache-0.4.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9a52009f2adffe195d0b605c25ec929d26b36ef986ba85244891dee3b294df21", size = 201012, upload-time = "2025-10-08T19:46:07.165Z" },
- { url = "https://files.pythonhosted.org/packages/5b/01/f1d0b57d136f294a142acf97f4ed58c8e5b974c21e543000968357115011/propcache-0.4.1-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:5d4e2366a9c7b837555cf02fb9be2e3167d333aff716332ef1b7c3a142ec40c5", size = 209491, upload-time = "2025-10-08T19:46:08.909Z" },
- { url = "https://files.pythonhosted.org/packages/a1/c8/038d909c61c5bb039070b3fb02ad5cccdb1dde0d714792e251cdb17c9c05/propcache-0.4.1-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:9d2b6caef873b4f09e26ea7e33d65f42b944837563a47a94719cc3544319a0db", size = 215319, upload-time = "2025-10-08T19:46:10.7Z" },
- { url = "https://files.pythonhosted.org/packages/08/57/8c87e93142b2c1fa2408e45695205a7ba05fb5db458c0bf5c06ba0e09ea6/propcache-0.4.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2b16ec437a8c8a965ecf95739448dd938b5c7f56e67ea009f4300d8df05f32b7", size = 196856, upload-time = "2025-10-08T19:46:12.003Z" },
- { url = "https://files.pythonhosted.org/packages/42/df/5615fec76aa561987a534759b3686008a288e73107faa49a8ae5795a9f7a/propcache-0.4.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:296f4c8ed03ca7476813fe666c9ea97869a8d7aec972618671b33a38a5182ef4", size = 193241, upload-time = "2025-10-08T19:46:13.495Z" },
- { url = "https://files.pythonhosted.org/packages/d5/21/62949eb3a7a54afe8327011c90aca7e03547787a88fb8bd9726806482fea/propcache-0.4.1-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:1f0978529a418ebd1f49dad413a2b68af33f85d5c5ca5c6ca2a3bed375a7ac60", size = 190552, upload-time = "2025-10-08T19:46:14.938Z" },
- { url = "https://files.pythonhosted.org/packages/30/ee/ab4d727dd70806e5b4de96a798ae7ac6e4d42516f030ee60522474b6b332/propcache-0.4.1-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:fd138803047fb4c062b1c1dd95462f5209456bfab55c734458f15d11da288f8f", size = 200113, upload-time = "2025-10-08T19:46:16.695Z" },
- { url = "https://files.pythonhosted.org/packages/8a/0b/38b46208e6711b016aa8966a3ac793eee0d05c7159d8342aa27fc0bc365e/propcache-0.4.1-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:8c9b3cbe4584636d72ff556d9036e0c9317fa27b3ac1f0f558e7e84d1c9c5900", size = 200778, upload-time = "2025-10-08T19:46:18.023Z" },
- { url = "https://files.pythonhosted.org/packages/cf/81/5abec54355ed344476bee711e9f04815d4b00a311ab0535599204eecc257/propcache-0.4.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:f93243fdc5657247533273ac4f86ae106cc6445a0efacb9a1bfe982fcfefd90c", size = 193047, upload-time = "2025-10-08T19:46:19.449Z" },
- { url = "https://files.pythonhosted.org/packages/ec/b6/1f237c04e32063cb034acd5f6ef34ef3a394f75502e72703545631ab1ef6/propcache-0.4.1-cp310-cp310-win32.whl", hash = "sha256:a0ee98db9c5f80785b266eb805016e36058ac72c51a064040f2bc43b61101cdb", size = 38093, upload-time = "2025-10-08T19:46:20.643Z" },
- { url = "https://files.pythonhosted.org/packages/a6/67/354aac4e0603a15f76439caf0427781bcd6797f370377f75a642133bc954/propcache-0.4.1-cp310-cp310-win_amd64.whl", hash = "sha256:1cdb7988c4e5ac7f6d175a28a9aa0c94cb6f2ebe52756a3c0cda98d2809a9e37", size = 41638, upload-time = "2025-10-08T19:46:21.935Z" },
- { url = "https://files.pythonhosted.org/packages/e0/e1/74e55b9fd1a4c209ff1a9a824bf6c8b3d1fc5a1ac3eabe23462637466785/propcache-0.4.1-cp310-cp310-win_arm64.whl", hash = "sha256:d82ad62b19645419fe79dd63b3f9253e15b30e955c0170e5cebc350c1844e581", size = 38229, upload-time = "2025-10-08T19:46:23.368Z" },
- { url = "https://files.pythonhosted.org/packages/8c/d4/4e2c9aaf7ac2242b9358f98dccd8f90f2605402f5afeff6c578682c2c491/propcache-0.4.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:60a8fda9644b7dfd5dece8c61d8a85e271cb958075bfc4e01083c148b61a7caf", size = 80208, upload-time = "2025-10-08T19:46:24.597Z" },
- { url = "https://files.pythonhosted.org/packages/c2/21/d7b68e911f9c8e18e4ae43bdbc1e1e9bbd971f8866eb81608947b6f585ff/propcache-0.4.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c30b53e7e6bda1d547cabb47c825f3843a0a1a42b0496087bb58d8fedf9f41b5", size = 45777, upload-time = "2025-10-08T19:46:25.733Z" },
- { url = "https://files.pythonhosted.org/packages/d3/1d/11605e99ac8ea9435651ee71ab4cb4bf03f0949586246476a25aadfec54a/propcache-0.4.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:6918ecbd897443087a3b7cd978d56546a812517dcaaca51b49526720571fa93e", size = 47647, upload-time = "2025-10-08T19:46:27.304Z" },
- { url = "https://files.pythonhosted.org/packages/58/1a/3c62c127a8466c9c843bccb503d40a273e5cc69838805f322e2826509e0d/propcache-0.4.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3d902a36df4e5989763425a8ab9e98cd8ad5c52c823b34ee7ef307fd50582566", size = 214929, upload-time = "2025-10-08T19:46:28.62Z" },
- { url = "https://files.pythonhosted.org/packages/56/b9/8fa98f850960b367c4b8fe0592e7fc341daa7a9462e925228f10a60cf74f/propcache-0.4.1-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a9695397f85973bb40427dedddf70d8dc4a44b22f1650dd4af9eedf443d45165", size = 221778, upload-time = "2025-10-08T19:46:30.358Z" },
- { url = "https://files.pythonhosted.org/packages/46/a6/0ab4f660eb59649d14b3d3d65c439421cf2f87fe5dd68591cbe3c1e78a89/propcache-0.4.1-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:2bb07ffd7eaad486576430c89f9b215f9e4be68c4866a96e97db9e97fead85dc", size = 228144, upload-time = "2025-10-08T19:46:32.607Z" },
- { url = "https://files.pythonhosted.org/packages/52/6a/57f43e054fb3d3a56ac9fc532bc684fc6169a26c75c353e65425b3e56eef/propcache-0.4.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fd6f30fdcf9ae2a70abd34da54f18da086160e4d7d9251f81f3da0ff84fc5a48", size = 210030, upload-time = "2025-10-08T19:46:33.969Z" },
- { url = "https://files.pythonhosted.org/packages/40/e2/27e6feebb5f6b8408fa29f5efbb765cd54c153ac77314d27e457a3e993b7/propcache-0.4.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:fc38cba02d1acba4e2869eef1a57a43dfbd3d49a59bf90dda7444ec2be6a5570", size = 208252, upload-time = "2025-10-08T19:46:35.309Z" },
- { url = "https://files.pythonhosted.org/packages/9e/f8/91c27b22ccda1dbc7967f921c42825564fa5336a01ecd72eb78a9f4f53c2/propcache-0.4.1-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:67fad6162281e80e882fb3ec355398cf72864a54069d060321f6cd0ade95fe85", size = 202064, upload-time = "2025-10-08T19:46:36.993Z" },
- { url = "https://files.pythonhosted.org/packages/f2/26/7f00bd6bd1adba5aafe5f4a66390f243acab58eab24ff1a08bebb2ef9d40/propcache-0.4.1-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:f10207adf04d08bec185bae14d9606a1444715bc99180f9331c9c02093e1959e", size = 212429, upload-time = "2025-10-08T19:46:38.398Z" },
- { url = "https://files.pythonhosted.org/packages/84/89/fd108ba7815c1117ddca79c228f3f8a15fc82a73bca8b142eb5de13b2785/propcache-0.4.1-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:e9b0d8d0845bbc4cfcdcbcdbf5086886bc8157aa963c31c777ceff7846c77757", size = 216727, upload-time = "2025-10-08T19:46:39.732Z" },
- { url = "https://files.pythonhosted.org/packages/79/37/3ec3f7e3173e73f1d600495d8b545b53802cbf35506e5732dd8578db3724/propcache-0.4.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:981333cb2f4c1896a12f4ab92a9cc8f09ea664e9b7dbdc4eff74627af3a11c0f", size = 205097, upload-time = "2025-10-08T19:46:41.025Z" },
- { url = "https://files.pythonhosted.org/packages/61/b0/b2631c19793f869d35f47d5a3a56fb19e9160d3c119f15ac7344fc3ccae7/propcache-0.4.1-cp311-cp311-win32.whl", hash = "sha256:f1d2f90aeec838a52f1c1a32fe9a619fefd5e411721a9117fbf82aea638fe8a1", size = 38084, upload-time = "2025-10-08T19:46:42.693Z" },
- { url = "https://files.pythonhosted.org/packages/f4/78/6cce448e2098e9f3bfc91bb877f06aa24b6ccace872e39c53b2f707c4648/propcache-0.4.1-cp311-cp311-win_amd64.whl", hash = "sha256:364426a62660f3f699949ac8c621aad6977be7126c5807ce48c0aeb8e7333ea6", size = 41637, upload-time = "2025-10-08T19:46:43.778Z" },
- { url = "https://files.pythonhosted.org/packages/9c/e9/754f180cccd7f51a39913782c74717c581b9cc8177ad0e949f4d51812383/propcache-0.4.1-cp311-cp311-win_arm64.whl", hash = "sha256:e53f3a38d3510c11953f3e6a33f205c6d1b001129f972805ca9b42fc308bc239", size = 38064, upload-time = "2025-10-08T19:46:44.872Z" },
- { url = "https://files.pythonhosted.org/packages/a2/0f/f17b1b2b221d5ca28b4b876e8bb046ac40466513960646bda8e1853cdfa2/propcache-0.4.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:e153e9cd40cc8945138822807139367f256f89c6810c2634a4f6902b52d3b4e2", size = 80061, upload-time = "2025-10-08T19:46:46.075Z" },
- { url = "https://files.pythonhosted.org/packages/76/47/8ccf75935f51448ba9a16a71b783eb7ef6b9ee60f5d14c7f8a8a79fbeed7/propcache-0.4.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:cd547953428f7abb73c5ad82cbb32109566204260d98e41e5dfdc682eb7f8403", size = 46037, upload-time = "2025-10-08T19:46:47.23Z" },
- { url = "https://files.pythonhosted.org/packages/0a/b6/5c9a0e42df4d00bfb4a3cbbe5cf9f54260300c88a0e9af1f47ca5ce17ac0/propcache-0.4.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f048da1b4f243fc44f205dfd320933a951b8d89e0afd4c7cacc762a8b9165207", size = 47324, upload-time = "2025-10-08T19:46:48.384Z" },
- { url = "https://files.pythonhosted.org/packages/9e/d3/6c7ee328b39a81ee877c962469f1e795f9db87f925251efeb0545e0020d0/propcache-0.4.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ec17c65562a827bba85e3872ead335f95405ea1674860d96483a02f5c698fa72", size = 225505, upload-time = "2025-10-08T19:46:50.055Z" },
- { url = "https://files.pythonhosted.org/packages/01/5d/1c53f4563490b1d06a684742cc6076ef944bc6457df6051b7d1a877c057b/propcache-0.4.1-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:405aac25c6394ef275dee4c709be43745d36674b223ba4eb7144bf4d691b7367", size = 230242, upload-time = "2025-10-08T19:46:51.815Z" },
- { url = "https://files.pythonhosted.org/packages/20/e1/ce4620633b0e2422207c3cb774a0ee61cac13abc6217763a7b9e2e3f4a12/propcache-0.4.1-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:0013cb6f8dde4b2a2f66903b8ba740bdfe378c943c4377a200551ceb27f379e4", size = 238474, upload-time = "2025-10-08T19:46:53.208Z" },
- { url = "https://files.pythonhosted.org/packages/46/4b/3aae6835b8e5f44ea6a68348ad90f78134047b503765087be2f9912140ea/propcache-0.4.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:15932ab57837c3368b024473a525e25d316d8353016e7cc0e5ba9eb343fbb1cf", size = 221575, upload-time = "2025-10-08T19:46:54.511Z" },
- { url = "https://files.pythonhosted.org/packages/6e/a5/8a5e8678bcc9d3a1a15b9a29165640d64762d424a16af543f00629c87338/propcache-0.4.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:031dce78b9dc099f4c29785d9cf5577a3faf9ebf74ecbd3c856a7b92768c3df3", size = 216736, upload-time = "2025-10-08T19:46:56.212Z" },
- { url = "https://files.pythonhosted.org/packages/f1/63/b7b215eddeac83ca1c6b934f89d09a625aa9ee4ba158338854c87210cc36/propcache-0.4.1-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:ab08df6c9a035bee56e31af99be621526bd237bea9f32def431c656b29e41778", size = 213019, upload-time = "2025-10-08T19:46:57.595Z" },
- { url = "https://files.pythonhosted.org/packages/57/74/f580099a58c8af587cac7ba19ee7cb418506342fbbe2d4a4401661cca886/propcache-0.4.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:4d7af63f9f93fe593afbf104c21b3b15868efb2c21d07d8732c0c4287e66b6a6", size = 220376, upload-time = "2025-10-08T19:46:59.067Z" },
- { url = "https://files.pythonhosted.org/packages/c4/ee/542f1313aff7eaf19c2bb758c5d0560d2683dac001a1c96d0774af799843/propcache-0.4.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:cfc27c945f422e8b5071b6e93169679e4eb5bf73bbcbf1ba3ae3a83d2f78ebd9", size = 226988, upload-time = "2025-10-08T19:47:00.544Z" },
- { url = "https://files.pythonhosted.org/packages/8f/18/9c6b015dd9c6930f6ce2229e1f02fb35298b847f2087ea2b436a5bfa7287/propcache-0.4.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:35c3277624a080cc6ec6f847cbbbb5b49affa3598c4535a0a4682a697aaa5c75", size = 215615, upload-time = "2025-10-08T19:47:01.968Z" },
- { url = "https://files.pythonhosted.org/packages/80/9e/e7b85720b98c45a45e1fca6a177024934dc9bc5f4d5dd04207f216fc33ed/propcache-0.4.1-cp312-cp312-win32.whl", hash = "sha256:671538c2262dadb5ba6395e26c1731e1d52534bfe9ae56d0b5573ce539266aa8", size = 38066, upload-time = "2025-10-08T19:47:03.503Z" },
- { url = "https://files.pythonhosted.org/packages/54/09/d19cff2a5aaac632ec8fc03737b223597b1e347416934c1b3a7df079784c/propcache-0.4.1-cp312-cp312-win_amd64.whl", hash = "sha256:cb2d222e72399fcf5890d1d5cc1060857b9b236adff2792ff48ca2dfd46c81db", size = 41655, upload-time = "2025-10-08T19:47:04.973Z" },
- { url = "https://files.pythonhosted.org/packages/68/ab/6b5c191bb5de08036a8c697b265d4ca76148efb10fa162f14af14fb5f076/propcache-0.4.1-cp312-cp312-win_arm64.whl", hash = "sha256:204483131fb222bdaaeeea9f9e6c6ed0cac32731f75dfc1d4a567fc1926477c1", size = 37789, upload-time = "2025-10-08T19:47:06.077Z" },
- { url = "https://files.pythonhosted.org/packages/bf/df/6d9c1b6ac12b003837dde8a10231a7344512186e87b36e855bef32241942/propcache-0.4.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:43eedf29202c08550aac1d14e0ee619b0430aaef78f85864c1a892294fbc28cf", size = 77750, upload-time = "2025-10-08T19:47:07.648Z" },
- { url = "https://files.pythonhosted.org/packages/8b/e8/677a0025e8a2acf07d3418a2e7ba529c9c33caf09d3c1f25513023c1db56/propcache-0.4.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:d62cdfcfd89ccb8de04e0eda998535c406bf5e060ffd56be6c586cbcc05b3311", size = 44780, upload-time = "2025-10-08T19:47:08.851Z" },
- { url = "https://files.pythonhosted.org/packages/89/a4/92380f7ca60f99ebae761936bc48a72a639e8a47b29050615eef757cb2a7/propcache-0.4.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:cae65ad55793da34db5f54e4029b89d3b9b9490d8abe1b4c7ab5d4b8ec7ebf74", size = 46308, upload-time = "2025-10-08T19:47:09.982Z" },
- { url = "https://files.pythonhosted.org/packages/2d/48/c5ac64dee5262044348d1d78a5f85dd1a57464a60d30daee946699963eb3/propcache-0.4.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:333ddb9031d2704a301ee3e506dc46b1fe5f294ec198ed6435ad5b6a085facfe", size = 208182, upload-time = "2025-10-08T19:47:11.319Z" },
- { url = "https://files.pythonhosted.org/packages/c6/0c/cd762dd011a9287389a6a3eb43aa30207bde253610cca06824aeabfe9653/propcache-0.4.1-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:fd0858c20f078a32cf55f7e81473d96dcf3b93fd2ccdb3d40fdf54b8573df3af", size = 211215, upload-time = "2025-10-08T19:47:13.146Z" },
- { url = "https://files.pythonhosted.org/packages/30/3e/49861e90233ba36890ae0ca4c660e95df565b2cd15d4a68556ab5865974e/propcache-0.4.1-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:678ae89ebc632c5c204c794f8dab2837c5f159aeb59e6ed0539500400577298c", size = 218112, upload-time = "2025-10-08T19:47:14.913Z" },
- { url = "https://files.pythonhosted.org/packages/f1/8b/544bc867e24e1bd48f3118cecd3b05c694e160a168478fa28770f22fd094/propcache-0.4.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d472aeb4fbf9865e0c6d622d7f4d54a4e101a89715d8904282bb5f9a2f476c3f", size = 204442, upload-time = "2025-10-08T19:47:16.277Z" },
- { url = "https://files.pythonhosted.org/packages/50/a6/4282772fd016a76d3e5c0df58380a5ea64900afd836cec2c2f662d1b9bb3/propcache-0.4.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:4d3df5fa7e36b3225954fba85589da77a0fe6a53e3976de39caf04a0db4c36f1", size = 199398, upload-time = "2025-10-08T19:47:17.962Z" },
- { url = "https://files.pythonhosted.org/packages/3e/ec/d8a7cd406ee1ddb705db2139f8a10a8a427100347bd698e7014351c7af09/propcache-0.4.1-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:ee17f18d2498f2673e432faaa71698032b0127ebf23ae5974eeaf806c279df24", size = 196920, upload-time = "2025-10-08T19:47:19.355Z" },
- { url = "https://files.pythonhosted.org/packages/f6/6c/f38ab64af3764f431e359f8baf9e0a21013e24329e8b85d2da32e8ed07ca/propcache-0.4.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:580e97762b950f993ae618e167e7be9256b8353c2dcd8b99ec100eb50f5286aa", size = 203748, upload-time = "2025-10-08T19:47:21.338Z" },
- { url = "https://files.pythonhosted.org/packages/d6/e3/fa846bd70f6534d647886621388f0a265254d30e3ce47e5c8e6e27dbf153/propcache-0.4.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:501d20b891688eb8e7aa903021f0b72d5a55db40ffaab27edefd1027caaafa61", size = 205877, upload-time = "2025-10-08T19:47:23.059Z" },
- { url = "https://files.pythonhosted.org/packages/e2/39/8163fc6f3133fea7b5f2827e8eba2029a0277ab2c5beee6c1db7b10fc23d/propcache-0.4.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9a0bd56e5b100aef69bd8562b74b46254e7c8812918d3baa700c8a8009b0af66", size = 199437, upload-time = "2025-10-08T19:47:24.445Z" },
- { url = "https://files.pythonhosted.org/packages/93/89/caa9089970ca49c7c01662bd0eeedfe85494e863e8043565aeb6472ce8fe/propcache-0.4.1-cp313-cp313-win32.whl", hash = "sha256:bcc9aaa5d80322bc2fb24bb7accb4a30f81e90ab8d6ba187aec0744bc302ad81", size = 37586, upload-time = "2025-10-08T19:47:25.736Z" },
- { url = "https://files.pythonhosted.org/packages/f5/ab/f76ec3c3627c883215b5c8080debb4394ef5a7a29be811f786415fc1e6fd/propcache-0.4.1-cp313-cp313-win_amd64.whl", hash = "sha256:381914df18634f5494334d201e98245c0596067504b9372d8cf93f4bb23e025e", size = 40790, upload-time = "2025-10-08T19:47:26.847Z" },
- { url = "https://files.pythonhosted.org/packages/59/1b/e71ae98235f8e2ba5004d8cb19765a74877abf189bc53fc0c80d799e56c3/propcache-0.4.1-cp313-cp313-win_arm64.whl", hash = "sha256:8873eb4460fd55333ea49b7d189749ecf6e55bf85080f11b1c4530ed3034cba1", size = 37158, upload-time = "2025-10-08T19:47:27.961Z" },
- { url = "https://files.pythonhosted.org/packages/83/ce/a31bbdfc24ee0dcbba458c8175ed26089cf109a55bbe7b7640ed2470cfe9/propcache-0.4.1-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:92d1935ee1f8d7442da9c0c4fa7ac20d07e94064184811b685f5c4fada64553b", size = 81451, upload-time = "2025-10-08T19:47:29.445Z" },
- { url = "https://files.pythonhosted.org/packages/25/9c/442a45a470a68456e710d96cacd3573ef26a1d0a60067e6a7d5e655621ed/propcache-0.4.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:473c61b39e1460d386479b9b2f337da492042447c9b685f28be4f74d3529e566", size = 46374, upload-time = "2025-10-08T19:47:30.579Z" },
- { url = "https://files.pythonhosted.org/packages/f4/bf/b1d5e21dbc3b2e889ea4327044fb16312a736d97640fb8b6aa3f9c7b3b65/propcache-0.4.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:c0ef0aaafc66fbd87842a3fe3902fd889825646bc21149eafe47be6072725835", size = 48396, upload-time = "2025-10-08T19:47:31.79Z" },
- { url = "https://files.pythonhosted.org/packages/f4/04/5b4c54a103d480e978d3c8a76073502b18db0c4bc17ab91b3cb5092ad949/propcache-0.4.1-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f95393b4d66bfae908c3ca8d169d5f79cd65636ae15b5e7a4f6e67af675adb0e", size = 275950, upload-time = "2025-10-08T19:47:33.481Z" },
- { url = "https://files.pythonhosted.org/packages/b4/c1/86f846827fb969c4b78b0af79bba1d1ea2156492e1b83dea8b8a6ae27395/propcache-0.4.1-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c07fda85708bc48578467e85099645167a955ba093be0a2dcba962195676e859", size = 273856, upload-time = "2025-10-08T19:47:34.906Z" },
- { url = "https://files.pythonhosted.org/packages/36/1d/fc272a63c8d3bbad6878c336c7a7dea15e8f2d23a544bda43205dfa83ada/propcache-0.4.1-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:af223b406d6d000830c6f65f1e6431783fc3f713ba3e6cc8c024d5ee96170a4b", size = 280420, upload-time = "2025-10-08T19:47:36.338Z" },
- { url = "https://files.pythonhosted.org/packages/07/0c/01f2219d39f7e53d52e5173bcb09c976609ba30209912a0680adfb8c593a/propcache-0.4.1-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a78372c932c90ee474559c5ddfffd718238e8673c340dc21fe45c5b8b54559a0", size = 263254, upload-time = "2025-10-08T19:47:37.692Z" },
- { url = "https://files.pythonhosted.org/packages/2d/18/cd28081658ce597898f0c4d174d4d0f3c5b6d4dc27ffafeef835c95eb359/propcache-0.4.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:564d9f0d4d9509e1a870c920a89b2fec951b44bf5ba7d537a9e7c1ccec2c18af", size = 261205, upload-time = "2025-10-08T19:47:39.659Z" },
- { url = "https://files.pythonhosted.org/packages/7a/71/1f9e22eb8b8316701c2a19fa1f388c8a3185082607da8e406a803c9b954e/propcache-0.4.1-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:17612831fda0138059cc5546f4d12a2aacfb9e47068c06af35c400ba58ba7393", size = 247873, upload-time = "2025-10-08T19:47:41.084Z" },
- { url = "https://files.pythonhosted.org/packages/4a/65/3d4b61f36af2b4eddba9def857959f1016a51066b4f1ce348e0cf7881f58/propcache-0.4.1-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:41a89040cb10bd345b3c1a873b2bf36413d48da1def52f268a055f7398514874", size = 262739, upload-time = "2025-10-08T19:47:42.51Z" },
- { url = "https://files.pythonhosted.org/packages/2a/42/26746ab087faa77c1c68079b228810436ccd9a5ce9ac85e2b7307195fd06/propcache-0.4.1-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:e35b88984e7fa64aacecea39236cee32dd9bd8c55f57ba8a75cf2399553f9bd7", size = 263514, upload-time = "2025-10-08T19:47:43.927Z" },
- { url = "https://files.pythonhosted.org/packages/94/13/630690fe201f5502d2403dd3cfd451ed8858fe3c738ee88d095ad2ff407b/propcache-0.4.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:6f8b465489f927b0df505cbe26ffbeed4d6d8a2bbc61ce90eb074ff129ef0ab1", size = 257781, upload-time = "2025-10-08T19:47:45.448Z" },
- { url = "https://files.pythonhosted.org/packages/92/f7/1d4ec5841505f423469efbfc381d64b7b467438cd5a4bbcbb063f3b73d27/propcache-0.4.1-cp313-cp313t-win32.whl", hash = "sha256:2ad890caa1d928c7c2965b48f3a3815c853180831d0e5503d35cf00c472f4717", size = 41396, upload-time = "2025-10-08T19:47:47.202Z" },
- { url = "https://files.pythonhosted.org/packages/48/f0/615c30622316496d2cbbc29f5985f7777d3ada70f23370608c1d3e081c1f/propcache-0.4.1-cp313-cp313t-win_amd64.whl", hash = "sha256:f7ee0e597f495cf415bcbd3da3caa3bd7e816b74d0d52b8145954c5e6fd3ff37", size = 44897, upload-time = "2025-10-08T19:47:48.336Z" },
- { url = "https://files.pythonhosted.org/packages/fd/ca/6002e46eccbe0e33dcd4069ef32f7f1c9e243736e07adca37ae8c4830ec3/propcache-0.4.1-cp313-cp313t-win_arm64.whl", hash = "sha256:929d7cbe1f01bb7baffb33dc14eb5691c95831450a26354cd210a8155170c93a", size = 39789, upload-time = "2025-10-08T19:47:49.876Z" },
- { url = "https://files.pythonhosted.org/packages/8e/5c/bca52d654a896f831b8256683457ceddd490ec18d9ec50e97dfd8fc726a8/propcache-0.4.1-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:3f7124c9d820ba5548d431afb4632301acf965db49e666aa21c305cbe8c6de12", size = 78152, upload-time = "2025-10-08T19:47:51.051Z" },
- { url = "https://files.pythonhosted.org/packages/65/9b/03b04e7d82a5f54fb16113d839f5ea1ede58a61e90edf515f6577c66fa8f/propcache-0.4.1-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:c0d4b719b7da33599dfe3b22d3db1ef789210a0597bc650b7cee9c77c2be8c5c", size = 44869, upload-time = "2025-10-08T19:47:52.594Z" },
- { url = "https://files.pythonhosted.org/packages/b2/fa/89a8ef0468d5833a23fff277b143d0573897cf75bd56670a6d28126c7d68/propcache-0.4.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:9f302f4783709a78240ebc311b793f123328716a60911d667e0c036bc5dcbded", size = 46596, upload-time = "2025-10-08T19:47:54.073Z" },
- { url = "https://files.pythonhosted.org/packages/86/bd/47816020d337f4a746edc42fe8d53669965138f39ee117414c7d7a340cfe/propcache-0.4.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c80ee5802e3fb9ea37938e7eecc307fb984837091d5fd262bb37238b1ae97641", size = 206981, upload-time = "2025-10-08T19:47:55.715Z" },
- { url = "https://files.pythonhosted.org/packages/df/f6/c5fa1357cc9748510ee55f37173eb31bfde6d94e98ccd9e6f033f2fc06e1/propcache-0.4.1-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ed5a841e8bb29a55fb8159ed526b26adc5bdd7e8bd7bf793ce647cb08656cdf4", size = 211490, upload-time = "2025-10-08T19:47:57.499Z" },
- { url = "https://files.pythonhosted.org/packages/80/1e/e5889652a7c4a3846683401a48f0f2e5083ce0ec1a8a5221d8058fbd1adf/propcache-0.4.1-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:55c72fd6ea2da4c318e74ffdf93c4fe4e926051133657459131a95c846d16d44", size = 215371, upload-time = "2025-10-08T19:47:59.317Z" },
- { url = "https://files.pythonhosted.org/packages/b2/f2/889ad4b2408f72fe1a4f6a19491177b30ea7bf1a0fd5f17050ca08cfc882/propcache-0.4.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8326e144341460402713f91df60ade3c999d601e7eb5ff8f6f7862d54de0610d", size = 201424, upload-time = "2025-10-08T19:48:00.67Z" },
- { url = "https://files.pythonhosted.org/packages/27/73/033d63069b57b0812c8bd19f311faebeceb6ba31b8f32b73432d12a0b826/propcache-0.4.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:060b16ae65bc098da7f6d25bf359f1f31f688384858204fe5d652979e0015e5b", size = 197566, upload-time = "2025-10-08T19:48:02.604Z" },
- { url = "https://files.pythonhosted.org/packages/dc/89/ce24f3dc182630b4e07aa6d15f0ff4b14ed4b9955fae95a0b54c58d66c05/propcache-0.4.1-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:89eb3fa9524f7bec9de6e83cf3faed9d79bffa560672c118a96a171a6f55831e", size = 193130, upload-time = "2025-10-08T19:48:04.499Z" },
- { url = "https://files.pythonhosted.org/packages/a9/24/ef0d5fd1a811fb5c609278d0209c9f10c35f20581fcc16f818da959fc5b4/propcache-0.4.1-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:dee69d7015dc235f526fe80a9c90d65eb0039103fe565776250881731f06349f", size = 202625, upload-time = "2025-10-08T19:48:06.213Z" },
- { url = "https://files.pythonhosted.org/packages/f5/02/98ec20ff5546f68d673df2f7a69e8c0d076b5abd05ca882dc7ee3a83653d/propcache-0.4.1-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:5558992a00dfd54ccbc64a32726a3357ec93825a418a401f5cc67df0ac5d9e49", size = 204209, upload-time = "2025-10-08T19:48:08.432Z" },
- { url = "https://files.pythonhosted.org/packages/a0/87/492694f76759b15f0467a2a93ab68d32859672b646aa8a04ce4864e7932d/propcache-0.4.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:c9b822a577f560fbd9554812526831712c1436d2c046cedee4c3796d3543b144", size = 197797, upload-time = "2025-10-08T19:48:09.968Z" },
- { url = "https://files.pythonhosted.org/packages/ee/36/66367de3575db1d2d3f3d177432bd14ee577a39d3f5d1b3d5df8afe3b6e2/propcache-0.4.1-cp314-cp314-win32.whl", hash = "sha256:ab4c29b49d560fe48b696cdcb127dd36e0bc2472548f3bf56cc5cb3da2b2984f", size = 38140, upload-time = "2025-10-08T19:48:11.232Z" },
- { url = "https://files.pythonhosted.org/packages/0c/2a/a758b47de253636e1b8aef181c0b4f4f204bf0dd964914fb2af90a95b49b/propcache-0.4.1-cp314-cp314-win_amd64.whl", hash = "sha256:5a103c3eb905fcea0ab98be99c3a9a5ab2de60228aa5aceedc614c0281cf6153", size = 41257, upload-time = "2025-10-08T19:48:12.707Z" },
- { url = "https://files.pythonhosted.org/packages/34/5e/63bd5896c3fec12edcbd6f12508d4890d23c265df28c74b175e1ef9f4f3b/propcache-0.4.1-cp314-cp314-win_arm64.whl", hash = "sha256:74c1fb26515153e482e00177a1ad654721bf9207da8a494a0c05e797ad27b992", size = 38097, upload-time = "2025-10-08T19:48:13.923Z" },
- { url = "https://files.pythonhosted.org/packages/99/85/9ff785d787ccf9bbb3f3106f79884a130951436f58392000231b4c737c80/propcache-0.4.1-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:824e908bce90fb2743bd6b59db36eb4f45cd350a39637c9f73b1c1ea66f5b75f", size = 81455, upload-time = "2025-10-08T19:48:15.16Z" },
- { url = "https://files.pythonhosted.org/packages/90/85/2431c10c8e7ddb1445c1f7c4b54d886e8ad20e3c6307e7218f05922cad67/propcache-0.4.1-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:c2b5e7db5328427c57c8e8831abda175421b709672f6cfc3d630c3b7e2146393", size = 46372, upload-time = "2025-10-08T19:48:16.424Z" },
- { url = "https://files.pythonhosted.org/packages/01/20/b0972d902472da9bcb683fa595099911f4d2e86e5683bcc45de60dd05dc3/propcache-0.4.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:6f6ff873ed40292cd4969ef5310179afd5db59fdf055897e282485043fc80ad0", size = 48411, upload-time = "2025-10-08T19:48:17.577Z" },
- { url = "https://files.pythonhosted.org/packages/e2/e3/7dc89f4f21e8f99bad3d5ddb3a3389afcf9da4ac69e3deb2dcdc96e74169/propcache-0.4.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:49a2dc67c154db2c1463013594c458881a069fcf98940e61a0569016a583020a", size = 275712, upload-time = "2025-10-08T19:48:18.901Z" },
- { url = "https://files.pythonhosted.org/packages/20/67/89800c8352489b21a8047c773067644e3897f02ecbbd610f4d46b7f08612/propcache-0.4.1-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:005f08e6a0529984491e37d8dbc3dd86f84bd78a8ceb5fa9a021f4c48d4984be", size = 273557, upload-time = "2025-10-08T19:48:20.762Z" },
- { url = "https://files.pythonhosted.org/packages/e2/a1/b52b055c766a54ce6d9c16d9aca0cad8059acd9637cdf8aa0222f4a026ef/propcache-0.4.1-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5c3310452e0d31390da9035c348633b43d7e7feb2e37be252be6da45abd1abcc", size = 280015, upload-time = "2025-10-08T19:48:22.592Z" },
- { url = "https://files.pythonhosted.org/packages/48/c8/33cee30bd890672c63743049f3c9e4be087e6780906bfc3ec58528be59c1/propcache-0.4.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4c3c70630930447f9ef1caac7728c8ad1c56bc5015338b20fed0d08ea2480b3a", size = 262880, upload-time = "2025-10-08T19:48:23.947Z" },
- { url = "https://files.pythonhosted.org/packages/0c/b1/8f08a143b204b418285c88b83d00edbd61afbc2c6415ffafc8905da7038b/propcache-0.4.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8e57061305815dfc910a3634dcf584f08168a8836e6999983569f51a8544cd89", size = 260938, upload-time = "2025-10-08T19:48:25.656Z" },
- { url = "https://files.pythonhosted.org/packages/cf/12/96e4664c82ca2f31e1c8dff86afb867348979eb78d3cb8546a680287a1e9/propcache-0.4.1-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:521a463429ef54143092c11a77e04056dd00636f72e8c45b70aaa3140d639726", size = 247641, upload-time = "2025-10-08T19:48:27.207Z" },
- { url = "https://files.pythonhosted.org/packages/18/ed/e7a9cfca28133386ba52278136d42209d3125db08d0a6395f0cba0c0285c/propcache-0.4.1-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:120c964da3fdc75e3731aa392527136d4ad35868cc556fd09bb6d09172d9a367", size = 262510, upload-time = "2025-10-08T19:48:28.65Z" },
- { url = "https://files.pythonhosted.org/packages/f5/76/16d8bf65e8845dd62b4e2b57444ab81f07f40caa5652b8969b87ddcf2ef6/propcache-0.4.1-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:d8f353eb14ee3441ee844ade4277d560cdd68288838673273b978e3d6d2c8f36", size = 263161, upload-time = "2025-10-08T19:48:30.133Z" },
- { url = "https://files.pythonhosted.org/packages/e7/70/c99e9edb5d91d5ad8a49fa3c1e8285ba64f1476782fed10ab251ff413ba1/propcache-0.4.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:ab2943be7c652f09638800905ee1bab2c544e537edb57d527997a24c13dc1455", size = 257393, upload-time = "2025-10-08T19:48:31.567Z" },
- { url = "https://files.pythonhosted.org/packages/08/02/87b25304249a35c0915d236575bc3574a323f60b47939a2262b77632a3ee/propcache-0.4.1-cp314-cp314t-win32.whl", hash = "sha256:05674a162469f31358c30bcaa8883cb7829fa3110bf9c0991fe27d7896c42d85", size = 42546, upload-time = "2025-10-08T19:48:32.872Z" },
- { url = "https://files.pythonhosted.org/packages/cb/ef/3c6ecf8b317aa982f309835e8f96987466123c6e596646d4e6a1dfcd080f/propcache-0.4.1-cp314-cp314t-win_amd64.whl", hash = "sha256:990f6b3e2a27d683cb7602ed6c86f15ee6b43b1194736f9baaeb93d0016633b1", size = 46259, upload-time = "2025-10-08T19:48:34.226Z" },
- { url = "https://files.pythonhosted.org/packages/c4/2d/346e946d4951f37eca1e4f55be0f0174c52cd70720f84029b02f296f4a38/propcache-0.4.1-cp314-cp314t-win_arm64.whl", hash = "sha256:ecef2343af4cc68e05131e45024ba34f6095821988a9d0a02aa7c73fcc448aa9", size = 40428, upload-time = "2025-10-08T19:48:35.441Z" },
- { url = "https://files.pythonhosted.org/packages/9b/01/0ebaec9003f5d619a7475165961f8e3083cf8644d704b60395df3601632d/propcache-0.4.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:3d233076ccf9e450c8b3bc6720af226b898ef5d051a2d145f7d765e6e9f9bcff", size = 80277, upload-time = "2025-10-08T19:48:36.647Z" },
- { url = "https://files.pythonhosted.org/packages/34/58/04af97ac586b4ef6b9026c3fd36ee7798b737a832f5d3440a4280dcebd3a/propcache-0.4.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:357f5bb5c377a82e105e44bd3d52ba22b616f7b9773714bff93573988ef0a5fb", size = 45865, upload-time = "2025-10-08T19:48:37.859Z" },
- { url = "https://files.pythonhosted.org/packages/7c/19/b65d98ae21384518b291d9939e24a8aeac4fdb5101b732576f8f7540e834/propcache-0.4.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:cbc3b6dfc728105b2a57c06791eb07a94229202ea75c59db644d7d496b698cac", size = 47636, upload-time = "2025-10-08T19:48:39.038Z" },
- { url = "https://files.pythonhosted.org/packages/b3/0f/317048c6d91c356c7154dca5af019e6effeb7ee15fa6a6db327cc19e12b4/propcache-0.4.1-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:182b51b421f0501952d938dc0b0eb45246a5b5153c50d42b495ad5fb7517c888", size = 201126, upload-time = "2025-10-08T19:48:40.774Z" },
- { url = "https://files.pythonhosted.org/packages/71/69/0b2a7a5a6ee83292b4b997dbd80549d8ce7d40b6397c1646c0d9495f5a85/propcache-0.4.1-cp39-cp39-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:4b536b39c5199b96fc6245eb5fb796c497381d3942f169e44e8e392b29c9ebcc", size = 209837, upload-time = "2025-10-08T19:48:42.167Z" },
- { url = "https://files.pythonhosted.org/packages/a5/92/c699ac495a6698df6e497fc2de27af4b6ace10d8e76528357ce153722e45/propcache-0.4.1-cp39-cp39-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:db65d2af507bbfbdcedb254a11149f894169d90488dd3e7190f7cdcb2d6cd57a", size = 215578, upload-time = "2025-10-08T19:48:43.56Z" },
- { url = "https://files.pythonhosted.org/packages/b3/ee/14de81c5eb02c0ee4f500b4e39c4e1bd0677c06e72379e6ab18923c773fc/propcache-0.4.1-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fd2dbc472da1f772a4dae4fa24be938a6c544671a912e30529984dd80400cd88", size = 197187, upload-time = "2025-10-08T19:48:45.309Z" },
- { url = "https://files.pythonhosted.org/packages/1d/94/48dce9aaa6d8dd5a0859bad75158ec522546d4ac23f8e2f05fac469477dd/propcache-0.4.1-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:daede9cd44e0f8bdd9e6cc9a607fc81feb80fae7a5fc6cecaff0e0bb32e42d00", size = 193478, upload-time = "2025-10-08T19:48:47.743Z" },
- { url = "https://files.pythonhosted.org/packages/60/b5/0516b563e801e1ace212afde869a0596a0d7115eec0b12d296d75633fb29/propcache-0.4.1-cp39-cp39-musllinux_1_2_armv7l.whl", hash = "sha256:71b749281b816793678ae7f3d0d84bd36e694953822eaad408d682efc5ca18e0", size = 190650, upload-time = "2025-10-08T19:48:49.373Z" },
- { url = "https://files.pythonhosted.org/packages/24/89/e0f7d4a5978cd56f8cd67735f74052f257dc471ec901694e430f0d1572fe/propcache-0.4.1-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:0002004213ee1f36cfb3f9a42b5066100c44276b9b72b4e1504cddd3d692e86e", size = 200251, upload-time = "2025-10-08T19:48:51.4Z" },
- { url = "https://files.pythonhosted.org/packages/06/7d/a1fac863d473876ed4406c914f2e14aa82d2f10dd207c9e16fc383cc5a24/propcache-0.4.1-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:fe49d0a85038f36ba9e3ffafa1103e61170b28e95b16622e11be0a0ea07c6781", size = 200919, upload-time = "2025-10-08T19:48:53.227Z" },
- { url = "https://files.pythonhosted.org/packages/c3/4e/f86a256ff24944cf5743e4e6c6994e3526f6acfcfb55e21694c2424f758c/propcache-0.4.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:99d43339c83aaf4d32bda60928231848eee470c6bda8d02599cc4cebe872d183", size = 193211, upload-time = "2025-10-08T19:48:55.027Z" },
- { url = "https://files.pythonhosted.org/packages/6e/3f/3fbad5f4356b068f1b047d300a6ff2c66614d7030f078cd50be3fec04228/propcache-0.4.1-cp39-cp39-win32.whl", hash = "sha256:a129e76735bc792794d5177069691c3217898b9f5cee2b2661471e52ffe13f19", size = 38314, upload-time = "2025-10-08T19:48:56.792Z" },
- { url = "https://files.pythonhosted.org/packages/a4/45/d78d136c3a3d215677abb886785aae744da2c3005bcb99e58640c56529b1/propcache-0.4.1-cp39-cp39-win_amd64.whl", hash = "sha256:948dab269721ae9a87fd16c514a0a2c2a1bdb23a9a61b969b0f9d9ee2968546f", size = 41912, upload-time = "2025-10-08T19:48:57.995Z" },
- { url = "https://files.pythonhosted.org/packages/fc/2a/b0632941f25139f4e58450b307242951f7c2717a5704977c6d5323a800af/propcache-0.4.1-cp39-cp39-win_arm64.whl", hash = "sha256:5fd37c406dd6dc85aa743e214cef35dc54bbdd1419baac4f6ae5e5b1a2976938", size = 38450, upload-time = "2025-10-08T19:48:59.349Z" },
- { url = "https://files.pythonhosted.org/packages/5b/5a/bc7b4a4ef808fa59a816c17b20c4bef6884daebbdf627ff2a161da67da19/propcache-0.4.1-py3-none-any.whl", hash = "sha256:af2a6052aeb6cf17d3e46ee169099044fd8224cbaf75c76a2ef596e8163e2237", size = 13305, upload-time = "2025-10-08T19:49:00.792Z" },
-]
-
-[[package]]
-name = "propcache"
-version = "0.5.2"
-source = { registry = "https://pypi.org/simple" }
-resolution-markers = [
- "python_full_version >= '3.10'",
-]
-sdist = { url = "https://files.pythonhosted.org/packages/ec/44/c87281c333769159c50594f22610f77398a47ccbfbbf23074e744e86f87c/propcache-0.5.2.tar.gz", hash = "sha256:01c4fc7480cd0598bb4b57022df55b9ca296da7fc5a8760bd8451a7e63a7d427", size = 50208, upload-time = "2026-05-08T21:02:12.199Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/5b/56/030b7b4719d53085722893e0009dffb9236aa10bca1b12121bdc5626ef16/propcache-0.5.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:d5a81be28596d6559f6131ef33e10200de6e17643b3c74ce03f9eb103be6ae8b", size = 93417, upload-time = "2026-05-08T20:59:15.597Z" },
- { url = "https://files.pythonhosted.org/packages/1a/55/1140a8e067b8ec093a18a4ae7bb0045d9db65da38a08618ddc5e2f1994aa/propcache-0.5.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:29cbaac5ea0212663e6845e04b5e188d5a6ae6dd919810ac835bf1d3b42c3f4c", size = 53847, upload-time = "2026-05-08T20:59:17.096Z" },
- { url = "https://files.pythonhosted.org/packages/20/42/0e7443c90310498561addf346e7d57fe3c6ba1914e1ba938b5464c7bbfd2/propcache-0.5.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:6bf3be92233808fcd338eba0fb4d0b59ec5772af4f4ecfcec450d1bfc0f8b5eb", size = 53512, upload-time = "2026-05-08T20:59:18.64Z" },
- { url = "https://files.pythonhosted.org/packages/b7/db/cf51a71bab2009517d1a7f0ee07657e3bd446c4d69f67e6966cf17bcf956/propcache-0.5.2-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2f8ea531c794b9d6274acd4e8d2c2ebcac590a4361d27482edd3010b79f1325e", size = 58068, upload-time = "2026-05-08T20:59:20.683Z" },
- { url = "https://files.pythonhosted.org/packages/b7/43/39b6bdee9699fa1e1641c519feeb64a67e2a9f93bb465c70776b37a7333f/propcache-0.5.2-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:decfca4c79dd53ebab484b00cc4b6717d8c369f86e74aa4ca395a64ac651495e", size = 61020, upload-time = "2026-05-08T20:59:22.112Z" },
- { url = "https://files.pythonhosted.org/packages/26/0b/843726fbb0a29a8c5684fdb25971823638399f31e52e9d1f06a02dc9aa6b/propcache-0.5.2-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:4621064bbf28fa77ff64dd5d94367c04684c67d3a5bf1dff25f0cd0d98a38f3b", size = 62732, upload-time = "2026-05-08T20:59:23.805Z" },
- { url = "https://files.pythonhosted.org/packages/39/6e/899fed76dc1942b8a64193a4f059d7f1a2c7ef65085e8a9366ed8ec0d199/propcache-0.5.2-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b96db7141a592cbc968daf1feea83a118e6ab378af4abbc72b248c895414c22d", size = 60140, upload-time = "2026-05-08T20:59:25.389Z" },
- { url = "https://files.pythonhosted.org/packages/ab/09/3da4be9b5b879219ad234aa535b3dd4a080ed1ad48d3a73ca07a9e798f22/propcache-0.5.2-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:1ca071adabaab6e9219924bbe00af821f1ee7de113a9eca1cdc292de3d120f4d", size = 60400, upload-time = "2026-05-08T20:59:27.238Z" },
- { url = "https://files.pythonhosted.org/packages/60/2f/09b72b874a9aa0044faf52a69807a6ed618e267ceaa9ec4a63195fa5b504/propcache-0.5.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:e4294d04a94dcab1b3bccd8b66d962dcad411a1d19414b2a41d1445f1de32ad0", size = 58155, upload-time = "2026-05-08T20:59:28.48Z" },
- { url = "https://files.pythonhosted.org/packages/8a/37/97489848c54c95578045473954f10956d619ce6a09e7ac137b71cdcb698b/propcache-0.5.2-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:a0e399a2eccb91ed18721f86aa85757727400b6865c89e88934781deb9c8498b", size = 57037, upload-time = "2026-05-08T20:59:30.146Z" },
- { url = "https://files.pythonhosted.org/packages/22/db/6c695285ccfc49012743ee9c98212b8c5dd0aed7b63cfd816d4a0f7a1601/propcache-0.5.2-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:823581fd5cb08b12a48bfa11fe962a7916766b6170c17b028fbdf762b85eb9bf", size = 61103, upload-time = "2026-05-08T20:59:31.626Z" },
- { url = "https://files.pythonhosted.org/packages/98/a9/1e500401ca593b0bdb6bf75a70bc2d723835fd53360edff6af70692c7546/propcache-0.5.2-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:949c91d1a990cf3b2e8188dfcfb25005e0b834a06c63fa4ef9f360878ce21ecf", size = 60394, upload-time = "2026-05-08T20:59:32.829Z" },
- { url = "https://files.pythonhosted.org/packages/1f/87/f638b6e375eae0f30a1a2325d8b34fd85fdc785bb9960cf805f3bf1ec69a/propcache-0.5.2-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:cc1177027eda740fdb152706bd215a3f124e3eea15afc39f2cb9fe351b50619e", size = 63084, upload-time = "2026-05-08T20:59:35.964Z" },
- { url = "https://files.pythonhosted.org/packages/f6/18/884573f5d97b6d9eba68de759a82c901b7e39d7904d30f7b8d58d42d2a12/propcache-0.5.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:b05d643f944a8c3c4bd86d65ffd87bf3264b617f87791940302bc474d2ff5274", size = 60999, upload-time = "2026-05-08T20:59:38.481Z" },
- { url = "https://files.pythonhosted.org/packages/8f/1a/c3915eb059ceec9e758a56e4cfd955292bc0f201be2176a46b76d94b303a/propcache-0.5.2-cp310-cp310-win32.whl", hash = "sha256:8114f28879e0904748e831c3a7774261bd9e75f49be089f389a76f959dcd13fe", size = 39036, upload-time = "2026-05-08T20:59:40.323Z" },
- { url = "https://files.pythonhosted.org/packages/5b/02/1dfd5607501a602d19c1c449d2d193b7d1c611f9246b4059026a1189a80e/propcache-0.5.2-cp310-cp310-win_amd64.whl", hash = "sha256:5fcb98e7598b1ee0addab320d90f65b530297a867dbfe9de52ea838077e16e3d", size = 42190, upload-time = "2026-05-08T20:59:42.232Z" },
- { url = "https://files.pythonhosted.org/packages/57/93/f71588ad08b3e6f4b555b5ef215808a3c02b042d0151ad82fa6f15be677a/propcache-0.5.2-cp310-cp310-win_arm64.whl", hash = "sha256:04dc2390d9edbbaef7461f33322555976ffddf0b650a038649d026358714e6c5", size = 38545, upload-time = "2026-05-08T20:59:44.087Z" },
- { url = "https://files.pythonhosted.org/packages/e7/f1/8a8cc1c2c7e7934ab77e0163414f736fadbc0f5e8dd9673b952355ac175b/propcache-0.5.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:74b70780220e2dd89175ca24b81b68b67c83db499ae611e7f2313cb329801c78", size = 90744, upload-time = "2026-05-08T20:59:45.799Z" },
- { url = "https://files.pythonhosted.org/packages/c2/f4/651b1225e976bd1a2ba5cfba0c29d096581c2636b437e3a9a7ab6276270a/propcache-0.5.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a4840ab0ae0216d952f4b53dc6d0b992bfc2bedbfe360bdd9b548bc184c08959", size = 52033, upload-time = "2026-05-08T20:59:47.408Z" },
- { url = "https://files.pythonhosted.org/packages/15/a8/8ede85d6aa1f79fc7dc2f8fd2c8d65920b8272c3892903c8a1affde48cfb/propcache-0.5.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:c6844ba6364fb12f403928a82cfd295ab103a2b315c77c747b2dbe4a41894ea7", size = 52754, upload-time = "2026-05-08T20:59:49.202Z" },
- { url = "https://files.pythonhosted.org/packages/7d/fe/b3551b41bbc2f5b5bb088fc6920567cd43101253e68fbaa261339eb96fe1/propcache-0.5.2-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2293949b855ce597f2826452d17c2d545fb5622379c4ea6fdf525e9b8e8a2511", size = 57573, upload-time = "2026-05-08T20:59:50.778Z" },
- { url = "https://files.pythonhosted.org/packages/83/27/ab851ebd1b7172e3e161f5f8d39e315d54a91bea246f01f4d872d3376aef/propcache-0.5.2-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:0fd59b5af35f74da48d905dcbad55449ba13be91823cb05a9bd590bbf5b61660", size = 60645, upload-time = "2026-05-08T20:59:52.227Z" },
- { url = "https://files.pythonhosted.org/packages/95/7d/466b3d18022e9897cbda9c735c493c5bd747d7a4c6f5ea1480b4cec434b6/propcache-0.5.2-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:29f9309a2e42b0d273be006fdb4be2d6c39a47f6f57d8fb1cf9f81481df81b66", size = 61563, upload-time = "2026-05-08T20:59:53.866Z" },
- { url = "https://files.pythonhosted.org/packages/27/1b/16ab7f2cf2041da2f60d156ba64c2484eadf9168075b4ff43c3ef60045af/propcache-0.5.2-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5aaa2b923c1944ac8febd6609cb373540a5563e7cbcb0fd770f75dace2eb817b", size = 58888, upload-time = "2026-05-08T20:59:55.457Z" },
- { url = "https://files.pythonhosted.org/packages/0a/67/bb777ffd907633563bf35fd859c4ce97b0512c32f4633cf5d1eb7c33512b/propcache-0.5.2-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:66ea454f095ddf5b6b14f56c064c0941c4788be11e18d2464cf643bf7203ff67", size = 59253, upload-time = "2026-05-08T20:59:57.075Z" },
- { url = "https://files.pythonhosted.org/packages/b9/42/64f8d90b73fd9cdc1499b48057ff6d9cd2a98a25734c9bb62ecf07e87061/propcache-0.5.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:95f1e3f4760d404b13c9976c0229b2b49a3c8e2c62a9ce92efdd2b11ada75e3f", size = 57558, upload-time = "2026-05-08T20:59:58.602Z" },
- { url = "https://files.pythonhosted.org/packages/eb/02/dba5bc03c9041f2092ea55a449caf5dfe68352c6654511b29ba0654ddb69/propcache-0.5.2-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:85341b12b9d55bad0bded24cac341bb34289469e03a11f3f583ea1cc1db0326c", size = 55007, upload-time = "2026-05-08T20:59:59.837Z" },
- { url = "https://files.pythonhosted.org/packages/14/c0/43f649c7aa2a77a3b100d84e9dea3a483120ecb608bfe36ce49eaff517fe/propcache-0.5.2-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:26a4dca084132874e639895c3135dfad5eb20bae209f62d1aeb31b03e601c3c0", size = 60355, upload-time = "2026-05-08T21:00:01.144Z" },
- { url = "https://files.pythonhosted.org/packages/83/c0/435dafd27f1cb4a495381dae60e25883ccfe4020bb72818e8184c1678092/propcache-0.5.2-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:3b199b9b2b3d6a7edf3183ba8a9a137a22b97f7df525feb5ae1eccf026d2a9c6", size = 59057, upload-time = "2026-05-08T21:00:02.401Z" },
- { url = "https://files.pythonhosted.org/packages/53/ae/6e292df9135d659944e96cb3389258e4a663e5b2b5f6c217ef0ddc8d2f73/propcache-0.5.2-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:e59bc9e66329185b93dab73f210f1a37f81cb40f321501db8017c9aea15dba27", size = 61938, upload-time = "2026-05-08T21:00:03.638Z" },
- { url = "https://files.pythonhosted.org/packages/0b/42/314ebc50d8159055411fd6b0bda322ff510e4b1f7d2e4927940ad0f6af20/propcache-0.5.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:552ffadf6ad409844bc5919c42a0a83d88314cedddaea0e41e80a8b8fffe881f", size = 59731, upload-time = "2026-05-08T21:00:04.881Z" },
- { url = "https://files.pythonhosted.org/packages/b8/9b/2da6dee38871c3c8772fabc2758325a5c9077d6d18c597737dc04dd884cd/propcache-0.5.2-cp311-cp311-win32.whl", hash = "sha256:cd416c1de191973c52ff1a12a57446bfc7642797b282d7caf2162d7d1b8aa9a0", size = 38966, upload-time = "2026-05-08T21:00:06.511Z" },
- { url = "https://files.pythonhosted.org/packages/42/4e/f17363fb58c0afe05b067361cb6d86ed2d29de6506779a27547c4d183075/propcache-0.5.2-cp311-cp311-win_amd64.whl", hash = "sha256:44e488ef40dbb452700b2b1f8188934121f6648f52c295055662d2191959ff82", size = 42135, upload-time = "2026-05-08T21:00:08.088Z" },
- { url = "https://files.pythonhosted.org/packages/c6/eb/6af6685077d22e8b33358d3c548e3282706a0b3cd85044ffba4e5dd08e3b/propcache-0.5.2-cp311-cp311-win_arm64.whl", hash = "sha256:54adaa85a22078d1e306304a40984dc5be99d599bf3dc0a24dc98f7daeab89ab", size = 38381, upload-time = "2026-05-08T21:00:09.692Z" },
- { url = "https://files.pythonhosted.org/packages/4a/cb/e27bc2b2737a0bb49962b275efa051e8f1c35a936df7d5139b6b658b7dc9/propcache-0.5.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:806719138ecd720339a12410fb9614ac9b2b2d3a5fdf8235d56981c36f4039ba", size = 95887, upload-time = "2026-05-08T21:00:11.277Z" },
- { url = "https://files.pythonhosted.org/packages/e6/13/b8ae04c59392f8d11c6cd9fb4011d1dc7c86b81225c770280300e259ffe1/propcache-0.5.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:db2b80ea58eab4f86b2beec3cc8b39e8ff9276ac20e96b7cce43c8ae84cd6b5a", size = 54654, upload-time = "2026-05-08T21:00:12.604Z" },
- { url = "https://files.pythonhosted.org/packages/2c/7d/49777a3e20b55863d4794384a38acd460c04157b0a00f8602b0d508b8431/propcache-0.5.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:e5cbfac9f61484f7e9f3597775500cd3ebe8274e9b050c38f9525c77c97520bf", size = 55190, upload-time = "2026-05-08T21:00:13.935Z" },
- { url = "https://files.pythonhosted.org/packages/44/c7/085d0cd63062e84044e3f05797749c3f8e3938ff3aeb0eb2f69d43fafc91/propcache-0.5.2-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5dbc581d2814337da56222fab8dc5f161cd798a434e49bac27930aaef798e144", size = 59995, upload-time = "2026-05-08T21:00:15.526Z" },
- { url = "https://files.pythonhosted.org/packages/9c/42/32cf8e3009e92b2645cf1e944f701e8ea4e924dffde1ee26db860bcbf7e4/propcache-0.5.2-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:857187f381f88c8e2fa2fe56ab94879d011b883d5a2ee5a1b60a8cd2a06846d9", size = 63422, upload-time = "2026-05-08T21:00:16.824Z" },
- { url = "https://files.pythonhosted.org/packages/9e/1b/f112433f99fc979431b87a39ef169e3f8df070d99a72792c56d6937ac48b/propcache-0.5.2-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:178b4a2cdaac1818e2bf1c5a99b94383fa73ea5382e032a48dec07dc5668dc42", size = 64342, upload-time = "2026-05-08T21:00:18.362Z" },
- { url = "https://files.pythonhosted.org/packages/14/15/5574111ae50dd6e879456888c0eadd4c5a869959775854e18e18a6b345f3/propcache-0.5.2-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6f328175a2cde1f0ff2c4ed8ce968b9dcfb55f3a7153f39e2957ed994da13476", size = 61639, upload-time = "2026-05-08T21:00:19.692Z" },
- { url = "https://files.pythonhosted.org/packages/cc/da/4d775080b1490c0ae604acda868bd71aabe3a89ed16f2aa4339eb8a283e7/propcache-0.5.2-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:5671d09a36b06d0fd4a3da0fccbcae360e9b1570924171a15e9e0997f0249fba", size = 61588, upload-time = "2026-05-08T21:00:21.155Z" },
- { url = "https://files.pythonhosted.org/packages/04/ac/f076982cbe2195ee9cf32de5a1e46951d9fb399fc207f390562dd0fd8fb2/propcache-0.5.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:80168e2ebe4d3ec6599d10ad8f520304ae1cad9b6c5a95372aef1b66b7bfb53a", size = 60029, upload-time = "2026-05-08T21:00:22.713Z" },
- { url = "https://files.pythonhosted.org/packages/70/60/189be62e0dd898dce3b331e1b8c7a543cd3a405ac0c81fe8ee8a9d5d77e1/propcache-0.5.2-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:45f11346f884bc47444f6e6647131055844134c3175b629f84952e2b5cd62b64", size = 56774, upload-time = "2026-05-08T21:00:24.001Z" },
- { url = "https://files.pythonhosted.org/packages/ea/9e/93377b9c7939c1ffae98f878dee955efadfd638078bc86dbc21f9d52f651/propcache-0.5.2-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:8e778ebd44ef4f66ed60a0416b06b489687db264a9c0b3620362f26489492913", size = 63532, upload-time = "2026-05-08T21:00:25.545Z" },
- { url = "https://files.pythonhosted.org/packages/14/f9/590ef6cfb9b8028d516d287812ece32bb0bc5f11fbb9c8bf6b2e6313fec8/propcache-0.5.2-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:c0cb9ed24c8964e172768d455a38254c2dd8a552905729ce006cad3d3dda59b1", size = 61592, upload-time = "2026-05-08T21:00:27.186Z" },
- { url = "https://files.pythonhosted.org/packages/b4/5e/70958b3034c297a630bba2f17ca7abc2d5f39a803ad7e370ab79d1ecd022/propcache-0.5.2-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:1d1ad32d9d4355e2be65574fd0bfd3677e7066b009cd5b9b2dee8aa6a6393b33", size = 64788, upload-time = "2026-05-08T21:00:28.8Z" },
- { url = "https://files.pythonhosted.org/packages/12/fd/77fe5936d8c3086ca9048f7f415f122ed82e53884a9ec193646b42deef06/propcache-0.5.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:c80f4ba3e8f00189165999a742ee526ebeccedf6c3f7beb0c7df821e9772435a", size = 62514, upload-time = "2026-05-08T21:00:30.098Z" },
- { url = "https://files.pythonhosted.org/packages/cf/74/66bd798b5b3be70aa1b391f5cc9d6a0a5532d7fd3b19ec0b213e72e6ad9d/propcache-0.5.2-cp312-cp312-win32.whl", hash = "sha256:8c7972d8f193740d9175f0998ab38717e6cd322d5935c5b0fef8c0d323fd9031", size = 39018, upload-time = "2026-05-08T21:00:31.622Z" },
- { url = "https://files.pythonhosted.org/packages/61/7c/5c0d34aa3024694d6dcb9271cdbdd08c4e47c1c0ad95ec7e7bc74cdea145/propcache-0.5.2-cp312-cp312-win_amd64.whl", hash = "sha256:d9ee8826a7d47863a08ac44e1a5f611a462eefc3a194b492da242128bec75b42", size = 42322, upload-time = "2026-05-08T21:00:32.918Z" },
- { url = "https://files.pythonhosted.org/packages/4d/91/875812f1a3feb20ceba818ef39fbe4d92f1081e04ac815c822496d0d038b/propcache-0.5.2-cp312-cp312-win_arm64.whl", hash = "sha256:2800a4a8ead6b28cccd1ec54b59346f0def7922ee1c7598e8499c733cfbb7c84", size = 38172, upload-time = "2026-05-08T21:00:35.124Z" },
- { url = "https://files.pythonhosted.org/packages/c5/09/f049e45385503fe67db75a6b6186a7b9f0c3930366dc960522c312a825b1/propcache-0.5.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:099aaf4b4d1a02265b92a977edf00b5c4f63b3b17ac6de39b0d637c9cac0188a", size = 94457, upload-time = "2026-05-08T21:00:36.355Z" },
- { url = "https://files.pythonhosted.org/packages/6b/65/83d1d05655baf63113731bd5a1008435e14f8d1e5a06cbe4ec5b23ad7a31/propcache-0.5.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:68ce1c44c7a813a7f71ea04315a8c7b330b63db99d059a797a4651bb6f69f117", size = 53835, upload-time = "2026-05-08T21:00:38.072Z" },
- { url = "https://files.pythonhosted.org/packages/a9/12/a6ba6482bb5ea3260c000c9b20881c95fa11c6b30173715668259f844ed7/propcache-0.5.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:fc299c129490f55f254cd90be0deca4764e36e9a7c08b4aa588479a3bbed3098", size = 54545, upload-time = "2026-05-08T21:00:39.319Z" },
- { url = "https://files.pythonhosted.org/packages/a9/19/7fa086f5764c59ec8a8e157cd93aa8497acc00aba9dcdec56bfffb32602d/propcache-0.5.2-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a6ae2198be502c10f09b2516e7b5d019816924bc3183a43ce792a7bd6625e6f4", size = 59886, upload-time = "2026-05-08T21:00:40.621Z" },
- { url = "https://files.pythonhosted.org/packages/a1/e4/5d7663dc8235956c8f5281698a3af1d351d8820341ddd890f59d9a9127f2/propcache-0.5.2-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:6041d31504dc1779d700e1edcfb08eea334b357620b06681a4eabb57a74e574e", size = 63261, upload-time = "2026-05-08T21:00:41.775Z" },
- { url = "https://files.pythonhosted.org/packages/4a/4a/15a03adee24d6350da4292caeac44c34c033d2afe5e87eb370f38854560f/propcache-0.5.2-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f7eabc04151c78a9f4d5bbb5f1faf571e4defeb4b585e0fe95b60ff2dbe4d3d7", size = 64184, upload-time = "2026-05-08T21:00:43.018Z" },
- { url = "https://files.pythonhosted.org/packages/8b/c6/979176efdaa3d239e36d503d5af63a0a773b36662ed8f52e5b6a6d9fd40e/propcache-0.5.2-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4db0ba63d693afd40d249bd93f842b5f144f8fcbb83de05660373bcf30517b1d", size = 61534, upload-time = "2026-05-08T21:00:44.507Z" },
- { url = "https://files.pythonhosted.org/packages/c8/22/63e8cd1bae4c2d2be6493b6b7d10566ddafad88137cfbc99964a1119853c/propcache-0.5.2-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:1dbcf7675229b35d31abb6547d8ebc8c27a830ac3f9a794edff6254873ec7c0a", size = 61500, upload-time = "2026-05-08T21:00:45.796Z" },
- { url = "https://files.pythonhosted.org/packages/60/5a/28e5d9acbac1cc9ccb67045e8c1b943aa8d79fdf39c93bd73cacd68008ea/propcache-0.5.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d310c013aad2c72f1c3f2f8dd3279d460a858c551f97aeb8c63e4693cca7b4d2", size = 59994, upload-time = "2026-05-08T21:00:47.093Z" },
- { url = "https://files.pythonhosted.org/packages/f3/40/db650677f554a95b9c01a7c9d93d629e93a15562f5deb4573c9ee136fed2/propcache-0.5.2-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:06187263ddad280d05b4d8a8b3bb7d164cbebd469236544a42e6d9b28ac6a4fa", size = 56884, upload-time = "2026-05-08T21:00:48.376Z" },
- { url = "https://files.pythonhosted.org/packages/80/45/70b39b89516ff8b96bf732fa6fded8cef20f293cb1508690101c3c07ec51/propcache-0.5.2-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:3115559b8effafd63b142ea5ed53d63a16ea6469cbc63dce4ee194b42db5d853", size = 63464, upload-time = "2026-05-08T21:00:49.954Z" },
- { url = "https://files.pythonhosted.org/packages/f9/e2/fa59d3a89eac5534293124af4f1d0d0ada091ce4a0ab4610ce03fd2bdd8d/propcache-0.5.2-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:c60462af8e6dc30c35407c7237ea908d777b22862bbee27bc4699c0d8bcdc45a", size = 61588, upload-time = "2026-05-08T21:00:51.281Z" },
- { url = "https://files.pythonhosted.org/packages/0b/97/efb547a55c4bc7381cfb202d6a2239ac621045277bc1ea5dfd3a7f0516c0/propcache-0.5.2-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:40314bca9ac559716fe374094fc81c11dcc34b64fd6c585360f5775690505704", size = 64667, upload-time = "2026-05-08T21:00:52.602Z" },
- { url = "https://files.pythonhosted.org/packages/92/56/f5c7d9b4b7595d5127da38974d791b2153f3d1eae6c674af3583ace92ad3/propcache-0.5.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:cfa21e036ce1e1db2be04ba3b85d2df1bb1702fa01932d984c5464c665228ff4", size = 62463, upload-time = "2026-05-08T21:00:54.303Z" },
- { url = "https://files.pythonhosted.org/packages/bd/3b/484a3a65fc9f9f60c41dcd17b428bace5389544e2c680994534a20755066/propcache-0.5.2-cp313-cp313-win32.whl", hash = "sha256:f156a3529f38063b6dbaf356e15602a7f95f8055b1295a438433a6386f10463d", size = 38621, upload-time = "2026-05-08T21:00:55.808Z" },
- { url = "https://files.pythonhosted.org/packages/1c/fd/3f0f10dba4dabad3bf53102be007abf55481067952bde0fdddff439e7c61/propcache-0.5.2-cp313-cp313-win_amd64.whl", hash = "sha256:dfed59d0a5aeb01e242e66ff0300bc4a265a7c05f612d30016f0b60b1017d757", size = 41649, upload-time = "2026-05-08T21:00:57.061Z" },
- { url = "https://files.pythonhosted.org/packages/90/ec/6ce619cc32bb500a482f811f9cd509368b4e58e638d13f2c68f370d6b475/propcache-0.5.2-cp313-cp313-win_arm64.whl", hash = "sha256:ba338430e87ceb9c8f0cf754de38a9860560261e56c00376debd628698a7364f", size = 37636, upload-time = "2026-05-08T21:00:58.646Z" },
- { url = "https://files.pythonhosted.org/packages/1b/82/c1d268bbbf2ef981c5bf0fbbe746db617c66e3bcefe431a1aa8943fbe23a/propcache-0.5.2-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:a592f5f3da71c8691c788c13cb6734b6d17663d2e1cb8caddf0673d01ef8847d", size = 98872, upload-time = "2026-05-08T21:00:59.889Z" },
- { url = "https://files.pythonhosted.org/packages/f4/d4/52c871e73e864e6b34c0e2d58ac1ec5ccd149497ddc7ad2137ae98323a35/propcache-0.5.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:6a997d0489e9668a384fcfd5061b857aa5361de73191cac204d04b889cfbbafa", size = 56257, upload-time = "2026-05-08T21:01:01.195Z" },
- { url = "https://files.pythonhosted.org/packages/67/f0/9b90ca2a210b3d09bcfcd96ecd0f55545c091535abce2a45de2775cfd357/propcache-0.5.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:10734b5484ea113152ee25a91dccedf81631791805d2c9ccb054958e51842c94", size = 56696, upload-time = "2026-05-08T21:01:02.941Z" },
- { url = "https://files.pythonhosted.org/packages/9d/0e/6e9d4ba07c8e56e21ddec1e75f12148142b21ca83a51871babce095334f4/propcache-0.5.2-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:cafca7e56c12bb02ae16d283742bef25a61122e9dab2b5b3f2ccbe589ce32164", size = 62378, upload-time = "2026-05-08T21:01:04.475Z" },
- { url = "https://files.pythonhosted.org/packages/65/19/c10badaa463dde8a27ce884f8ee2ec37e6035b7c9f5ff0c8f74f06f08dac/propcache-0.5.2-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f064f8d2b59177878b7615df1735cd8fe3462ed6be8c7b217d17a276489c2b7f", size = 65283, upload-time = "2026-05-08T21:01:05.959Z" },
- { url = "https://files.pythonhosted.org/packages/b0/b6/93bea99ca80e19cef6512a8580e5b7857bbe09422d9daa7fd4ef5723306c/propcache-0.5.2-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f78abfa8dfc32376fd1aacf597b2f2fbbe0ea751419aee718af5d4f82537ef8c", size = 66616, upload-time = "2026-05-08T21:01:07.228Z" },
- { url = "https://files.pythonhosted.org/packages/83/e4/5c7462e50625f051f37fb38b8224f7639f667184bbd34424ec83819bb1b7/propcache-0.5.2-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f7467da8a9822bf1a55336f877340c5bcbd3c482afc43a99771169f74a26dedc", size = 63773, upload-time = "2026-05-08T21:01:08.514Z" },
- { url = "https://files.pythonhosted.org/packages/ca/b6/99238894047b13c823be25027e736626cd414a52a5e30d2c3347c2733529/propcache-0.5.2-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a6ddc6ac9e25de626c1f129c1b467d7ecd33ce2237d3fd0c4e429feef0a7ee1f", size = 63664, upload-time = "2026-05-08T21:01:09.874Z" },
- { url = "https://files.pythonhosted.org/packages/85/1e/a3a1a63116a2b8edb415a8bb9a6f0c34bd03830b1e18e8ce2904e1dc1cf4/propcache-0.5.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:2f22cbbac9e26a8e864c0985ff1268d5d939d53d9d9411a9824279097e03a2cb", size = 62643, upload-time = "2026-05-08T21:01:11.132Z" },
- { url = "https://files.pythonhosted.org/packages/e4/03/893cf147de2fc6543c5eaa07ad833170e7e2a2385725bbebe8c0503723bb/propcache-0.5.2-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:fc76378c62a0f04d0cd82fbb1a2cd2d7e28fcb40d5873f28a6c44e388aaa2751", size = 59595, upload-time = "2026-05-08T21:01:12.387Z" },
- { url = "https://files.pythonhosted.org/packages/86/3b/04c1a2e12c57766568ba75ba72b3bf2042818d4c1425fab6fc07155c7cff/propcache-0.5.2-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:acd2c8edba48e31e58a363b8cf4e5c7db3b04b3f9e371f601df30d9b0d244836", size = 65711, upload-time = "2026-05-08T21:01:13.676Z" },
- { url = "https://files.pythonhosted.org/packages/1c/34/80f8d0099f8d6bacc4de1624c85672681c8cd1149ca2da0e38fd120b817f/propcache-0.5.2-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:452b5065457eb9991ec5eb38ff41d6cd4c991c9ac7c531c4d5849ae473a9a13f", size = 64247, upload-time = "2026-05-08T21:01:14.936Z" },
- { url = "https://files.pythonhosted.org/packages/f3/1a/8b08f3a5f1037e9e370c55883ceeeee0f6dd0416fb2d2d67b8bfc91f2a79/propcache-0.5.2-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:3430bb2bfe1331885c427745a751e774ee679fd4344f80b97bf879815fe8fa55", size = 67102, upload-time = "2026-05-08T21:01:16.281Z" },
- { url = "https://files.pythonhosted.org/packages/34/68/8bdb7bb7756d76e005490649d10e4a8369e610c74d619f71e1aedf889e9c/propcache-0.5.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:cef6cea3922890dd6c9654971001fa797b526c16ab5e1e46c05fd6f877be7568", size = 64964, upload-time = "2026-05-08T21:01:17.57Z" },
- { url = "https://files.pythonhosted.org/packages/0a/aa/50fb0b5d3968b61a510926ff8b8465f1d6e976b3ab74496d7a4b9fc42515/propcache-0.5.2-cp313-cp313t-win32.whl", hash = "sha256:72d61e16dd78228b58c5d47be830ff3da7e5f139abdf0aef9d86cde1c5cf2191", size = 42546, upload-time = "2026-05-08T21:01:18.946Z" },
- { url = "https://files.pythonhosted.org/packages/ae/4c/0ddbae64321bd4a95bcbfc19307238016b5b1fee645c84626c8d539e5b74/propcache-0.5.2-cp313-cp313t-win_amd64.whl", hash = "sha256:0958834041a0166d343b8d2cedcd8bcbaeb4fdbe0cf08320c5379f143c3be6e7", size = 46330, upload-time = "2026-05-08T21:01:20.162Z" },
- { url = "https://files.pythonhosted.org/packages/00/d9/9cddc8efb78d8af264c5ec9f6d10b62f57c515feda8d321595f56010fb23/propcache-0.5.2-cp313-cp313t-win_arm64.whl", hash = "sha256:6de8bd93ddde9b992cf2b2e0d796d501a19026b5b9fd87356d7d0779531a8d96", size = 40521, upload-time = "2026-05-08T21:01:21.399Z" },
- { url = "https://files.pythonhosted.org/packages/e2/ea/23ee535d90ce8bcc465a3028eb3cc0ce3bd1005f4bb27710b30587de798d/propcache-0.5.2-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:46088abff4cba581dea21ae0467a480526cb25aa5f3c269e909f800328bc3999", size = 94662, upload-time = "2026-05-08T21:01:22.683Z" },
- { url = "https://files.pythonhosted.org/packages/b5/06/c5a52f419b5d8972f8d46a7577476090d8e3263ff589ce40b5ca4968d5be/propcache-0.5.2-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:fc88b26f08d634f7bc819a7852e5214f5802641ab8d9fd5326892292eee1993e", size = 53928, upload-time = "2026-05-08T21:01:23.986Z" },
- { url = "https://files.pythonhosted.org/packages/63/b1/4260d67d6bd85e58a66b72d54ce15d5de789b6f3870cc6bedf8ff9667401/propcache-0.5.2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:97797ebb098e670a2f92dd66f32897e30d7615b14e7f59711de23e30a9072539", size = 54650, upload-time = "2026-05-08T21:01:25.305Z" },
- { url = "https://files.pythonhosted.org/packages/70/06/2f46c318e3307cd7a6a7481def374ce838c0fe20084b39dd54b0879d0e99/propcache-0.5.2-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ba57fffe4ac99c5d30076161b5866336d97600769bad35cc68f7774b15298a4e", size = 59912, upload-time = "2026-05-08T21:01:26.545Z" },
- { url = "https://files.pythonhosted.org/packages/4c/29/fe1aebec2ce57ab985a9c382bded1124431f85078113aa222c5d278430d4/propcache-0.5.2-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:583c19759d9eec1e5b69e2fbef36a7d9c326041be9746cb822d335c8cedc2979", size = 63300, upload-time = "2026-05-08T21:01:27.937Z" },
- { url = "https://files.pythonhosted.org/packages/b4/18/2334b26768b6c82be8c69e83671b767d5ef426aa09b0cba6c2ea47816774/propcache-0.5.2-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d0326e2e5e1f3163fa306c834e48e8d490e5fae607a097a40c0648109b47ba80", size = 64208, upload-time = "2026-05-08T21:01:29.484Z" },
- { url = "https://files.pythonhosted.org/packages/2b/76/7f1bfd6afff4c5e38e36a3c6d68eb5f4b7311ea80baf693db78d95b603c4/propcache-0.5.2-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e00820e192c8dbebcafb383ebbf99030895f09905e7a0eb2e0340a0bcc2bc825", size = 61633, upload-time = "2026-05-08T21:01:31.068Z" },
- { url = "https://files.pythonhosted.org/packages/c4/46/b3ff8aba2b4953a3e50de2cf72f1b5748b8eca93b15f3dc2c84339084c09/propcache-0.5.2-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c66afea89b1e43725731d2004732a046fe6fe955d51f952c3e95a7314a284a39", size = 61724, upload-time = "2026-05-08T21:01:32.374Z" },
- { url = "https://files.pythonhosted.org/packages/c5/01/814cfcafbcff954f94c01cf30e097ddc88a076b5440fbcf4570753437d40/propcache-0.5.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:d4dc37dec6c6cdad0b57881a5658fd14fbf53e333b1a86cf86559f190e1d9ec4", size = 60069, upload-time = "2026-05-08T21:01:33.67Z" },
- { url = "https://files.pythonhosted.org/packages/da/68/5c6f7622d510cc666a300687e06fd060c1a43361c0c9b20d284f06d8096a/propcache-0.5.2-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:5570dbcc97571c15f68068e529c92715a12f8d54030e272d264b377e22bd17a5", size = 57099, upload-time = "2026-05-08T21:01:34.915Z" },
- { url = "https://files.pythonhosted.org/packages/55/27/9cb0b4c679124085327957d42521c99dba04c88c90c3e55a6f0b633ebccc/propcache-0.5.2-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:f814362777a9f841adddb200ecdf8f5cb1e5a3c4b7a86378edbd6ccb26edd702", size = 63391, upload-time = "2026-05-08T21:01:36.231Z" },
- { url = "https://files.pythonhosted.org/packages/f0/9d/7258aaa5bdf60fc6f27591eef6fe52768cb0beda7140be477c8b12c9794a/propcache-0.5.2-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:196913dea116aeb5a2ba95af4ddcb7ea85559ae07d8eee8751688310d09168c3", size = 61626, upload-time = "2026-05-08T21:01:37.545Z" },
- { url = "https://files.pythonhosted.org/packages/8e/0d/41c602003e8a9b16fe1e7eadf62c7bfba9d5474370b24200bf48b315f45f/propcache-0.5.2-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:6e7b8719005dd1175be4ab1cd25e9b98659a5e0347331506ec6760d2773a7fb5", size = 64781, upload-time = "2026-05-08T21:01:38.83Z" },
- { url = "https://files.pythonhosted.org/packages/8b/f3/38e66b1856e9bd079deea015bc4a55f7767c0e4db2f7dcf69e7e680ba4ce/propcache-0.5.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:51f96d685ab16e88cab128cd37a52c5da540809c8b879fa047731bfcb4ad35a4", size = 62570, upload-time = "2026-05-08T21:01:40.415Z" },
- { url = "https://files.pythonhosted.org/packages/95/ca/bbfe9b910ce57dde8bb4876b4520fc02a4e89497c10de26be936758a3aaa/propcache-0.5.2-cp314-cp314-win32.whl", hash = "sha256:cc6fc3cc62e8501d3ed62894425040d2728ecddb1ed072737a5c70bd537aa9f0", size = 39436, upload-time = "2026-05-08T21:01:41.654Z" },
- { url = "https://files.pythonhosted.org/packages/61/d2/45c9defbaa1ea297035d9d4cce9e8f80daafbf19319c6007f157c6256ea9/propcache-0.5.2-cp314-cp314-win_amd64.whl", hash = "sha256:81e3a30b0bb60caa22033dd0f8a3618d1d67356212514f62c57db75cb0ef410c", size = 42373, upload-time = "2026-05-08T21:01:43.041Z" },
- { url = "https://files.pythonhosted.org/packages/44/68/9ea5103f41d5217d7d6ec24db90018e23aebec070c3f9a6e54d12b841fd8/propcache-0.5.2-cp314-cp314-win_arm64.whl", hash = "sha256:0d2c9bf8528f135dbb805ce027567e09164f7efa51a2be07458a2c0420f292d0", size = 38554, upload-time = "2026-05-08T21:01:44.336Z" },
- { url = "https://files.pythonhosted.org/packages/8a/81/fadf555f42d3b762eea8a53950b0489fdc0aa9da5f8ed9e10ce0a4e01b48/propcache-0.5.2-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:4bc8ff1feffc6a61c7002ffe84634c41b822e104990ae009f44a0834430070bb", size = 99395, upload-time = "2026-05-08T21:01:45.883Z" },
- { url = "https://files.pythonhosted.org/packages/f5/c9/c61e134a686949cf7971af3a390148b1156f7be81c73bc0cd12c873e2d48/propcache-0.5.2-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:79aa3ff0a9b566633b642fa9caf7e21ed1c13d6feca718187873f199e1514078", size = 56653, upload-time = "2026-05-08T21:01:47.307Z" },
- { url = "https://files.pythonhosted.org/packages/cb/73/daf935ea7048ddd7ec8eec5345b4a40b619d2d178b3c0a0900796bc3c794/propcache-0.5.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:1b31822f4474c4036bae62de9402710051d431a606d6a0f907fec79935a071aa", size = 56914, upload-time = "2026-05-08T21:01:48.573Z" },
- { url = "https://files.pythonhosted.org/packages/79/9f/aba959b435ea18617edd7cf0a7ad0b9c574b8fc7e3d2cd55fb59cb255d33/propcache-0.5.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:13fef48778b5a2a756523fdb781326b028ca75e32858b04f2cdd19f394564917", size = 62567, upload-time = "2026-05-08T21:01:49.903Z" },
- { url = "https://files.pythonhosted.org/packages/6c/a1/859942de9a791ff42f6141736f5b37749b8f53e65edfa49638c67dd67e6a/propcache-0.5.2-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:8b73ab70f1a3351fbc71f663b3e645af6dd0329100c353081cf69c37433fc6fe", size = 65542, upload-time = "2026-05-08T21:01:51.204Z" },
- { url = "https://files.pythonhosted.org/packages/b5/61/315bc0fd6c0fc7f80a528b8afd209e5fc4a875ea79571b91b8f50f442907/propcache-0.5.2-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5538d2c13d93e4698af7e092b57bc7298fd35d1d58e656ae18f23ee0d0378e03", size = 66845, upload-time = "2026-05-08T21:01:52.539Z" },
- { url = "https://files.pythonhosted.org/packages/47/f7/9f8122e3132e8e354ac41975ef8f1099be7d5a16bc7ae562734e993665c0/propcache-0.5.2-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:cd645f03898405cabe694fb8bc35241e3a9c332ec85627584fe3de201452b335", size = 63985, upload-time = "2026-05-08T21:01:53.847Z" },
- { url = "https://files.pythonhosted.org/packages/c8/54/c317819ec157cbf6f35df9df9657a6f82daf34d5faf15948b2f639c2192e/propcache-0.5.2-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a473b3440261e0c60706e732b2ed2f517857344fc21bf48fdfe211e2d98eb285", size = 63999, upload-time = "2026-05-08T21:01:55.179Z" },
- { url = "https://files.pythonhosted.org/packages/5a/56/387e3f7dfce0a9233df41fb888aa1c30222cb4bbbf09537c02dd9bd85fe2/propcache-0.5.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:7afa37062e6650640e932e4cc9297d81f9f42d9944029cc386b8247dea4da837", size = 62779, upload-time = "2026-05-08T21:01:57.489Z" },
- { url = "https://files.pythonhosted.org/packages/a1/9c/596784cb5824ed61ee960d3f8655a3f0993e107c6e98ab6c818b7fb92ccb/propcache-0.5.2-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:8a90efd5777e996e42d568db9ac740b944d691e565cbfd31b2f7832f9184b2b8", size = 59796, upload-time = "2026-05-08T21:01:58.736Z" },
- { url = "https://files.pythonhosted.org/packages/c2/3d/1a6cfa1726a48542c1e8784a0761421476a5b68e09b7f36bf95eb954aaba/propcache-0.5.2-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:f19bb891234d72535764d703bfed1153cc34f4214d5bd7150aee1eec9e8f4366", size = 66023, upload-time = "2026-05-08T21:02:00.228Z" },
- { url = "https://files.pythonhosted.org/packages/e4/0e/05fd6990369477076e4e280bcb970de760fddf0161a46e988bc95f7940ec/propcache-0.5.2-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:32775082acd2d807ee3db715c7770d38767b817870acfa08c29e057f3c4d5b56", size = 64448, upload-time = "2026-05-08T21:02:01.888Z" },
- { url = "https://files.pythonhosted.org/packages/cd/86/5f8da315a4309c62c10c0b2516b17492d5d3bbe1bb862b96604db67e2a37/propcache-0.5.2-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:9282fb1a3bccd038da9f768b927b24a0c753e466c086b7c4f3c6982851eefb2d", size = 67329, upload-time = "2026-05-08T21:02:03.484Z" },
- { url = "https://files.pythonhosted.org/packages/da/d3/3368efe79ab21f0cdf86ef49895811c9cc933131d4cde1f28a624e22e712/propcache-0.5.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:cc49723e2f60d6b32a0f0b08a3fd6d13203c07f1cd9566cfce0f12a917c967a2", size = 65172, upload-time = "2026-05-08T21:02:04.745Z" },
- { url = "https://files.pythonhosted.org/packages/d5/07/127e8b0bacfb325396196f9d976a22453049b89b9b2b08477cc3145faa44/propcache-0.5.2-cp314-cp314t-win32.whl", hash = "sha256:2d7aa89ebca5acc98cba9d1472d976e394782f587bad6661003602a619fd1821", size = 43813, upload-time = "2026-05-08T21:02:06.025Z" },
- { url = "https://files.pythonhosted.org/packages/88/fb/46dad6c0ae49ed230ab1b16c890c2b6314e2403e6c412976f4a72d64a527/propcache-0.5.2-cp314-cp314t-win_amd64.whl", hash = "sha256:d447bb0b3054be5818458fbb171208b1d9ff11eba14e18ca18b90cbb45767370", size = 47764, upload-time = "2026-05-08T21:02:07.353Z" },
- { url = "https://files.pythonhosted.org/packages/e7/c4/a47d0a63aa309d10d59ede6e9d4cff03a344a79d1f0f4cd0cd74997b53e0/propcache-0.5.2-cp314-cp314t-win_arm64.whl", hash = "sha256:fe67a3d11cd9b4efabfa45c3d00ffba2b26811442a73a581a94b67c2b5faccf6", size = 41140, upload-time = "2026-05-08T21:02:09.065Z" },
- { url = "https://files.pythonhosted.org/packages/3a/ed/1cdcab6ba3d6ab7feca11fc14f0eeea80755bb53ef4e892079f31b10a25f/propcache-0.5.2-py3-none-any.whl", hash = "sha256:be1ddfcbb376e3de5d2e2db1d58d6d67463e6b4f9f040c000de8e300295465fe", size = 14036, upload-time = "2026-05-08T21:02:10.673Z" },
-]
-
-[[package]]
-name = "pydantic"
-version = "2.13.4"
-source = { registry = "https://pypi.org/simple" }
-dependencies = [
- { name = "annotated-types", version = "0.7.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" },
- { name = "annotated-types", version = "0.8.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" },
- { name = "pydantic-core" },
- { name = "typing-extensions" },
- { name = "typing-inspection" },
-]
-sdist = { url = "https://files.pythonhosted.org/packages/18/a5/b60d21ac674192f8ab0ba4e9fd860690f9b4a6e51ca5df118733b487d8d6/pydantic-2.13.4.tar.gz", hash = "sha256:c40756b57adaa8b1efeeced5c196f3f3b7c435f90e84ea7f443901bec8099ef6", size = 844775, upload-time = "2026-05-06T13:43:05.343Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/fd/7b/122376b1fd3c62c1ed9dc80c931ace4844b3c55407b6fb2d199377c9736f/pydantic-2.13.4-py3-none-any.whl", hash = "sha256:45a282cde31d808236fd7ea9d919b128653c8b38b393d1c4ab335c62924d9aba", size = 472262, upload-time = "2026-05-06T13:43:02.641Z" },
-]
-
-[[package]]
-name = "pydantic-core"
-version = "2.46.4"
-source = { registry = "https://pypi.org/simple" }
-dependencies = [
- { name = "typing-extensions" },
-]
-sdist = { url = "https://files.pythonhosted.org/packages/9d/56/921726b776ace8d8f5db44c4ef961006580d91dc52b803c489fafd1aa249/pydantic_core-2.46.4.tar.gz", hash = "sha256:62f875393d7f270851f20523dd2e29f082bcc82292d66db2b64ea71f64b6e1c1", size = 471464, upload-time = "2026-05-06T13:37:06.98Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/e7/08/f1ba952f1c8ae5581c70fa9c6da89f247b83e3dd8c09c035d5d7931fc23d/pydantic_core-2.46.4-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:a396dcc17e5a0b164dbe026896245a4fa9ff402edca1dff0be3d53a517f74de4", size = 2113146, upload-time = "2026-05-06T13:37:36.537Z" },
- { url = "https://files.pythonhosted.org/packages/56/c6/65f646c7ff09bd257f660434adb45c4dfcbbcebcc030562fecf6f5bf887d/pydantic_core-2.46.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:da4b951fe36dc7c3a1ccb4e3cd1747c3542b8c9ceede8fc86cae054e764485f5", size = 1949769, upload-time = "2026-05-06T13:37:46.365Z" },
- { url = "https://files.pythonhosted.org/packages/64/ba/bfb1d928fd5b49e1258935ff104ae356e9fd89384a55bf9f847e9193ad40/pydantic_core-2.46.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bb63e0198ca18aad131c089b9204c23079c3afa95487e561f4c522d519e55aba", size = 1974958, upload-time = "2026-05-06T13:37:28.611Z" },
- { url = "https://files.pythonhosted.org/packages/4e/74/76223bfb117b64af743c9b6670d1364516f5c0604f96b48f3272f6af6cc6/pydantic_core-2.46.4-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f47286a97f0bc9b8859519809077b91b2cefe4ae47fcbf5e466a009c1c5d742b", size = 2042118, upload-time = "2026-05-06T13:36:55.216Z" },
- { url = "https://files.pythonhosted.org/packages/cb/7b/848732968bc8f48f3187542f08358b9d842db564147b256669426ebb1652/pydantic_core-2.46.4-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:905a0ed8ea6f2d61c1738835f99b699348d7857379083e5fc497fa0c967a407c", size = 2222876, upload-time = "2026-05-06T13:38:25.455Z" },
- { url = "https://files.pythonhosted.org/packages/b5/2f/e90b63ee2e14bd8d3db8f705a6d75d64e6ee1b7c2c8833747ce706e1e0ce/pydantic_core-2.46.4-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ea793e075b70290d89d8142074262885d3f7da19634845135751bd6344f73b50", size = 2286703, upload-time = "2026-05-06T13:37:53.304Z" },
- { url = "https://files.pythonhosted.org/packages/ba/1e/acc4d70f88a0a277e4a1fa77ebb985ceabaf900430f875bf9338e11c9420/pydantic_core-2.46.4-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:395aebd9183f9d112f569aeb5b2214d1a10a33bec8456447f7fbdfa51d38d4cd", size = 2092042, upload-time = "2026-05-06T13:38:46.981Z" },
- { url = "https://files.pythonhosted.org/packages/a9/da/0a422b57bf8504102bf3c4ccea9c41bab5a5cee6a54650acf8faf67f5a24/pydantic_core-2.46.4-cp310-cp310-manylinux_2_31_riscv64.whl", hash = "sha256:b078afbc25f3a1436c7a1d2cd3e322497ee99615ba97c563566fdf46aff1ee01", size = 2117231, upload-time = "2026-05-06T13:39:23.146Z" },
- { url = "https://files.pythonhosted.org/packages/bd/2a/2ac13c3af305843e23c5078c53d135656b3f05a2fd78cb7bbbb12e97b473/pydantic_core-2.46.4-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:f747929cf940cddb5b3668a390056ddd5ba2e5010615ea2dcf4f9c4f3ab8791d", size = 2168388, upload-time = "2026-05-06T13:40:08.06Z" },
- { url = "https://files.pythonhosted.org/packages/72/04/2beacf7e1607e93eefe4aed1b4709f079b905fb77530179d4f7c71745f22/pydantic_core-2.46.4-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:daa27d92c36f24388fe3ad306b174781c747627f134452e4f128ea00ce1fe8c4", size = 2184769, upload-time = "2026-05-06T13:38:13.901Z" },
- { url = "https://files.pythonhosted.org/packages/9e/29/d2b9fd9f539133548eaf622c06a4ce176cb46ac59f32d0359c4abc0de047/pydantic_core-2.46.4-cp310-cp310-musllinux_1_1_armv7l.whl", hash = "sha256:19e51f073cd3df251856a8a4189fbdf1de4012c3ebacfb1884f94f1eb406079f", size = 2319312, upload-time = "2026-05-06T13:39:08.24Z" },
- { url = "https://files.pythonhosted.org/packages/7c/af/0f7a5b85fec6075bea96e3ef9187de38fccced0de92c1e7feda8d5cc7bb9/pydantic_core-2.46.4-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:c1747f85cee84c26985853c6f3d9bd3e75da5212912443fa111c113b9c246f39", size = 2361817, upload-time = "2026-05-06T13:38:43.2Z" },
- { url = "https://files.pythonhosted.org/packages/25/a4/73363fec545fd3ec025490bdda2743c56d0dd5b6266b1a53bbe9e4265375/pydantic_core-2.46.4-cp310-cp310-win32.whl", hash = "sha256:2f84c03c8607173d16b5a854ec68a2f9079ae03237a54fb506d13af47e1d018d", size = 1987085, upload-time = "2026-05-06T13:39:25.497Z" },
- { url = "https://files.pythonhosted.org/packages/01/aa/62f082da2c91fac1c234bc9ee0066257ce83f0604abd72e4c9d5991f2d84/pydantic_core-2.46.4-cp310-cp310-win_amd64.whl", hash = "sha256:8358a950c8909158e3df31538a7e4edc2d7265a7c54b47f0864d9e5bae9dcebf", size = 2074311, upload-time = "2026-05-06T13:39:59.922Z" },
- { url = "https://files.pythonhosted.org/packages/5c/fa/6d7708d2cfc1a832acb6aeb0cd16e801902df8a0f583bb3b4b527fde022e/pydantic_core-2.46.4-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:0e96592440881c74a213e5ad528e2b24d3d4f940de2766bed9010ab1d9e51594", size = 2111872, upload-time = "2026-05-06T13:40:27.596Z" },
- { url = "https://files.pythonhosted.org/packages/ae/6f/aa064a3e74b5745afbdf250594f38e7ead05e2d651bcb35994b9417a0d4d/pydantic_core-2.46.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e0d65b8c354be7fb5f720c3caa8bc940bc2d20ce749c8e06135f07f8ed95dd7c", size = 1948255, upload-time = "2026-05-06T13:39:12.574Z" },
- { url = "https://files.pythonhosted.org/packages/43/3a/41114a9f7569b84b4d84e7a018c57c56347dac30c0d4a872946ec4e36c46/pydantic_core-2.46.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7bfb192b3f4b9e8a89b6277b6ce787564f62cfd272055f6e685726b111dc7826", size = 1972827, upload-time = "2026-05-06T13:38:19.841Z" },
- { url = "https://files.pythonhosted.org/packages/ef/25/1ab42e8048fe551934d9884e8d64daa7e990ad386f310a15981aeb6a5b08/pydantic_core-2.46.4-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:9037063db01f09b09e237c282b6792bd4da634b5402c4e7f0c61effed7701a04", size = 2041051, upload-time = "2026-05-06T13:38:10.447Z" },
- { url = "https://files.pythonhosted.org/packages/94/c2/1a934597ddf08da410385b3b7aae91956a5a76c635effef456074fad7e88/pydantic_core-2.46.4-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fc010ab034c8c7452522748bf937df58020d256ccae0874463d1f4d01758af8e", size = 2221314, upload-time = "2026-05-06T13:40:13.089Z" },
- { url = "https://files.pythonhosted.org/packages/02/6d/9e8ad178c9c4df27ad3c8f25d1fe2a7ab0d2ba0559fad4aee5d3d1f16771/pydantic_core-2.46.4-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8c5dac79fa1614d1e06ca695109c6105923bd9c7d1d6c918d4e637b7e6b32fd3", size = 2285146, upload-time = "2026-05-06T13:38:59.224Z" },
- { url = "https://files.pythonhosted.org/packages/80/50/540cd3aeefc041beb111125c4bff779831a2111fc6b15a9138cda277d32c/pydantic_core-2.46.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f9fa868638bf362d3d138ea55829cefb3d5f4b0d7f142234382a15e2485dbec4", size = 2089685, upload-time = "2026-05-06T13:38:17.762Z" },
- { url = "https://files.pythonhosted.org/packages/6b/a4/b440ad35f05f6a38f89fa0f149accb3f0e02be94ca5e15f3c449a61b4bc9/pydantic_core-2.46.4-cp311-cp311-manylinux_2_31_riscv64.whl", hash = "sha256:17299feefe090f2caa5b8e37222bb5f663e4935a8bfa6931d4102e5df1a9f398", size = 2115420, upload-time = "2026-05-06T13:37:58.195Z" },
- { url = "https://files.pythonhosted.org/packages/99/61/de4f55db8dfd57bfdfa9a12ec90fe1b57c4f41062f7ca86f08586b3e0ac0/pydantic_core-2.46.4-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:4c63ebc82684aa89d9a3bcbd13d515b3be44250dc68dd3bd81526c1cb31286c3", size = 2165122, upload-time = "2026-05-06T13:37:01.167Z" },
- { url = "https://files.pythonhosted.org/packages/f7/52/7c529d7bdb2d1068bd52f51fe32572c8301f9a4febf1948f10639f1436f5/pydantic_core-2.46.4-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:aaa2a54443eff1950ba5ddc6b6ccda0d9c84a364276a62f969bdf2a390650848", size = 2182573, upload-time = "2026-05-06T13:38:45.04Z" },
- { url = "https://files.pythonhosted.org/packages/37/b3/7c40325848ba78247f2812dcf9c7274e38cd801820ca6dd9fe63bcfb0eb4/pydantic_core-2.46.4-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:18e5ceec2ab67e6d5f1a9085e5a24c9c4e2ac4545730bfe668680bca05e555f3", size = 2317139, upload-time = "2026-05-06T13:37:15.539Z" },
- { url = "https://files.pythonhosted.org/packages/d9/37/f913f81a657c865b75da6c0dbed79876073c2a43b5bd9edbe8da785e4d49/pydantic_core-2.46.4-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:a0f62d0a58f4e7da165457e995725421e0064f2255d8eccebc49f41bbc23b109", size = 2360433, upload-time = "2026-05-06T13:37:30.099Z" },
- { url = "https://files.pythonhosted.org/packages/c4/67/6acaa1be2567f9256b056d8477158cac7240813956ce86e49deae8e173b4/pydantic_core-2.46.4-cp311-cp311-win32.whl", hash = "sha256:041bde0a48fd37cf71cab1c9d56d3e8625a3793fef1f7dd232b3ff37e978ecda", size = 1985513, upload-time = "2026-05-06T13:38:15.669Z" },
- { url = "https://files.pythonhosted.org/packages/aa/e6/c505f83dfeda9a2e5c995cfd872949e4d05e12f7feb3dca72f633daefa94/pydantic_core-2.46.4-cp311-cp311-win_amd64.whl", hash = "sha256:6f2eeda33a839975441c86a4119e1383c50b47faf0cbb5176985565c6bb02c33", size = 2071114, upload-time = "2026-05-06T13:40:35.416Z" },
- { url = "https://files.pythonhosted.org/packages/0f/da/7a263a96d965d9d0df5e8de8a475f33495451117035b09acb110288c381f/pydantic_core-2.46.4-cp311-cp311-win_arm64.whl", hash = "sha256:14f4c5d6db102bd796a627bbb3a17b4cf4574b9ae861d8b7c9a9661c6dd3362d", size = 2044298, upload-time = "2026-05-06T13:38:29.754Z" },
- { url = "https://files.pythonhosted.org/packages/ce/8c/af022f0af448d7747c5154288d46b5f2bc5f17366eaa0e23e9aa04d59f3b/pydantic_core-2.46.4-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:3245406455a5d98187ec35530fd772b1d799b26667980872c8d4614991e2c4a2", size = 2106158, upload-time = "2026-05-06T13:38:57.215Z" },
- { url = "https://files.pythonhosted.org/packages/19/95/6195171e385007300f0f5574592e467c568becce2d937a0b6804f218bc49/pydantic_core-2.46.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:962ccbab7b642487b1d8b7df90ef677e03134cf1fd8880bf698649b22a69371f", size = 1951724, upload-time = "2026-05-06T13:37:02.697Z" },
- { url = "https://files.pythonhosted.org/packages/8e/bc/f47d1ff9cbb1620e1b5b697eef06010035735f07820180e74178226b27b3/pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8233f2947cf85404441fd7e0085f53b10c93e0ee78611099b5c7237e36aacbf7", size = 1975742, upload-time = "2026-05-06T13:37:09.448Z" },
- { url = "https://files.pythonhosted.org/packages/5b/11/9b9a5b0306345664a2da6410877af6e8082481b5884b3ddd78d47c6013ce/pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3a233125ac121aa3ffba9a2b59edfc4a985a76092dc8279586ab4b71390875e7", size = 2052418, upload-time = "2026-05-06T13:37:38.234Z" },
- { url = "https://files.pythonhosted.org/packages/f1/b7/a65fec226f5d78fc39f4a13c4cc0c768c22b113438f60c14adc9d2865038/pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5b712b53160b79a5850310b912a5ef8e57e56947c8ad690c227f5c9d7e561712", size = 2232274, upload-time = "2026-05-06T13:38:27.753Z" },
- { url = "https://files.pythonhosted.org/packages/68/f0/92039db98b907ef49269a8271f67db9cb78ae2fc68062ef7e4e77adb5f61/pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9401557acd873c3a7f3eb9383edef8ac4968f9510e340f4808d427e75667e7b4", size = 2309940, upload-time = "2026-05-06T13:38:05.353Z" },
- { url = "https://files.pythonhosted.org/packages/5f/97/2aab507d3d00ca626e8e57c1eac6a79e4e5fbcc63eb99733ff55d1717f65/pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:926c9541b14b12b1681dca8a0b75feb510b06c6341b70a8e500c2fdcff837cce", size = 2094516, upload-time = "2026-05-06T13:39:10.577Z" },
- { url = "https://files.pythonhosted.org/packages/22/37/a8aca44d40d737dde2bc05b3c6c07dff0de07ce6f82e9f3167aeaf4d5dea/pydantic_core-2.46.4-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:56cb4851bcaf3d117eddcef4fe66afd750a50274b0da8e22be256d10e5611987", size = 2136854, upload-time = "2026-05-06T13:40:22.59Z" },
- { url = "https://files.pythonhosted.org/packages/24/99/fcef1b79238c06a8cbec70819ac722ba76e02bc8ada9b0fd66eba40da01b/pydantic_core-2.46.4-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c68fcd102d71ea85c5b2dfac3f4f8476eff42a9e078fd5faefff6d145063536b", size = 2180306, upload-time = "2026-05-06T13:40:10.666Z" },
- { url = "https://files.pythonhosted.org/packages/ae/6c/fc44000918855b42779d007ae63b0532794739027b2f417321cddbc44f6a/pydantic_core-2.46.4-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:b2f69dec1725e79a012d920df1707de5caf7ed5e08f3be4435e25803efc47458", size = 2190044, upload-time = "2026-05-06T13:40:43.231Z" },
- { url = "https://files.pythonhosted.org/packages/6b/65/d9cadc9f1920d7a127ad2edba16c1db7916e59719285cd6c94600b0080ba/pydantic_core-2.46.4-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:8d0820e8192167f80d88d64038e609c31452eeca865b4e1d9950a27a4609b00b", size = 2329133, upload-time = "2026-05-06T13:39:57.365Z" },
- { url = "https://files.pythonhosted.org/packages/d0/cf/c873d91679f3a30bcf5e7ac280ce5573483e72295307685120d0d5ad3416/pydantic_core-2.46.4-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:fbdb89b3e1c94a30cc5edfce477c6e6a5dc4d8f84665b455c27582f211a1c72c", size = 2374464, upload-time = "2026-05-06T13:38:06.976Z" },
- { url = "https://files.pythonhosted.org/packages/47/bd/6f2fc8188f31bf10590f1e98e7b306336161fac930a8c514cd7bd828c7dc/pydantic_core-2.46.4-cp312-cp312-win32.whl", hash = "sha256:9aa768456404a8bf48a4406685ac2bec8e72b62c69313734fa3b73cf33b3a894", size = 1974823, upload-time = "2026-05-06T13:40:47.985Z" },
- { url = "https://files.pythonhosted.org/packages/40/8c/985c1d41ea1107c2534abd9870e4ed5c8e7669b5c308297835c001e7a1c4/pydantic_core-2.46.4-cp312-cp312-win_amd64.whl", hash = "sha256:e9c26f834c65f5752f3f06cb08cb86a913ceb7274d0db6e267808a708b46bc89", size = 2072919, upload-time = "2026-05-06T13:39:21.153Z" },
- { url = "https://files.pythonhosted.org/packages/c4/ba/f463d006e0c47373ca7ec5e1a261c59dc01ef4d62b2657af925fb0deee3a/pydantic_core-2.46.4-cp312-cp312-win_arm64.whl", hash = "sha256:4fc73cb559bdb54b1134a706a2802a4cddd27a0633f5abb7e53056268751ac6a", size = 2027604, upload-time = "2026-05-06T13:39:03.753Z" },
- { url = "https://files.pythonhosted.org/packages/51/a2/5d30b469c5267a17b39dec53208222f76a8d351dfac4af661888c5aee77d/pydantic_core-2.46.4-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:5d5902252db0d3cedf8d4a1bc68f70eeb430f7e4c7104c8c476753519b423008", size = 2106306, upload-time = "2026-05-06T13:37:48.029Z" },
- { url = "https://files.pythonhosted.org/packages/c1/81/4fa520eaffa8bd7d1525e644cd6d39e7d60b1592bc5b516693c7340b50f1/pydantic_core-2.46.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:c94f0688e7b8d0a67abf40e57a7eaaecd17cc9586706a31b76c031f63df052b4", size = 1951906, upload-time = "2026-05-06T13:37:17.012Z" },
- { url = "https://files.pythonhosted.org/packages/03/d5/fd02da45b659668b05923b17ba3a0100a0a3d5541e3bd8fcc4ecb711309e/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f027324c56cd5406ca49c124b0db10e56c69064fec039acc571c29020cc87c76", size = 1976802, upload-time = "2026-05-06T13:37:35.113Z" },
- { url = "https://files.pythonhosted.org/packages/21/f2/95727e1368be3d3ed485eaab7adbd7dda408f33f7a36e8b48e0144002b91/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e739fee756ba1010f8bcccb534252e85a35fe45ae92c295a06059ce58b74ccd3", size = 2052446, upload-time = "2026-05-06T13:37:12.313Z" },
- { url = "https://files.pythonhosted.org/packages/9c/86/5d99feea3f77c7234b8718075b23db11532773c1a0dbd9b9490215dc2eeb/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9d56801be94b86a9da183e5f3766e6310752b99ff647e38b09a9500d88e46e76", size = 2232757, upload-time = "2026-05-06T13:39:01.149Z" },
- { url = "https://files.pythonhosted.org/packages/d2/3a/508ac615935ef7588cf6d9e9b91309fdc2da751af865e02a9098de88258c/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2412e734dcb48da14d4e4006b82b46b74f2518b8a26ee7e58c6844a6cd6d03c4", size = 2309275, upload-time = "2026-05-06T13:37:41.406Z" },
- { url = "https://files.pythonhosted.org/packages/07/f8/41db9de19d7987d6b04715a02b3b40aea467000275d9d758ffaa31af7d50/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9551187363ffc0de2a00b2e47c25aeaeb1020b69b668762966df15fc5659dd5a", size = 2094467, upload-time = "2026-05-06T13:39:18.847Z" },
- { url = "https://files.pythonhosted.org/packages/2c/e2/f35033184cb11d0052daf4416e8e10a502ea2ac006fc4f459aee872727d1/pydantic_core-2.46.4-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:0186750b482eefa11d7f435892b09c5c606193ef3375bcf94aa00ae6bfb66262", size = 2134417, upload-time = "2026-05-06T13:40:17.944Z" },
- { url = "https://files.pythonhosted.org/packages/7e/7b/6ceeb1cc90e193862f444ebe373d8fdf613f0a82572dde03fb10734c6c71/pydantic_core-2.46.4-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:5855698a4856556d86e8e6cd8434bc3ac0314ee8e12089ae0e143f64c6256e4e", size = 2179782, upload-time = "2026-05-06T13:40:32.618Z" },
- { url = "https://files.pythonhosted.org/packages/5a/f2/c8d7773ede6af08036423a00ae0ceffce266c3c52a096c435d68c896083f/pydantic_core-2.46.4-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:cbaf13819775b7f769bf4a1f066cb6df7a28d4480081a589828ef190226881cd", size = 2188782, upload-time = "2026-05-06T13:36:51.018Z" },
- { url = "https://files.pythonhosted.org/packages/59/31/0c864784e31f09f05cdd87606f08923b9c9e7f6e51dd27f20f62f975ce9f/pydantic_core-2.46.4-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:633147d34cf4550417f12e2b1a0383973bdf5cdfde212cb09e9a581cf10820be", size = 2328334, upload-time = "2026-05-06T13:40:37.764Z" },
- { url = "https://files.pythonhosted.org/packages/c2/eb/4f6c8a41efa30baa755590f4141abf3a8c370fab610915733e74134a7270/pydantic_core-2.46.4-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:82cf5301172168103724d49a1444d3378cb20cdee30b116a1bd6031236298a5d", size = 2372986, upload-time = "2026-05-06T13:39:34.152Z" },
- { url = "https://files.pythonhosted.org/packages/5b/24/b375a480d53113860c299764bfe9f349a3dc9108b3adc0d7f0d786492ebf/pydantic_core-2.46.4-cp313-cp313-win32.whl", hash = "sha256:9fa8ae11da9e2b3126c6426f147e0fba88d96d65921799bb30c6abd1cb2c97fb", size = 1973693, upload-time = "2026-05-06T13:37:55.072Z" },
- { url = "https://files.pythonhosted.org/packages/7e/e8/cff247591966f2d22ec8c003cd7587e27b7ba7b81ab2fb888e3ab75dc285/pydantic_core-2.46.4-cp313-cp313-win_amd64.whl", hash = "sha256:6b3ace8194b0e5204818c92802dcdca7fc6d88aabbb799d7c795540d9cd6d292", size = 2071819, upload-time = "2026-05-06T13:38:49.139Z" },
- { url = "https://files.pythonhosted.org/packages/c6/1a/f4aee670d5670e9e148e0c82c7db98d780be566c6e6a97ee8035528ca0b3/pydantic_core-2.46.4-cp313-cp313-win_arm64.whl", hash = "sha256:184c081504d17f1c1066e430e117142b2c77d9448a97f7b65c6ac9fd9aee238d", size = 2027411, upload-time = "2026-05-06T13:40:45.796Z" },
- { url = "https://files.pythonhosted.org/packages/8d/74/228a26ddad29c6672b805d9fd78e8d251cd04004fa7eed0e622096cd0250/pydantic_core-2.46.4-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:428e04521a40150c85216fc8b85e8d39fece235a9cf5e383761238c7fa9b96fb", size = 2102079, upload-time = "2026-05-06T13:38:41.019Z" },
- { url = "https://files.pythonhosted.org/packages/ad/1f/8970b150a4b4365623ae00fc88603491f763c627311ae8031e3111356d6e/pydantic_core-2.46.4-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:23ace664830ee0bfe014a0c7bc248b1f7f25ed7ad103852c317624a1083af462", size = 1952179, upload-time = "2026-05-06T13:36:59.812Z" },
- { url = "https://files.pythonhosted.org/packages/95/30/5211a831ae054928054b2f79731661087a2bc5c01e825c672b3a4a8f1b3e/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ce5c1d2a8b27468f433ca974829c44060b8097eedc39933e3c206a90ee49c4a9", size = 1978926, upload-time = "2026-05-06T13:37:39.933Z" },
- { url = "https://files.pythonhosted.org/packages/57/e9/689668733b1eb67adeef047db3c2e8788fcf65a7fd9c9e2b46b7744fe245/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:7283d57845ecf5a163403eb0702dfc220cc4fbdd18919cb5ccea4f95ee1cdab4", size = 2046785, upload-time = "2026-05-06T13:38:01.995Z" },
- { url = "https://files.pythonhosted.org/packages/60/d9/6715260422ff50a2109878fd24d948a6c3446bb2664f34ee78cd972b3acd/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8daafc69c93ee8a0204506a3b6b30f586ef54028f52aeeeb5c4cfc5184fd5914", size = 2228733, upload-time = "2026-05-06T13:40:50.371Z" },
- { url = "https://files.pythonhosted.org/packages/18/ae/fdb2f64316afca925640f8e70bb1a564b0ec2721c1389e25b8eb4bf9a299/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cd2213145bcc2ba85884d0ac63d222fece9209678f77b9b4d76f054c561adb28", size = 2307534, upload-time = "2026-05-06T13:37:21.531Z" },
- { url = "https://files.pythonhosted.org/packages/89/1d/8eff589b45bb8190a9d12c49cfad0f176a5cbd1534908a6b5125e2886239/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7a5f930472650a82629163023e630d160863fce524c616f4e5186e5de9d9a49b", size = 2099732, upload-time = "2026-05-06T13:39:31.942Z" },
- { url = "https://files.pythonhosted.org/packages/06/d5/ee5a3366637fee41dee51a1fc91562dcf12ddbc68fda34e6b253da2324bb/pydantic_core-2.46.4-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:c1b3f518abeca3aa13c712fd202306e145abf59a18b094a6bafb2d2bbf59192c", size = 2129627, upload-time = "2026-05-06T13:37:25.033Z" },
- { url = "https://files.pythonhosted.org/packages/94/33/2414be571d2c6a6c4d08be21f9292b6d3fdb08949a97b6dfe985017821db/pydantic_core-2.46.4-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1a7dd0b3ee80d90150e3495a3a13ac34dbcbfd4f012996a6a1d8900e91b5c0fb", size = 2179141, upload-time = "2026-05-06T13:37:14.046Z" },
- { url = "https://files.pythonhosted.org/packages/7b/79/7daa95be995be0eecc4cf75064cb33f9bbbfe3fe0158caf2f0d4a996a5c7/pydantic_core-2.46.4-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:3fb702cd90b0446a3a1c5e470bfa0dd23c0233b676a9099ddcc964fa6ca13898", size = 2184325, upload-time = "2026-05-06T13:36:53.615Z" },
- { url = "https://files.pythonhosted.org/packages/9f/cb/d0a382f5c0de8a222dc61c65348e0ce831b1f68e0a018450d31c2cace3a5/pydantic_core-2.46.4-cp314-cp314-musllinux_1_1_armv7l.whl", hash = "sha256:b8458003118a712e66286df6a707db01c52c0f52f7db8e4a38f0da1d3b94fc4e", size = 2323990, upload-time = "2026-05-06T13:40:29.971Z" },
- { url = "https://files.pythonhosted.org/packages/05/db/d9ba624cc4a5aced1598e88c04fdbd8310c8a69b9d38b9a3d39ce3a61ed7/pydantic_core-2.46.4-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:372429a130e469c9cd698925ce5fc50940b7a1336b0d82038e63d5bbc4edc519", size = 2369978, upload-time = "2026-05-06T13:37:23.027Z" },
- { url = "https://files.pythonhosted.org/packages/f2/20/d15df15ba918c423461905802bfd2981c3af0bfa0e40d05e13edbfa48bc3/pydantic_core-2.46.4-cp314-cp314-win32.whl", hash = "sha256:85bb3611ff1802f3ee7fdd7dbff26b56f343fb432d57a4728fdd49b6ef35e2f4", size = 1966354, upload-time = "2026-05-06T13:38:03.499Z" },
- { url = "https://files.pythonhosted.org/packages/fc/b6/6b8de4c0a7d7ab3004c439c80c5c1e0a3e8d78bbae19379b01960383d9e5/pydantic_core-2.46.4-cp314-cp314-win_amd64.whl", hash = "sha256:811ff8e9c313ab425368bcbb36e5c4ebd7108c2bbf4e4089cfbb0b01eff63fac", size = 2072238, upload-time = "2026-05-06T13:39:40.807Z" },
- { url = "https://files.pythonhosted.org/packages/32/36/51eb763beec1f4cf59b1db243a7dcc39cbb41230f050a09b9d69faaf0a48/pydantic_core-2.46.4-cp314-cp314-win_arm64.whl", hash = "sha256:bfec22eab3c8cc2ceec0248aec886624116dc079afa027ecc8ad4a7e62010f8a", size = 2018251, upload-time = "2026-05-06T13:37:26.72Z" },
- { url = "https://files.pythonhosted.org/packages/e8/91/855af51d625b23aa987116a19e231d2aaef9c4a415273ddc189b79a45fee/pydantic_core-2.46.4-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:af8244b2bef6aaad6d92cda81372de7f8c8d36c9f0c3ea36e827c60e7d9467a0", size = 2099593, upload-time = "2026-05-06T13:39:47.682Z" },
- { url = "https://files.pythonhosted.org/packages/fb/1b/8784a54c65edb5f49f0a14d6977cf1b209bba85a4c77445b255c2de58ab3/pydantic_core-2.46.4-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:5a4330cdbc57162e4b3aa303f588ba752257694c9c9be3e7ebb11b4aca659b5d", size = 1935226, upload-time = "2026-05-06T13:40:40.428Z" },
- { url = "https://files.pythonhosted.org/packages/e8/e7/1955d28d1afc56dd4b3ad7cc0cf39df1b9852964cf16e5d13912756d6d6b/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:29c61fc04a3d840155ff08e475a04809278972fe6aef51e2720554e96367e34b", size = 1974605, upload-time = "2026-05-06T13:37:32.029Z" },
- { url = "https://files.pythonhosted.org/packages/93/e2/3fedbf0ba7a22850e6e9fd78117f1c0f10f950182344d8a6c535d468fdd8/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c50f2528cf200c5eed56faf3f4e22fcd5f38c157a8b78576e6ba3168ec35f000", size = 2030777, upload-time = "2026-05-06T13:38:55.239Z" },
- { url = "https://files.pythonhosted.org/packages/f8/61/46be275fcaaba0b4f5b9669dd852267ce1ff616592dccf7a7845588df091/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0cbe8b01f948de4286c74cdd6c667aceb38f5c1e26f0693b3983d9d74887c65e", size = 2236641, upload-time = "2026-05-06T13:37:08.096Z" },
- { url = "https://files.pythonhosted.org/packages/60/db/12e93e46a8bac9988be3c016860f83293daea8c716c029c9ace279036f2f/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:617d7e2ca7dcb8c5cf6bcb8c59b8832c94b36196bbf1cbd1bfb56ed341905edd", size = 2286404, upload-time = "2026-05-06T13:40:20.221Z" },
- { url = "https://files.pythonhosted.org/packages/e2/4a/4d8b19008f38d31c53b8219cfedc2e3d5de5fe99d90076b7e767de29274f/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7027560ee92211647d0d34e3f7cd6f50da56399d26a9c8ad0da286d3869a53f3", size = 2109219, upload-time = "2026-05-06T13:38:12.153Z" },
- { url = "https://files.pythonhosted.org/packages/88/70/3cbc40978fefb7bb09c6708d40d4ad1a5d70fd7213c3d17f971de868ec1f/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:f99626688942fb746e545232e7726926f3be91b5975f8b55327665fafda991c7", size = 2110594, upload-time = "2026-05-06T13:40:02.971Z" },
- { url = "https://files.pythonhosted.org/packages/9d/20/b8d36736216e29491125531685b2f9e61aa5b4b2599893f8268551da3338/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:fc3e9034a63de20e15e8ade85358bc6efc614008cab72898b4b4952bea0509ff", size = 2159542, upload-time = "2026-05-06T13:39:27.506Z" },
- { url = "https://files.pythonhosted.org/packages/1d/a2/367df868eb584dacf6bf82a389272406d7178e301c4ac82545ab98bc2dd9/pydantic_core-2.46.4-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:97e7cf2be5c77b7d1a9713a05605d49460d02c6078d38d8bef3cbe323c548424", size = 2168146, upload-time = "2026-05-06T13:38:31.93Z" },
- { url = "https://files.pythonhosted.org/packages/c1/b8/4460f77f7e201893f649a29ab355dddd3beee8a97bcb1a320db414f9a06e/pydantic_core-2.46.4-cp314-cp314t-musllinux_1_1_armv7l.whl", hash = "sha256:3bf92c5d0e00fefaab325a4d27828fe6b6e2a21848686b5b60d2d9eeb09d76c6", size = 2306309, upload-time = "2026-05-06T13:37:44.717Z" },
- { url = "https://files.pythonhosted.org/packages/64/c4/be2639293acd87dc8ddbcec41a73cee9b2ebf996fe6d892a1a74e88ad3f7/pydantic_core-2.46.4-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:3ecbc122d18468d06ca279dc26a8c2e2d5acb10943bb35e36ae92096dc3b5565", size = 2369736, upload-time = "2026-05-06T13:37:05.645Z" },
- { url = "https://files.pythonhosted.org/packages/30/a6/9f9f380dbb301f67023bf8f707aaa75daadf84f7152d95c410fd7e81d994/pydantic_core-2.46.4-cp314-cp314t-win32.whl", hash = "sha256:e846ae7835bf0703ae43f534ab79a867146dadd59dc9ca5c8b53d5c8f7c9ef02", size = 1955575, upload-time = "2026-05-06T13:38:51.116Z" },
- { url = "https://files.pythonhosted.org/packages/40/1f/f1eb9eb350e795d1af8586289746f5c5677d16043040d63710e22abc43c9/pydantic_core-2.46.4-cp314-cp314t-win_amd64.whl", hash = "sha256:2108ba5c1c1eca18030634489dc544844144ee36357f2f9f780b93e7ddbb44b5", size = 2051624, upload-time = "2026-05-06T13:38:21.672Z" },
- { url = "https://files.pythonhosted.org/packages/f6/d2/42dd53d0a85c27606f316d3aa5d2869c4e8470a5ed6dec30e4a1abe19192/pydantic_core-2.46.4-cp314-cp314t-win_arm64.whl", hash = "sha256:4fcbe087dbc2068af7eda3aa87634eba216dbda64d1ae73c8684b621d33f6596", size = 2017325, upload-time = "2026-05-06T13:40:52.723Z" },
- { url = "https://files.pythonhosted.org/packages/5d/00/13a0c039569d1e583779ee1b8d7df6bfe275a0db83fcae14f01d6856c16e/pydantic_core-2.46.4-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:fd8b3d9fd264be37976686c7f65cd52a83f5e84f4bfd2adf9c1d469676bbb6ae", size = 2115337, upload-time = "2026-05-06T13:38:37.741Z" },
- { url = "https://files.pythonhosted.org/packages/41/60/e70fa1ee03e243bdfd4b1fddf1e1f2a8fba681df3034b51b9376c0fb5bf5/pydantic_core-2.46.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:9f444c499b3eefd3a92e348059471ea0c3a6e303d9c1cec09fa748fd9f895201", size = 1957976, upload-time = "2026-05-06T13:37:33.478Z" },
- { url = "https://files.pythonhosted.org/packages/11/9a/78fb5f2ea849f767ea802de8b4e8f5a0c4a48ddbe4bc66bd19ac2f55a01c/pydantic_core-2.46.4-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3447661d99f75a3683a4cf5c87da72f2161964611864dbbeac7fbb118bb4bfc0", size = 1979390, upload-time = "2026-05-06T13:36:52.419Z" },
- { url = "https://files.pythonhosted.org/packages/f5/7d/3acfdcd000bad9735de0430a88355948469781f62cb841fd63e8a307e80e/pydantic_core-2.46.4-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:8b9bab013d1c7a79d3501ff86d0bc9c31bf587db4551677b96bec07df78c6b15", size = 2043263, upload-time = "2026-05-06T13:39:54.798Z" },
- { url = "https://files.pythonhosted.org/packages/35/60/1325e5a8d7f9697416481c7f7c1c304738d6b961a7fd1ea0f054ce0f14fb/pydantic_core-2.46.4-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d995260fdf4e1db774581b4900e0f832abe3c7c84996726bbc161b19c8f29e76", size = 2225708, upload-time = "2026-05-06T13:40:24.887Z" },
- { url = "https://files.pythonhosted.org/packages/6a/b0/9ec8c38f33b26db0b612cb7fd165bb0a370773710432a2a74fa31287b430/pydantic_core-2.46.4-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f13a646d65d09fbf1bc6b3a9635d30095c8e7e5cc419ff35ecc563c5fd04cd49", size = 2288494, upload-time = "2026-05-06T13:38:00.091Z" },
- { url = "https://files.pythonhosted.org/packages/65/05/497446a9586d1b2d24ee25ebe208beb15388f1875d783e1e014055d150ac/pydantic_core-2.46.4-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:432c179df7874eeb73307aad2df0755e1ae0efa61ff0ea89b93e194411ae3928", size = 2095629, upload-time = "2026-05-06T13:38:23.632Z" },
- { url = "https://files.pythonhosted.org/packages/93/d9/cd5fa98f9d94f9294c15459396c8a2383c164469e679ac178d6d42cfee6b/pydantic_core-2.46.4-cp39-cp39-manylinux_2_31_riscv64.whl", hash = "sha256:e68b7a074f65a2fd746c52a7ce6142ab7006074ac269ace0c25cd8ba171f8066", size = 2119309, upload-time = "2026-05-06T13:39:50.144Z" },
- { url = "https://files.pythonhosted.org/packages/20/1b/64cec655451ddbf3976df5dc9706b240df4fdaebdeebeadd4f59a8dab926/pydantic_core-2.46.4-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:4a05d69cba51d852c5c3e92758653245a50c0b646ced0cf05bd793ed592839d6", size = 2170216, upload-time = "2026-05-06T13:39:14.561Z" },
- { url = "https://files.pythonhosted.org/packages/2a/21/fe9f039138c9ea3be10ccdb6ec490acb54dcbef5a5e96dbdf1411f82b929/pydantic_core-2.46.4-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:228ee9bae8bef5b1e97ec58302f80357c37199e0d0a99174e138d28e6957b9d9", size = 2186726, upload-time = "2026-05-06T13:37:51.597Z" },
- { url = "https://files.pythonhosted.org/packages/44/cb/19ca0da64821d1aefcef65f253aa9ecbdd0dde360f607d0f9b3d95db2b4e/pydantic_core-2.46.4-cp39-cp39-musllinux_1_1_armv7l.whl", hash = "sha256:10e17cbb10a330363733efc4d7c4d0dd827ac0909b8f6a6542298fed1ea62f29", size = 2320400, upload-time = "2026-05-06T13:39:36.29Z" },
- { url = "https://files.pythonhosted.org/packages/cd/14/fe3fbf6e845bf2080dc2f282d75085ddf79d037b35634ecde68f33c217b4/pydantic_core-2.46.4-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:91a06d2e259ecfbd8c901d70c3c507900458498142b3026a296b7de4d1322cc9", size = 2363318, upload-time = "2026-05-06T13:38:53.039Z" },
- { url = "https://files.pythonhosted.org/packages/62/88/60b110889507a426eecf626f7536566cb290ada71147eff49b6e2724ca62/pydantic_core-2.46.4-cp39-cp39-win32.whl", hash = "sha256:d80ee3d731373b24cebbc10d689ca4ee1875caf0d5703a245db18efd4dd37fc1", size = 1988880, upload-time = "2026-05-06T13:39:16.572Z" },
- { url = "https://files.pythonhosted.org/packages/0b/d6/8ede2f98f17e1e4e127d37be0eced4eee931a511c62cd68af50e1b25bfa9/pydantic_core-2.46.4-cp39-cp39-win_amd64.whl", hash = "sha256:3be77f45df024d789a672ae34f8b06fb346c4f9f46ea714956660ea4862e89ac", size = 2079257, upload-time = "2026-05-06T13:39:38.498Z" },
- { url = "https://files.pythonhosted.org/packages/ee/a4/73995fd4ebbb46ba0ee51e6fa049b8f02c40daebb762208feda8a6b7894d/pydantic_core-2.46.4-graalpy311-graalpy242_311_native-macosx_10_12_x86_64.whl", hash = "sha256:14d4edf427bdcf950a8a02d7cb44a08614388dd6e1bdcbf4f67504fa7887da9c", size = 2111589, upload-time = "2026-05-06T13:37:10.817Z" },
- { url = "https://files.pythonhosted.org/packages/fb/7f/f37d3a5e8bfcc2e403f5c57a730f2d815693fb42119e8ea48b3789335af1/pydantic_core-2.46.4-graalpy311-graalpy242_311_native-macosx_11_0_arm64.whl", hash = "sha256:0ce40cd7b21210e99342afafbd4d0f76d784eb5b1d60f3bdc566be4983c6c73b", size = 1944552, upload-time = "2026-05-06T13:36:56.717Z" },
- { url = "https://files.pythonhosted.org/packages/15/3c/d7eb777b3ff43e8433a4efb39a17aa8fd98a4ee8561a24a67ef5db07b2d6/pydantic_core-2.46.4-graalpy311-graalpy242_311_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:90884113d8b48f760e9587002789ddd741e76ab9f89518cd1e43b1f1a52ec44b", size = 1982984, upload-time = "2026-05-06T13:39:06.207Z" },
- { url = "https://files.pythonhosted.org/packages/63/87/70b9f40170a81afd55ca26c9b2acb25c20d64bcfbf888fafecb3ba077d4c/pydantic_core-2.46.4-graalpy311-graalpy242_311_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:66ce7632c22d837c95301830e111ad0128a32b8207533b60896a96c4915192ea", size = 2138417, upload-time = "2026-05-06T13:39:45.476Z" },
- { url = "https://files.pythonhosted.org/packages/9d/1d/8987ad40f65ae1432753072f214fb5c74fe47ffbd0698bb9cbbb585664f8/pydantic_core-2.46.4-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:1d8ba486450b14f3b1d63bc521d410ec7565e52f887b9fb671791886436a42f7", size = 2095527, upload-time = "2026-05-06T13:39:52.283Z" },
- { url = "https://files.pythonhosted.org/packages/64/d3/84c282a7eee1d3ac4c0377546ef5a1ea436ce26840d9ac3b7ed54a377507/pydantic_core-2.46.4-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:3009f12e4e90b7f88b4f9adb1b0c4a3d58fe7820f3238c190047209d148026df", size = 1936024, upload-time = "2026-05-06T13:40:15.671Z" },
- { url = "https://files.pythonhosted.org/packages/d7/ca/eac61596cdeb4d7e174d3dc0bd8a6238f14f75f97a24e7b7db4c7e7340a0/pydantic_core-2.46.4-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ad785e92e6dc634c21555edc8bd6b64957ab844541bcb96a1366c202951ae526", size = 1990696, upload-time = "2026-05-06T13:38:34.717Z" },
- { url = "https://files.pythonhosted.org/packages/fa/c3/7c8b240552251faf6b3a957db200fcfbbcec36763c050428b601e0c9b83b/pydantic_core-2.46.4-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:00c603d540afdd6b80eb39f078f33ebd46211f02f33e34a32d9f053bba711de0", size = 2147590, upload-time = "2026-05-06T13:39:29.883Z" },
- { url = "https://files.pythonhosted.org/packages/11/cb/428de0385b6c8d44b716feba566abfacfbd23ee3c4439faa789a1456242f/pydantic_core-2.46.4-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:0c563b08bca408dc7f65f700633d8442fffb2421fc47b8101377e9fd65051ff0", size = 2112782, upload-time = "2026-05-06T13:37:04.016Z" },
- { url = "https://files.pythonhosted.org/packages/0b/b5/6a17bdadd0fc1f170adfd05a20d37c832f52b117b4d9131da1f41bb097ce/pydantic_core-2.46.4-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:db06ffe51636ffe9ca531fe9023dd64bdd794be8754cb5df57c5498ae5b518a7", size = 1952146, upload-time = "2026-05-06T13:39:43.092Z" },
- { url = "https://files.pythonhosted.org/packages/2a/dc/03734d80e362cd43ef65428e9de77c730ce7f2f11c60d2b1e1b39f0fbf99/pydantic_core-2.46.4-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:133878133d271ade3d41d1bfb2a45ec38dbdbda40bc065921c6b04e4630127e2", size = 2134492, upload-time = "2026-05-06T13:36:58.124Z" },
- { url = "https://files.pythonhosted.org/packages/de/df/5e5ffc085ed07cc22d298134d3d911c63e91f6a0eb91fe646750a3209910/pydantic_core-2.46.4-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:9bc519fbf2b7578398853d815009ae5e4d4603d12f4e3f91da8c06852d3da3e9", size = 2156604, upload-time = "2026-05-06T13:37:49.88Z" },
- { url = "https://files.pythonhosted.org/packages/81/44/6e112a4253e56f5705467cbab7ab5e91ee7398ba3d56d358635958893d3e/pydantic_core-2.46.4-pp311-pypy311_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:c7a7bd4e39e8e4c12c39cd480356842b6a8a06e41b23a55a5e3e191718838ddf", size = 2183828, upload-time = "2026-05-06T13:37:43.053Z" },
- { url = "https://files.pythonhosted.org/packages/ac/ad/5565071e937d8e752842ac241463944c9eb14c87e2d269f2658a5bd05e98/pydantic_core-2.46.4-pp311-pypy311_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:d396ec2b979760aaf3218e76c24e65bd0aca24983298653b3a9d7a45f9e47b30", size = 2310000, upload-time = "2026-05-06T13:37:56.694Z" },
- { url = "https://files.pythonhosted.org/packages/4f/c3/66883a5cec183e7fba4d024b4cbbe61851a63750ef606b0afecc46d1f2bf/pydantic_core-2.46.4-pp311-pypy311_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:86e1a4418c6cd97d60c95c71164158eaf7324fae7b0923264016baa993eba6fc", size = 2361286, upload-time = "2026-05-06T13:40:05.667Z" },
- { url = "https://files.pythonhosted.org/packages/4b/2d/69abac8f838090bbecd5df894befb2c2619e7996a98ddb949db9f3b93225/pydantic_core-2.46.4-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:d51026d73fcfd93610abc7b27789c26b313920fcfb20e27462d74a7f8b06e983", size = 2193071, upload-time = "2026-05-06T13:38:08.682Z" },
-]
-
-[[package]]
-name = "sniffio"
-version = "1.3.1"
-source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/a2/87/a6771e1546d97e7e041b6ae58d80074f81b7d5121207425c964ddf5cfdbd/sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc", size = 20372, upload-time = "2024-02-25T23:20:04.057Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/e9/44/75a9c9421471a6c4805dbf2356f7c181a29c1879239abab1ea2cc8f38b40/sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2", size = 10235, upload-time = "2024-02-25T23:20:01.196Z" },
-]
-
-[[package]]
-name = "typing-extensions"
-version = "4.16.0"
-source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/f6/cc/6253133b5bb138fc3306cebfbda2c520f545d36b5be2c7255cc528bb45d6/typing_extensions-4.16.0.tar.gz", hash = "sha256:dc983d19a509c94dba722ee6abd33940f7c05a89e243c47e907eb4db6f1a43e5", size = 113555, upload-time = "2026-07-02T08:40:05.92Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/49/d3/b8441a820a491ddfc024b0b0cf0393375b75ea13866d9c66727e54c2fc80/typing_extensions-4.16.0-py3-none-any.whl", hash = "sha256:481caa481374e813c1b176ada14e97f1f67a4539ce9cfeb3f350d78d6370c2e8", size = 45571, upload-time = "2026-07-02T08:40:04.659Z" },
-]
-
-[[package]]
-name = "typing-inspection"
-version = "0.4.2"
-source = { registry = "https://pypi.org/simple" }
-dependencies = [
- { name = "typing-extensions" },
-]
-sdist = { url = "https://files.pythonhosted.org/packages/55/e3/70399cb7dd41c10ac53367ae42139cf4b1ca5f36bb3dc6c9d33acdb43655/typing_inspection-0.4.2.tar.gz", hash = "sha256:ba561c48a67c5958007083d386c3295464928b01faa735ab8547c5692e87f464", size = 75949, upload-time = "2025-10-01T02:14:41.687Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/dc/9b/47798a6c91d8bdb567fe2698fe81e0c6b7cb7ef4d13da4114b41d239f65d/typing_inspection-0.4.2-py3-none-any.whl", hash = "sha256:4ed1cacbdc298c220f1bd249ed5287caa16f34d44ef4e9c3d0cbad5b521545e7", size = 14611, upload-time = "2025-10-01T02:14:40.154Z" },
-]
-
-[[package]]
-name = "yarl"
-version = "1.22.0"
-source = { registry = "https://pypi.org/simple" }
-resolution-markers = [
- "python_full_version < '3.10'",
-]
-dependencies = [
- { name = "idna" },
- { name = "multidict" },
- { name = "propcache", version = "0.4.1", source = { registry = "https://pypi.org/simple" } },
-]
-sdist = { url = "https://files.pythonhosted.org/packages/57/63/0c6ebca57330cd313f6102b16dd57ffaf3ec4c83403dcb45dbd15c6f3ea1/yarl-1.22.0.tar.gz", hash = "sha256:bebf8557577d4401ba8bd9ff33906f1376c877aa78d1fe216ad01b4d6745af71", size = 187169, upload-time = "2025-10-06T14:12:55.963Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/d1/43/a2204825342f37c337f5edb6637040fa14e365b2fcc2346960201d457579/yarl-1.22.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:c7bd6683587567e5a49ee6e336e0612bec8329be1b7d4c8af5687dcdeb67ee1e", size = 140517, upload-time = "2025-10-06T14:08:42.494Z" },
- { url = "https://files.pythonhosted.org/packages/44/6f/674f3e6f02266428c56f704cd2501c22f78e8b2eeb23f153117cc86fb28a/yarl-1.22.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:5cdac20da754f3a723cceea5b3448e1a2074866406adeb4ef35b469d089adb8f", size = 93495, upload-time = "2025-10-06T14:08:46.2Z" },
- { url = "https://files.pythonhosted.org/packages/b8/12/5b274d8a0f30c07b91b2f02cba69152600b47830fcfb465c108880fcee9c/yarl-1.22.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:07a524d84df0c10f41e3ee918846e1974aba4ec017f990dc735aad487a0bdfdf", size = 94400, upload-time = "2025-10-06T14:08:47.855Z" },
- { url = "https://files.pythonhosted.org/packages/e2/7f/df1b6949b1fa1aa9ff6de6e2631876ad4b73c4437822026e85d8acb56bb1/yarl-1.22.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e1b329cb8146d7b736677a2440e422eadd775d1806a81db2d4cded80a48efc1a", size = 347545, upload-time = "2025-10-06T14:08:49.683Z" },
- { url = "https://files.pythonhosted.org/packages/84/09/f92ed93bd6cd77872ab6c3462df45ca45cd058d8f1d0c9b4f54c1704429f/yarl-1.22.0-cp310-cp310-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:75976c6945d85dbb9ee6308cd7ff7b1fb9409380c82d6119bd778d8fcfe2931c", size = 319598, upload-time = "2025-10-06T14:08:51.215Z" },
- { url = "https://files.pythonhosted.org/packages/c3/97/ac3f3feae7d522cf7ccec3d340bb0b2b61c56cb9767923df62a135092c6b/yarl-1.22.0-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:80ddf7a5f8c86cb3eb4bc9028b07bbbf1f08a96c5c0bc1244be5e8fefcb94147", size = 363893, upload-time = "2025-10-06T14:08:53.144Z" },
- { url = "https://files.pythonhosted.org/packages/06/49/f3219097403b9c84a4d079b1d7bda62dd9b86d0d6e4428c02d46ab2c77fc/yarl-1.22.0-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d332fc2e3c94dad927f2112395772a4e4fedbcf8f80efc21ed7cdfae4d574fdb", size = 371240, upload-time = "2025-10-06T14:08:55.036Z" },
- { url = "https://files.pythonhosted.org/packages/35/9f/06b765d45c0e44e8ecf0fe15c9eacbbde342bb5b7561c46944f107bfb6c3/yarl-1.22.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0cf71bf877efeac18b38d3930594c0948c82b64547c1cf420ba48722fe5509f6", size = 346965, upload-time = "2025-10-06T14:08:56.722Z" },
- { url = "https://files.pythonhosted.org/packages/c5/69/599e7cea8d0fcb1694323b0db0dda317fa3162f7b90166faddecf532166f/yarl-1.22.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:663e1cadaddae26be034a6ab6072449a8426ddb03d500f43daf952b74553bba0", size = 342026, upload-time = "2025-10-06T14:08:58.563Z" },
- { url = "https://files.pythonhosted.org/packages/95/6f/9dfd12c8bc90fea9eab39832ee32ea48f8e53d1256252a77b710c065c89f/yarl-1.22.0-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:6dcbb0829c671f305be48a7227918cfcd11276c2d637a8033a99a02b67bf9eda", size = 335637, upload-time = "2025-10-06T14:09:00.506Z" },
- { url = "https://files.pythonhosted.org/packages/57/2e/34c5b4eb9b07e16e873db5b182c71e5f06f9b5af388cdaa97736d79dd9a6/yarl-1.22.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:f0d97c18dfd9a9af4490631905a3f131a8e4c9e80a39353919e2cfed8f00aedc", size = 359082, upload-time = "2025-10-06T14:09:01.936Z" },
- { url = "https://files.pythonhosted.org/packages/31/71/fa7e10fb772d273aa1f096ecb8ab8594117822f683bab7d2c5a89914c92a/yarl-1.22.0-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:437840083abe022c978470b942ff832c3940b2ad3734d424b7eaffcd07f76737", size = 357811, upload-time = "2025-10-06T14:09:03.445Z" },
- { url = "https://files.pythonhosted.org/packages/26/da/11374c04e8e1184a6a03cf9c8f5688d3e5cec83ed6f31ad3481b3207f709/yarl-1.22.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:a899cbd98dce6f5d8de1aad31cb712ec0a530abc0a86bd6edaa47c1090138467", size = 351223, upload-time = "2025-10-06T14:09:05.401Z" },
- { url = "https://files.pythonhosted.org/packages/82/8f/e2d01f161b0c034a30410e375e191a5d27608c1f8693bab1a08b089ca096/yarl-1.22.0-cp310-cp310-win32.whl", hash = "sha256:595697f68bd1f0c1c159fcb97b661fc9c3f5db46498043555d04805430e79bea", size = 82118, upload-time = "2025-10-06T14:09:11.148Z" },
- { url = "https://files.pythonhosted.org/packages/62/46/94c76196642dbeae634c7a61ba3da88cd77bed875bf6e4a8bed037505aa6/yarl-1.22.0-cp310-cp310-win_amd64.whl", hash = "sha256:cb95a9b1adaa48e41815a55ae740cfda005758104049a640a398120bf02515ca", size = 86852, upload-time = "2025-10-06T14:09:12.958Z" },
- { url = "https://files.pythonhosted.org/packages/af/af/7df4f179d3b1a6dcb9a4bd2ffbc67642746fcafdb62580e66876ce83fff4/yarl-1.22.0-cp310-cp310-win_arm64.whl", hash = "sha256:b85b982afde6df99ecc996990d4ad7ccbdbb70e2a4ba4de0aecde5922ba98a0b", size = 82012, upload-time = "2025-10-06T14:09:14.664Z" },
- { url = "https://files.pythonhosted.org/packages/4d/27/5ab13fc84c76a0250afd3d26d5936349a35be56ce5785447d6c423b26d92/yarl-1.22.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:1ab72135b1f2db3fed3997d7e7dc1b80573c67138023852b6efb336a5eae6511", size = 141607, upload-time = "2025-10-06T14:09:16.298Z" },
- { url = "https://files.pythonhosted.org/packages/6a/a1/d065d51d02dc02ce81501d476b9ed2229d9a990818332242a882d5d60340/yarl-1.22.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:669930400e375570189492dc8d8341301578e8493aec04aebc20d4717f899dd6", size = 94027, upload-time = "2025-10-06T14:09:17.786Z" },
- { url = "https://files.pythonhosted.org/packages/c1/da/8da9f6a53f67b5106ffe902c6fa0164e10398d4e150d85838b82f424072a/yarl-1.22.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:792a2af6d58177ef7c19cbf0097aba92ca1b9cb3ffdd9c7470e156c8f9b5e028", size = 94963, upload-time = "2025-10-06T14:09:19.662Z" },
- { url = "https://files.pythonhosted.org/packages/68/fe/2c1f674960c376e29cb0bec1249b117d11738db92a6ccc4a530b972648db/yarl-1.22.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3ea66b1c11c9150f1372f69afb6b8116f2dd7286f38e14ea71a44eee9ec51b9d", size = 368406, upload-time = "2025-10-06T14:09:21.402Z" },
- { url = "https://files.pythonhosted.org/packages/95/26/812a540e1c3c6418fec60e9bbd38e871eaba9545e94fa5eff8f4a8e28e1e/yarl-1.22.0-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:3e2daa88dc91870215961e96a039ec73e4937da13cf77ce17f9cad0c18df3503", size = 336581, upload-time = "2025-10-06T14:09:22.98Z" },
- { url = "https://files.pythonhosted.org/packages/0b/f5/5777b19e26fdf98563985e481f8be3d8a39f8734147a6ebf459d0dab5a6b/yarl-1.22.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ba440ae430c00eee41509353628600212112cd5018d5def7e9b05ea7ac34eb65", size = 388924, upload-time = "2025-10-06T14:09:24.655Z" },
- { url = "https://files.pythonhosted.org/packages/86/08/24bd2477bd59c0bbd994fe1d93b126e0472e4e3df5a96a277b0a55309e89/yarl-1.22.0-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:e6438cc8f23a9c1478633d216b16104a586b9761db62bfacb6425bac0a36679e", size = 392890, upload-time = "2025-10-06T14:09:26.617Z" },
- { url = "https://files.pythonhosted.org/packages/46/00/71b90ed48e895667ecfb1eaab27c1523ee2fa217433ed77a73b13205ca4b/yarl-1.22.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4c52a6e78aef5cf47a98ef8e934755abf53953379b7d53e68b15ff4420e6683d", size = 365819, upload-time = "2025-10-06T14:09:28.544Z" },
- { url = "https://files.pythonhosted.org/packages/30/2d/f715501cae832651d3282387c6a9236cd26bd00d0ff1e404b3dc52447884/yarl-1.22.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:3b06bcadaac49c70f4c88af4ffcfbe3dc155aab3163e75777818092478bcbbe7", size = 363601, upload-time = "2025-10-06T14:09:30.568Z" },
- { url = "https://files.pythonhosted.org/packages/f8/f9/a678c992d78e394e7126ee0b0e4e71bd2775e4334d00a9278c06a6cce96a/yarl-1.22.0-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:6944b2dc72c4d7f7052683487e3677456050ff77fcf5e6204e98caf785ad1967", size = 358072, upload-time = "2025-10-06T14:09:32.528Z" },
- { url = "https://files.pythonhosted.org/packages/2c/d1/b49454411a60edb6fefdcad4f8e6dbba7d8019e3a508a1c5836cba6d0781/yarl-1.22.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:d5372ca1df0f91a86b047d1277c2aaf1edb32d78bbcefffc81b40ffd18f027ed", size = 385311, upload-time = "2025-10-06T14:09:34.634Z" },
- { url = "https://files.pythonhosted.org/packages/87/e5/40d7a94debb8448c7771a916d1861d6609dddf7958dc381117e7ba36d9e8/yarl-1.22.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:51af598701f5299012b8416486b40fceef8c26fc87dc6d7d1f6fc30609ea0aa6", size = 381094, upload-time = "2025-10-06T14:09:36.268Z" },
- { url = "https://files.pythonhosted.org/packages/35/d8/611cc282502381ad855448643e1ad0538957fc82ae83dfe7762c14069e14/yarl-1.22.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:b266bd01fedeffeeac01a79ae181719ff848a5a13ce10075adbefc8f1daee70e", size = 370944, upload-time = "2025-10-06T14:09:37.872Z" },
- { url = "https://files.pythonhosted.org/packages/2d/df/fadd00fb1c90e1a5a8bd731fa3d3de2e165e5a3666a095b04e31b04d9cb6/yarl-1.22.0-cp311-cp311-win32.whl", hash = "sha256:a9b1ba5610a4e20f655258d5a1fdc7ebe3d837bb0e45b581398b99eb98b1f5ca", size = 81804, upload-time = "2025-10-06T14:09:39.359Z" },
- { url = "https://files.pythonhosted.org/packages/b5/f7/149bb6f45f267cb5c074ac40c01c6b3ea6d8a620d34b337f6321928a1b4d/yarl-1.22.0-cp311-cp311-win_amd64.whl", hash = "sha256:078278b9b0b11568937d9509b589ee83ef98ed6d561dfe2020e24a9fd08eaa2b", size = 86858, upload-time = "2025-10-06T14:09:41.068Z" },
- { url = "https://files.pythonhosted.org/packages/2b/13/88b78b93ad3f2f0b78e13bfaaa24d11cbc746e93fe76d8c06bf139615646/yarl-1.22.0-cp311-cp311-win_arm64.whl", hash = "sha256:b6a6f620cfe13ccec221fa312139135166e47ae169f8253f72a0abc0dae94376", size = 81637, upload-time = "2025-10-06T14:09:42.712Z" },
- { url = "https://files.pythonhosted.org/packages/75/ff/46736024fee3429b80a165a732e38e5d5a238721e634ab41b040d49f8738/yarl-1.22.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:e340382d1afa5d32b892b3ff062436d592ec3d692aeea3bef3a5cfe11bbf8c6f", size = 142000, upload-time = "2025-10-06T14:09:44.631Z" },
- { url = "https://files.pythonhosted.org/packages/5a/9a/b312ed670df903145598914770eb12de1bac44599549b3360acc96878df8/yarl-1.22.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:f1e09112a2c31ffe8d80be1b0988fa6a18c5d5cad92a9ffbb1c04c91bfe52ad2", size = 94338, upload-time = "2025-10-06T14:09:46.372Z" },
- { url = "https://files.pythonhosted.org/packages/ba/f5/0601483296f09c3c65e303d60c070a5c19fcdbc72daa061e96170785bc7d/yarl-1.22.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:939fe60db294c786f6b7c2d2e121576628468f65453d86b0fe36cb52f987bd74", size = 94909, upload-time = "2025-10-06T14:09:48.648Z" },
- { url = "https://files.pythonhosted.org/packages/60/41/9a1fe0b73dbcefce72e46cf149b0e0a67612d60bfc90fb59c2b2efdfbd86/yarl-1.22.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e1651bf8e0398574646744c1885a41198eba53dc8a9312b954073f845c90a8df", size = 372940, upload-time = "2025-10-06T14:09:50.089Z" },
- { url = "https://files.pythonhosted.org/packages/17/7a/795cb6dfee561961c30b800f0ed616b923a2ec6258b5def2a00bf8231334/yarl-1.22.0-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:b8a0588521a26bf92a57a1705b77b8b59044cdceccac7151bd8d229e66b8dedb", size = 345825, upload-time = "2025-10-06T14:09:52.142Z" },
- { url = "https://files.pythonhosted.org/packages/d7/93/a58f4d596d2be2ae7bab1a5846c4d270b894958845753b2c606d666744d3/yarl-1.22.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:42188e6a615c1a75bcaa6e150c3fe8f3e8680471a6b10150c5f7e83f47cc34d2", size = 386705, upload-time = "2025-10-06T14:09:54.128Z" },
- { url = "https://files.pythonhosted.org/packages/61/92/682279d0e099d0e14d7fd2e176bd04f48de1484f56546a3e1313cd6c8e7c/yarl-1.22.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f6d2cb59377d99718913ad9a151030d6f83ef420a2b8f521d94609ecc106ee82", size = 396518, upload-time = "2025-10-06T14:09:55.762Z" },
- { url = "https://files.pythonhosted.org/packages/db/0f/0d52c98b8a885aeda831224b78f3be7ec2e1aa4a62091f9f9188c3c65b56/yarl-1.22.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:50678a3b71c751d58d7908edc96d332af328839eea883bb554a43f539101277a", size = 377267, upload-time = "2025-10-06T14:09:57.958Z" },
- { url = "https://files.pythonhosted.org/packages/22/42/d2685e35908cbeaa6532c1fc73e89e7f2efb5d8a7df3959ea8e37177c5a3/yarl-1.22.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:1e8fbaa7cec507aa24ea27a01456e8dd4b6fab829059b69844bd348f2d467124", size = 365797, upload-time = "2025-10-06T14:09:59.527Z" },
- { url = "https://files.pythonhosted.org/packages/a2/83/cf8c7bcc6355631762f7d8bdab920ad09b82efa6b722999dfb05afa6cfac/yarl-1.22.0-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:433885ab5431bc3d3d4f2f9bd15bfa1614c522b0f1405d62c4f926ccd69d04fa", size = 365535, upload-time = "2025-10-06T14:10:01.139Z" },
- { url = "https://files.pythonhosted.org/packages/25/e1/5302ff9b28f0c59cac913b91fe3f16c59a033887e57ce9ca5d41a3a94737/yarl-1.22.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:b790b39c7e9a4192dc2e201a282109ed2985a1ddbd5ac08dc56d0e121400a8f7", size = 382324, upload-time = "2025-10-06T14:10:02.756Z" },
- { url = "https://files.pythonhosted.org/packages/bf/cd/4617eb60f032f19ae3a688dc990d8f0d89ee0ea378b61cac81ede3e52fae/yarl-1.22.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:31f0b53913220599446872d757257be5898019c85e7971599065bc55065dc99d", size = 383803, upload-time = "2025-10-06T14:10:04.552Z" },
- { url = "https://files.pythonhosted.org/packages/59/65/afc6e62bb506a319ea67b694551dab4a7e6fb7bf604e9bd9f3e11d575fec/yarl-1.22.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:a49370e8f711daec68d09b821a34e1167792ee2d24d405cbc2387be4f158b520", size = 374220, upload-time = "2025-10-06T14:10:06.489Z" },
- { url = "https://files.pythonhosted.org/packages/e7/3d/68bf18d50dc674b942daec86a9ba922d3113d8399b0e52b9897530442da2/yarl-1.22.0-cp312-cp312-win32.whl", hash = "sha256:70dfd4f241c04bd9239d53b17f11e6ab672b9f1420364af63e8531198e3f5fe8", size = 81589, upload-time = "2025-10-06T14:10:09.254Z" },
- { url = "https://files.pythonhosted.org/packages/c8/9a/6ad1a9b37c2f72874f93e691b2e7ecb6137fb2b899983125db4204e47575/yarl-1.22.0-cp312-cp312-win_amd64.whl", hash = "sha256:8884d8b332a5e9b88e23f60bb166890009429391864c685e17bd73a9eda9105c", size = 87213, upload-time = "2025-10-06T14:10:11.369Z" },
- { url = "https://files.pythonhosted.org/packages/44/c5/c21b562d1680a77634d748e30c653c3ca918beb35555cff24986fff54598/yarl-1.22.0-cp312-cp312-win_arm64.whl", hash = "sha256:ea70f61a47f3cc93bdf8b2f368ed359ef02a01ca6393916bc8ff877427181e74", size = 81330, upload-time = "2025-10-06T14:10:13.112Z" },
- { url = "https://files.pythonhosted.org/packages/ea/f3/d67de7260456ee105dc1d162d43a019ecad6b91e2f51809d6cddaa56690e/yarl-1.22.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:8dee9c25c74997f6a750cd317b8ca63545169c098faee42c84aa5e506c819b53", size = 139980, upload-time = "2025-10-06T14:10:14.601Z" },
- { url = "https://files.pythonhosted.org/packages/01/88/04d98af0b47e0ef42597b9b28863b9060bb515524da0a65d5f4db160b2d5/yarl-1.22.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:01e73b85a5434f89fc4fe27dcda2aff08ddf35e4d47bbbea3bdcd25321af538a", size = 93424, upload-time = "2025-10-06T14:10:16.115Z" },
- { url = "https://files.pythonhosted.org/packages/18/91/3274b215fd8442a03975ce6bee5fe6aa57a8326b29b9d3d56234a1dca244/yarl-1.22.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:22965c2af250d20c873cdbee8ff958fb809940aeb2e74ba5f20aaf6b7ac8c70c", size = 93821, upload-time = "2025-10-06T14:10:17.993Z" },
- { url = "https://files.pythonhosted.org/packages/61/3a/caf4e25036db0f2da4ca22a353dfeb3c9d3c95d2761ebe9b14df8fc16eb0/yarl-1.22.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b4f15793aa49793ec8d1c708ab7f9eded1aa72edc5174cae703651555ed1b601", size = 373243, upload-time = "2025-10-06T14:10:19.44Z" },
- { url = "https://files.pythonhosted.org/packages/6e/9e/51a77ac7516e8e7803b06e01f74e78649c24ee1021eca3d6a739cb6ea49c/yarl-1.22.0-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:e5542339dcf2747135c5c85f68680353d5cb9ffd741c0f2e8d832d054d41f35a", size = 342361, upload-time = "2025-10-06T14:10:21.124Z" },
- { url = "https://files.pythonhosted.org/packages/d4/f8/33b92454789dde8407f156c00303e9a891f1f51a0330b0fad7c909f87692/yarl-1.22.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:5c401e05ad47a75869c3ab3e35137f8468b846770587e70d71e11de797d113df", size = 387036, upload-time = "2025-10-06T14:10:22.902Z" },
- { url = "https://files.pythonhosted.org/packages/d9/9a/c5db84ea024f76838220280f732970aa4ee154015d7f5c1bfb60a267af6f/yarl-1.22.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:243dda95d901c733f5b59214d28b0120893d91777cb8aa043e6ef059d3cddfe2", size = 397671, upload-time = "2025-10-06T14:10:24.523Z" },
- { url = "https://files.pythonhosted.org/packages/11/c9/cd8538dc2e7727095e0c1d867bad1e40c98f37763e6d995c1939f5fdc7b1/yarl-1.22.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bec03d0d388060058f5d291a813f21c011041938a441c593374da6077fe21b1b", size = 377059, upload-time = "2025-10-06T14:10:26.406Z" },
- { url = "https://files.pythonhosted.org/packages/a1/b9/ab437b261702ced75122ed78a876a6dec0a1b0f5e17a4ac7a9a2482d8abe/yarl-1.22.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:b0748275abb8c1e1e09301ee3cf90c8a99678a4e92e4373705f2a2570d581273", size = 365356, upload-time = "2025-10-06T14:10:28.461Z" },
- { url = "https://files.pythonhosted.org/packages/b2/9d/8e1ae6d1d008a9567877b08f0ce4077a29974c04c062dabdb923ed98e6fe/yarl-1.22.0-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:47fdb18187e2a4e18fda2c25c05d8251a9e4a521edaed757fef033e7d8498d9a", size = 361331, upload-time = "2025-10-06T14:10:30.541Z" },
- { url = "https://files.pythonhosted.org/packages/ca/5a/09b7be3905962f145b73beb468cdd53db8aa171cf18c80400a54c5b82846/yarl-1.22.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:c7044802eec4524fde550afc28edda0dd5784c4c45f0be151a2d3ba017daca7d", size = 382590, upload-time = "2025-10-06T14:10:33.352Z" },
- { url = "https://files.pythonhosted.org/packages/aa/7f/59ec509abf90eda5048b0bc3e2d7b5099dffdb3e6b127019895ab9d5ef44/yarl-1.22.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:139718f35149ff544caba20fce6e8a2f71f1e39b92c700d8438a0b1d2a631a02", size = 385316, upload-time = "2025-10-06T14:10:35.034Z" },
- { url = "https://files.pythonhosted.org/packages/e5/84/891158426bc8036bfdfd862fabd0e0fa25df4176ec793e447f4b85cf1be4/yarl-1.22.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e1b51bebd221006d3d2f95fbe124b22b247136647ae5dcc8c7acafba66e5ee67", size = 374431, upload-time = "2025-10-06T14:10:37.76Z" },
- { url = "https://files.pythonhosted.org/packages/bb/49/03da1580665baa8bef5e8ed34c6df2c2aca0a2f28bf397ed238cc1bbc6f2/yarl-1.22.0-cp313-cp313-win32.whl", hash = "sha256:d3e32536234a95f513bd374e93d717cf6b2231a791758de6c509e3653f234c95", size = 81555, upload-time = "2025-10-06T14:10:39.649Z" },
- { url = "https://files.pythonhosted.org/packages/9a/ee/450914ae11b419eadd067c6183ae08381cfdfcb9798b90b2b713bbebddda/yarl-1.22.0-cp313-cp313-win_amd64.whl", hash = "sha256:47743b82b76d89a1d20b83e60d5c20314cbd5ba2befc9cda8f28300c4a08ed4d", size = 86965, upload-time = "2025-10-06T14:10:41.313Z" },
- { url = "https://files.pythonhosted.org/packages/98/4d/264a01eae03b6cf629ad69bae94e3b0e5344741e929073678e84bf7a3e3b/yarl-1.22.0-cp313-cp313-win_arm64.whl", hash = "sha256:5d0fcda9608875f7d052eff120c7a5da474a6796fe4d83e152e0e4d42f6d1a9b", size = 81205, upload-time = "2025-10-06T14:10:43.167Z" },
- { url = "https://files.pythonhosted.org/packages/88/fc/6908f062a2f77b5f9f6d69cecb1747260831ff206adcbc5b510aff88df91/yarl-1.22.0-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:719ae08b6972befcba4310e49edb1161a88cdd331e3a694b84466bd938a6ab10", size = 146209, upload-time = "2025-10-06T14:10:44.643Z" },
- { url = "https://files.pythonhosted.org/packages/65/47/76594ae8eab26210b4867be6f49129861ad33da1f1ebdf7051e98492bf62/yarl-1.22.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:47d8a5c446df1c4db9d21b49619ffdba90e77c89ec6e283f453856c74b50b9e3", size = 95966, upload-time = "2025-10-06T14:10:46.554Z" },
- { url = "https://files.pythonhosted.org/packages/ab/ce/05e9828a49271ba6b5b038b15b3934e996980dd78abdfeb52a04cfb9467e/yarl-1.22.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:cfebc0ac8333520d2d0423cbbe43ae43c8838862ddb898f5ca68565e395516e9", size = 97312, upload-time = "2025-10-06T14:10:48.007Z" },
- { url = "https://files.pythonhosted.org/packages/d1/c5/7dffad5e4f2265b29c9d7ec869c369e4223166e4f9206fc2243ee9eea727/yarl-1.22.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4398557cbf484207df000309235979c79c4356518fd5c99158c7d38203c4da4f", size = 361967, upload-time = "2025-10-06T14:10:49.997Z" },
- { url = "https://files.pythonhosted.org/packages/50/b2/375b933c93a54bff7fc041e1a6ad2c0f6f733ffb0c6e642ce56ee3b39970/yarl-1.22.0-cp313-cp313t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:2ca6fd72a8cd803be290d42f2dec5cdcd5299eeb93c2d929bf060ad9efaf5de0", size = 323949, upload-time = "2025-10-06T14:10:52.004Z" },
- { url = "https://files.pythonhosted.org/packages/66/50/bfc2a29a1d78644c5a7220ce2f304f38248dc94124a326794e677634b6cf/yarl-1.22.0-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ca1f59c4e1ab6e72f0a23c13fca5430f889634166be85dbf1013683e49e3278e", size = 361818, upload-time = "2025-10-06T14:10:54.078Z" },
- { url = "https://files.pythonhosted.org/packages/46/96/f3941a46af7d5d0f0498f86d71275696800ddcdd20426298e572b19b91ff/yarl-1.22.0-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:6c5010a52015e7c70f86eb967db0f37f3c8bd503a695a49f8d45700144667708", size = 372626, upload-time = "2025-10-06T14:10:55.767Z" },
- { url = "https://files.pythonhosted.org/packages/c1/42/8b27c83bb875cd89448e42cd627e0fb971fa1675c9ec546393d18826cb50/yarl-1.22.0-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9d7672ecf7557476642c88497c2f8d8542f8e36596e928e9bcba0e42e1e7d71f", size = 341129, upload-time = "2025-10-06T14:10:57.985Z" },
- { url = "https://files.pythonhosted.org/packages/49/36/99ca3122201b382a3cf7cc937b95235b0ac944f7e9f2d5331d50821ed352/yarl-1.22.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:3b7c88eeef021579d600e50363e0b6ee4f7f6f728cd3486b9d0f3ee7b946398d", size = 346776, upload-time = "2025-10-06T14:10:59.633Z" },
- { url = "https://files.pythonhosted.org/packages/85/b4/47328bf996acd01a4c16ef9dcd2f59c969f495073616586f78cd5f2efb99/yarl-1.22.0-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:f4afb5c34f2c6fecdcc182dfcfc6af6cccf1aa923eed4d6a12e9d96904e1a0d8", size = 334879, upload-time = "2025-10-06T14:11:01.454Z" },
- { url = "https://files.pythonhosted.org/packages/c2/ad/b77d7b3f14a4283bffb8e92c6026496f6de49751c2f97d4352242bba3990/yarl-1.22.0-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:59c189e3e99a59cf8d83cbb31d4db02d66cda5a1a4374e8a012b51255341abf5", size = 350996, upload-time = "2025-10-06T14:11:03.452Z" },
- { url = "https://files.pythonhosted.org/packages/81/c8/06e1d69295792ba54d556f06686cbd6a7ce39c22307100e3fb4a2c0b0a1d/yarl-1.22.0-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:5a3bf7f62a289fa90f1990422dc8dff5a458469ea71d1624585ec3a4c8d6960f", size = 356047, upload-time = "2025-10-06T14:11:05.115Z" },
- { url = "https://files.pythonhosted.org/packages/4b/b8/4c0e9e9f597074b208d18cef227d83aac36184bfbc6eab204ea55783dbc5/yarl-1.22.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:de6b9a04c606978fdfe72666fa216ffcf2d1a9f6a381058d4378f8d7b1e5de62", size = 342947, upload-time = "2025-10-06T14:11:08.137Z" },
- { url = "https://files.pythonhosted.org/packages/e0/e5/11f140a58bf4c6ad7aca69a892bff0ee638c31bea4206748fc0df4ebcb3a/yarl-1.22.0-cp313-cp313t-win32.whl", hash = "sha256:1834bb90991cc2999f10f97f5f01317f99b143284766d197e43cd5b45eb18d03", size = 86943, upload-time = "2025-10-06T14:11:10.284Z" },
- { url = "https://files.pythonhosted.org/packages/31/74/8b74bae38ed7fe6793d0c15a0c8207bbb819cf287788459e5ed230996cdd/yarl-1.22.0-cp313-cp313t-win_amd64.whl", hash = "sha256:ff86011bd159a9d2dfc89c34cfd8aff12875980e3bd6a39ff097887520e60249", size = 93715, upload-time = "2025-10-06T14:11:11.739Z" },
- { url = "https://files.pythonhosted.org/packages/69/66/991858aa4b5892d57aef7ee1ba6b4d01ec3b7eb3060795d34090a3ca3278/yarl-1.22.0-cp313-cp313t-win_arm64.whl", hash = "sha256:7861058d0582b847bc4e3a4a4c46828a410bca738673f35a29ba3ca5db0b473b", size = 83857, upload-time = "2025-10-06T14:11:13.586Z" },
- { url = "https://files.pythonhosted.org/packages/46/b3/e20ef504049f1a1c54a814b4b9bed96d1ac0e0610c3b4da178f87209db05/yarl-1.22.0-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:34b36c2c57124530884d89d50ed2c1478697ad7473efd59cfd479945c95650e4", size = 140520, upload-time = "2025-10-06T14:11:15.465Z" },
- { url = "https://files.pythonhosted.org/packages/e4/04/3532d990fdbab02e5ede063676b5c4260e7f3abea2151099c2aa745acc4c/yarl-1.22.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:0dd9a702591ca2e543631c2a017e4a547e38a5c0f29eece37d9097e04a7ac683", size = 93504, upload-time = "2025-10-06T14:11:17.106Z" },
- { url = "https://files.pythonhosted.org/packages/11/63/ff458113c5c2dac9a9719ac68ee7c947cb621432bcf28c9972b1c0e83938/yarl-1.22.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:594fcab1032e2d2cc3321bb2e51271e7cd2b516c7d9aee780ece81b07ff8244b", size = 94282, upload-time = "2025-10-06T14:11:19.064Z" },
- { url = "https://files.pythonhosted.org/packages/a7/bc/315a56aca762d44a6aaaf7ad253f04d996cb6b27bad34410f82d76ea8038/yarl-1.22.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f3d7a87a78d46a2e3d5b72587ac14b4c16952dd0887dbb051451eceac774411e", size = 372080, upload-time = "2025-10-06T14:11:20.996Z" },
- { url = "https://files.pythonhosted.org/packages/3f/3f/08e9b826ec2e099ea6e7c69a61272f4f6da62cb5b1b63590bb80ca2e4a40/yarl-1.22.0-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:852863707010316c973162e703bddabec35e8757e67fcb8ad58829de1ebc8590", size = 338696, upload-time = "2025-10-06T14:11:22.847Z" },
- { url = "https://files.pythonhosted.org/packages/e3/9f/90360108e3b32bd76789088e99538febfea24a102380ae73827f62073543/yarl-1.22.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:131a085a53bfe839a477c0845acf21efc77457ba2bcf5899618136d64f3303a2", size = 387121, upload-time = "2025-10-06T14:11:24.889Z" },
- { url = "https://files.pythonhosted.org/packages/98/92/ab8d4657bd5b46a38094cfaea498f18bb70ce6b63508fd7e909bd1f93066/yarl-1.22.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:078a8aefd263f4d4f923a9677b942b445a2be970ca24548a8102689a3a8ab8da", size = 394080, upload-time = "2025-10-06T14:11:27.307Z" },
- { url = "https://files.pythonhosted.org/packages/f5/e7/d8c5a7752fef68205296201f8ec2bf718f5c805a7a7e9880576c67600658/yarl-1.22.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bca03b91c323036913993ff5c738d0842fc9c60c4648e5c8d98331526df89784", size = 372661, upload-time = "2025-10-06T14:11:29.387Z" },
- { url = "https://files.pythonhosted.org/packages/b6/2e/f4d26183c8db0bb82d491b072f3127fb8c381a6206a3a56332714b79b751/yarl-1.22.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:68986a61557d37bb90d3051a45b91fa3d5c516d177dfc6dd6f2f436a07ff2b6b", size = 364645, upload-time = "2025-10-06T14:11:31.423Z" },
- { url = "https://files.pythonhosted.org/packages/80/7c/428e5812e6b87cd00ee8e898328a62c95825bf37c7fa87f0b6bb2ad31304/yarl-1.22.0-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:4792b262d585ff0dff6bcb787f8492e40698443ec982a3568c2096433660c694", size = 355361, upload-time = "2025-10-06T14:11:33.055Z" },
- { url = "https://files.pythonhosted.org/packages/ec/2a/249405fd26776f8b13c067378ef4d7dd49c9098d1b6457cdd152a99e96a9/yarl-1.22.0-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:ebd4549b108d732dba1d4ace67614b9545b21ece30937a63a65dd34efa19732d", size = 381451, upload-time = "2025-10-06T14:11:35.136Z" },
- { url = "https://files.pythonhosted.org/packages/67/a8/fb6b1adbe98cf1e2dd9fad71003d3a63a1bc22459c6e15f5714eb9323b93/yarl-1.22.0-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:f87ac53513d22240c7d59203f25cc3beac1e574c6cd681bbfd321987b69f95fd", size = 383814, upload-time = "2025-10-06T14:11:37.094Z" },
- { url = "https://files.pythonhosted.org/packages/d9/f9/3aa2c0e480fb73e872ae2814c43bc1e734740bb0d54e8cb2a95925f98131/yarl-1.22.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:22b029f2881599e2f1b06f8f1db2ee63bd309e2293ba2d566e008ba12778b8da", size = 370799, upload-time = "2025-10-06T14:11:38.83Z" },
- { url = "https://files.pythonhosted.org/packages/50/3c/af9dba3b8b5eeb302f36f16f92791f3ea62e3f47763406abf6d5a4a3333b/yarl-1.22.0-cp314-cp314-win32.whl", hash = "sha256:6a635ea45ba4ea8238463b4f7d0e721bad669f80878b7bfd1f89266e2ae63da2", size = 82990, upload-time = "2025-10-06T14:11:40.624Z" },
- { url = "https://files.pythonhosted.org/packages/ac/30/ac3a0c5bdc1d6efd1b41fa24d4897a4329b3b1e98de9449679dd327af4f0/yarl-1.22.0-cp314-cp314-win_amd64.whl", hash = "sha256:0d6e6885777af0f110b0e5d7e5dda8b704efed3894da26220b7f3d887b839a79", size = 88292, upload-time = "2025-10-06T14:11:42.578Z" },
- { url = "https://files.pythonhosted.org/packages/df/0a/227ab4ff5b998a1b7410abc7b46c9b7a26b0ca9e86c34ba4b8d8bc7c63d5/yarl-1.22.0-cp314-cp314-win_arm64.whl", hash = "sha256:8218f4e98d3c10d683584cb40f0424f4b9fd6e95610232dd75e13743b070ee33", size = 82888, upload-time = "2025-10-06T14:11:44.863Z" },
- { url = "https://files.pythonhosted.org/packages/06/5e/a15eb13db90abd87dfbefb9760c0f3f257ac42a5cac7e75dbc23bed97a9f/yarl-1.22.0-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:45c2842ff0e0d1b35a6bf1cd6c690939dacb617a70827f715232b2e0494d55d1", size = 146223, upload-time = "2025-10-06T14:11:46.796Z" },
- { url = "https://files.pythonhosted.org/packages/18/82/9665c61910d4d84f41a5bf6837597c89e665fa88aa4941080704645932a9/yarl-1.22.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:d947071e6ebcf2e2bee8fce76e10faca8f7a14808ca36a910263acaacef08eca", size = 95981, upload-time = "2025-10-06T14:11:48.845Z" },
- { url = "https://files.pythonhosted.org/packages/5d/9a/2f65743589809af4d0a6d3aa749343c4b5f4c380cc24a8e94a3c6625a808/yarl-1.22.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:334b8721303e61b00019474cc103bdac3d7b1f65e91f0bfedeec2d56dfe74b53", size = 97303, upload-time = "2025-10-06T14:11:50.897Z" },
- { url = "https://files.pythonhosted.org/packages/b0/ab/5b13d3e157505c43c3b43b5a776cbf7b24a02bc4cccc40314771197e3508/yarl-1.22.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1e7ce67c34138a058fd092f67d07a72b8e31ff0c9236e751957465a24b28910c", size = 361820, upload-time = "2025-10-06T14:11:52.549Z" },
- { url = "https://files.pythonhosted.org/packages/fb/76/242a5ef4677615cf95330cfc1b4610e78184400699bdda0acb897ef5e49a/yarl-1.22.0-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:d77e1b2c6d04711478cb1c4ab90db07f1609ccf06a287d5607fcd90dc9863acf", size = 323203, upload-time = "2025-10-06T14:11:54.225Z" },
- { url = "https://files.pythonhosted.org/packages/8c/96/475509110d3f0153b43d06164cf4195c64d16999e0c7e2d8a099adcd6907/yarl-1.22.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c4647674b6150d2cae088fc07de2738a84b8bcedebef29802cf0b0a82ab6face", size = 363173, upload-time = "2025-10-06T14:11:56.069Z" },
- { url = "https://files.pythonhosted.org/packages/c9/66/59db471aecfbd559a1fd48aedd954435558cd98c7d0da8b03cc6c140a32c/yarl-1.22.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:efb07073be061c8f79d03d04139a80ba33cbd390ca8f0297aae9cce6411e4c6b", size = 373562, upload-time = "2025-10-06T14:11:58.783Z" },
- { url = "https://files.pythonhosted.org/packages/03/1f/c5d94abc91557384719da10ff166b916107c1b45e4d0423a88457071dd88/yarl-1.22.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e51ac5435758ba97ad69617e13233da53908beccc6cfcd6c34bbed8dcbede486", size = 339828, upload-time = "2025-10-06T14:12:00.686Z" },
- { url = "https://files.pythonhosted.org/packages/5f/97/aa6a143d3afba17b6465733681c70cf175af89f76ec8d9286e08437a7454/yarl-1.22.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:33e32a0dd0c8205efa8e83d04fc9f19313772b78522d1bdc7d9aed706bfd6138", size = 347551, upload-time = "2025-10-06T14:12:02.628Z" },
- { url = "https://files.pythonhosted.org/packages/43/3c/45a2b6d80195959239a7b2a8810506d4eea5487dce61c2a3393e7fc3c52e/yarl-1.22.0-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:bf4a21e58b9cde0e401e683ebd00f6ed30a06d14e93f7c8fd059f8b6e8f87b6a", size = 334512, upload-time = "2025-10-06T14:12:04.871Z" },
- { url = "https://files.pythonhosted.org/packages/86/a0/c2ab48d74599c7c84cb104ebd799c5813de252bea0f360ffc29d270c2caa/yarl-1.22.0-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:e4b582bab49ac33c8deb97e058cd67c2c50dac0dd134874106d9c774fd272529", size = 352400, upload-time = "2025-10-06T14:12:06.624Z" },
- { url = "https://files.pythonhosted.org/packages/32/75/f8919b2eafc929567d3d8411f72bdb1a2109c01caaab4ebfa5f8ffadc15b/yarl-1.22.0-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:0b5bcc1a9c4839e7e30b7b30dd47fe5e7e44fb7054ec29b5bb8d526aa1041093", size = 357140, upload-time = "2025-10-06T14:12:08.362Z" },
- { url = "https://files.pythonhosted.org/packages/cf/72/6a85bba382f22cf78add705d8c3731748397d986e197e53ecc7835e76de7/yarl-1.22.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:c0232bce2170103ec23c454e54a57008a9a72b5d1c3105dc2496750da8cfa47c", size = 341473, upload-time = "2025-10-06T14:12:10.994Z" },
- { url = "https://files.pythonhosted.org/packages/35/18/55e6011f7c044dc80b98893060773cefcfdbf60dfefb8cb2f58b9bacbd83/yarl-1.22.0-cp314-cp314t-win32.whl", hash = "sha256:8009b3173bcd637be650922ac455946197d858b3630b6d8787aa9e5c4564533e", size = 89056, upload-time = "2025-10-06T14:12:13.317Z" },
- { url = "https://files.pythonhosted.org/packages/f9/86/0f0dccb6e59a9e7f122c5afd43568b1d31b8ab7dda5f1b01fb5c7025c9a9/yarl-1.22.0-cp314-cp314t-win_amd64.whl", hash = "sha256:9fb17ea16e972c63d25d4a97f016d235c78dd2344820eb35bc034bc32012ee27", size = 96292, upload-time = "2025-10-06T14:12:15.398Z" },
- { url = "https://files.pythonhosted.org/packages/48/b7/503c98092fb3b344a179579f55814b613c1fbb1c23b3ec14a7b008a66a6e/yarl-1.22.0-cp314-cp314t-win_arm64.whl", hash = "sha256:9f6d73c1436b934e3f01df1e1b21ff765cd1d28c77dfb9ace207f746d4610ee1", size = 85171, upload-time = "2025-10-06T14:12:16.935Z" },
- { url = "https://files.pythonhosted.org/packages/94/fd/6480106702a79bcceda5fd9c63cb19a04a6506bd5ce7fd8d9b63742f0021/yarl-1.22.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:3aa27acb6de7a23785d81557577491f6c38a5209a254d1191519d07d8fe51748", size = 141301, upload-time = "2025-10-06T14:12:19.01Z" },
- { url = "https://files.pythonhosted.org/packages/42/e1/6d95d21b17a93e793e4ec420a925fe1f6a9342338ca7a563ed21129c0990/yarl-1.22.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:af74f05666a5e531289cb1cc9c883d1de2088b8e5b4de48004e5ca8a830ac859", size = 93864, upload-time = "2025-10-06T14:12:21.05Z" },
- { url = "https://files.pythonhosted.org/packages/32/58/b8055273c203968e89808413ea4c984988b6649baabf10f4522e67c22d2f/yarl-1.22.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:62441e55958977b8167b2709c164c91a6363e25da322d87ae6dd9c6019ceecf9", size = 94706, upload-time = "2025-10-06T14:12:23.287Z" },
- { url = "https://files.pythonhosted.org/packages/18/91/d7bfbc28a88c2895ecd0da6a874def0c147de78afc52c773c28e1aa233a3/yarl-1.22.0-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b580e71cac3f8113d3135888770903eaf2f507e9421e5697d6ee6d8cd1c7f054", size = 347100, upload-time = "2025-10-06T14:12:28.527Z" },
- { url = "https://files.pythonhosted.org/packages/bd/e8/37a1e7b99721c0564b1fc7b0a4d1f595ef6fb8060d82ca61775b644185f7/yarl-1.22.0-cp39-cp39-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:e81fda2fb4a07eda1a2252b216aa0df23ebcd4d584894e9612e80999a78fd95b", size = 318902, upload-time = "2025-10-06T14:12:30.528Z" },
- { url = "https://files.pythonhosted.org/packages/1c/ef/34724449d7ef2db4f22df644f2dac0b8a275d20f585e526937b3ae47b02d/yarl-1.22.0-cp39-cp39-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:99b6fc1d55782461b78221e95fc357b47ad98b041e8e20f47c1411d0aacddc60", size = 363302, upload-time = "2025-10-06T14:12:32.295Z" },
- { url = "https://files.pythonhosted.org/packages/8a/04/88a39a5dad39889f192cce8d66cc4c58dbeca983e83f9b6bf23822a7ed91/yarl-1.22.0-cp39-cp39-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:088e4e08f033db4be2ccd1f34cf29fe994772fb54cfe004bbf54db320af56890", size = 370816, upload-time = "2025-10-06T14:12:34.01Z" },
- { url = "https://files.pythonhosted.org/packages/6b/1f/5e895e547129413f56c76be2c3ce4b96c797d2d0ff3e16a817d9269b12e6/yarl-1.22.0-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2e4e1f6f0b4da23e61188676e3ed027ef0baa833a2e633c29ff8530800edccba", size = 346465, upload-time = "2025-10-06T14:12:35.977Z" },
- { url = "https://files.pythonhosted.org/packages/11/13/a750e9fd6f9cc9ed3a52a70fe58ffe505322f0efe0d48e1fd9ffe53281f5/yarl-1.22.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:84fc3ec96fce86ce5aa305eb4aa9358279d1aa644b71fab7b8ed33fe3ba1a7ca", size = 341506, upload-time = "2025-10-06T14:12:37.788Z" },
- { url = "https://files.pythonhosted.org/packages/3c/67/bb6024de76e7186611ebe626aec5b71a2d2ecf9453e795f2dbd80614784c/yarl-1.22.0-cp39-cp39-musllinux_1_2_armv7l.whl", hash = "sha256:5dbeefd6ca588b33576a01b0ad58aa934bc1b41ef89dee505bf2932b22ddffba", size = 335030, upload-time = "2025-10-06T14:12:39.775Z" },
- { url = "https://files.pythonhosted.org/packages/a2/be/50b38447fd94a7992996a62b8b463d0579323fcfc08c61bdba949eef8a5d/yarl-1.22.0-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:14291620375b1060613f4aab9ebf21850058b6b1b438f386cc814813d901c60b", size = 358560, upload-time = "2025-10-06T14:12:41.547Z" },
- { url = "https://files.pythonhosted.org/packages/e2/89/c020b6f547578c4e3dbb6335bf918f26e2f34ad0d1e515d72fd33ac0c635/yarl-1.22.0-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:a4fcfc8eb2c34148c118dfa02e6427ca278bfd0f3df7c5f99e33d2c0e81eae3e", size = 357290, upload-time = "2025-10-06T14:12:43.861Z" },
- { url = "https://files.pythonhosted.org/packages/8c/52/c49a619ee35a402fa3a7019a4fa8d26878fec0d1243f6968bbf516789578/yarl-1.22.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:029866bde8d7b0878b9c160e72305bbf0a7342bcd20b9999381704ae03308dc8", size = 350700, upload-time = "2025-10-06T14:12:46.868Z" },
- { url = "https://files.pythonhosted.org/packages/ab/c9/f5042d87777bf6968435f04a2bbb15466b2f142e6e47fa4f34d1a3f32f0c/yarl-1.22.0-cp39-cp39-win32.whl", hash = "sha256:4dcc74149ccc8bba31ce1944acee24813e93cfdee2acda3c172df844948ddf7b", size = 82323, upload-time = "2025-10-06T14:12:48.633Z" },
- { url = "https://files.pythonhosted.org/packages/fd/58/d00f7cad9eba20c4eefac2682f34661d1d1b3a942fc0092eb60e78cfb733/yarl-1.22.0-cp39-cp39-win_amd64.whl", hash = "sha256:10619d9fdee46d20edc49d3479e2f8269d0779f1b031e6f7c2aa1c76be04b7ed", size = 87145, upload-time = "2025-10-06T14:12:50.241Z" },
- { url = "https://files.pythonhosted.org/packages/c2/a3/70904f365080780d38b919edd42d224b8c4ce224a86950d2eaa2a24366ad/yarl-1.22.0-cp39-cp39-win_arm64.whl", hash = "sha256:dd7afd3f8b0bfb4e0d9fc3c31bfe8a4ec7debe124cfd90619305def3c8ca8cd2", size = 82173, upload-time = "2025-10-06T14:12:51.869Z" },
- { url = "https://files.pythonhosted.org/packages/73/ae/b48f95715333080afb75a4504487cbe142cae1268afc482d06692d605ae6/yarl-1.22.0-py3-none-any.whl", hash = "sha256:1380560bdba02b6b6c90de54133c81c9f2a453dee9912fe58c1dcced1edb7cff", size = 46814, upload-time = "2025-10-06T14:12:53.872Z" },
-]
-
-[[package]]
-name = "yarl"
-version = "1.24.5"
-source = { registry = "https://pypi.org/simple" }
-resolution-markers = [
- "python_full_version >= '3.10'",
-]
-dependencies = [
- { name = "idna" },
- { name = "multidict" },
- { name = "propcache", version = "0.5.2", source = { registry = "https://pypi.org/simple" } },
-]
-sdist = { url = "https://files.pythonhosted.org/packages/31/33/ebe9e3d1f86c7a0b51094c0a146392045ca1631d2664889539dec8088a33/yarl-1.24.5.tar.gz", hash = "sha256:e81b83143bee16329c23db3c1b2d82b29892fcbcb849186d2f6e98a5abe9a57f", size = 228679, upload-time = "2026-07-20T02:07:45.435Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/48/ac/cacdda1f0a90441297210bc34cf7e4ac1b7318c8030ebd83bdf6fe82f1db/yarl-1.24.5-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:88f50c94e21a0a7f14042c015b0eba1881af78562e7bf007e0033e624da59750", size = 135466, upload-time = "2026-07-20T02:04:21.695Z" },
- { url = "https://files.pythonhosted.org/packages/c0/a5/1b2ceace0230e40c52ab1b263148059a43a6303219b996affc68f8381836/yarl-1.24.5-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:6efbccc3d7f75d5b03105172a8dc86d82ba4da86817952529dd93185f4a88be2", size = 97291, upload-time = "2026-07-20T02:04:24.045Z" },
- { url = "https://files.pythonhosted.org/packages/59/1d/340d1a0db7bbce1f291afc044255ebf4ebbce2b25ab1b3f7d3d069080f5d/yarl-1.24.5-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:0ebfaffe1a16cb72141c8e09f18cc76856dbe58639f393a4f2b26e474b96b871", size = 97154, upload-time = "2026-07-20T02:04:25.761Z" },
- { url = "https://files.pythonhosted.org/packages/05/41/25596a33c2fb5098dca8dc3773b04221db64ded0b7f8f09885647d864610/yarl-1.24.5-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8ac73abdc7ab75610f95a8fd994c6457e87752b02a63987e188f937a1fc180f0", size = 109196, upload-time = "2026-07-20T02:04:27.543Z" },
- { url = "https://files.pythonhosted.org/packages/f2/df/dd9f2fb8a5c6054fbefd1538d2b9b1127e612d2ee64b307a070173b57afd/yarl-1.24.5-cp310-cp310-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:4d97a951a81039050e45f04e96689b58b8243fa5e62aa14fe67cb6075300885e", size = 102556, upload-time = "2026-07-20T02:04:29.16Z" },
- { url = "https://files.pythonhosted.org/packages/cb/57/4754b9d2c8945880290ecba0864e8b0441e117bba70534fe819e3645e174/yarl-1.24.5-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:fe7b7bb170daccbba19ad33012d2b15f1e7942296fd4d45fc1b79013da8cc0f2", size = 117965, upload-time = "2026-07-20T02:04:30.845Z" },
- { url = "https://files.pythonhosted.org/packages/74/b5/6a9ece27d2043c3386f902dd078ab35d29ef5126b3206ebffb673283a7cb/yarl-1.24.5-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:89a1bbb58e0e3f7a283653d854b1e95d65e5cfd4af224dac5f02629ec1a3e621", size = 116266, upload-time = "2026-07-20T02:04:32.573Z" },
- { url = "https://files.pythonhosted.org/packages/9e/bc/a6653249f6ee59ec85dcfec008d9cbc16586dad613963bb17a91b2b993a5/yarl-1.24.5-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7fa5e51397466ea7e98de493fa2ff1b8193cfef8a7b0f9b4842f92d342df0dba", size = 110758, upload-time = "2026-07-20T02:04:34.235Z" },
- { url = "https://files.pythonhosted.org/packages/65/7e/c5a12fb8208df7b981bc82256e7831ce428eeaf893f7bbe6179c57bb9252/yarl-1.24.5-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:4103b77b8a8225e413107d2349b65eb3c1c52627b5cc5c3c4c1c6a798b218950", size = 110120, upload-time = "2026-07-20T02:04:35.85Z" },
- { url = "https://files.pythonhosted.org/packages/04/6c/1b659b964626694667b3ec01bf4bcff564b73ae7c48ea1fbfe588b78b461/yarl-1.24.5-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:f9f3e9c8a9ecffa57bef8fb4fa19e5fa4d2d8307cf6bac5b1fca5e5860f4ba00", size = 108834, upload-time = "2026-07-20T02:04:37.67Z" },
- { url = "https://files.pythonhosted.org/packages/74/a6/bf48f55c2104e40c15b7b13fad0a5756a11552a55f01c90bc90a66ab81c3/yarl-1.24.5-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:c0ebc836c47a6477e182169c6a476fc691d12b518894bf7dd2572f0d59f1c7ed", size = 103442, upload-time = "2026-07-20T02:04:39.576Z" },
- { url = "https://files.pythonhosted.org/packages/37/ac/84b273ac133ecdce598fc1f4140a08a1bf2044048bff8106371d207d105f/yarl-1.24.5-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:96d30286dd02679e32a39aa8f0b7498fc847fcda46cfc09df5513e82ce252440", size = 117413, upload-time = "2026-07-20T02:04:41.549Z" },
- { url = "https://files.pythonhosted.org/packages/a4/55/9307e03977d3b290dfa42e5d2bae7b6140808fd1786fbe70cd9d3bee53c5/yarl-1.24.5-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:fd8c81f346b58f45818d09ea11db69a8d5fd34a224b79871f6d44f12cd7977b1", size = 109498, upload-time = "2026-07-20T02:04:43.468Z" },
- { url = "https://files.pythonhosted.org/packages/fc/be/791a6f314cb4c989c19f8e3a10271f1e469c077143915e52474d80f26b4b/yarl-1.24.5-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:5c55256dee8f4b27bfbf636c8363383c7c8db7890c7cba5217d7bd5f5f21dab6", size = 116062, upload-time = "2026-07-20T02:04:45.319Z" },
- { url = "https://files.pythonhosted.org/packages/19/1a/ddd3807b86055010e2f99aa89b3c640effdb65696766c20597f696f48a1c/yarl-1.24.5-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:9f4d8cf085a4c6a40fb97ea0f46938a8df43c85d31f9d45e2a8867ea9293790d", size = 110941, upload-time = "2026-07-20T02:04:47.054Z" },
- { url = "https://files.pythonhosted.org/packages/6d/03/f34271bba042d2187508bf62aea20a14129efb5a1acfc6a2efe7544630b4/yarl-1.24.5-cp310-cp310-win_amd64.whl", hash = "sha256:240cbec09667c1fed4c6cd0060b9ec57332427d7441289a2ed8875dc9fb2b224", size = 97534, upload-time = "2026-07-20T02:04:48.774Z" },
- { url = "https://files.pythonhosted.org/packages/e4/02/ecc8dc31b9f355731e700f8402b8075d2ea1737dbc4baf4abf0f0fc64288/yarl-1.24.5-cp310-cp310-win_arm64.whl", hash = "sha256:8a6987eaad834cb32dd57d9d582225f0054a5d1af706ccfbbdba735af4927e13", size = 93603, upload-time = "2026-07-20T02:04:50.686Z" },
- { url = "https://files.pythonhosted.org/packages/fe/db/3cb5df059756a45761cc3dee8fd25ec82b83a6585ea3542b969fda850f99/yarl-1.24.5-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:2c1fe720934a16ea8e7146175cba2126f87f54912c8c5435e7f7c7a51ef808d3", size = 135043, upload-time = "2026-07-20T02:04:52.39Z" },
- { url = "https://files.pythonhosted.org/packages/44/f8/767d6bd5a03db63bc467df2fb56d6fafeae9667d74aea92cd6af399f828b/yarl-1.24.5-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c687ed078e145f5fd53a14854beff320e1d2ab76df03e2009c98f39a0f68f39a", size = 96942, upload-time = "2026-07-20T02:04:54.26Z" },
- { url = "https://files.pythonhosted.org/packages/ce/97/10b939c44d7b28d1dbc389cfc7012306d1ea8dba01eaef44b39fffaee52a/yarl-1.24.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:709f1efed56c4a145793c046cd4939f9959bcd818979a787b77d8e09c57a0840", size = 97046, upload-time = "2026-07-20T02:04:56.638Z" },
- { url = "https://files.pythonhosted.org/packages/5b/7a/b410dbe39b6255c55fb2a2bcee96eb844d0789235ddc381a889a90dc72d6/yarl-1.24.5-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:874019bd513008b009f58657134e5d0c5e030b3559bd0553976837adf52fe966", size = 110512, upload-time = "2026-07-20T02:04:58.955Z" },
- { url = "https://files.pythonhosted.org/packages/83/c7/da591971f78a5617e1f21f5699858ebccd836fe181a6493788ffc91ba69b/yarl-1.24.5-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:a4582acf7ef76482f6f511ebaf1946dae7f2e85ec4728b81a678c01df63bd723", size = 102454, upload-time = "2026-07-20T02:05:00.623Z" },
- { url = "https://files.pythonhosted.org/packages/c4/8e/73b0ed4de47289a78a96045d76d1cfe5e41848bf0da59ce25b2ec87ee05d/yarl-1.24.5-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2cabe6546e41dabe439999a23fcb5246e0c3b595b4315b96ef755252be90caeb", size = 117617, upload-time = "2026-07-20T02:05:02.325Z" },
- { url = "https://files.pythonhosted.org/packages/cf/14/b744747bc4f57a8d55bd744df463457524583e1e9f7538b5ace0346ab92e/yarl-1.24.5-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:17f57620f5475b3c69109376cc87e42a7af5db13c9398e4292772a706ff10780", size = 116135, upload-time = "2026-07-20T02:05:04.05Z" },
- { url = "https://files.pythonhosted.org/packages/66/ca/95aa4d0e5b7ea4f20e4d577c42d001ed9df207569fdb063cc5ed4ebb496b/yarl-1.24.5-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:570fec8fbd22b032733625f03f10b7ff023bc399213db15e72a7acaef28c2f4e", size = 111935, upload-time = "2026-07-20T02:05:05.738Z" },
- { url = "https://files.pythonhosted.org/packages/72/0d/d2ad8d6b147832d177a4e720ba1962fe686eb0913b74503b3eca094b8bba/yarl-1.24.5-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:5fede79c6f73ff2c3ef822864cb1ada23196e62756df53bc6231d351a49516a2", size = 110010, upload-time = "2026-07-20T02:05:07.471Z" },
- { url = "https://files.pythonhosted.org/packages/50/18/eb335e4120903903f4865041355ae46256a2406eb2865bc24827f4f27b61/yarl-1.24.5-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8ccf9aca873b767977c73df497a85dbedee4ee086ae9ae49dc461333b9b79f58", size = 110058, upload-time = "2026-07-20T02:05:09.246Z" },
- { url = "https://files.pythonhosted.org/packages/44/70/97353add32c62ad6f206d948ac5a5ee84398225e534dc6ed6433d1b335b6/yarl-1.24.5-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:ad5d8201d310b031e6cd839d9bac2d4e5a01533ce5d3d5b50b7de1ef3af1de61", size = 103308, upload-time = "2026-07-20T02:05:11.31Z" },
- { url = "https://files.pythonhosted.org/packages/68/39/5e7398d4b6f6b3c9062823ebc60802df5b272e3fe9e788f9734c6ee46c85/yarl-1.24.5-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:841f0852f48fefea3b12c9dfec00704dfa3aef5215d0e3ce564bb3d7cd8d57c6", size = 116898, upload-time = "2026-07-20T02:05:13.099Z" },
- { url = "https://files.pythonhosted.org/packages/e4/c9/09e52f2239e8b96357eccca05915382e4ba5405ebfb623b6036040d99654/yarl-1.24.5-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:9baafc71b04f8f4bb0703b21d6fc9f0c30b346c636a532ff16ec8491a5ea4b1f", size = 109400, upload-time = "2026-07-20T02:05:14.821Z" },
- { url = "https://files.pythonhosted.org/packages/4b/6a/e94133d4c2d1a14d2384310bf3e79d9cf32c9d1eae1c6f034fb80d098fa1/yarl-1.24.5-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:d897129df1a22b12aeed2c2c98df0785a2e8e6e0bde87b389491d0025c187077", size = 115934, upload-time = "2026-07-20T02:05:17.78Z" },
- { url = "https://files.pythonhosted.org/packages/4e/3c/34955ed967b976fc38edcbb6d538dee79dbda4cb7fc7f72a0907a7c78e0f/yarl-1.24.5-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:dd625535328fd9882374356269227670189adfcc6a2d90284f323c05862eecbd", size = 112178, upload-time = "2026-07-20T02:05:19.675Z" },
- { url = "https://files.pythonhosted.org/packages/f5/46/d7bd3a8859d47dcfaffd7127af7076032a7da278a9a02e17b5f37bfb6712/yarl-1.24.5-cp311-cp311-win_amd64.whl", hash = "sha256:f4239bbec5a3577ddb49e4b50aeb32d8e5792098262ae2f63723f916a29b1a25", size = 97544, upload-time = "2026-07-20T02:05:21.523Z" },
- { url = "https://files.pythonhosted.org/packages/01/69/c1bfd21e32c638974ea2c542a0b8c53ef1fa9eff336020f5d014f9503ff2/yarl-1.24.5-cp311-cp311-win_arm64.whl", hash = "sha256:3ac6aff147deb9c09461b2d4bbdf6256831198f5d8a23f5d37138213090b6d8a", size = 93359, upload-time = "2026-07-20T02:05:23.493Z" },
- { url = "https://files.pythonhosted.org/packages/1b/84/71d051c850b5af41d168c679d9eb67eb7c55283ac4ee131673edf134bc4e/yarl-1.24.5-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:d693396e5aea78db03decd60aec9ece16c9b40ba00a587f089615ff4e718a81d", size = 136035, upload-time = "2026-07-20T02:05:25.489Z" },
- { url = "https://files.pythonhosted.org/packages/03/4d/8ad27f9a1b7e69313cca5d695b925b48efe51208d3490e0844bae97cabc0/yarl-1.24.5-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:3363fcc96e665878946ad7a106b9a13eac0541766a690ef287c0232ac768b6ec", size = 97642, upload-time = "2026-07-20T02:05:27.429Z" },
- { url = "https://files.pythonhosted.org/packages/ea/b4/05b4131c407006cd1e410e9c6539f16a0945724677e5364447313c15ea3e/yarl-1.24.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:9d399bdcfb4a0f659b9b3788bbc89babe63d9a6a65aacdf4d4e7065ff2e6316c", size = 97323, upload-time = "2026-07-20T02:05:29.441Z" },
- { url = "https://files.pythonhosted.org/packages/20/16/e618c875c73e0e39611f20a581b3d5e8d59b8857bf001bee3263044c6deb/yarl-1.24.5-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:90333fd89b43c0d08ac85f3f1447593fc2c66de18c3d6378d7125ea118dc7a54", size = 107741, upload-time = "2026-07-20T02:05:31.367Z" },
- { url = "https://files.pythonhosted.org/packages/d9/9a/c4defeaf3ed33fcb346aacf9c6e971a8d4e2bde04a0310e79abb208e7965/yarl-1.24.5-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:665b0a2c463cc9423dd647e0bfd9f4ccc9b50f768c55304d5e9f80b177c1de12", size = 103570, upload-time = "2026-07-20T02:05:33.303Z" },
- { url = "https://files.pythonhosted.org/packages/5f/e7/0e0e0de5865ebd5914537ef486f36c727a59865c3ac0cf5ff1b32aececbf/yarl-1.24.5-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e006d3a974c4ee19512e5f058abedb6eef36a5e553c14812bdeba1758d812e6d", size = 115815, upload-time = "2026-07-20T02:05:35.292Z" },
- { url = "https://files.pythonhosted.org/packages/2b/27/ca56b700cb170aba25a3893b75355b213935657dc5714d2383354a270e62/yarl-1.24.5-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:e7d42c531243450ef0d4d9c172e7ed6ef052640f195629065041b5add4e058d1", size = 116025, upload-time = "2026-07-20T02:05:37.503Z" },
- { url = "https://files.pythonhosted.org/packages/d6/d0/d56c859b8222116f5d68459199f48359e0bf121b6f65a69bf329b3602ba0/yarl-1.24.5-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f08c7513ecef5aad65687bfdf6bc601ae9fccd04a42904501f8f7141abad9eb9", size = 109835, upload-time = "2026-07-20T02:05:39.506Z" },
- { url = "https://files.pythonhosted.org/packages/70/a2/3a35557e4d1a79425040eba202ccaf08bdc8717680fc77e2498a1ad2e0a5/yarl-1.24.5-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:6c95b17fe34ed802f17e205112e6e10db92275c34fee290aa9bdc55a9c724027", size = 108884, upload-time = "2026-07-20T02:05:41.584Z" },
- { url = "https://files.pythonhosted.org/packages/e4/35/ef4c26356b7913c68983bac2d72a4212b3347af551cb8d250b99b5ed7b7f/yarl-1.24.5-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:56b149b22de33b23b0c6077ab9518c6dcb538ad462e1830e68d06591ccf6e38b", size = 107308, upload-time = "2026-07-20T02:05:43.697Z" },
- { url = "https://files.pythonhosted.org/packages/d5/91/ff0dc66c2ccf3e0153ab97ff61eabab4400e6a5264af427ab30cd69f1857/yarl-1.24.5-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:a8fe66b8f300da93798025a785a5b90b42f3810dc2b72283ff84a41aaaebc293", size = 103646, upload-time = "2026-07-20T02:05:45.895Z" },
- { url = "https://files.pythonhosted.org/packages/74/f0/33b9271c7f881766359d58266fa0811d2e5210ed860e28da7dc6d7786344/yarl-1.24.5-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:377fe3732edbaf78ee74efdf2c9f49f6e99f20e7f9d2649fda3eb4badd77d76e", size = 115305, upload-time = "2026-07-20T02:05:47.832Z" },
- { url = "https://files.pythonhosted.org/packages/ef/65/fd79fb1868c4a80db8661091de525bf430f63c3bea1b20e8b6a84fc7d359/yarl-1.24.5-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:e8ffa78582120024f476a611d7befc123cee59e47e8309d470cf667d806e613b", size = 108404, upload-time = "2026-07-20T02:05:49.604Z" },
- { url = "https://files.pythonhosted.org/packages/ff/ba/dbabe6b262f17a816c70cfc09558dbf03ece3ec76684d02f911a3d3a189c/yarl-1.24.5-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:daba5e594f06114e37db186efd2dd916609071e59daca901a0a2e71f02b142ce", size = 115940, upload-time = "2026-07-20T02:05:51.741Z" },
- { url = "https://files.pythonhosted.org/packages/a5/43/fab2d1dad9d340a268cdde63756a123d069723efff6a372d123fa74a9517/yarl-1.24.5-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:65be18ec59496c13908f02a2472751d9ef840b4f3fb5726f129306bf6a2a7bba", size = 110006, upload-time = "2026-07-20T02:05:53.554Z" },
- { url = "https://files.pythonhosted.org/packages/c4/27/41eb51bbd1b8d89546b83897cfb0164f1e109304fd408dbb151b639eec0f/yarl-1.24.5-cp312-cp312-win_amd64.whl", hash = "sha256:a929d878fec099030c292803b31e5d5540a7b6a31e6a3cc76cb4685fc2a2f51b", size = 97618, upload-time = "2026-07-20T02:05:55.57Z" },
- { url = "https://files.pythonhosted.org/packages/3c/25/b2553764b3d65db711d8f45416351ec4f420847558eb669edcbcaadf5780/yarl-1.24.5-cp312-cp312-win_arm64.whl", hash = "sha256:7ce27823052e2013b597e0c738b13e7e36b8ccb9400df8959417b052ab0fd92c", size = 93018, upload-time = "2026-07-20T02:05:57.554Z" },
- { url = "https://files.pythonhosted.org/packages/e1/63/64ef361967cc983573149dc1515d531db5da8a4c92d22bb833d59e01b313/yarl-1.24.5-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:79af890482fc94648e8cde4c68620378f7fef60932710fa17a66abc039244da2", size = 135075, upload-time = "2026-07-20T02:05:59.671Z" },
- { url = "https://files.pythonhosted.org/packages/bb/89/55920fd853ce43e608adbc3962456f0d649d6bb15250dc2988321da0fe1c/yarl-1.24.5-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:46c2f213e23a04b93a392942d782eb9e413e6ef6bf7c8c53884e599a5c174dcb", size = 97225, upload-time = "2026-07-20T02:06:01.769Z" },
- { url = "https://files.pythonhosted.org/packages/15/f0/7688d3f2cfff7590df2af38ec46d969f4281a4dddb08a9ad2eafbcdddf98/yarl-1.24.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:92ab3e11448f2ff7bf53c5a26eff0edc086898ec8b21fb154b85839ce1d88075", size = 96751, upload-time = "2026-07-20T02:06:03.676Z" },
- { url = "https://files.pythonhosted.org/packages/05/1a/a851a0f94aaaf379dd4f901bfc80f634280bec51eb260b47363e2a4cd62e/yarl-1.24.5-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ebb0ec7f17803063d5aeb982f3b1bd2b2f4e4fae6751226cbd6ba1fcfe9e63ff", size = 107960, upload-time = "2026-07-20T02:06:05.699Z" },
- { url = "https://files.pythonhosted.org/packages/6c/a8/faea066c12f9c77ca0de90641f1655f9dd7b412477bf28c76d692f3aecff/yarl-1.24.5-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:82632daed195dcc8ea664e8556dc9bdbd671960fb3776bd92806ce05792c2448", size = 103500, upload-time = "2026-07-20T02:06:07.556Z" },
- { url = "https://files.pythonhosted.org/packages/fb/9c/1e67084c2a6e2f2db0e3be798328cb3be42c0119b621d25461479a224d21/yarl-1.24.5-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:53e549287ef628fecba270045c9701b0c564563a9b0577d24a4ec75b8ab8040f", size = 115780, upload-time = "2026-07-20T02:06:09.599Z" },
- { url = "https://files.pythonhosted.org/packages/58/86/1f94664e147474337e3359f52012cf3d02f825f694317b178bfba1078c62/yarl-1.24.5-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:fcd3b77e2f17bbe4ca56ec7bcb07992647d19d0b9c05d84886dcd6f9eb810afd", size = 115308, upload-time = "2026-07-20T02:06:11.352Z" },
- { url = "https://files.pythonhosted.org/packages/0a/43/8e55ae7538ba5f28ccb3c845c6dd4549cf7016d5992e5326512519107cdd/yarl-1.24.5-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d46b86567dd4e248c6c159fcbcdcce01e0a5c8a7cd2334a0fff759d0fa075b16", size = 110574, upload-time = "2026-07-20T02:06:13.129Z" },
- { url = "https://files.pythonhosted.org/packages/ce/ba/a889ec8765cedcf2ac44dcb02d6a21e4861399b243b263c5f2dde27ee740/yarl-1.24.5-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:7f72c74aa99359e27a2ee8d6613fefa28b5f76a983c083074dfc2aaa4ab46213", size = 109914, upload-time = "2026-07-20T02:06:15.243Z" },
- { url = "https://files.pythonhosted.org/packages/9c/c3/e45f821af67b791c2dbbe4a9f4137a1d33f8d386654a05a0c3f47bdfa25d/yarl-1.24.5-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:3f45789ce415a7ec0820dc4f82925f9b5f7732070be1dec1f5f23ec381435a24", size = 107712, upload-time = "2026-07-20T02:06:17.443Z" },
- { url = "https://files.pythonhosted.org/packages/02/00/2ab0f42c9857fcb490bfaa6647b14540b53d241ab209f23220b958cc5832/yarl-1.24.5-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:6e73e7fe93f17a7b191f52ec9da9dd8c06a8fe735a1ecbd13b97d1c723bff385", size = 104251, upload-time = "2026-07-20T02:06:19.259Z" },
- { url = "https://files.pythonhosted.org/packages/7a/70/709d9a286e98af2c7fd8e4e6cada658b5c0e30d87dd7e2a63c2fb5767217/yarl-1.24.5-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:4a36f9becdd4c5c52a20c3e9484128b070b1dcfc8944c006f3a528295a359a9c", size = 115319, upload-time = "2026-07-20T02:06:21.207Z" },
- { url = "https://files.pythonhosted.org/packages/5c/6c/3eaa515142991fe84cfc483ff986492211f1978f90161ccefdbec919d09b/yarl-1.24.5-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:7bcbe0fcf850eae67b6b01749815a4f7161c560a844c769ad7b48fcd99f791c4", size = 109163, upload-time = "2026-07-20T02:06:23.006Z" },
- { url = "https://files.pythonhosted.org/packages/bb/64/711dafce66c323a3144d470547a71c5384c57623308ac8bb5e4b903ac148/yarl-1.24.5-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:24e861e9630e0daddcb9191fb187f60f034e17a4426f8101279f0c475cd74144", size = 115435, upload-time = "2026-07-20T02:06:24.923Z" },
- { url = "https://files.pythonhosted.org/packages/cf/f3/9b9d0e6d84bea851eb1ba99e4bdc755b86fd813e49ec86dfe42f26befdef/yarl-1.24.5-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9335a099ad87287c37fe5d1a982ff392fa5efe5d14b40a730b1ec1d6a41382b4", size = 110691, upload-time = "2026-07-20T02:06:26.973Z" },
- { url = "https://files.pythonhosted.org/packages/86/e4/62a06b7e87c4246ac76b7c2da136f972eb4a3a1fc94abb07e7022d6fdb0a/yarl-1.24.5-cp313-cp313-win_amd64.whl", hash = "sha256:2dbe06fc16bc91502bca713704022182e5729861ae00277c3a23354b40929740", size = 97454, upload-time = "2026-07-20T02:06:29.163Z" },
- { url = "https://files.pythonhosted.org/packages/9e/c9/5fc8025b318ab10db413b61056bd0d95c557a70e8df4210c7511f866329c/yarl-1.24.5-cp313-cp313-win_arm64.whl", hash = "sha256:6b8536851f9f65e7f00c7a1d49ba7f2be0ffe2c11555367fc9f50d9f842410a1", size = 92813, upload-time = "2026-07-20T02:06:31.113Z" },
- { url = "https://files.pythonhosted.org/packages/a9/08/5f3085fef9564217074db9dd8573de1795bc82cde61a7ad10b6a7234a569/yarl-1.24.5-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:2729fcfc4f6a596fb0c50f32090400aa9367774ac296a00387e65098c0befa76", size = 135680, upload-time = "2026-07-20T02:06:33.273Z" },
- { url = "https://files.pythonhosted.org/packages/98/35/ba9436e579bd48a8801f2021d842d9ab4994c26e4c7dd3a4c1f1bcb57a9e/yarl-1.24.5-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:ff330d3c30db4eb6b01d79e29d2d0b407a7ecad39cfd9ec993ece57396a2ec0d", size = 97395, upload-time = "2026-07-20T02:06:35.259Z" },
- { url = "https://files.pythonhosted.org/packages/18/a9/a07f76f3c44e02b25cc743af5ef93eef27f7013eadca770451b6a6ccb5db/yarl-1.24.5-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:e42d75862735da90e7fc5a7b23db0c976f737113a54b3c9777a9b665e9cbff75", size = 97223, upload-time = "2026-07-20T02:06:37.216Z" },
- { url = "https://files.pythonhosted.org/packages/77/f7/a9a1d6fa7dd9e388f95b30f6ad3ec4e285f6c8f61f44ce16070c3fcfe414/yarl-1.24.5-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a3732e66413163e72508da9eff9ce9d2846fde51fae45d3605393d3e6cd303e9", size = 108777, upload-time = "2026-07-20T02:06:39.292Z" },
- { url = "https://files.pythonhosted.org/packages/2f/44/e0b86c302471fabd6f02808ecf2ac52b8412b624787849d4bf2cdb466f6f/yarl-1.24.5-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:5b8ee53be440a0cffc991a27be3057e0530122548dbe7c0892df08822fce5ede", size = 103119, upload-time = "2026-07-20T02:06:41.456Z" },
- { url = "https://files.pythonhosted.org/packages/d1/16/9c16d180bf8faaf223225eb50e1245870ff1ae0e302a27153988e65c51fd/yarl-1.24.5-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:af3aefa655adb5869491fa907e652290386800ae99cc50095cba71e2c6aefdca", size = 116471, upload-time = "2026-07-20T02:06:43.696Z" },
- { url = "https://files.pythonhosted.org/packages/d2/8d/b219b9df28a02ce95cfbdd41d2f7caa5669d0ff979c1c9975697145e33c5/yarl-1.24.5-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:2120b96872df4a117cde97d270bac96aea7cc52205d305cf4611df694a487027", size = 115974, upload-time = "2026-07-20T02:06:45.874Z" },
- { url = "https://files.pythonhosted.org/packages/9b/e8/f20557aca240d88e69850ad1ee91756821d094bb1310565c04d25c6682a2/yarl-1.24.5-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:66410eb6345d467151934b49bfa70fb32f5b35a6140baa40ad97d6436abea2e9", size = 110830, upload-time = "2026-07-20T02:06:47.852Z" },
- { url = "https://files.pythonhosted.org/packages/db/18/199b85109a53eeca64ee19c9cca228287e8e4ab0cc1a09b28f530e65cce0/yarl-1.24.5-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:4af7b7e1be0a69bee8210735fe6dcfc38879adfac6d62e789d53ba432d1ffa41", size = 110054, upload-time = "2026-07-20T02:06:49.84Z" },
- { url = "https://files.pythonhosted.org/packages/aa/2f/ed28147f8cd7f48c49367c90713b30a555284b6105a6a56f3a05568da795/yarl-1.24.5-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:fa139875ff98ab97da323cfadfaff08900d1ad42f1b5087b0b812a55c5a06373", size = 108312, upload-time = "2026-07-20T02:06:51.835Z" },
- { url = "https://files.pythonhosted.org/packages/c5/c5/55e16ae0a5c227cea8df1c6871ba57d614a34243146c05729caf2a1bd9c5/yarl-1.24.5-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:0055afc45e864b92729ac7600e2d102c17bef060647e74bca75fa84d66b9ff36", size = 103662, upload-time = "2026-07-20T02:06:54.061Z" },
- { url = "https://files.pythonhosted.org/packages/8d/ea/dbd7c2caec459c9a426f18b02688ecbfb58620d0f6a3422d24769fbaf8ab/yarl-1.24.5-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:f0e466ed7511fe9d459a819edbc6c2585c0b6eabde9fa8a8947552468a7a6ef0", size = 116090, upload-time = "2026-07-20T02:06:56.015Z" },
- { url = "https://files.pythonhosted.org/packages/06/84/39ce4ce3059e07fece5fbdbee8c4053406af9aca911ce9fa5f8548aab6af/yarl-1.24.5-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:f141474e85b7e54998ec5180530a7cda99ab29e282fa50e0756d89981a9b43c5", size = 109523, upload-time = "2026-07-20T02:06:57.926Z" },
- { url = "https://files.pythonhosted.org/packages/a9/8b/71ff44137b405c64a7788075669c24010019f57a7464b78c3a6cbee539d9/yarl-1.24.5-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:e2935f8c39e3b03e83519292d78f075189978f3f4adc15a78144c7c8e2a1cba5", size = 116084, upload-time = "2026-07-20T02:06:59.868Z" },
- { url = "https://files.pythonhosted.org/packages/62/c0/423078fdd4042e1862c11f0ffd977a0ffa393783c12bee94685923bc189e/yarl-1.24.5-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:9d1216a7f6f77836617dba35687c5b78a4170afc3c3f18fc788f785ba26565c4", size = 111006, upload-time = "2026-07-20T02:07:01.907Z" },
- { url = "https://files.pythonhosted.org/packages/cf/52/6daa2ee9d95e5c98b8128f8df91eb692eb423ab274b8cf08db52152fad26/yarl-1.24.5-cp314-cp314-win_amd64.whl", hash = "sha256:5ba4f78df2bcc19f764a4b26a8a4f5049c110090ad5825993aacb052bf8003ad", size = 99215, upload-time = "2026-07-20T02:07:03.852Z" },
- { url = "https://files.pythonhosted.org/packages/ec/0e/464a847d7359e0da75dd9fc5c1d1aa35d0159ea31e5f8e66a3c1c29ff3d0/yarl-1.24.5-cp314-cp314-win_arm64.whl", hash = "sha256:9e4e16c73d717c5cf27626c524d0a2e261ad20e46932b2670f64ad5dde23e26f", size = 94566, upload-time = "2026-07-20T02:07:06.074Z" },
- { url = "https://files.pythonhosted.org/packages/e2/55/e03acc4446772660bc335e86e41ef31e4d0d838fd641531a11a5ee33b493/yarl-1.24.5-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:e1ae548a9d901adca07899a4147a7c826bbcc06239d3ce9a59f57886a28a4c88", size = 142533, upload-time = "2026-07-20T02:07:08.284Z" },
- { url = "https://files.pythonhosted.org/packages/ae/71/4acd3a1fc7cf14345cdb302665ecd2097f62c365b4f14ca17d4f37775cf9/yarl-1.24.5-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:ff405d91509d88e8d44129cd87b18d70acd1f0c1aeabd7bc3c46792b1fe2acba", size = 100776, upload-time = "2026-07-20T02:07:10.197Z" },
- { url = "https://files.pythonhosted.org/packages/ff/0b/cfb76b7fe99686db264bff829779a539d923e7564ffd7ef18da6c54c3774/yarl-1.24.5-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:47e98aab9d8d82ff682e7b0b5dded33bf138a32b817fcf7fa3b27b2d7c412928", size = 100913, upload-time = "2026-07-20T02:07:12.357Z" },
- { url = "https://files.pythonhosted.org/packages/8b/3f/7116e782992abbd4fb6948488aec72078895e929a23078290739e8396fce/yarl-1.24.5-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f0a658a6d3fafee5c6f63c58f3e785c8c43c93fbc02bf9f2b6663f8185e0971f", size = 106507, upload-time = "2026-07-20T02:07:14.173Z" },
- { url = "https://files.pythonhosted.org/packages/33/90/d4d2d73ee78229cc889872eb8e085d8f5c6f51abdb178409fd9b23cf74fd/yarl-1.24.5-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:4377407001ca3c057773f44d8ddd6358fa5f691407c1ba92210bd3cf8d9e4c95", size = 99219, upload-time = "2026-07-20T02:07:16.019Z" },
- { url = "https://files.pythonhosted.org/packages/3e/fa/a6df1a9bccd644eec00abee0dff4277416222cec435330fd1f2858523ec1/yarl-1.24.5-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:7c0494a31a1ac5461a226e7947a9c9b78c44e1dc7185164fa7e9651557a5d9bc", size = 111804, upload-time = "2026-07-20T02:07:18.141Z" },
- { url = "https://files.pythonhosted.org/packages/8a/9e/7b2a1f4bcc20e9447156dd2b1c4d01f70d9df0759025ee7d09a84ffae134/yarl-1.24.5-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a7cff474ab7cd149765bb784cf6d78b32e18e20473fb7bda860bce98ab58e9da", size = 110943, upload-time = "2026-07-20T02:07:20.06Z" },
- { url = "https://files.pythonhosted.org/packages/08/ff/22c92affb0f9b623ca753d27d968b5625b868f12c6378d049d55ae247643/yarl-1.24.5-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:cbb833ccacdb5519eff9b8b71ee618cc2801c878e77e288775d77c3a2ced858a", size = 108251, upload-time = "2026-07-20T02:07:22.217Z" },
- { url = "https://files.pythonhosted.org/packages/45/44/5769b96298c1e195fb412997b6090af2a84105cf59c17613558a2d011d1f/yarl-1.24.5-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:82f75e05912e84b7a0fe57075d9c59de3cb352b928330f2eb69b2e1f54c3e1f0", size = 106025, upload-time = "2026-07-20T02:07:24.083Z" },
- { url = "https://files.pythonhosted.org/packages/4c/40/009e8e791fd9762c0e1567e69248acb4f49064597e1680874c16dd8bb798/yarl-1.24.5-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:16a2f5010280020e90f5330257e6944bc33e73593b136cc5a241e6c1dc292498", size = 106573, upload-time = "2026-07-20T02:07:26.248Z" },
- { url = "https://files.pythonhosted.org/packages/20/c6/b7480578f8a0a80946f36ad6df547ecec704f9ba69d2de60f8aa6f1c1cbf/yarl-1.24.5-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:ffcd54362564dc1a30fb74d8b8a6e5a6b11ebd5e27266adc3b7427a21a6c9104", size = 100751, upload-time = "2026-07-20T02:07:28.098Z" },
- { url = "https://files.pythonhosted.org/packages/d4/27/4476f3360b91a48c5cf125e91f59a3bd35299d84a431a258d57f5977bb11/yarl-1.24.5-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:0465ec8cedc2349b97a6b595ace64084a50c6e839eca40aa0626f38b8350e331", size = 111643, upload-time = "2026-07-20T02:07:30.88Z" },
- { url = "https://files.pythonhosted.org/packages/4c/4b/5cdd3e5ee944e8af31e52f6cd3d3af5fd7b937e036ccbbba2c9ffebede95/yarl-1.24.5-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:4db9aecb141cb7a5447171b57aa1ed3a8fee06af40b992ffc31206c0b0121550", size = 106312, upload-time = "2026-07-20T02:07:33.06Z" },
- { url = "https://files.pythonhosted.org/packages/18/86/f406b0c2a6f99575de2da671ef47aa06f89a5be83a27a46971c3b86cecdb/yarl-1.24.5-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:f540c013589084679a6c7fac07096b10159737918174f5dfc5e11bf5bca4dfe6", size = 110379, upload-time = "2026-07-20T02:07:35.155Z" },
- { url = "https://files.pythonhosted.org/packages/f0/6c/9f3adfbd3b30b4fa0f7ccb3a83eba2c1152d3fff554d535e640ba0f7ba2b/yarl-1.24.5-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:a61834fb15d81322d872eaafd333838ae7c9cea84067f232656f75965933d047", size = 108497, upload-time = "2026-07-20T02:07:37.35Z" },
- { url = "https://files.pythonhosted.org/packages/dd/37/91eb2e5ca883a529c1b390348a74cd9fc0512171727f547ce70bfe02be5c/yarl-1.24.5-cp314-cp314t-win_amd64.whl", hash = "sha256:5c88e5815a49d289e599f3513aa7fde0bc2092ff188f99c940f007f90f53d104", size = 102450, upload-time = "2026-07-20T02:07:39.578Z" },
- { url = "https://files.pythonhosted.org/packages/bf/f4/ed5c402ac8fde4403ed3366c2716bfddc8a6677ebd59f3d62772cc7fe468/yarl-1.24.5-cp314-cp314t-win_arm64.whl", hash = "sha256:cf139c02f5f23ef6532040a30ff662c00a318c952334f211046b8e60b7f17688", size = 97222, upload-time = "2026-07-20T02:07:41.55Z" },
- { url = "https://files.pythonhosted.org/packages/61/02/962c1cbfc401a30c1d034dc67ff395f64b52302c6d62de556c1fca99acc0/yarl-1.24.5-py3-none-any.whl", hash = "sha256:a33700d13d9b7d84fd10947b09ff69fb9a792e519c8cb9764a3ca70baa6c23a7", size = 58612, upload-time = "2026-07-20T02:07:43.461Z" },
-]
From b211a4f83b6a3e9eb4b9e41e8493057f57be6cbf Mon Sep 17 00:00:00 2001
From: "kernel-internal[bot]"
<260533166+kernel-internal[bot]@users.noreply.github.com>
Date: Fri, 7 Aug 2026 13:23:09 +0000
Subject: [PATCH 167/366] feat: Expose profile save behavior in browser
responses
Stainless-Generated-From: 74632566cb147b88cffc6d024d19d5439aae563c
---
src/kernel/types/browser_create_response.py | 6 ++++++
src/kernel/types/browser_list_response.py | 6 ++++++
src/kernel/types/browser_pool_acquire_response.py | 6 ++++++
src/kernel/types/browser_retrieve_response.py | 6 ++++++
src/kernel/types/browser_update_response.py | 6 ++++++
src/kernel/types/invocation_list_browsers_response.py | 6 ++++++
6 files changed, 36 insertions(+)
diff --git a/src/kernel/types/browser_create_response.py b/src/kernel/types/browser_create_response.py
index 8008e6de..0dbf867c 100644
--- a/src/kernel/types/browser_create_response.py
+++ b/src/kernel/types/browser_create_response.py
@@ -73,6 +73,12 @@ class BrowserCreateResponse(BaseModel):
profile: Optional[Profile] = None
"""Browser profile metadata."""
+ profile_save_changes: Optional[bool] = None
+ """
+ Whether changes made during this browser session are saved back to its profile
+ when the session ends. Omitted when no profile is attached.
+ """
+
proxy_id: Optional[str] = None
"""ID of the proxy associated with this browser session, if any."""
diff --git a/src/kernel/types/browser_list_response.py b/src/kernel/types/browser_list_response.py
index 79e1c44f..f3215ed5 100644
--- a/src/kernel/types/browser_list_response.py
+++ b/src/kernel/types/browser_list_response.py
@@ -73,6 +73,12 @@ class BrowserListResponse(BaseModel):
profile: Optional[Profile] = None
"""Browser profile metadata."""
+ profile_save_changes: Optional[bool] = None
+ """
+ Whether changes made during this browser session are saved back to its profile
+ when the session ends. Omitted when no profile is attached.
+ """
+
proxy_id: Optional[str] = None
"""ID of the proxy associated with this browser session, if any."""
diff --git a/src/kernel/types/browser_pool_acquire_response.py b/src/kernel/types/browser_pool_acquire_response.py
index 45fe471d..1dabc492 100644
--- a/src/kernel/types/browser_pool_acquire_response.py
+++ b/src/kernel/types/browser_pool_acquire_response.py
@@ -73,6 +73,12 @@ class BrowserPoolAcquireResponse(BaseModel):
profile: Optional[Profile] = None
"""Browser profile metadata."""
+ profile_save_changes: Optional[bool] = None
+ """
+ Whether changes made during this browser session are saved back to its profile
+ when the session ends. Omitted when no profile is attached.
+ """
+
proxy_id: Optional[str] = None
"""ID of the proxy associated with this browser session, if any."""
diff --git a/src/kernel/types/browser_retrieve_response.py b/src/kernel/types/browser_retrieve_response.py
index 66c1420e..5563ab98 100644
--- a/src/kernel/types/browser_retrieve_response.py
+++ b/src/kernel/types/browser_retrieve_response.py
@@ -73,6 +73,12 @@ class BrowserRetrieveResponse(BaseModel):
profile: Optional[Profile] = None
"""Browser profile metadata."""
+ profile_save_changes: Optional[bool] = None
+ """
+ Whether changes made during this browser session are saved back to its profile
+ when the session ends. Omitted when no profile is attached.
+ """
+
proxy_id: Optional[str] = None
"""ID of the proxy associated with this browser session, if any."""
diff --git a/src/kernel/types/browser_update_response.py b/src/kernel/types/browser_update_response.py
index 5a7c7383..e4602bd5 100644
--- a/src/kernel/types/browser_update_response.py
+++ b/src/kernel/types/browser_update_response.py
@@ -73,6 +73,12 @@ class BrowserUpdateResponse(BaseModel):
profile: Optional[Profile] = None
"""Browser profile metadata."""
+ profile_save_changes: Optional[bool] = None
+ """
+ Whether changes made during this browser session are saved back to its profile
+ when the session ends. Omitted when no profile is attached.
+ """
+
proxy_id: Optional[str] = None
"""ID of the proxy associated with this browser session, if any."""
diff --git a/src/kernel/types/invocation_list_browsers_response.py b/src/kernel/types/invocation_list_browsers_response.py
index a37d180a..9abb0fa6 100644
--- a/src/kernel/types/invocation_list_browsers_response.py
+++ b/src/kernel/types/invocation_list_browsers_response.py
@@ -73,6 +73,12 @@ class Browser(BaseModel):
profile: Optional[Profile] = None
"""Browser profile metadata."""
+ profile_save_changes: Optional[bool] = None
+ """
+ Whether changes made during this browser session are saved back to its profile
+ when the session ends. Omitted when no profile is attached.
+ """
+
proxy_id: Optional[str] = None
"""ID of the proxy associated with this browser session, if any."""
From 47ba0547e248280eca96a0acf762f357b3393afe Mon Sep 17 00:00:00 2001
From: sjmiller609 <7516283+sjmiller609@users.noreply.github.com>
Date: Fri, 7 Aug 2026 18:42:47 +0000
Subject: [PATCH 168/366] Build SDK
Stainless-Generated-From: 752706f5daca4d21f33a833f0267a2666d93f153
---
.stats.yml | 2 +-
api.md | 26 +-
src/kernel/_client.py | 2 +-
src/kernel/resources/audit_logs/__init__.py | 33 +
.../resources/{ => audit_logs}/audit_logs.py | 58 +-
.../audit_logs/export_destinations.py | 691 ++++++++++++++++++
src/kernel/types/audit_logs/__init__.py | 11 +
.../audit_log_export_destination.py | 79 ++
...udit_log_export_destination_test_result.py | 22 +
.../export_destination_create_params.py | 23 +
.../export_destination_list_params.py | 15 +
.../export_destination_update_params.py | 26 +
tests/api_resources/audit_logs/__init__.py | 1 +
.../audit_logs/test_export_destinations.py | 592 +++++++++++++++
14 files changed, 1567 insertions(+), 14 deletions(-)
create mode 100644 src/kernel/resources/audit_logs/__init__.py
rename src/kernel/resources/{ => audit_logs}/audit_logs.py (89%)
create mode 100644 src/kernel/resources/audit_logs/export_destinations.py
create mode 100644 src/kernel/types/audit_logs/__init__.py
create mode 100644 src/kernel/types/audit_logs/audit_log_export_destination.py
create mode 100644 src/kernel/types/audit_logs/audit_log_export_destination_test_result.py
create mode 100644 src/kernel/types/audit_logs/export_destination_create_params.py
create mode 100644 src/kernel/types/audit_logs/export_destination_list_params.py
create mode 100644 src/kernel/types/audit_logs/export_destination_update_params.py
create mode 100644 tests/api_resources/audit_logs/__init__.py
create mode 100644 tests/api_resources/audit_logs/test_export_destinations.py
diff --git a/.stats.yml b/.stats.yml
index 71c320fc..4bb27f8b 100644
--- a/.stats.yml
+++ b/.stats.yml
@@ -1 +1 @@
-configured_endpoints: 127
+configured_endpoints: 133
diff --git a/api.md b/api.md
index 2685a9d5..bf8b28ae 100644
--- a/api.md
+++ b/api.md
@@ -458,8 +458,30 @@ from kernel.types import AuditLogEntry
Methods:
-- client.audit_logs.list(\*\*params) -> SyncPageTokenPagination[AuditLogEntry]
-- client.audit_logs.export_chunk(\*\*params) -> BinaryAPIResponse
+- client.audit_logs.list(\*\*params) -> SyncPageTokenPagination[AuditLogEntry]
+- client.audit_logs.export_chunk(\*\*params) -> BinaryAPIResponse
+
+## ExportDestinations
+
+Types:
+
+```python
+from kernel.types.audit_logs import (
+ AuditLogExportDestination,
+ AuditLogExportDestinationTestResult,
+ CreateAuditLogExportDestinationRequest,
+ UpdateAuditLogExportDestinationRequest,
+)
+```
+
+Methods:
+
+- client.audit_logs.export_destinations.create(\*\*params) -> AuditLogExportDestination
+- client.audit_logs.export_destinations.retrieve(id) -> AuditLogExportDestination
+- client.audit_logs.export_destinations.update(id, \*\*params) -> AuditLogExportDestination
+- client.audit_logs.export_destinations.list(\*\*params) -> SyncOffsetPagination[AuditLogExportDestination]
+- client.audit_logs.export_destinations.delete(id) -> None
+- client.audit_logs.export_destinations.test(id) -> AuditLogExportDestinationTestResult
# APIKeys
diff --git a/src/kernel/_client.py b/src/kernel/_client.py
index 64c7136b..62bd2014 100644
--- a/src/kernel/_client.py
+++ b/src/kernel/_client.py
@@ -68,7 +68,6 @@
from .resources.api_keys import APIKeysResource, AsyncAPIKeysResource
from .resources.profiles import ProfilesResource, AsyncProfilesResource
from .resources.auth.auth import AuthResource, AsyncAuthResource
- from .resources.audit_logs import AuditLogsResource, AsyncAuditLogsResource
from .resources.extensions import ExtensionsResource, AsyncExtensionsResource
from .resources.credentials import CredentialsResource, AsyncCredentialsResource
from .resources.deployments import DeploymentsResource, AsyncDeploymentsResource
@@ -77,6 +76,7 @@
from .resources.browsers.browsers import BrowsersResource, AsyncBrowsersResource
from .resources.projects.projects import ProjectsResource, AsyncProjectsResource
from .resources.credential_providers import CredentialProvidersResource, AsyncCredentialProvidersResource
+ from .resources.audit_logs.audit_logs import AuditLogsResource, AsyncAuditLogsResource
from .resources.organization.organization import OrganizationResource, AsyncOrganizationResource
__all__ = [
diff --git a/src/kernel/resources/audit_logs/__init__.py b/src/kernel/resources/audit_logs/__init__.py
new file mode 100644
index 00000000..5127038d
--- /dev/null
+++ b/src/kernel/resources/audit_logs/__init__.py
@@ -0,0 +1,33 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from .audit_logs import (
+ AuditLogsResource,
+ AsyncAuditLogsResource,
+ AuditLogsResourceWithRawResponse,
+ AsyncAuditLogsResourceWithRawResponse,
+ AuditLogsResourceWithStreamingResponse,
+ AsyncAuditLogsResourceWithStreamingResponse,
+)
+from .export_destinations import (
+ ExportDestinationsResource,
+ AsyncExportDestinationsResource,
+ ExportDestinationsResourceWithRawResponse,
+ AsyncExportDestinationsResourceWithRawResponse,
+ ExportDestinationsResourceWithStreamingResponse,
+ AsyncExportDestinationsResourceWithStreamingResponse,
+)
+
+__all__ = [
+ "ExportDestinationsResource",
+ "AsyncExportDestinationsResource",
+ "ExportDestinationsResourceWithRawResponse",
+ "AsyncExportDestinationsResourceWithRawResponse",
+ "ExportDestinationsResourceWithStreamingResponse",
+ "AsyncExportDestinationsResourceWithStreamingResponse",
+ "AuditLogsResource",
+ "AsyncAuditLogsResource",
+ "AuditLogsResourceWithRawResponse",
+ "AsyncAuditLogsResourceWithRawResponse",
+ "AuditLogsResourceWithStreamingResponse",
+ "AsyncAuditLogsResourceWithStreamingResponse",
+]
diff --git a/src/kernel/resources/audit_logs.py b/src/kernel/resources/audit_logs/audit_logs.py
similarity index 89%
rename from src/kernel/resources/audit_logs.py
rename to src/kernel/resources/audit_logs/audit_logs.py
index aac5d71c..d6aa80a6 100644
--- a/src/kernel/resources/audit_logs.py
+++ b/src/kernel/resources/audit_logs/audit_logs.py
@@ -8,12 +8,12 @@
import httpx
-from ..types import audit_log_list_params, audit_log_export_chunk_params
-from .._types import Body, Omit, Query, Headers, NotGiven, SequenceNotStr, omit, not_given
-from .._utils import maybe_transform, async_maybe_transform
-from .._compat import cached_property
-from .._resource import SyncAPIResource, AsyncAPIResource
-from .._response import (
+from ...types import audit_log_list_params, audit_log_export_chunk_params
+from ..._types import Body, Omit, Query, Headers, NotGiven, SequenceNotStr, omit, not_given
+from ..._utils import maybe_transform, async_maybe_transform
+from ..._compat import cached_property
+from ..._resource import SyncAPIResource, AsyncAPIResource
+from ..._response import (
BinaryAPIResponse,
AsyncBinaryAPIResponse,
StreamedBinaryAPIResponse,
@@ -27,10 +27,18 @@
async_to_custom_raw_response_wrapper,
async_to_custom_streamed_response_wrapper,
)
-from ..pagination import SyncPageTokenPagination, AsyncPageTokenPagination
-from .._base_client import AsyncPaginator, make_request_options
-from ..types.audit_log_entry import AuditLogEntry
-from ..lib.audit_log_download import (
+from ...pagination import SyncPageTokenPagination, AsyncPageTokenPagination
+from ..._base_client import AsyncPaginator, make_request_options
+from .export_destinations import (
+ ExportDestinationsResource,
+ AsyncExportDestinationsResource,
+ ExportDestinationsResourceWithRawResponse,
+ AsyncExportDestinationsResourceWithRawResponse,
+ ExportDestinationsResourceWithStreamingResponse,
+ AsyncExportDestinationsResourceWithStreamingResponse,
+)
+from ...types.audit_log_entry import AuditLogEntry
+from ...lib.audit_log_download import (
ProgressCallback,
AsyncProgressCallback,
AuditLogDownloadResult,
@@ -44,6 +52,11 @@
class AuditLogsResource(SyncAPIResource):
"""Read audit log records for the authenticated organization."""
+ @cached_property
+ def export_destinations(self) -> ExportDestinationsResource:
+ """Read audit log records for the authenticated organization."""
+ return ExportDestinationsResource(self._client)
+
@cached_property
def with_raw_response(self) -> AuditLogsResourceWithRawResponse:
"""
@@ -287,6 +300,11 @@ def fetch_chunk(cursor: str | None) -> ContextManager[StreamedBinaryAPIResponse]
class AsyncAuditLogsResource(AsyncAPIResource):
"""Read audit log records for the authenticated organization."""
+ @cached_property
+ def export_destinations(self) -> AsyncExportDestinationsResource:
+ """Read audit log records for the authenticated organization."""
+ return AsyncExportDestinationsResource(self._client)
+
@cached_property
def with_raw_response(self) -> AsyncAuditLogsResourceWithRawResponse:
"""
@@ -539,6 +557,11 @@ def __init__(self, audit_logs: AuditLogsResource) -> None:
BinaryAPIResponse,
)
+ @cached_property
+ def export_destinations(self) -> ExportDestinationsResourceWithRawResponse:
+ """Read audit log records for the authenticated organization."""
+ return ExportDestinationsResourceWithRawResponse(self._audit_logs.export_destinations)
+
class AsyncAuditLogsResourceWithRawResponse:
def __init__(self, audit_logs: AsyncAuditLogsResource) -> None:
@@ -552,6 +575,11 @@ def __init__(self, audit_logs: AsyncAuditLogsResource) -> None:
AsyncBinaryAPIResponse,
)
+ @cached_property
+ def export_destinations(self) -> AsyncExportDestinationsResourceWithRawResponse:
+ """Read audit log records for the authenticated organization."""
+ return AsyncExportDestinationsResourceWithRawResponse(self._audit_logs.export_destinations)
+
class AuditLogsResourceWithStreamingResponse:
def __init__(self, audit_logs: AuditLogsResource) -> None:
@@ -565,6 +593,11 @@ def __init__(self, audit_logs: AuditLogsResource) -> None:
StreamedBinaryAPIResponse,
)
+ @cached_property
+ def export_destinations(self) -> ExportDestinationsResourceWithStreamingResponse:
+ """Read audit log records for the authenticated organization."""
+ return ExportDestinationsResourceWithStreamingResponse(self._audit_logs.export_destinations)
+
class AsyncAuditLogsResourceWithStreamingResponse:
def __init__(self, audit_logs: AsyncAuditLogsResource) -> None:
@@ -577,3 +610,8 @@ def __init__(self, audit_logs: AsyncAuditLogsResource) -> None:
audit_logs.export_chunk,
AsyncStreamedBinaryAPIResponse,
)
+
+ @cached_property
+ def export_destinations(self) -> AsyncExportDestinationsResourceWithStreamingResponse:
+ """Read audit log records for the authenticated organization."""
+ return AsyncExportDestinationsResourceWithStreamingResponse(self._audit_logs.export_destinations)
diff --git a/src/kernel/resources/audit_logs/export_destinations.py b/src/kernel/resources/audit_logs/export_destinations.py
new file mode 100644
index 00000000..c5d79624
--- /dev/null
+++ b/src/kernel/resources/audit_logs/export_destinations.py
@@ -0,0 +1,691 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from __future__ import annotations
+
+from typing_extensions import Literal
+
+import httpx
+
+from ..._types import Body, Omit, Query, Headers, NoneType, NotGiven, omit, not_given
+from ..._utils import path_template, maybe_transform, async_maybe_transform
+from ..._compat import cached_property
+from ..._resource import SyncAPIResource, AsyncAPIResource
+from ..._response import (
+ to_raw_response_wrapper,
+ to_streamed_response_wrapper,
+ async_to_raw_response_wrapper,
+ async_to_streamed_response_wrapper,
+)
+from ...pagination import SyncOffsetPagination, AsyncOffsetPagination
+from ..._base_client import AsyncPaginator, make_request_options
+from ...types.audit_logs import (
+ export_destination_list_params,
+ export_destination_create_params,
+ export_destination_update_params,
+)
+from ...types.audit_logs.audit_log_export_destination import AuditLogExportDestination
+from ...types.audit_logs.audit_log_export_destination_test_result import AuditLogExportDestinationTestResult
+
+__all__ = ["ExportDestinationsResource", "AsyncExportDestinationsResource"]
+
+
+class ExportDestinationsResource(SyncAPIResource):
+ """Read audit log records for the authenticated organization."""
+
+ @cached_property
+ def with_raw_response(self) -> ExportDestinationsResourceWithRawResponse:
+ """
+ This property can be used as a prefix for any HTTP method call to return
+ the raw response object instead of the parsed content.
+
+ For more information, see https://www.github.com/kernel/kernel-python-sdk#accessing-raw-response-data-eg-headers
+ """
+ return ExportDestinationsResourceWithRawResponse(self)
+
+ @cached_property
+ def with_streaming_response(self) -> ExportDestinationsResourceWithStreamingResponse:
+ """
+ An alternative to `.with_raw_response` that doesn't eagerly read the response body.
+
+ For more information, see https://www.github.com/kernel/kernel-python-sdk#with_streaming_response
+ """
+ return ExportDestinationsResourceWithStreamingResponse(self)
+
+ def create(
+ self,
+ *,
+ bucket: str,
+ format: Literal["jsonl.gz"],
+ prefix: str,
+ region: str,
+ role_arn: str,
+ type: Literal["s3"],
+ kms_key_id: str | Omit = omit,
+ # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
+ # The extra values given here take precedence over values defined on the client or passed to this method.
+ extra_headers: Headers | None = None,
+ extra_query: Query | None = None,
+ extra_body: Body | None = None,
+ timeout: float | httpx.Timeout | None | NotGiven = not_given,
+ ) -> AuditLogExportDestination:
+ """Create a paused destination.
+
+ Activate it with a status update once the
+ destination test passes. Requires an active Enterprise plan.
+
+ Args:
+ extra_headers: Send extra headers
+
+ extra_query: Add additional query parameters to the request
+
+ extra_body: Add additional JSON properties to the request
+
+ timeout: Override the client-level default timeout for this request, in seconds
+ """
+ return self._post(
+ "/audit-logs/export/destinations",
+ body=maybe_transform(
+ {
+ "bucket": bucket,
+ "format": format,
+ "prefix": prefix,
+ "region": region,
+ "role_arn": role_arn,
+ "type": type,
+ "kms_key_id": kms_key_id,
+ },
+ export_destination_create_params.ExportDestinationCreateParams,
+ ),
+ options=make_request_options(
+ extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
+ ),
+ cast_to=AuditLogExportDestination,
+ )
+
+ def retrieve(
+ self,
+ id: str,
+ *,
+ # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
+ # The extra values given here take precedence over values defined on the client or passed to this method.
+ extra_headers: Headers | None = None,
+ extra_query: Query | None = None,
+ extra_body: Body | None = None,
+ timeout: float | httpx.Timeout | None | NotGiven = not_given,
+ ) -> AuditLogExportDestination:
+ """
+ Retrieve details for a single audit log export destination by its ID.
+
+ Args:
+ extra_headers: Send extra headers
+
+ extra_query: Add additional query parameters to the request
+
+ extra_body: Add additional JSON properties to the request
+
+ timeout: Override the client-level default timeout for this request, in seconds
+ """
+ if not id:
+ raise ValueError(f"Expected a non-empty value for `id` but received {id!r}")
+ return self._get(
+ path_template("/audit-logs/export/destinations/{id}", id=id),
+ options=make_request_options(
+ extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
+ ),
+ cast_to=AuditLogExportDestination,
+ )
+
+ def update(
+ self,
+ id: str,
+ *,
+ bucket: str | Omit = omit,
+ kms_key_id: str | Omit = omit,
+ prefix: str | Omit = omit,
+ region: str | Omit = omit,
+ role_arn: str | Omit = omit,
+ status: Literal["active", "paused"] | 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,
+ ) -> AuditLogExportDestination:
+ """Apply a partial update to a destination.
+
+ Requires an active Enterprise plan.
+ Returns 409 when the destination was changed concurrently, because the merged
+ configuration this request validated is no longer the one that would be stored;
+ retry against fresh state. Pausing prevents new delivery attempts, but an S3
+ upload already in progress may complete after the response.
+
+ Args:
+ kms_key_id: KMS key ID, alias, or ARN. Set to an empty string to remove the configured KMS
+ key; omit or send null to leave unchanged.
+
+ 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("/audit-logs/export/destinations/{id}", id=id),
+ body=maybe_transform(
+ {
+ "bucket": bucket,
+ "kms_key_id": kms_key_id,
+ "prefix": prefix,
+ "region": region,
+ "role_arn": role_arn,
+ "status": status,
+ },
+ export_destination_update_params.ExportDestinationUpdateParams,
+ ),
+ options=make_request_options(
+ extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
+ ),
+ cast_to=AuditLogExportDestination,
+ )
+
+ def list(
+ self,
+ *,
+ limit: int | Omit = omit,
+ offset: int | Omit = omit,
+ # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
+ # The extra values given here take precedence over values defined on the client or passed to this method.
+ extra_headers: Headers | None = None,
+ extra_query: Query | None = None,
+ extra_body: Body | None = None,
+ timeout: float | httpx.Timeout | None | NotGiven = not_given,
+ ) -> SyncOffsetPagination[AuditLogExportDestination]:
+ """
+ List audit log export destinations for the organization with pagination support.
+
+ Args:
+ limit: Limit the number of destinations to return.
+
+ offset: Offset the number of destinations to return.
+
+ 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
+ """
+ return self._get_api_list(
+ "/audit-logs/export/destinations",
+ page=SyncOffsetPagination[AuditLogExportDestination],
+ options=make_request_options(
+ extra_headers=extra_headers,
+ extra_query=extra_query,
+ extra_body=extra_body,
+ timeout=timeout,
+ query=maybe_transform(
+ {
+ "limit": limit,
+ "offset": offset,
+ },
+ export_destination_list_params.ExportDestinationListParams,
+ ),
+ ),
+ model=AuditLogExportDestination,
+ )
+
+ def delete(
+ self,
+ id: str,
+ *,
+ # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
+ # The extra values given here take precedence over values defined on the client or passed to this method.
+ extra_headers: Headers | None = None,
+ extra_query: Query | None = None,
+ extra_body: Body | None = None,
+ timeout: float | httpx.Timeout | None | NotGiven = not_given,
+ ) -> None:
+ """Soft delete the destination and prevent new delivery attempts.
+
+ An S3 upload
+ already in progress may complete after the response.
+
+ Args:
+ extra_headers: Send extra headers
+
+ extra_query: Add additional query parameters to the request
+
+ extra_body: Add additional JSON properties to the request
+
+ timeout: Override the client-level default timeout for this request, in seconds
+ """
+ if not id:
+ raise ValueError(f"Expected a non-empty value for `id` but received {id!r}")
+ extra_headers = {"Accept": "*/*", **(extra_headers or {})}
+ return self._delete(
+ path_template("/audit-logs/export/destinations/{id}", id=id),
+ options=make_request_options(
+ extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
+ ),
+ cast_to=NoneType,
+ )
+
+ def test(
+ self,
+ id: str,
+ *,
+ # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
+ # The extra values given here take precedence over values defined on the client or passed to this method.
+ extra_headers: Headers | None = None,
+ extra_query: Query | None = None,
+ extra_body: Body | None = None,
+ timeout: float | httpx.Timeout | None | NotGiven = not_given,
+ ) -> AuditLogExportDestinationTestResult:
+ """
+ Verify the destination is writable by assuming the configured role and uploading
+ a temporary probe object with the same request metadata as a real delivery.
+ Requires an active Enterprise plan.
+
+ Args:
+ extra_headers: Send extra headers
+
+ extra_query: Add additional query parameters to the request
+
+ extra_body: Add additional JSON properties to the request
+
+ timeout: Override the client-level default timeout for this request, in seconds
+ """
+ if not id:
+ raise ValueError(f"Expected a non-empty value for `id` but received {id!r}")
+ return self._post(
+ path_template("/audit-logs/export/destinations/{id}/test", id=id),
+ options=make_request_options(
+ extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
+ ),
+ cast_to=AuditLogExportDestinationTestResult,
+ )
+
+
+class AsyncExportDestinationsResource(AsyncAPIResource):
+ """Read audit log records for the authenticated organization."""
+
+ @cached_property
+ def with_raw_response(self) -> AsyncExportDestinationsResourceWithRawResponse:
+ """
+ This property can be used as a prefix for any HTTP method call to return
+ the raw response object instead of the parsed content.
+
+ For more information, see https://www.github.com/kernel/kernel-python-sdk#accessing-raw-response-data-eg-headers
+ """
+ return AsyncExportDestinationsResourceWithRawResponse(self)
+
+ @cached_property
+ def with_streaming_response(self) -> AsyncExportDestinationsResourceWithStreamingResponse:
+ """
+ An alternative to `.with_raw_response` that doesn't eagerly read the response body.
+
+ For more information, see https://www.github.com/kernel/kernel-python-sdk#with_streaming_response
+ """
+ return AsyncExportDestinationsResourceWithStreamingResponse(self)
+
+ async def create(
+ self,
+ *,
+ bucket: str,
+ format: Literal["jsonl.gz"],
+ prefix: str,
+ region: str,
+ role_arn: str,
+ type: Literal["s3"],
+ kms_key_id: str | Omit = omit,
+ # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
+ # The extra values given here take precedence over values defined on the client or passed to this method.
+ extra_headers: Headers | None = None,
+ extra_query: Query | None = None,
+ extra_body: Body | None = None,
+ timeout: float | httpx.Timeout | None | NotGiven = not_given,
+ ) -> AuditLogExportDestination:
+ """Create a paused destination.
+
+ Activate it with a status update once the
+ destination test passes. Requires an active Enterprise plan.
+
+ Args:
+ extra_headers: Send extra headers
+
+ extra_query: Add additional query parameters to the request
+
+ extra_body: Add additional JSON properties to the request
+
+ timeout: Override the client-level default timeout for this request, in seconds
+ """
+ return await self._post(
+ "/audit-logs/export/destinations",
+ body=await async_maybe_transform(
+ {
+ "bucket": bucket,
+ "format": format,
+ "prefix": prefix,
+ "region": region,
+ "role_arn": role_arn,
+ "type": type,
+ "kms_key_id": kms_key_id,
+ },
+ export_destination_create_params.ExportDestinationCreateParams,
+ ),
+ options=make_request_options(
+ extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
+ ),
+ cast_to=AuditLogExportDestination,
+ )
+
+ async def retrieve(
+ self,
+ id: str,
+ *,
+ # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
+ # The extra values given here take precedence over values defined on the client or passed to this method.
+ extra_headers: Headers | None = None,
+ extra_query: Query | None = None,
+ extra_body: Body | None = None,
+ timeout: float | httpx.Timeout | None | NotGiven = not_given,
+ ) -> AuditLogExportDestination:
+ """
+ Retrieve details for a single audit log export destination by its ID.
+
+ Args:
+ extra_headers: Send extra headers
+
+ extra_query: Add additional query parameters to the request
+
+ extra_body: Add additional JSON properties to the request
+
+ timeout: Override the client-level default timeout for this request, in seconds
+ """
+ if not id:
+ raise ValueError(f"Expected a non-empty value for `id` but received {id!r}")
+ return await self._get(
+ path_template("/audit-logs/export/destinations/{id}", id=id),
+ options=make_request_options(
+ extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
+ ),
+ cast_to=AuditLogExportDestination,
+ )
+
+ async def update(
+ self,
+ id: str,
+ *,
+ bucket: str | Omit = omit,
+ kms_key_id: str | Omit = omit,
+ prefix: str | Omit = omit,
+ region: str | Omit = omit,
+ role_arn: str | Omit = omit,
+ status: Literal["active", "paused"] | 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,
+ ) -> AuditLogExportDestination:
+ """Apply a partial update to a destination.
+
+ Requires an active Enterprise plan.
+ Returns 409 when the destination was changed concurrently, because the merged
+ configuration this request validated is no longer the one that would be stored;
+ retry against fresh state. Pausing prevents new delivery attempts, but an S3
+ upload already in progress may complete after the response.
+
+ Args:
+ kms_key_id: KMS key ID, alias, or ARN. Set to an empty string to remove the configured KMS
+ key; omit or send null to leave unchanged.
+
+ 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("/audit-logs/export/destinations/{id}", id=id),
+ body=await async_maybe_transform(
+ {
+ "bucket": bucket,
+ "kms_key_id": kms_key_id,
+ "prefix": prefix,
+ "region": region,
+ "role_arn": role_arn,
+ "status": status,
+ },
+ export_destination_update_params.ExportDestinationUpdateParams,
+ ),
+ options=make_request_options(
+ extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
+ ),
+ cast_to=AuditLogExportDestination,
+ )
+
+ def list(
+ self,
+ *,
+ limit: int | Omit = omit,
+ offset: int | Omit = omit,
+ # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
+ # The extra values given here take precedence over values defined on the client or passed to this method.
+ extra_headers: Headers | None = None,
+ extra_query: Query | None = None,
+ extra_body: Body | None = None,
+ timeout: float | httpx.Timeout | None | NotGiven = not_given,
+ ) -> AsyncPaginator[AuditLogExportDestination, AsyncOffsetPagination[AuditLogExportDestination]]:
+ """
+ List audit log export destinations for the organization with pagination support.
+
+ Args:
+ limit: Limit the number of destinations to return.
+
+ offset: Offset the number of destinations to return.
+
+ 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
+ """
+ return self._get_api_list(
+ "/audit-logs/export/destinations",
+ page=AsyncOffsetPagination[AuditLogExportDestination],
+ options=make_request_options(
+ extra_headers=extra_headers,
+ extra_query=extra_query,
+ extra_body=extra_body,
+ timeout=timeout,
+ query=maybe_transform(
+ {
+ "limit": limit,
+ "offset": offset,
+ },
+ export_destination_list_params.ExportDestinationListParams,
+ ),
+ ),
+ model=AuditLogExportDestination,
+ )
+
+ async def delete(
+ self,
+ id: str,
+ *,
+ # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
+ # The extra values given here take precedence over values defined on the client or passed to this method.
+ extra_headers: Headers | None = None,
+ extra_query: Query | None = None,
+ extra_body: Body | None = None,
+ timeout: float | httpx.Timeout | None | NotGiven = not_given,
+ ) -> None:
+ """Soft delete the destination and prevent new delivery attempts.
+
+ An S3 upload
+ already in progress may complete after the response.
+
+ Args:
+ extra_headers: Send extra headers
+
+ extra_query: Add additional query parameters to the request
+
+ extra_body: Add additional JSON properties to the request
+
+ timeout: Override the client-level default timeout for this request, in seconds
+ """
+ if not id:
+ raise ValueError(f"Expected a non-empty value for `id` but received {id!r}")
+ extra_headers = {"Accept": "*/*", **(extra_headers or {})}
+ return await self._delete(
+ path_template("/audit-logs/export/destinations/{id}", id=id),
+ options=make_request_options(
+ extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
+ ),
+ cast_to=NoneType,
+ )
+
+ async def test(
+ self,
+ id: str,
+ *,
+ # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
+ # The extra values given here take precedence over values defined on the client or passed to this method.
+ extra_headers: Headers | None = None,
+ extra_query: Query | None = None,
+ extra_body: Body | None = None,
+ timeout: float | httpx.Timeout | None | NotGiven = not_given,
+ ) -> AuditLogExportDestinationTestResult:
+ """
+ Verify the destination is writable by assuming the configured role and uploading
+ a temporary probe object with the same request metadata as a real delivery.
+ Requires an active Enterprise plan.
+
+ Args:
+ extra_headers: Send extra headers
+
+ extra_query: Add additional query parameters to the request
+
+ extra_body: Add additional JSON properties to the request
+
+ timeout: Override the client-level default timeout for this request, in seconds
+ """
+ if not id:
+ raise ValueError(f"Expected a non-empty value for `id` but received {id!r}")
+ return await self._post(
+ path_template("/audit-logs/export/destinations/{id}/test", id=id),
+ options=make_request_options(
+ extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
+ ),
+ cast_to=AuditLogExportDestinationTestResult,
+ )
+
+
+class ExportDestinationsResourceWithRawResponse:
+ def __init__(self, export_destinations: ExportDestinationsResource) -> None:
+ self._export_destinations = export_destinations
+
+ self.create = to_raw_response_wrapper(
+ export_destinations.create,
+ )
+ self.retrieve = to_raw_response_wrapper(
+ export_destinations.retrieve,
+ )
+ self.update = to_raw_response_wrapper(
+ export_destinations.update,
+ )
+ self.list = to_raw_response_wrapper(
+ export_destinations.list,
+ )
+ self.delete = to_raw_response_wrapper(
+ export_destinations.delete,
+ )
+ self.test = to_raw_response_wrapper(
+ export_destinations.test,
+ )
+
+
+class AsyncExportDestinationsResourceWithRawResponse:
+ def __init__(self, export_destinations: AsyncExportDestinationsResource) -> None:
+ self._export_destinations = export_destinations
+
+ self.create = async_to_raw_response_wrapper(
+ export_destinations.create,
+ )
+ self.retrieve = async_to_raw_response_wrapper(
+ export_destinations.retrieve,
+ )
+ self.update = async_to_raw_response_wrapper(
+ export_destinations.update,
+ )
+ self.list = async_to_raw_response_wrapper(
+ export_destinations.list,
+ )
+ self.delete = async_to_raw_response_wrapper(
+ export_destinations.delete,
+ )
+ self.test = async_to_raw_response_wrapper(
+ export_destinations.test,
+ )
+
+
+class ExportDestinationsResourceWithStreamingResponse:
+ def __init__(self, export_destinations: ExportDestinationsResource) -> None:
+ self._export_destinations = export_destinations
+
+ self.create = to_streamed_response_wrapper(
+ export_destinations.create,
+ )
+ self.retrieve = to_streamed_response_wrapper(
+ export_destinations.retrieve,
+ )
+ self.update = to_streamed_response_wrapper(
+ export_destinations.update,
+ )
+ self.list = to_streamed_response_wrapper(
+ export_destinations.list,
+ )
+ self.delete = to_streamed_response_wrapper(
+ export_destinations.delete,
+ )
+ self.test = to_streamed_response_wrapper(
+ export_destinations.test,
+ )
+
+
+class AsyncExportDestinationsResourceWithStreamingResponse:
+ def __init__(self, export_destinations: AsyncExportDestinationsResource) -> None:
+ self._export_destinations = export_destinations
+
+ self.create = async_to_streamed_response_wrapper(
+ export_destinations.create,
+ )
+ self.retrieve = async_to_streamed_response_wrapper(
+ export_destinations.retrieve,
+ )
+ self.update = async_to_streamed_response_wrapper(
+ export_destinations.update,
+ )
+ self.list = async_to_streamed_response_wrapper(
+ export_destinations.list,
+ )
+ self.delete = async_to_streamed_response_wrapper(
+ export_destinations.delete,
+ )
+ self.test = async_to_streamed_response_wrapper(
+ export_destinations.test,
+ )
diff --git a/src/kernel/types/audit_logs/__init__.py b/src/kernel/types/audit_logs/__init__.py
new file mode 100644
index 00000000..0a6f41ef
--- /dev/null
+++ b/src/kernel/types/audit_logs/__init__.py
@@ -0,0 +1,11 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from __future__ import annotations
+
+from .audit_log_export_destination import AuditLogExportDestination as AuditLogExportDestination
+from .export_destination_list_params import ExportDestinationListParams as ExportDestinationListParams
+from .export_destination_create_params import ExportDestinationCreateParams as ExportDestinationCreateParams
+from .export_destination_update_params import ExportDestinationUpdateParams as ExportDestinationUpdateParams
+from .audit_log_export_destination_test_result import (
+ AuditLogExportDestinationTestResult as AuditLogExportDestinationTestResult,
+)
diff --git a/src/kernel/types/audit_logs/audit_log_export_destination.py b/src/kernel/types/audit_logs/audit_log_export_destination.py
new file mode 100644
index 00000000..0897cdf7
--- /dev/null
+++ b/src/kernel/types/audit_logs/audit_log_export_destination.py
@@ -0,0 +1,79 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from typing import Optional
+from datetime import datetime
+from typing_extensions import Literal
+
+from ..._models import BaseModel
+
+__all__ = ["AuditLogExportDestination"]
+
+
+class AuditLogExportDestination(BaseModel):
+ """An organization-scoped audit log export destination.
+
+ Delivery is at-least-once for rows visible when their window is committed: a delivery that is retried rewrites the same object, and the same `event_id` can appear in more than one object, so consumers must deduplicate on `event_id`. Each event-time window is held for ten minutes before it commits; a row that becomes visible after its window is committed may not be delivered.
+
+ Objects are written as `/destination_id=/org_id=/date=/hour=/-.jsonl.gz`, where `date` and `hour` are the UTC calendar hour that fully contains every row in the object, so the layout is safe to register as a Hive-partitioned table. The object name is derived from the rows it holds, so a retried delivery rewrites its own object.
+ """
+
+ id: str
+
+ bucket: str
+
+ consecutive_failures: int
+
+ created_at: datetime
+
+ external_id: str
+
+ format: Literal["jsonl.gz"]
+
+ kernel_role_arn: str
+ """The Kernel role that assumes `role_arn` in your account to deliver logs.
+
+ Allow this role as the principal in your role's trust policy, and require
+ `external_id` as the `sts:ExternalId` condition.
+
+ Recreating a destination issues a new `external_id`, which the trust policy has
+ to be updated to match.
+ """
+
+ prefix: str
+
+ region: str
+
+ role_arn: str
+
+ status: Literal["active", "paused"]
+ """Pausing prevents new delivery attempts.
+
+ An S3 upload already in progress may complete after the pause response; its rows
+ can appear again after the destination is resumed.
+ """
+
+ type: Literal["s3"]
+
+ updated_at: datetime
+
+ kms_key_id: Optional[str] = None
+
+ last_error: Optional[str] = None
+ """Sanitized description of the most recent delivery failure."""
+
+ last_error_at: Optional[datetime] = None
+
+ last_exported_cursor: Optional[str] = None
+ """Opaque, versioned checkpoint for forward-only continuous export.
+
+ This value is not compatible with audit-log list page tokens.
+
+ Delivery starts at the moment the destination is activated, so events recorded
+ before that are not delivered. Pausing stops delivery and resuming starts again
+ from the time of the resume: events recorded while a destination was paused are
+ never exported, and pausing is not a way to defer delivery.
+ """
+
+ last_success_at: Optional[datetime] = None
+
+ next_attempt_at: Optional[datetime] = None
diff --git a/src/kernel/types/audit_logs/audit_log_export_destination_test_result.py b/src/kernel/types/audit_logs/audit_log_export_destination_test_result.py
new file mode 100644
index 00000000..ca1eda6f
--- /dev/null
+++ b/src/kernel/types/audit_logs/audit_log_export_destination_test_result.py
@@ -0,0 +1,22 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from typing import Optional
+from typing_extensions import Literal
+
+from ..._models import BaseModel
+
+__all__ = ["AuditLogExportDestinationTestResult", "Error"]
+
+
+class Error(BaseModel):
+ code: Literal["assume_role_failed", "put_object_failed"]
+
+ message: str
+
+
+class AuditLogExportDestinationTestResult(BaseModel):
+ stage: Literal["assume_role", "put_object", "complete"]
+
+ success: bool
+
+ error: Optional[Error] = None
diff --git a/src/kernel/types/audit_logs/export_destination_create_params.py b/src/kernel/types/audit_logs/export_destination_create_params.py
new file mode 100644
index 00000000..b01cdee0
--- /dev/null
+++ b/src/kernel/types/audit_logs/export_destination_create_params.py
@@ -0,0 +1,23 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from __future__ import annotations
+
+from typing_extensions import Literal, Required, TypedDict
+
+__all__ = ["ExportDestinationCreateParams"]
+
+
+class ExportDestinationCreateParams(TypedDict, total=False):
+ bucket: Required[str]
+
+ format: Required[Literal["jsonl.gz"]]
+
+ prefix: Required[str]
+
+ region: Required[str]
+
+ role_arn: Required[str]
+
+ type: Required[Literal["s3"]]
+
+ kms_key_id: str
diff --git a/src/kernel/types/audit_logs/export_destination_list_params.py b/src/kernel/types/audit_logs/export_destination_list_params.py
new file mode 100644
index 00000000..2026a4b1
--- /dev/null
+++ b/src/kernel/types/audit_logs/export_destination_list_params.py
@@ -0,0 +1,15 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from __future__ import annotations
+
+from typing_extensions import TypedDict
+
+__all__ = ["ExportDestinationListParams"]
+
+
+class ExportDestinationListParams(TypedDict, total=False):
+ limit: int
+ """Limit the number of destinations to return."""
+
+ offset: int
+ """Offset the number of destinations to return."""
diff --git a/src/kernel/types/audit_logs/export_destination_update_params.py b/src/kernel/types/audit_logs/export_destination_update_params.py
new file mode 100644
index 00000000..e21538f0
--- /dev/null
+++ b/src/kernel/types/audit_logs/export_destination_update_params.py
@@ -0,0 +1,26 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from __future__ import annotations
+
+from typing_extensions import Literal, TypedDict
+
+__all__ = ["ExportDestinationUpdateParams"]
+
+
+class ExportDestinationUpdateParams(TypedDict, total=False):
+ bucket: str
+
+ kms_key_id: str
+ """KMS key ID, alias, or ARN.
+
+ Set to an empty string to remove the configured KMS key; omit or send null to
+ leave unchanged.
+ """
+
+ prefix: str
+
+ region: str
+
+ role_arn: str
+
+ status: Literal["active", "paused"]
diff --git a/tests/api_resources/audit_logs/__init__.py b/tests/api_resources/audit_logs/__init__.py
new file mode 100644
index 00000000..fd8019a9
--- /dev/null
+++ b/tests/api_resources/audit_logs/__init__.py
@@ -0,0 +1 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
diff --git a/tests/api_resources/audit_logs/test_export_destinations.py b/tests/api_resources/audit_logs/test_export_destinations.py
new file mode 100644
index 00000000..34b4fde8
--- /dev/null
+++ b/tests/api_resources/audit_logs/test_export_destinations.py
@@ -0,0 +1,592 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from __future__ import annotations
+
+import os
+from typing import Any, cast
+
+import pytest
+
+from kernel import Kernel, AsyncKernel
+from tests.utils import assert_matches_type
+from kernel.pagination import SyncOffsetPagination, AsyncOffsetPagination
+from kernel.types.audit_logs import (
+ AuditLogExportDestination,
+ AuditLogExportDestinationTestResult,
+)
+
+base_url = os.environ.get("TEST_API_BASE_URL", "http://127.0.0.1:4010")
+
+
+class TestExportDestinations:
+ parametrize = pytest.mark.parametrize("client", [False, True], indirect=True, ids=["loose", "strict"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ def test_method_create(self, client: Kernel) -> None:
+ export_destination = client.audit_logs.export_destinations.create(
+ bucket="xxx",
+ format="jsonl.gz",
+ prefix="prefix",
+ region="x",
+ role_arn="x",
+ type="s3",
+ )
+ assert_matches_type(AuditLogExportDestination, export_destination, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ def test_method_create_with_all_params(self, client: Kernel) -> None:
+ export_destination = client.audit_logs.export_destinations.create(
+ bucket="xxx",
+ format="jsonl.gz",
+ prefix="prefix",
+ region="x",
+ role_arn="x",
+ type="s3",
+ kms_key_id="kms_key_id",
+ )
+ assert_matches_type(AuditLogExportDestination, export_destination, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ def test_raw_response_create(self, client: Kernel) -> None:
+ response = client.audit_logs.export_destinations.with_raw_response.create(
+ bucket="xxx",
+ format="jsonl.gz",
+ prefix="prefix",
+ region="x",
+ role_arn="x",
+ type="s3",
+ )
+
+ assert response.is_closed is True
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+ export_destination = response.parse()
+ assert_matches_type(AuditLogExportDestination, export_destination, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ def test_streaming_response_create(self, client: Kernel) -> None:
+ with client.audit_logs.export_destinations.with_streaming_response.create(
+ bucket="xxx",
+ format="jsonl.gz",
+ prefix="prefix",
+ region="x",
+ role_arn="x",
+ type="s3",
+ ) as response:
+ assert not response.is_closed
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+
+ export_destination = response.parse()
+ assert_matches_type(AuditLogExportDestination, export_destination, path=["response"])
+
+ assert cast(Any, response.is_closed) is True
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ def test_method_retrieve(self, client: Kernel) -> None:
+ export_destination = client.audit_logs.export_destinations.retrieve(
+ "id",
+ )
+ assert_matches_type(AuditLogExportDestination, export_destination, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ def test_raw_response_retrieve(self, client: Kernel) -> None:
+ response = client.audit_logs.export_destinations.with_raw_response.retrieve(
+ "id",
+ )
+
+ assert response.is_closed is True
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+ export_destination = response.parse()
+ assert_matches_type(AuditLogExportDestination, export_destination, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ def test_streaming_response_retrieve(self, client: Kernel) -> None:
+ with client.audit_logs.export_destinations.with_streaming_response.retrieve(
+ "id",
+ ) as response:
+ assert not response.is_closed
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+
+ export_destination = response.parse()
+ assert_matches_type(AuditLogExportDestination, export_destination, path=["response"])
+
+ assert cast(Any, response.is_closed) is True
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ def test_path_params_retrieve(self, client: Kernel) -> None:
+ with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"):
+ client.audit_logs.export_destinations.with_raw_response.retrieve(
+ "",
+ )
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ def test_method_update(self, client: Kernel) -> None:
+ export_destination = client.audit_logs.export_destinations.update(
+ id="id",
+ )
+ assert_matches_type(AuditLogExportDestination, export_destination, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ def test_method_update_with_all_params(self, client: Kernel) -> None:
+ export_destination = client.audit_logs.export_destinations.update(
+ id="id",
+ bucket="xxx",
+ kms_key_id="kms_key_id",
+ prefix="prefix",
+ region="x",
+ role_arn="x",
+ status="active",
+ )
+ assert_matches_type(AuditLogExportDestination, export_destination, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ def test_raw_response_update(self, client: Kernel) -> None:
+ response = client.audit_logs.export_destinations.with_raw_response.update(
+ id="id",
+ )
+
+ assert response.is_closed is True
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+ export_destination = response.parse()
+ assert_matches_type(AuditLogExportDestination, export_destination, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ def test_streaming_response_update(self, client: Kernel) -> None:
+ with client.audit_logs.export_destinations.with_streaming_response.update(
+ id="id",
+ ) as response:
+ assert not response.is_closed
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+
+ export_destination = response.parse()
+ assert_matches_type(AuditLogExportDestination, export_destination, 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.audit_logs.export_destinations.with_raw_response.update(
+ id="",
+ )
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ def test_method_list(self, client: Kernel) -> None:
+ export_destination = client.audit_logs.export_destinations.list()
+ assert_matches_type(SyncOffsetPagination[AuditLogExportDestination], export_destination, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ def test_method_list_with_all_params(self, client: Kernel) -> None:
+ export_destination = client.audit_logs.export_destinations.list(
+ limit=1,
+ offset=0,
+ )
+ assert_matches_type(SyncOffsetPagination[AuditLogExportDestination], export_destination, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ def test_raw_response_list(self, client: Kernel) -> None:
+ response = client.audit_logs.export_destinations.with_raw_response.list()
+
+ assert response.is_closed is True
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+ export_destination = response.parse()
+ assert_matches_type(SyncOffsetPagination[AuditLogExportDestination], export_destination, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ def test_streaming_response_list(self, client: Kernel) -> None:
+ with client.audit_logs.export_destinations.with_streaming_response.list() as response:
+ assert not response.is_closed
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+
+ export_destination = response.parse()
+ assert_matches_type(SyncOffsetPagination[AuditLogExportDestination], export_destination, path=["response"])
+
+ assert cast(Any, response.is_closed) is True
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ def test_method_delete(self, client: Kernel) -> None:
+ export_destination = client.audit_logs.export_destinations.delete(
+ "id",
+ )
+ assert export_destination is None
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ def test_raw_response_delete(self, client: Kernel) -> None:
+ response = client.audit_logs.export_destinations.with_raw_response.delete(
+ "id",
+ )
+
+ assert response.is_closed is True
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+ export_destination = response.parse()
+ assert export_destination is None
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ def test_streaming_response_delete(self, client: Kernel) -> None:
+ with client.audit_logs.export_destinations.with_streaming_response.delete(
+ "id",
+ ) as response:
+ assert not response.is_closed
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+
+ export_destination = response.parse()
+ assert export_destination is None
+
+ assert cast(Any, response.is_closed) is True
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ def test_path_params_delete(self, client: Kernel) -> None:
+ with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"):
+ client.audit_logs.export_destinations.with_raw_response.delete(
+ "",
+ )
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ def test_method_test(self, client: Kernel) -> None:
+ export_destination = client.audit_logs.export_destinations.test(
+ "id",
+ )
+ assert_matches_type(AuditLogExportDestinationTestResult, export_destination, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ def test_raw_response_test(self, client: Kernel) -> None:
+ response = client.audit_logs.export_destinations.with_raw_response.test(
+ "id",
+ )
+
+ assert response.is_closed is True
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+ export_destination = response.parse()
+ assert_matches_type(AuditLogExportDestinationTestResult, export_destination, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ def test_streaming_response_test(self, client: Kernel) -> None:
+ with client.audit_logs.export_destinations.with_streaming_response.test(
+ "id",
+ ) as response:
+ assert not response.is_closed
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+
+ export_destination = response.parse()
+ assert_matches_type(AuditLogExportDestinationTestResult, export_destination, path=["response"])
+
+ assert cast(Any, response.is_closed) is True
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ def test_path_params_test(self, client: Kernel) -> None:
+ with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"):
+ client.audit_logs.export_destinations.with_raw_response.test(
+ "",
+ )
+
+
+class TestAsyncExportDestinations:
+ parametrize = pytest.mark.parametrize(
+ "async_client", [False, True, {"http_client": "aiohttp"}], indirect=True, ids=["loose", "strict", "aiohttp"]
+ )
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ async def test_method_create(self, async_client: AsyncKernel) -> None:
+ export_destination = await async_client.audit_logs.export_destinations.create(
+ bucket="xxx",
+ format="jsonl.gz",
+ prefix="prefix",
+ region="x",
+ role_arn="x",
+ type="s3",
+ )
+ assert_matches_type(AuditLogExportDestination, export_destination, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ async def test_method_create_with_all_params(self, async_client: AsyncKernel) -> None:
+ export_destination = await async_client.audit_logs.export_destinations.create(
+ bucket="xxx",
+ format="jsonl.gz",
+ prefix="prefix",
+ region="x",
+ role_arn="x",
+ type="s3",
+ kms_key_id="kms_key_id",
+ )
+ assert_matches_type(AuditLogExportDestination, export_destination, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ async def test_raw_response_create(self, async_client: AsyncKernel) -> None:
+ response = await async_client.audit_logs.export_destinations.with_raw_response.create(
+ bucket="xxx",
+ format="jsonl.gz",
+ prefix="prefix",
+ region="x",
+ role_arn="x",
+ type="s3",
+ )
+
+ assert response.is_closed is True
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+ export_destination = await response.parse()
+ assert_matches_type(AuditLogExportDestination, export_destination, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ async def test_streaming_response_create(self, async_client: AsyncKernel) -> None:
+ async with async_client.audit_logs.export_destinations.with_streaming_response.create(
+ bucket="xxx",
+ format="jsonl.gz",
+ prefix="prefix",
+ region="x",
+ role_arn="x",
+ type="s3",
+ ) as response:
+ assert not response.is_closed
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+
+ export_destination = await response.parse()
+ assert_matches_type(AuditLogExportDestination, export_destination, path=["response"])
+
+ assert cast(Any, response.is_closed) is True
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ async def test_method_retrieve(self, async_client: AsyncKernel) -> None:
+ export_destination = await async_client.audit_logs.export_destinations.retrieve(
+ "id",
+ )
+ assert_matches_type(AuditLogExportDestination, export_destination, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ async def test_raw_response_retrieve(self, async_client: AsyncKernel) -> None:
+ response = await async_client.audit_logs.export_destinations.with_raw_response.retrieve(
+ "id",
+ )
+
+ assert response.is_closed is True
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+ export_destination = await response.parse()
+ assert_matches_type(AuditLogExportDestination, export_destination, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ async def test_streaming_response_retrieve(self, async_client: AsyncKernel) -> None:
+ async with async_client.audit_logs.export_destinations.with_streaming_response.retrieve(
+ "id",
+ ) as response:
+ assert not response.is_closed
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+
+ export_destination = await response.parse()
+ assert_matches_type(AuditLogExportDestination, export_destination, 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_retrieve(self, async_client: AsyncKernel) -> None:
+ with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"):
+ await async_client.audit_logs.export_destinations.with_raw_response.retrieve(
+ "",
+ )
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ async def test_method_update(self, async_client: AsyncKernel) -> None:
+ export_destination = await async_client.audit_logs.export_destinations.update(
+ id="id",
+ )
+ assert_matches_type(AuditLogExportDestination, export_destination, 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:
+ export_destination = await async_client.audit_logs.export_destinations.update(
+ id="id",
+ bucket="xxx",
+ kms_key_id="kms_key_id",
+ prefix="prefix",
+ region="x",
+ role_arn="x",
+ status="active",
+ )
+ assert_matches_type(AuditLogExportDestination, export_destination, 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.audit_logs.export_destinations.with_raw_response.update(
+ id="id",
+ )
+
+ assert response.is_closed is True
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+ export_destination = await response.parse()
+ assert_matches_type(AuditLogExportDestination, export_destination, 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.audit_logs.export_destinations.with_streaming_response.update(
+ id="id",
+ ) as response:
+ assert not response.is_closed
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+
+ export_destination = await response.parse()
+ assert_matches_type(AuditLogExportDestination, export_destination, 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.audit_logs.export_destinations.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:
+ export_destination = await async_client.audit_logs.export_destinations.list()
+ assert_matches_type(AsyncOffsetPagination[AuditLogExportDestination], export_destination, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ async def test_method_list_with_all_params(self, async_client: AsyncKernel) -> None:
+ export_destination = await async_client.audit_logs.export_destinations.list(
+ limit=1,
+ offset=0,
+ )
+ assert_matches_type(AsyncOffsetPagination[AuditLogExportDestination], export_destination, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ async def test_raw_response_list(self, async_client: AsyncKernel) -> None:
+ response = await async_client.audit_logs.export_destinations.with_raw_response.list()
+
+ assert response.is_closed is True
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+ export_destination = await response.parse()
+ assert_matches_type(AsyncOffsetPagination[AuditLogExportDestination], export_destination, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ async def test_streaming_response_list(self, async_client: AsyncKernel) -> None:
+ async with async_client.audit_logs.export_destinations.with_streaming_response.list() as response:
+ assert not response.is_closed
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+
+ export_destination = await response.parse()
+ assert_matches_type(AsyncOffsetPagination[AuditLogExportDestination], export_destination, path=["response"])
+
+ assert cast(Any, response.is_closed) is True
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ async def test_method_delete(self, async_client: AsyncKernel) -> None:
+ export_destination = await async_client.audit_logs.export_destinations.delete(
+ "id",
+ )
+ assert export_destination is None
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ async def test_raw_response_delete(self, async_client: AsyncKernel) -> None:
+ response = await async_client.audit_logs.export_destinations.with_raw_response.delete(
+ "id",
+ )
+
+ assert response.is_closed is True
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+ export_destination = await response.parse()
+ assert export_destination is None
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ async def test_streaming_response_delete(self, async_client: AsyncKernel) -> None:
+ async with async_client.audit_logs.export_destinations.with_streaming_response.delete(
+ "id",
+ ) as response:
+ assert not response.is_closed
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+
+ export_destination = await response.parse()
+ assert export_destination is None
+
+ assert cast(Any, response.is_closed) is True
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ async def test_path_params_delete(self, async_client: AsyncKernel) -> None:
+ with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"):
+ await async_client.audit_logs.export_destinations.with_raw_response.delete(
+ "",
+ )
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ async def test_method_test(self, async_client: AsyncKernel) -> None:
+ export_destination = await async_client.audit_logs.export_destinations.test(
+ "id",
+ )
+ assert_matches_type(AuditLogExportDestinationTestResult, export_destination, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ async def test_raw_response_test(self, async_client: AsyncKernel) -> None:
+ response = await async_client.audit_logs.export_destinations.with_raw_response.test(
+ "id",
+ )
+
+ assert response.is_closed is True
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+ export_destination = await response.parse()
+ assert_matches_type(AuditLogExportDestinationTestResult, export_destination, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ async def test_streaming_response_test(self, async_client: AsyncKernel) -> None:
+ async with async_client.audit_logs.export_destinations.with_streaming_response.test(
+ "id",
+ ) as response:
+ assert not response.is_closed
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+
+ export_destination = await response.parse()
+ assert_matches_type(AuditLogExportDestinationTestResult, export_destination, 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_test(self, async_client: AsyncKernel) -> None:
+ with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"):
+ await async_client.audit_logs.export_destinations.with_raw_response.test(
+ "",
+ )
From 18fdd7b7e37c0346805e43e7cf4b41f0e75ff29e Mon Sep 17 00:00:00 2001
From: "kernel-internal[bot]"
<260533166+kernel-internal[bot]@users.noreply.github.com>
Date: Fri, 7 Aug 2026 19:51:01 +0000
Subject: [PATCH 169/366] feat: Add audit logs plan paywall to the dashboard
Stainless-Generated-From: 752706f5daca4d21f33a833f0267a2666d93f153
---
.stats.yml | 2 +-
api.md | 26 +-
src/kernel/_client.py | 2 +-
src/kernel/resources/audit_logs/__init__.py | 33 +
.../resources/{ => audit_logs}/audit_logs.py | 58 +-
.../audit_logs/export_destinations.py | 691 ++++++++++++++++++
src/kernel/types/audit_logs/__init__.py | 11 +
.../audit_log_export_destination.py | 79 ++
...udit_log_export_destination_test_result.py | 22 +
.../export_destination_create_params.py | 23 +
.../export_destination_list_params.py | 15 +
.../export_destination_update_params.py | 26 +
tests/api_resources/audit_logs/__init__.py | 1 +
.../audit_logs/test_export_destinations.py | 592 +++++++++++++++
14 files changed, 1567 insertions(+), 14 deletions(-)
create mode 100644 src/kernel/resources/audit_logs/__init__.py
rename src/kernel/resources/{ => audit_logs}/audit_logs.py (89%)
create mode 100644 src/kernel/resources/audit_logs/export_destinations.py
create mode 100644 src/kernel/types/audit_logs/__init__.py
create mode 100644 src/kernel/types/audit_logs/audit_log_export_destination.py
create mode 100644 src/kernel/types/audit_logs/audit_log_export_destination_test_result.py
create mode 100644 src/kernel/types/audit_logs/export_destination_create_params.py
create mode 100644 src/kernel/types/audit_logs/export_destination_list_params.py
create mode 100644 src/kernel/types/audit_logs/export_destination_update_params.py
create mode 100644 tests/api_resources/audit_logs/__init__.py
create mode 100644 tests/api_resources/audit_logs/test_export_destinations.py
diff --git a/.stats.yml b/.stats.yml
index 71c320fc..4bb27f8b 100644
--- a/.stats.yml
+++ b/.stats.yml
@@ -1 +1 @@
-configured_endpoints: 127
+configured_endpoints: 133
diff --git a/api.md b/api.md
index 2685a9d5..bf8b28ae 100644
--- a/api.md
+++ b/api.md
@@ -458,8 +458,30 @@ from kernel.types import AuditLogEntry
Methods:
-- client.audit_logs.list(\*\*params) -> SyncPageTokenPagination[AuditLogEntry]
-- client.audit_logs.export_chunk(\*\*params) -> BinaryAPIResponse
+- client.audit_logs.list(\*\*params) -> SyncPageTokenPagination[AuditLogEntry]
+- client.audit_logs.export_chunk(\*\*params) -> BinaryAPIResponse
+
+## ExportDestinations
+
+Types:
+
+```python
+from kernel.types.audit_logs import (
+ AuditLogExportDestination,
+ AuditLogExportDestinationTestResult,
+ CreateAuditLogExportDestinationRequest,
+ UpdateAuditLogExportDestinationRequest,
+)
+```
+
+Methods:
+
+- client.audit_logs.export_destinations.create(\*\*params) -> AuditLogExportDestination
+- client.audit_logs.export_destinations.retrieve(id) -> AuditLogExportDestination
+- client.audit_logs.export_destinations.update(id, \*\*params) -> AuditLogExportDestination
+- client.audit_logs.export_destinations.list(\*\*params) -> SyncOffsetPagination[AuditLogExportDestination]
+- client.audit_logs.export_destinations.delete(id) -> None
+- client.audit_logs.export_destinations.test(id) -> AuditLogExportDestinationTestResult
# APIKeys
diff --git a/src/kernel/_client.py b/src/kernel/_client.py
index 64c7136b..62bd2014 100644
--- a/src/kernel/_client.py
+++ b/src/kernel/_client.py
@@ -68,7 +68,6 @@
from .resources.api_keys import APIKeysResource, AsyncAPIKeysResource
from .resources.profiles import ProfilesResource, AsyncProfilesResource
from .resources.auth.auth import AuthResource, AsyncAuthResource
- from .resources.audit_logs import AuditLogsResource, AsyncAuditLogsResource
from .resources.extensions import ExtensionsResource, AsyncExtensionsResource
from .resources.credentials import CredentialsResource, AsyncCredentialsResource
from .resources.deployments import DeploymentsResource, AsyncDeploymentsResource
@@ -77,6 +76,7 @@
from .resources.browsers.browsers import BrowsersResource, AsyncBrowsersResource
from .resources.projects.projects import ProjectsResource, AsyncProjectsResource
from .resources.credential_providers import CredentialProvidersResource, AsyncCredentialProvidersResource
+ from .resources.audit_logs.audit_logs import AuditLogsResource, AsyncAuditLogsResource
from .resources.organization.organization import OrganizationResource, AsyncOrganizationResource
__all__ = [
diff --git a/src/kernel/resources/audit_logs/__init__.py b/src/kernel/resources/audit_logs/__init__.py
new file mode 100644
index 00000000..5127038d
--- /dev/null
+++ b/src/kernel/resources/audit_logs/__init__.py
@@ -0,0 +1,33 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from .audit_logs import (
+ AuditLogsResource,
+ AsyncAuditLogsResource,
+ AuditLogsResourceWithRawResponse,
+ AsyncAuditLogsResourceWithRawResponse,
+ AuditLogsResourceWithStreamingResponse,
+ AsyncAuditLogsResourceWithStreamingResponse,
+)
+from .export_destinations import (
+ ExportDestinationsResource,
+ AsyncExportDestinationsResource,
+ ExportDestinationsResourceWithRawResponse,
+ AsyncExportDestinationsResourceWithRawResponse,
+ ExportDestinationsResourceWithStreamingResponse,
+ AsyncExportDestinationsResourceWithStreamingResponse,
+)
+
+__all__ = [
+ "ExportDestinationsResource",
+ "AsyncExportDestinationsResource",
+ "ExportDestinationsResourceWithRawResponse",
+ "AsyncExportDestinationsResourceWithRawResponse",
+ "ExportDestinationsResourceWithStreamingResponse",
+ "AsyncExportDestinationsResourceWithStreamingResponse",
+ "AuditLogsResource",
+ "AsyncAuditLogsResource",
+ "AuditLogsResourceWithRawResponse",
+ "AsyncAuditLogsResourceWithRawResponse",
+ "AuditLogsResourceWithStreamingResponse",
+ "AsyncAuditLogsResourceWithStreamingResponse",
+]
diff --git a/src/kernel/resources/audit_logs.py b/src/kernel/resources/audit_logs/audit_logs.py
similarity index 89%
rename from src/kernel/resources/audit_logs.py
rename to src/kernel/resources/audit_logs/audit_logs.py
index aac5d71c..d6aa80a6 100644
--- a/src/kernel/resources/audit_logs.py
+++ b/src/kernel/resources/audit_logs/audit_logs.py
@@ -8,12 +8,12 @@
import httpx
-from ..types import audit_log_list_params, audit_log_export_chunk_params
-from .._types import Body, Omit, Query, Headers, NotGiven, SequenceNotStr, omit, not_given
-from .._utils import maybe_transform, async_maybe_transform
-from .._compat import cached_property
-from .._resource import SyncAPIResource, AsyncAPIResource
-from .._response import (
+from ...types import audit_log_list_params, audit_log_export_chunk_params
+from ..._types import Body, Omit, Query, Headers, NotGiven, SequenceNotStr, omit, not_given
+from ..._utils import maybe_transform, async_maybe_transform
+from ..._compat import cached_property
+from ..._resource import SyncAPIResource, AsyncAPIResource
+from ..._response import (
BinaryAPIResponse,
AsyncBinaryAPIResponse,
StreamedBinaryAPIResponse,
@@ -27,10 +27,18 @@
async_to_custom_raw_response_wrapper,
async_to_custom_streamed_response_wrapper,
)
-from ..pagination import SyncPageTokenPagination, AsyncPageTokenPagination
-from .._base_client import AsyncPaginator, make_request_options
-from ..types.audit_log_entry import AuditLogEntry
-from ..lib.audit_log_download import (
+from ...pagination import SyncPageTokenPagination, AsyncPageTokenPagination
+from ..._base_client import AsyncPaginator, make_request_options
+from .export_destinations import (
+ ExportDestinationsResource,
+ AsyncExportDestinationsResource,
+ ExportDestinationsResourceWithRawResponse,
+ AsyncExportDestinationsResourceWithRawResponse,
+ ExportDestinationsResourceWithStreamingResponse,
+ AsyncExportDestinationsResourceWithStreamingResponse,
+)
+from ...types.audit_log_entry import AuditLogEntry
+from ...lib.audit_log_download import (
ProgressCallback,
AsyncProgressCallback,
AuditLogDownloadResult,
@@ -44,6 +52,11 @@
class AuditLogsResource(SyncAPIResource):
"""Read audit log records for the authenticated organization."""
+ @cached_property
+ def export_destinations(self) -> ExportDestinationsResource:
+ """Read audit log records for the authenticated organization."""
+ return ExportDestinationsResource(self._client)
+
@cached_property
def with_raw_response(self) -> AuditLogsResourceWithRawResponse:
"""
@@ -287,6 +300,11 @@ def fetch_chunk(cursor: str | None) -> ContextManager[StreamedBinaryAPIResponse]
class AsyncAuditLogsResource(AsyncAPIResource):
"""Read audit log records for the authenticated organization."""
+ @cached_property
+ def export_destinations(self) -> AsyncExportDestinationsResource:
+ """Read audit log records for the authenticated organization."""
+ return AsyncExportDestinationsResource(self._client)
+
@cached_property
def with_raw_response(self) -> AsyncAuditLogsResourceWithRawResponse:
"""
@@ -539,6 +557,11 @@ def __init__(self, audit_logs: AuditLogsResource) -> None:
BinaryAPIResponse,
)
+ @cached_property
+ def export_destinations(self) -> ExportDestinationsResourceWithRawResponse:
+ """Read audit log records for the authenticated organization."""
+ return ExportDestinationsResourceWithRawResponse(self._audit_logs.export_destinations)
+
class AsyncAuditLogsResourceWithRawResponse:
def __init__(self, audit_logs: AsyncAuditLogsResource) -> None:
@@ -552,6 +575,11 @@ def __init__(self, audit_logs: AsyncAuditLogsResource) -> None:
AsyncBinaryAPIResponse,
)
+ @cached_property
+ def export_destinations(self) -> AsyncExportDestinationsResourceWithRawResponse:
+ """Read audit log records for the authenticated organization."""
+ return AsyncExportDestinationsResourceWithRawResponse(self._audit_logs.export_destinations)
+
class AuditLogsResourceWithStreamingResponse:
def __init__(self, audit_logs: AuditLogsResource) -> None:
@@ -565,6 +593,11 @@ def __init__(self, audit_logs: AuditLogsResource) -> None:
StreamedBinaryAPIResponse,
)
+ @cached_property
+ def export_destinations(self) -> ExportDestinationsResourceWithStreamingResponse:
+ """Read audit log records for the authenticated organization."""
+ return ExportDestinationsResourceWithStreamingResponse(self._audit_logs.export_destinations)
+
class AsyncAuditLogsResourceWithStreamingResponse:
def __init__(self, audit_logs: AsyncAuditLogsResource) -> None:
@@ -577,3 +610,8 @@ def __init__(self, audit_logs: AsyncAuditLogsResource) -> None:
audit_logs.export_chunk,
AsyncStreamedBinaryAPIResponse,
)
+
+ @cached_property
+ def export_destinations(self) -> AsyncExportDestinationsResourceWithStreamingResponse:
+ """Read audit log records for the authenticated organization."""
+ return AsyncExportDestinationsResourceWithStreamingResponse(self._audit_logs.export_destinations)
diff --git a/src/kernel/resources/audit_logs/export_destinations.py b/src/kernel/resources/audit_logs/export_destinations.py
new file mode 100644
index 00000000..c5d79624
--- /dev/null
+++ b/src/kernel/resources/audit_logs/export_destinations.py
@@ -0,0 +1,691 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from __future__ import annotations
+
+from typing_extensions import Literal
+
+import httpx
+
+from ..._types import Body, Omit, Query, Headers, NoneType, NotGiven, omit, not_given
+from ..._utils import path_template, maybe_transform, async_maybe_transform
+from ..._compat import cached_property
+from ..._resource import SyncAPIResource, AsyncAPIResource
+from ..._response import (
+ to_raw_response_wrapper,
+ to_streamed_response_wrapper,
+ async_to_raw_response_wrapper,
+ async_to_streamed_response_wrapper,
+)
+from ...pagination import SyncOffsetPagination, AsyncOffsetPagination
+from ..._base_client import AsyncPaginator, make_request_options
+from ...types.audit_logs import (
+ export_destination_list_params,
+ export_destination_create_params,
+ export_destination_update_params,
+)
+from ...types.audit_logs.audit_log_export_destination import AuditLogExportDestination
+from ...types.audit_logs.audit_log_export_destination_test_result import AuditLogExportDestinationTestResult
+
+__all__ = ["ExportDestinationsResource", "AsyncExportDestinationsResource"]
+
+
+class ExportDestinationsResource(SyncAPIResource):
+ """Read audit log records for the authenticated organization."""
+
+ @cached_property
+ def with_raw_response(self) -> ExportDestinationsResourceWithRawResponse:
+ """
+ This property can be used as a prefix for any HTTP method call to return
+ the raw response object instead of the parsed content.
+
+ For more information, see https://www.github.com/kernel/kernel-python-sdk#accessing-raw-response-data-eg-headers
+ """
+ return ExportDestinationsResourceWithRawResponse(self)
+
+ @cached_property
+ def with_streaming_response(self) -> ExportDestinationsResourceWithStreamingResponse:
+ """
+ An alternative to `.with_raw_response` that doesn't eagerly read the response body.
+
+ For more information, see https://www.github.com/kernel/kernel-python-sdk#with_streaming_response
+ """
+ return ExportDestinationsResourceWithStreamingResponse(self)
+
+ def create(
+ self,
+ *,
+ bucket: str,
+ format: Literal["jsonl.gz"],
+ prefix: str,
+ region: str,
+ role_arn: str,
+ type: Literal["s3"],
+ kms_key_id: str | Omit = omit,
+ # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
+ # The extra values given here take precedence over values defined on the client or passed to this method.
+ extra_headers: Headers | None = None,
+ extra_query: Query | None = None,
+ extra_body: Body | None = None,
+ timeout: float | httpx.Timeout | None | NotGiven = not_given,
+ ) -> AuditLogExportDestination:
+ """Create a paused destination.
+
+ Activate it with a status update once the
+ destination test passes. Requires an active Enterprise plan.
+
+ Args:
+ extra_headers: Send extra headers
+
+ extra_query: Add additional query parameters to the request
+
+ extra_body: Add additional JSON properties to the request
+
+ timeout: Override the client-level default timeout for this request, in seconds
+ """
+ return self._post(
+ "/audit-logs/export/destinations",
+ body=maybe_transform(
+ {
+ "bucket": bucket,
+ "format": format,
+ "prefix": prefix,
+ "region": region,
+ "role_arn": role_arn,
+ "type": type,
+ "kms_key_id": kms_key_id,
+ },
+ export_destination_create_params.ExportDestinationCreateParams,
+ ),
+ options=make_request_options(
+ extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
+ ),
+ cast_to=AuditLogExportDestination,
+ )
+
+ def retrieve(
+ self,
+ id: str,
+ *,
+ # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
+ # The extra values given here take precedence over values defined on the client or passed to this method.
+ extra_headers: Headers | None = None,
+ extra_query: Query | None = None,
+ extra_body: Body | None = None,
+ timeout: float | httpx.Timeout | None | NotGiven = not_given,
+ ) -> AuditLogExportDestination:
+ """
+ Retrieve details for a single audit log export destination by its ID.
+
+ Args:
+ extra_headers: Send extra headers
+
+ extra_query: Add additional query parameters to the request
+
+ extra_body: Add additional JSON properties to the request
+
+ timeout: Override the client-level default timeout for this request, in seconds
+ """
+ if not id:
+ raise ValueError(f"Expected a non-empty value for `id` but received {id!r}")
+ return self._get(
+ path_template("/audit-logs/export/destinations/{id}", id=id),
+ options=make_request_options(
+ extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
+ ),
+ cast_to=AuditLogExportDestination,
+ )
+
+ def update(
+ self,
+ id: str,
+ *,
+ bucket: str | Omit = omit,
+ kms_key_id: str | Omit = omit,
+ prefix: str | Omit = omit,
+ region: str | Omit = omit,
+ role_arn: str | Omit = omit,
+ status: Literal["active", "paused"] | 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,
+ ) -> AuditLogExportDestination:
+ """Apply a partial update to a destination.
+
+ Requires an active Enterprise plan.
+ Returns 409 when the destination was changed concurrently, because the merged
+ configuration this request validated is no longer the one that would be stored;
+ retry against fresh state. Pausing prevents new delivery attempts, but an S3
+ upload already in progress may complete after the response.
+
+ Args:
+ kms_key_id: KMS key ID, alias, or ARN. Set to an empty string to remove the configured KMS
+ key; omit or send null to leave unchanged.
+
+ 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("/audit-logs/export/destinations/{id}", id=id),
+ body=maybe_transform(
+ {
+ "bucket": bucket,
+ "kms_key_id": kms_key_id,
+ "prefix": prefix,
+ "region": region,
+ "role_arn": role_arn,
+ "status": status,
+ },
+ export_destination_update_params.ExportDestinationUpdateParams,
+ ),
+ options=make_request_options(
+ extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
+ ),
+ cast_to=AuditLogExportDestination,
+ )
+
+ def list(
+ self,
+ *,
+ limit: int | Omit = omit,
+ offset: int | Omit = omit,
+ # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
+ # The extra values given here take precedence over values defined on the client or passed to this method.
+ extra_headers: Headers | None = None,
+ extra_query: Query | None = None,
+ extra_body: Body | None = None,
+ timeout: float | httpx.Timeout | None | NotGiven = not_given,
+ ) -> SyncOffsetPagination[AuditLogExportDestination]:
+ """
+ List audit log export destinations for the organization with pagination support.
+
+ Args:
+ limit: Limit the number of destinations to return.
+
+ offset: Offset the number of destinations to return.
+
+ 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
+ """
+ return self._get_api_list(
+ "/audit-logs/export/destinations",
+ page=SyncOffsetPagination[AuditLogExportDestination],
+ options=make_request_options(
+ extra_headers=extra_headers,
+ extra_query=extra_query,
+ extra_body=extra_body,
+ timeout=timeout,
+ query=maybe_transform(
+ {
+ "limit": limit,
+ "offset": offset,
+ },
+ export_destination_list_params.ExportDestinationListParams,
+ ),
+ ),
+ model=AuditLogExportDestination,
+ )
+
+ def delete(
+ self,
+ id: str,
+ *,
+ # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
+ # The extra values given here take precedence over values defined on the client or passed to this method.
+ extra_headers: Headers | None = None,
+ extra_query: Query | None = None,
+ extra_body: Body | None = None,
+ timeout: float | httpx.Timeout | None | NotGiven = not_given,
+ ) -> None:
+ """Soft delete the destination and prevent new delivery attempts.
+
+ An S3 upload
+ already in progress may complete after the response.
+
+ Args:
+ extra_headers: Send extra headers
+
+ extra_query: Add additional query parameters to the request
+
+ extra_body: Add additional JSON properties to the request
+
+ timeout: Override the client-level default timeout for this request, in seconds
+ """
+ if not id:
+ raise ValueError(f"Expected a non-empty value for `id` but received {id!r}")
+ extra_headers = {"Accept": "*/*", **(extra_headers or {})}
+ return self._delete(
+ path_template("/audit-logs/export/destinations/{id}", id=id),
+ options=make_request_options(
+ extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
+ ),
+ cast_to=NoneType,
+ )
+
+ def test(
+ self,
+ id: str,
+ *,
+ # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
+ # The extra values given here take precedence over values defined on the client or passed to this method.
+ extra_headers: Headers | None = None,
+ extra_query: Query | None = None,
+ extra_body: Body | None = None,
+ timeout: float | httpx.Timeout | None | NotGiven = not_given,
+ ) -> AuditLogExportDestinationTestResult:
+ """
+ Verify the destination is writable by assuming the configured role and uploading
+ a temporary probe object with the same request metadata as a real delivery.
+ Requires an active Enterprise plan.
+
+ Args:
+ extra_headers: Send extra headers
+
+ extra_query: Add additional query parameters to the request
+
+ extra_body: Add additional JSON properties to the request
+
+ timeout: Override the client-level default timeout for this request, in seconds
+ """
+ if not id:
+ raise ValueError(f"Expected a non-empty value for `id` but received {id!r}")
+ return self._post(
+ path_template("/audit-logs/export/destinations/{id}/test", id=id),
+ options=make_request_options(
+ extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
+ ),
+ cast_to=AuditLogExportDestinationTestResult,
+ )
+
+
+class AsyncExportDestinationsResource(AsyncAPIResource):
+ """Read audit log records for the authenticated organization."""
+
+ @cached_property
+ def with_raw_response(self) -> AsyncExportDestinationsResourceWithRawResponse:
+ """
+ This property can be used as a prefix for any HTTP method call to return
+ the raw response object instead of the parsed content.
+
+ For more information, see https://www.github.com/kernel/kernel-python-sdk#accessing-raw-response-data-eg-headers
+ """
+ return AsyncExportDestinationsResourceWithRawResponse(self)
+
+ @cached_property
+ def with_streaming_response(self) -> AsyncExportDestinationsResourceWithStreamingResponse:
+ """
+ An alternative to `.with_raw_response` that doesn't eagerly read the response body.
+
+ For more information, see https://www.github.com/kernel/kernel-python-sdk#with_streaming_response
+ """
+ return AsyncExportDestinationsResourceWithStreamingResponse(self)
+
+ async def create(
+ self,
+ *,
+ bucket: str,
+ format: Literal["jsonl.gz"],
+ prefix: str,
+ region: str,
+ role_arn: str,
+ type: Literal["s3"],
+ kms_key_id: str | Omit = omit,
+ # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
+ # The extra values given here take precedence over values defined on the client or passed to this method.
+ extra_headers: Headers | None = None,
+ extra_query: Query | None = None,
+ extra_body: Body | None = None,
+ timeout: float | httpx.Timeout | None | NotGiven = not_given,
+ ) -> AuditLogExportDestination:
+ """Create a paused destination.
+
+ Activate it with a status update once the
+ destination test passes. Requires an active Enterprise plan.
+
+ Args:
+ extra_headers: Send extra headers
+
+ extra_query: Add additional query parameters to the request
+
+ extra_body: Add additional JSON properties to the request
+
+ timeout: Override the client-level default timeout for this request, in seconds
+ """
+ return await self._post(
+ "/audit-logs/export/destinations",
+ body=await async_maybe_transform(
+ {
+ "bucket": bucket,
+ "format": format,
+ "prefix": prefix,
+ "region": region,
+ "role_arn": role_arn,
+ "type": type,
+ "kms_key_id": kms_key_id,
+ },
+ export_destination_create_params.ExportDestinationCreateParams,
+ ),
+ options=make_request_options(
+ extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
+ ),
+ cast_to=AuditLogExportDestination,
+ )
+
+ async def retrieve(
+ self,
+ id: str,
+ *,
+ # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
+ # The extra values given here take precedence over values defined on the client or passed to this method.
+ extra_headers: Headers | None = None,
+ extra_query: Query | None = None,
+ extra_body: Body | None = None,
+ timeout: float | httpx.Timeout | None | NotGiven = not_given,
+ ) -> AuditLogExportDestination:
+ """
+ Retrieve details for a single audit log export destination by its ID.
+
+ Args:
+ extra_headers: Send extra headers
+
+ extra_query: Add additional query parameters to the request
+
+ extra_body: Add additional JSON properties to the request
+
+ timeout: Override the client-level default timeout for this request, in seconds
+ """
+ if not id:
+ raise ValueError(f"Expected a non-empty value for `id` but received {id!r}")
+ return await self._get(
+ path_template("/audit-logs/export/destinations/{id}", id=id),
+ options=make_request_options(
+ extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
+ ),
+ cast_to=AuditLogExportDestination,
+ )
+
+ async def update(
+ self,
+ id: str,
+ *,
+ bucket: str | Omit = omit,
+ kms_key_id: str | Omit = omit,
+ prefix: str | Omit = omit,
+ region: str | Omit = omit,
+ role_arn: str | Omit = omit,
+ status: Literal["active", "paused"] | 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,
+ ) -> AuditLogExportDestination:
+ """Apply a partial update to a destination.
+
+ Requires an active Enterprise plan.
+ Returns 409 when the destination was changed concurrently, because the merged
+ configuration this request validated is no longer the one that would be stored;
+ retry against fresh state. Pausing prevents new delivery attempts, but an S3
+ upload already in progress may complete after the response.
+
+ Args:
+ kms_key_id: KMS key ID, alias, or ARN. Set to an empty string to remove the configured KMS
+ key; omit or send null to leave unchanged.
+
+ 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("/audit-logs/export/destinations/{id}", id=id),
+ body=await async_maybe_transform(
+ {
+ "bucket": bucket,
+ "kms_key_id": kms_key_id,
+ "prefix": prefix,
+ "region": region,
+ "role_arn": role_arn,
+ "status": status,
+ },
+ export_destination_update_params.ExportDestinationUpdateParams,
+ ),
+ options=make_request_options(
+ extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
+ ),
+ cast_to=AuditLogExportDestination,
+ )
+
+ def list(
+ self,
+ *,
+ limit: int | Omit = omit,
+ offset: int | Omit = omit,
+ # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
+ # The extra values given here take precedence over values defined on the client or passed to this method.
+ extra_headers: Headers | None = None,
+ extra_query: Query | None = None,
+ extra_body: Body | None = None,
+ timeout: float | httpx.Timeout | None | NotGiven = not_given,
+ ) -> AsyncPaginator[AuditLogExportDestination, AsyncOffsetPagination[AuditLogExportDestination]]:
+ """
+ List audit log export destinations for the organization with pagination support.
+
+ Args:
+ limit: Limit the number of destinations to return.
+
+ offset: Offset the number of destinations to return.
+
+ 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
+ """
+ return self._get_api_list(
+ "/audit-logs/export/destinations",
+ page=AsyncOffsetPagination[AuditLogExportDestination],
+ options=make_request_options(
+ extra_headers=extra_headers,
+ extra_query=extra_query,
+ extra_body=extra_body,
+ timeout=timeout,
+ query=maybe_transform(
+ {
+ "limit": limit,
+ "offset": offset,
+ },
+ export_destination_list_params.ExportDestinationListParams,
+ ),
+ ),
+ model=AuditLogExportDestination,
+ )
+
+ async def delete(
+ self,
+ id: str,
+ *,
+ # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
+ # The extra values given here take precedence over values defined on the client or passed to this method.
+ extra_headers: Headers | None = None,
+ extra_query: Query | None = None,
+ extra_body: Body | None = None,
+ timeout: float | httpx.Timeout | None | NotGiven = not_given,
+ ) -> None:
+ """Soft delete the destination and prevent new delivery attempts.
+
+ An S3 upload
+ already in progress may complete after the response.
+
+ Args:
+ extra_headers: Send extra headers
+
+ extra_query: Add additional query parameters to the request
+
+ extra_body: Add additional JSON properties to the request
+
+ timeout: Override the client-level default timeout for this request, in seconds
+ """
+ if not id:
+ raise ValueError(f"Expected a non-empty value for `id` but received {id!r}")
+ extra_headers = {"Accept": "*/*", **(extra_headers or {})}
+ return await self._delete(
+ path_template("/audit-logs/export/destinations/{id}", id=id),
+ options=make_request_options(
+ extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
+ ),
+ cast_to=NoneType,
+ )
+
+ async def test(
+ self,
+ id: str,
+ *,
+ # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
+ # The extra values given here take precedence over values defined on the client or passed to this method.
+ extra_headers: Headers | None = None,
+ extra_query: Query | None = None,
+ extra_body: Body | None = None,
+ timeout: float | httpx.Timeout | None | NotGiven = not_given,
+ ) -> AuditLogExportDestinationTestResult:
+ """
+ Verify the destination is writable by assuming the configured role and uploading
+ a temporary probe object with the same request metadata as a real delivery.
+ Requires an active Enterprise plan.
+
+ Args:
+ extra_headers: Send extra headers
+
+ extra_query: Add additional query parameters to the request
+
+ extra_body: Add additional JSON properties to the request
+
+ timeout: Override the client-level default timeout for this request, in seconds
+ """
+ if not id:
+ raise ValueError(f"Expected a non-empty value for `id` but received {id!r}")
+ return await self._post(
+ path_template("/audit-logs/export/destinations/{id}/test", id=id),
+ options=make_request_options(
+ extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
+ ),
+ cast_to=AuditLogExportDestinationTestResult,
+ )
+
+
+class ExportDestinationsResourceWithRawResponse:
+ def __init__(self, export_destinations: ExportDestinationsResource) -> None:
+ self._export_destinations = export_destinations
+
+ self.create = to_raw_response_wrapper(
+ export_destinations.create,
+ )
+ self.retrieve = to_raw_response_wrapper(
+ export_destinations.retrieve,
+ )
+ self.update = to_raw_response_wrapper(
+ export_destinations.update,
+ )
+ self.list = to_raw_response_wrapper(
+ export_destinations.list,
+ )
+ self.delete = to_raw_response_wrapper(
+ export_destinations.delete,
+ )
+ self.test = to_raw_response_wrapper(
+ export_destinations.test,
+ )
+
+
+class AsyncExportDestinationsResourceWithRawResponse:
+ def __init__(self, export_destinations: AsyncExportDestinationsResource) -> None:
+ self._export_destinations = export_destinations
+
+ self.create = async_to_raw_response_wrapper(
+ export_destinations.create,
+ )
+ self.retrieve = async_to_raw_response_wrapper(
+ export_destinations.retrieve,
+ )
+ self.update = async_to_raw_response_wrapper(
+ export_destinations.update,
+ )
+ self.list = async_to_raw_response_wrapper(
+ export_destinations.list,
+ )
+ self.delete = async_to_raw_response_wrapper(
+ export_destinations.delete,
+ )
+ self.test = async_to_raw_response_wrapper(
+ export_destinations.test,
+ )
+
+
+class ExportDestinationsResourceWithStreamingResponse:
+ def __init__(self, export_destinations: ExportDestinationsResource) -> None:
+ self._export_destinations = export_destinations
+
+ self.create = to_streamed_response_wrapper(
+ export_destinations.create,
+ )
+ self.retrieve = to_streamed_response_wrapper(
+ export_destinations.retrieve,
+ )
+ self.update = to_streamed_response_wrapper(
+ export_destinations.update,
+ )
+ self.list = to_streamed_response_wrapper(
+ export_destinations.list,
+ )
+ self.delete = to_streamed_response_wrapper(
+ export_destinations.delete,
+ )
+ self.test = to_streamed_response_wrapper(
+ export_destinations.test,
+ )
+
+
+class AsyncExportDestinationsResourceWithStreamingResponse:
+ def __init__(self, export_destinations: AsyncExportDestinationsResource) -> None:
+ self._export_destinations = export_destinations
+
+ self.create = async_to_streamed_response_wrapper(
+ export_destinations.create,
+ )
+ self.retrieve = async_to_streamed_response_wrapper(
+ export_destinations.retrieve,
+ )
+ self.update = async_to_streamed_response_wrapper(
+ export_destinations.update,
+ )
+ self.list = async_to_streamed_response_wrapper(
+ export_destinations.list,
+ )
+ self.delete = async_to_streamed_response_wrapper(
+ export_destinations.delete,
+ )
+ self.test = async_to_streamed_response_wrapper(
+ export_destinations.test,
+ )
diff --git a/src/kernel/types/audit_logs/__init__.py b/src/kernel/types/audit_logs/__init__.py
new file mode 100644
index 00000000..0a6f41ef
--- /dev/null
+++ b/src/kernel/types/audit_logs/__init__.py
@@ -0,0 +1,11 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from __future__ import annotations
+
+from .audit_log_export_destination import AuditLogExportDestination as AuditLogExportDestination
+from .export_destination_list_params import ExportDestinationListParams as ExportDestinationListParams
+from .export_destination_create_params import ExportDestinationCreateParams as ExportDestinationCreateParams
+from .export_destination_update_params import ExportDestinationUpdateParams as ExportDestinationUpdateParams
+from .audit_log_export_destination_test_result import (
+ AuditLogExportDestinationTestResult as AuditLogExportDestinationTestResult,
+)
diff --git a/src/kernel/types/audit_logs/audit_log_export_destination.py b/src/kernel/types/audit_logs/audit_log_export_destination.py
new file mode 100644
index 00000000..0897cdf7
--- /dev/null
+++ b/src/kernel/types/audit_logs/audit_log_export_destination.py
@@ -0,0 +1,79 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from typing import Optional
+from datetime import datetime
+from typing_extensions import Literal
+
+from ..._models import BaseModel
+
+__all__ = ["AuditLogExportDestination"]
+
+
+class AuditLogExportDestination(BaseModel):
+ """An organization-scoped audit log export destination.
+
+ Delivery is at-least-once for rows visible when their window is committed: a delivery that is retried rewrites the same object, and the same `event_id` can appear in more than one object, so consumers must deduplicate on `event_id`. Each event-time window is held for ten minutes before it commits; a row that becomes visible after its window is committed may not be delivered.
+
+ Objects are written as `/destination_id=/org_id=/date=/hour=/-.jsonl.gz`, where `date` and `hour` are the UTC calendar hour that fully contains every row in the object, so the layout is safe to register as a Hive-partitioned table. The object name is derived from the rows it holds, so a retried delivery rewrites its own object.
+ """
+
+ id: str
+
+ bucket: str
+
+ consecutive_failures: int
+
+ created_at: datetime
+
+ external_id: str
+
+ format: Literal["jsonl.gz"]
+
+ kernel_role_arn: str
+ """The Kernel role that assumes `role_arn` in your account to deliver logs.
+
+ Allow this role as the principal in your role's trust policy, and require
+ `external_id` as the `sts:ExternalId` condition.
+
+ Recreating a destination issues a new `external_id`, which the trust policy has
+ to be updated to match.
+ """
+
+ prefix: str
+
+ region: str
+
+ role_arn: str
+
+ status: Literal["active", "paused"]
+ """Pausing prevents new delivery attempts.
+
+ An S3 upload already in progress may complete after the pause response; its rows
+ can appear again after the destination is resumed.
+ """
+
+ type: Literal["s3"]
+
+ updated_at: datetime
+
+ kms_key_id: Optional[str] = None
+
+ last_error: Optional[str] = None
+ """Sanitized description of the most recent delivery failure."""
+
+ last_error_at: Optional[datetime] = None
+
+ last_exported_cursor: Optional[str] = None
+ """Opaque, versioned checkpoint for forward-only continuous export.
+
+ This value is not compatible with audit-log list page tokens.
+
+ Delivery starts at the moment the destination is activated, so events recorded
+ before that are not delivered. Pausing stops delivery and resuming starts again
+ from the time of the resume: events recorded while a destination was paused are
+ never exported, and pausing is not a way to defer delivery.
+ """
+
+ last_success_at: Optional[datetime] = None
+
+ next_attempt_at: Optional[datetime] = None
diff --git a/src/kernel/types/audit_logs/audit_log_export_destination_test_result.py b/src/kernel/types/audit_logs/audit_log_export_destination_test_result.py
new file mode 100644
index 00000000..ca1eda6f
--- /dev/null
+++ b/src/kernel/types/audit_logs/audit_log_export_destination_test_result.py
@@ -0,0 +1,22 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from typing import Optional
+from typing_extensions import Literal
+
+from ..._models import BaseModel
+
+__all__ = ["AuditLogExportDestinationTestResult", "Error"]
+
+
+class Error(BaseModel):
+ code: Literal["assume_role_failed", "put_object_failed"]
+
+ message: str
+
+
+class AuditLogExportDestinationTestResult(BaseModel):
+ stage: Literal["assume_role", "put_object", "complete"]
+
+ success: bool
+
+ error: Optional[Error] = None
diff --git a/src/kernel/types/audit_logs/export_destination_create_params.py b/src/kernel/types/audit_logs/export_destination_create_params.py
new file mode 100644
index 00000000..b01cdee0
--- /dev/null
+++ b/src/kernel/types/audit_logs/export_destination_create_params.py
@@ -0,0 +1,23 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from __future__ import annotations
+
+from typing_extensions import Literal, Required, TypedDict
+
+__all__ = ["ExportDestinationCreateParams"]
+
+
+class ExportDestinationCreateParams(TypedDict, total=False):
+ bucket: Required[str]
+
+ format: Required[Literal["jsonl.gz"]]
+
+ prefix: Required[str]
+
+ region: Required[str]
+
+ role_arn: Required[str]
+
+ type: Required[Literal["s3"]]
+
+ kms_key_id: str
diff --git a/src/kernel/types/audit_logs/export_destination_list_params.py b/src/kernel/types/audit_logs/export_destination_list_params.py
new file mode 100644
index 00000000..2026a4b1
--- /dev/null
+++ b/src/kernel/types/audit_logs/export_destination_list_params.py
@@ -0,0 +1,15 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from __future__ import annotations
+
+from typing_extensions import TypedDict
+
+__all__ = ["ExportDestinationListParams"]
+
+
+class ExportDestinationListParams(TypedDict, total=False):
+ limit: int
+ """Limit the number of destinations to return."""
+
+ offset: int
+ """Offset the number of destinations to return."""
diff --git a/src/kernel/types/audit_logs/export_destination_update_params.py b/src/kernel/types/audit_logs/export_destination_update_params.py
new file mode 100644
index 00000000..e21538f0
--- /dev/null
+++ b/src/kernel/types/audit_logs/export_destination_update_params.py
@@ -0,0 +1,26 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from __future__ import annotations
+
+from typing_extensions import Literal, TypedDict
+
+__all__ = ["ExportDestinationUpdateParams"]
+
+
+class ExportDestinationUpdateParams(TypedDict, total=False):
+ bucket: str
+
+ kms_key_id: str
+ """KMS key ID, alias, or ARN.
+
+ Set to an empty string to remove the configured KMS key; omit or send null to
+ leave unchanged.
+ """
+
+ prefix: str
+
+ region: str
+
+ role_arn: str
+
+ status: Literal["active", "paused"]
diff --git a/tests/api_resources/audit_logs/__init__.py b/tests/api_resources/audit_logs/__init__.py
new file mode 100644
index 00000000..fd8019a9
--- /dev/null
+++ b/tests/api_resources/audit_logs/__init__.py
@@ -0,0 +1 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
diff --git a/tests/api_resources/audit_logs/test_export_destinations.py b/tests/api_resources/audit_logs/test_export_destinations.py
new file mode 100644
index 00000000..34b4fde8
--- /dev/null
+++ b/tests/api_resources/audit_logs/test_export_destinations.py
@@ -0,0 +1,592 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from __future__ import annotations
+
+import os
+from typing import Any, cast
+
+import pytest
+
+from kernel import Kernel, AsyncKernel
+from tests.utils import assert_matches_type
+from kernel.pagination import SyncOffsetPagination, AsyncOffsetPagination
+from kernel.types.audit_logs import (
+ AuditLogExportDestination,
+ AuditLogExportDestinationTestResult,
+)
+
+base_url = os.environ.get("TEST_API_BASE_URL", "http://127.0.0.1:4010")
+
+
+class TestExportDestinations:
+ parametrize = pytest.mark.parametrize("client", [False, True], indirect=True, ids=["loose", "strict"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ def test_method_create(self, client: Kernel) -> None:
+ export_destination = client.audit_logs.export_destinations.create(
+ bucket="xxx",
+ format="jsonl.gz",
+ prefix="prefix",
+ region="x",
+ role_arn="x",
+ type="s3",
+ )
+ assert_matches_type(AuditLogExportDestination, export_destination, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ def test_method_create_with_all_params(self, client: Kernel) -> None:
+ export_destination = client.audit_logs.export_destinations.create(
+ bucket="xxx",
+ format="jsonl.gz",
+ prefix="prefix",
+ region="x",
+ role_arn="x",
+ type="s3",
+ kms_key_id="kms_key_id",
+ )
+ assert_matches_type(AuditLogExportDestination, export_destination, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ def test_raw_response_create(self, client: Kernel) -> None:
+ response = client.audit_logs.export_destinations.with_raw_response.create(
+ bucket="xxx",
+ format="jsonl.gz",
+ prefix="prefix",
+ region="x",
+ role_arn="x",
+ type="s3",
+ )
+
+ assert response.is_closed is True
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+ export_destination = response.parse()
+ assert_matches_type(AuditLogExportDestination, export_destination, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ def test_streaming_response_create(self, client: Kernel) -> None:
+ with client.audit_logs.export_destinations.with_streaming_response.create(
+ bucket="xxx",
+ format="jsonl.gz",
+ prefix="prefix",
+ region="x",
+ role_arn="x",
+ type="s3",
+ ) as response:
+ assert not response.is_closed
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+
+ export_destination = response.parse()
+ assert_matches_type(AuditLogExportDestination, export_destination, path=["response"])
+
+ assert cast(Any, response.is_closed) is True
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ def test_method_retrieve(self, client: Kernel) -> None:
+ export_destination = client.audit_logs.export_destinations.retrieve(
+ "id",
+ )
+ assert_matches_type(AuditLogExportDestination, export_destination, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ def test_raw_response_retrieve(self, client: Kernel) -> None:
+ response = client.audit_logs.export_destinations.with_raw_response.retrieve(
+ "id",
+ )
+
+ assert response.is_closed is True
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+ export_destination = response.parse()
+ assert_matches_type(AuditLogExportDestination, export_destination, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ def test_streaming_response_retrieve(self, client: Kernel) -> None:
+ with client.audit_logs.export_destinations.with_streaming_response.retrieve(
+ "id",
+ ) as response:
+ assert not response.is_closed
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+
+ export_destination = response.parse()
+ assert_matches_type(AuditLogExportDestination, export_destination, path=["response"])
+
+ assert cast(Any, response.is_closed) is True
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ def test_path_params_retrieve(self, client: Kernel) -> None:
+ with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"):
+ client.audit_logs.export_destinations.with_raw_response.retrieve(
+ "",
+ )
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ def test_method_update(self, client: Kernel) -> None:
+ export_destination = client.audit_logs.export_destinations.update(
+ id="id",
+ )
+ assert_matches_type(AuditLogExportDestination, export_destination, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ def test_method_update_with_all_params(self, client: Kernel) -> None:
+ export_destination = client.audit_logs.export_destinations.update(
+ id="id",
+ bucket="xxx",
+ kms_key_id="kms_key_id",
+ prefix="prefix",
+ region="x",
+ role_arn="x",
+ status="active",
+ )
+ assert_matches_type(AuditLogExportDestination, export_destination, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ def test_raw_response_update(self, client: Kernel) -> None:
+ response = client.audit_logs.export_destinations.with_raw_response.update(
+ id="id",
+ )
+
+ assert response.is_closed is True
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+ export_destination = response.parse()
+ assert_matches_type(AuditLogExportDestination, export_destination, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ def test_streaming_response_update(self, client: Kernel) -> None:
+ with client.audit_logs.export_destinations.with_streaming_response.update(
+ id="id",
+ ) as response:
+ assert not response.is_closed
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+
+ export_destination = response.parse()
+ assert_matches_type(AuditLogExportDestination, export_destination, 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.audit_logs.export_destinations.with_raw_response.update(
+ id="",
+ )
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ def test_method_list(self, client: Kernel) -> None:
+ export_destination = client.audit_logs.export_destinations.list()
+ assert_matches_type(SyncOffsetPagination[AuditLogExportDestination], export_destination, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ def test_method_list_with_all_params(self, client: Kernel) -> None:
+ export_destination = client.audit_logs.export_destinations.list(
+ limit=1,
+ offset=0,
+ )
+ assert_matches_type(SyncOffsetPagination[AuditLogExportDestination], export_destination, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ def test_raw_response_list(self, client: Kernel) -> None:
+ response = client.audit_logs.export_destinations.with_raw_response.list()
+
+ assert response.is_closed is True
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+ export_destination = response.parse()
+ assert_matches_type(SyncOffsetPagination[AuditLogExportDestination], export_destination, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ def test_streaming_response_list(self, client: Kernel) -> None:
+ with client.audit_logs.export_destinations.with_streaming_response.list() as response:
+ assert not response.is_closed
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+
+ export_destination = response.parse()
+ assert_matches_type(SyncOffsetPagination[AuditLogExportDestination], export_destination, path=["response"])
+
+ assert cast(Any, response.is_closed) is True
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ def test_method_delete(self, client: Kernel) -> None:
+ export_destination = client.audit_logs.export_destinations.delete(
+ "id",
+ )
+ assert export_destination is None
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ def test_raw_response_delete(self, client: Kernel) -> None:
+ response = client.audit_logs.export_destinations.with_raw_response.delete(
+ "id",
+ )
+
+ assert response.is_closed is True
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+ export_destination = response.parse()
+ assert export_destination is None
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ def test_streaming_response_delete(self, client: Kernel) -> None:
+ with client.audit_logs.export_destinations.with_streaming_response.delete(
+ "id",
+ ) as response:
+ assert not response.is_closed
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+
+ export_destination = response.parse()
+ assert export_destination is None
+
+ assert cast(Any, response.is_closed) is True
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ def test_path_params_delete(self, client: Kernel) -> None:
+ with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"):
+ client.audit_logs.export_destinations.with_raw_response.delete(
+ "",
+ )
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ def test_method_test(self, client: Kernel) -> None:
+ export_destination = client.audit_logs.export_destinations.test(
+ "id",
+ )
+ assert_matches_type(AuditLogExportDestinationTestResult, export_destination, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ def test_raw_response_test(self, client: Kernel) -> None:
+ response = client.audit_logs.export_destinations.with_raw_response.test(
+ "id",
+ )
+
+ assert response.is_closed is True
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+ export_destination = response.parse()
+ assert_matches_type(AuditLogExportDestinationTestResult, export_destination, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ def test_streaming_response_test(self, client: Kernel) -> None:
+ with client.audit_logs.export_destinations.with_streaming_response.test(
+ "id",
+ ) as response:
+ assert not response.is_closed
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+
+ export_destination = response.parse()
+ assert_matches_type(AuditLogExportDestinationTestResult, export_destination, path=["response"])
+
+ assert cast(Any, response.is_closed) is True
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ def test_path_params_test(self, client: Kernel) -> None:
+ with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"):
+ client.audit_logs.export_destinations.with_raw_response.test(
+ "",
+ )
+
+
+class TestAsyncExportDestinations:
+ parametrize = pytest.mark.parametrize(
+ "async_client", [False, True, {"http_client": "aiohttp"}], indirect=True, ids=["loose", "strict", "aiohttp"]
+ )
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ async def test_method_create(self, async_client: AsyncKernel) -> None:
+ export_destination = await async_client.audit_logs.export_destinations.create(
+ bucket="xxx",
+ format="jsonl.gz",
+ prefix="prefix",
+ region="x",
+ role_arn="x",
+ type="s3",
+ )
+ assert_matches_type(AuditLogExportDestination, export_destination, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ async def test_method_create_with_all_params(self, async_client: AsyncKernel) -> None:
+ export_destination = await async_client.audit_logs.export_destinations.create(
+ bucket="xxx",
+ format="jsonl.gz",
+ prefix="prefix",
+ region="x",
+ role_arn="x",
+ type="s3",
+ kms_key_id="kms_key_id",
+ )
+ assert_matches_type(AuditLogExportDestination, export_destination, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ async def test_raw_response_create(self, async_client: AsyncKernel) -> None:
+ response = await async_client.audit_logs.export_destinations.with_raw_response.create(
+ bucket="xxx",
+ format="jsonl.gz",
+ prefix="prefix",
+ region="x",
+ role_arn="x",
+ type="s3",
+ )
+
+ assert response.is_closed is True
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+ export_destination = await response.parse()
+ assert_matches_type(AuditLogExportDestination, export_destination, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ async def test_streaming_response_create(self, async_client: AsyncKernel) -> None:
+ async with async_client.audit_logs.export_destinations.with_streaming_response.create(
+ bucket="xxx",
+ format="jsonl.gz",
+ prefix="prefix",
+ region="x",
+ role_arn="x",
+ type="s3",
+ ) as response:
+ assert not response.is_closed
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+
+ export_destination = await response.parse()
+ assert_matches_type(AuditLogExportDestination, export_destination, path=["response"])
+
+ assert cast(Any, response.is_closed) is True
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ async def test_method_retrieve(self, async_client: AsyncKernel) -> None:
+ export_destination = await async_client.audit_logs.export_destinations.retrieve(
+ "id",
+ )
+ assert_matches_type(AuditLogExportDestination, export_destination, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ async def test_raw_response_retrieve(self, async_client: AsyncKernel) -> None:
+ response = await async_client.audit_logs.export_destinations.with_raw_response.retrieve(
+ "id",
+ )
+
+ assert response.is_closed is True
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+ export_destination = await response.parse()
+ assert_matches_type(AuditLogExportDestination, export_destination, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ async def test_streaming_response_retrieve(self, async_client: AsyncKernel) -> None:
+ async with async_client.audit_logs.export_destinations.with_streaming_response.retrieve(
+ "id",
+ ) as response:
+ assert not response.is_closed
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+
+ export_destination = await response.parse()
+ assert_matches_type(AuditLogExportDestination, export_destination, 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_retrieve(self, async_client: AsyncKernel) -> None:
+ with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"):
+ await async_client.audit_logs.export_destinations.with_raw_response.retrieve(
+ "",
+ )
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ async def test_method_update(self, async_client: AsyncKernel) -> None:
+ export_destination = await async_client.audit_logs.export_destinations.update(
+ id="id",
+ )
+ assert_matches_type(AuditLogExportDestination, export_destination, 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:
+ export_destination = await async_client.audit_logs.export_destinations.update(
+ id="id",
+ bucket="xxx",
+ kms_key_id="kms_key_id",
+ prefix="prefix",
+ region="x",
+ role_arn="x",
+ status="active",
+ )
+ assert_matches_type(AuditLogExportDestination, export_destination, 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.audit_logs.export_destinations.with_raw_response.update(
+ id="id",
+ )
+
+ assert response.is_closed is True
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+ export_destination = await response.parse()
+ assert_matches_type(AuditLogExportDestination, export_destination, 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.audit_logs.export_destinations.with_streaming_response.update(
+ id="id",
+ ) as response:
+ assert not response.is_closed
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+
+ export_destination = await response.parse()
+ assert_matches_type(AuditLogExportDestination, export_destination, 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.audit_logs.export_destinations.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:
+ export_destination = await async_client.audit_logs.export_destinations.list()
+ assert_matches_type(AsyncOffsetPagination[AuditLogExportDestination], export_destination, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ async def test_method_list_with_all_params(self, async_client: AsyncKernel) -> None:
+ export_destination = await async_client.audit_logs.export_destinations.list(
+ limit=1,
+ offset=0,
+ )
+ assert_matches_type(AsyncOffsetPagination[AuditLogExportDestination], export_destination, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ async def test_raw_response_list(self, async_client: AsyncKernel) -> None:
+ response = await async_client.audit_logs.export_destinations.with_raw_response.list()
+
+ assert response.is_closed is True
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+ export_destination = await response.parse()
+ assert_matches_type(AsyncOffsetPagination[AuditLogExportDestination], export_destination, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ async def test_streaming_response_list(self, async_client: AsyncKernel) -> None:
+ async with async_client.audit_logs.export_destinations.with_streaming_response.list() as response:
+ assert not response.is_closed
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+
+ export_destination = await response.parse()
+ assert_matches_type(AsyncOffsetPagination[AuditLogExportDestination], export_destination, path=["response"])
+
+ assert cast(Any, response.is_closed) is True
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ async def test_method_delete(self, async_client: AsyncKernel) -> None:
+ export_destination = await async_client.audit_logs.export_destinations.delete(
+ "id",
+ )
+ assert export_destination is None
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ async def test_raw_response_delete(self, async_client: AsyncKernel) -> None:
+ response = await async_client.audit_logs.export_destinations.with_raw_response.delete(
+ "id",
+ )
+
+ assert response.is_closed is True
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+ export_destination = await response.parse()
+ assert export_destination is None
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ async def test_streaming_response_delete(self, async_client: AsyncKernel) -> None:
+ async with async_client.audit_logs.export_destinations.with_streaming_response.delete(
+ "id",
+ ) as response:
+ assert not response.is_closed
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+
+ export_destination = await response.parse()
+ assert export_destination is None
+
+ assert cast(Any, response.is_closed) is True
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ async def test_path_params_delete(self, async_client: AsyncKernel) -> None:
+ with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"):
+ await async_client.audit_logs.export_destinations.with_raw_response.delete(
+ "",
+ )
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ async def test_method_test(self, async_client: AsyncKernel) -> None:
+ export_destination = await async_client.audit_logs.export_destinations.test(
+ "id",
+ )
+ assert_matches_type(AuditLogExportDestinationTestResult, export_destination, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ async def test_raw_response_test(self, async_client: AsyncKernel) -> None:
+ response = await async_client.audit_logs.export_destinations.with_raw_response.test(
+ "id",
+ )
+
+ assert response.is_closed is True
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+ export_destination = await response.parse()
+ assert_matches_type(AuditLogExportDestinationTestResult, export_destination, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ async def test_streaming_response_test(self, async_client: AsyncKernel) -> None:
+ async with async_client.audit_logs.export_destinations.with_streaming_response.test(
+ "id",
+ ) as response:
+ assert not response.is_closed
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+
+ export_destination = await response.parse()
+ assert_matches_type(AuditLogExportDestinationTestResult, export_destination, 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_test(self, async_client: AsyncKernel) -> None:
+ with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"):
+ await async_client.audit_logs.export_destinations.with_raw_response.test(
+ "",
+ )
From 9da25705b9d0c5deca086bc78a41436f2d123837 Mon Sep 17 00:00:00 2001
From: "kernel-internal[bot]"
<260533166+kernel-internal[bot]@users.noreply.github.com>
Date: Fri, 7 Aug 2026 21:08:22 +0000
Subject: [PATCH 170/366] feat: Expose authenticated request context
Stainless-Generated-From: b06245d06729c7754c443005d0479ad52efa2b7f
---
.stats.yml | 2 +-
api.md | 12 ++
src/kernel/resources/auth/__init__.py | 14 +++
src/kernel/resources/auth/auth.py | 38 ++++++
src/kernel/resources/auth/context.py | 147 +++++++++++++++++++++++
src/kernel/types/auth/__init__.py | 1 +
src/kernel/types/auth/auth_context.py | 98 +++++++++++++++
tests/api_resources/auth/test_context.py | 80 ++++++++++++
8 files changed, 391 insertions(+), 1 deletion(-)
create mode 100644 src/kernel/resources/auth/context.py
create mode 100644 src/kernel/types/auth/auth_context.py
create mode 100644 tests/api_resources/auth/test_context.py
diff --git a/.stats.yml b/.stats.yml
index 4bb27f8b..50996d49 100644
--- a/.stats.yml
+++ b/.stats.yml
@@ -1 +1 @@
-configured_endpoints: 133
+configured_endpoints: 134
diff --git a/api.md b/api.md
index bf8b28ae..7de716a0 100644
--- a/api.md
+++ b/api.md
@@ -294,6 +294,18 @@ Methods:
# Auth
+## Context
+
+Types:
+
+```python
+from kernel.types.auth import AuthContext
+```
+
+Methods:
+
+- client.auth.context.retrieve() -> AuthContext
+
## Connections
Types:
diff --git a/src/kernel/resources/auth/__init__.py b/src/kernel/resources/auth/__init__.py
index a863677f..37b41273 100644
--- a/src/kernel/resources/auth/__init__.py
+++ b/src/kernel/resources/auth/__init__.py
@@ -8,6 +8,14 @@
AuthResourceWithStreamingResponse,
AsyncAuthResourceWithStreamingResponse,
)
+from .context import (
+ ContextResource,
+ AsyncContextResource,
+ ContextResourceWithRawResponse,
+ AsyncContextResourceWithRawResponse,
+ ContextResourceWithStreamingResponse,
+ AsyncContextResourceWithStreamingResponse,
+)
from .connections import (
ConnectionsResource,
AsyncConnectionsResource,
@@ -18,6 +26,12 @@
)
__all__ = [
+ "ContextResource",
+ "AsyncContextResource",
+ "ContextResourceWithRawResponse",
+ "AsyncContextResourceWithRawResponse",
+ "ContextResourceWithStreamingResponse",
+ "AsyncContextResourceWithStreamingResponse",
"ConnectionsResource",
"AsyncConnectionsResource",
"ConnectionsResourceWithRawResponse",
diff --git a/src/kernel/resources/auth/auth.py b/src/kernel/resources/auth/auth.py
index f8744950..b301af28 100644
--- a/src/kernel/resources/auth/auth.py
+++ b/src/kernel/resources/auth/auth.py
@@ -2,6 +2,14 @@
from __future__ import annotations
+from .context import (
+ ContextResource,
+ AsyncContextResource,
+ ContextResourceWithRawResponse,
+ AsyncContextResourceWithRawResponse,
+ ContextResourceWithStreamingResponse,
+ AsyncContextResourceWithStreamingResponse,
+)
from ..._compat import cached_property
from ..._resource import SyncAPIResource, AsyncAPIResource
from .connections import (
@@ -17,6 +25,11 @@
class AuthResource(SyncAPIResource):
+ @cached_property
+ def context(self) -> ContextResource:
+ """Inspect the identity and authorization context for the current request."""
+ return ContextResource(self._client)
+
@cached_property
def connections(self) -> ConnectionsResource:
"""Create and manage auth connections for automated credential capture and login."""
@@ -43,6 +56,11 @@ def with_streaming_response(self) -> AuthResourceWithStreamingResponse:
class AsyncAuthResource(AsyncAPIResource):
+ @cached_property
+ def context(self) -> AsyncContextResource:
+ """Inspect the identity and authorization context for the current request."""
+ return AsyncContextResource(self._client)
+
@cached_property
def connections(self) -> AsyncConnectionsResource:
"""Create and manage auth connections for automated credential capture and login."""
@@ -72,6 +90,11 @@ class AuthResourceWithRawResponse:
def __init__(self, auth: AuthResource) -> None:
self._auth = auth
+ @cached_property
+ def context(self) -> ContextResourceWithRawResponse:
+ """Inspect the identity and authorization context for the current request."""
+ return ContextResourceWithRawResponse(self._auth.context)
+
@cached_property
def connections(self) -> ConnectionsResourceWithRawResponse:
"""Create and manage auth connections for automated credential capture and login."""
@@ -82,6 +105,11 @@ class AsyncAuthResourceWithRawResponse:
def __init__(self, auth: AsyncAuthResource) -> None:
self._auth = auth
+ @cached_property
+ def context(self) -> AsyncContextResourceWithRawResponse:
+ """Inspect the identity and authorization context for the current request."""
+ return AsyncContextResourceWithRawResponse(self._auth.context)
+
@cached_property
def connections(self) -> AsyncConnectionsResourceWithRawResponse:
"""Create and manage auth connections for automated credential capture and login."""
@@ -92,6 +120,11 @@ class AuthResourceWithStreamingResponse:
def __init__(self, auth: AuthResource) -> None:
self._auth = auth
+ @cached_property
+ def context(self) -> ContextResourceWithStreamingResponse:
+ """Inspect the identity and authorization context for the current request."""
+ return ContextResourceWithStreamingResponse(self._auth.context)
+
@cached_property
def connections(self) -> ConnectionsResourceWithStreamingResponse:
"""Create and manage auth connections for automated credential capture and login."""
@@ -102,6 +135,11 @@ class AsyncAuthResourceWithStreamingResponse:
def __init__(self, auth: AsyncAuthResource) -> None:
self._auth = auth
+ @cached_property
+ def context(self) -> AsyncContextResourceWithStreamingResponse:
+ """Inspect the identity and authorization context for the current request."""
+ return AsyncContextResourceWithStreamingResponse(self._auth.context)
+
@cached_property
def connections(self) -> AsyncConnectionsResourceWithStreamingResponse:
"""Create and manage auth connections for automated credential capture and login."""
diff --git a/src/kernel/resources/auth/context.py b/src/kernel/resources/auth/context.py
new file mode 100644
index 00000000..a4f66f97
--- /dev/null
+++ b/src/kernel/resources/auth/context.py
@@ -0,0 +1,147 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from __future__ import annotations
+
+import httpx
+
+from ..._types import Body, Query, Headers, NotGiven, not_given
+from ..._compat import cached_property
+from ..._resource import SyncAPIResource, AsyncAPIResource
+from ..._response import (
+ to_raw_response_wrapper,
+ to_streamed_response_wrapper,
+ async_to_raw_response_wrapper,
+ async_to_streamed_response_wrapper,
+)
+from ..._base_client import make_request_options
+from ...types.auth.auth_context import AuthContext
+
+__all__ = ["ContextResource", "AsyncContextResource"]
+
+
+class ContextResource(SyncAPIResource):
+ """Inspect the identity and authorization context for the current request."""
+
+ @cached_property
+ def with_raw_response(self) -> ContextResourceWithRawResponse:
+ """
+ This property can be used as a prefix for any HTTP method call to return
+ the raw response object instead of the parsed content.
+
+ For more information, see https://www.github.com/kernel/kernel-python-sdk#accessing-raw-response-data-eg-headers
+ """
+ return ContextResourceWithRawResponse(self)
+
+ @cached_property
+ def with_streaming_response(self) -> ContextResourceWithStreamingResponse:
+ """
+ An alternative to `.with_raw_response` that doesn't eagerly read the response body.
+
+ For more information, see https://www.github.com/kernel/kernel-python-sdk#with_streaming_response
+ """
+ return ContextResourceWithStreamingResponse(self)
+
+ def retrieve(
+ self,
+ *,
+ # 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,
+ ) -> AuthContext:
+ """
+ Returns the authenticated principal, organization, credential scope, and
+ effective request scope. The response is derived from the verified request
+ context and does not expose credential secrets.
+ """
+ return self._get(
+ "/auth/context",
+ options=make_request_options(
+ extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
+ ),
+ cast_to=AuthContext,
+ )
+
+
+class AsyncContextResource(AsyncAPIResource):
+ """Inspect the identity and authorization context for the current request."""
+
+ @cached_property
+ def with_raw_response(self) -> AsyncContextResourceWithRawResponse:
+ """
+ This property can be used as a prefix for any HTTP method call to return
+ the raw response object instead of the parsed content.
+
+ For more information, see https://www.github.com/kernel/kernel-python-sdk#accessing-raw-response-data-eg-headers
+ """
+ return AsyncContextResourceWithRawResponse(self)
+
+ @cached_property
+ def with_streaming_response(self) -> AsyncContextResourceWithStreamingResponse:
+ """
+ An alternative to `.with_raw_response` that doesn't eagerly read the response body.
+
+ For more information, see https://www.github.com/kernel/kernel-python-sdk#with_streaming_response
+ """
+ return AsyncContextResourceWithStreamingResponse(self)
+
+ async def retrieve(
+ self,
+ *,
+ # 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,
+ ) -> AuthContext:
+ """
+ Returns the authenticated principal, organization, credential scope, and
+ effective request scope. The response is derived from the verified request
+ context and does not expose credential secrets.
+ """
+ return await self._get(
+ "/auth/context",
+ options=make_request_options(
+ extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
+ ),
+ cast_to=AuthContext,
+ )
+
+
+class ContextResourceWithRawResponse:
+ def __init__(self, context: ContextResource) -> None:
+ self._context = context
+
+ self.retrieve = to_raw_response_wrapper(
+ context.retrieve,
+ )
+
+
+class AsyncContextResourceWithRawResponse:
+ def __init__(self, context: AsyncContextResource) -> None:
+ self._context = context
+
+ self.retrieve = async_to_raw_response_wrapper(
+ context.retrieve,
+ )
+
+
+class ContextResourceWithStreamingResponse:
+ def __init__(self, context: ContextResource) -> None:
+ self._context = context
+
+ self.retrieve = to_streamed_response_wrapper(
+ context.retrieve,
+ )
+
+
+class AsyncContextResourceWithStreamingResponse:
+ def __init__(self, context: AsyncContextResource) -> None:
+ self._context = context
+
+ self.retrieve = async_to_streamed_response_wrapper(
+ context.retrieve,
+ )
diff --git a/src/kernel/types/auth/__init__.py b/src/kernel/types/auth/__init__.py
index a70816fb..2d0cf1b0 100644
--- a/src/kernel/types/auth/__init__.py
+++ b/src/kernel/types/auth/__init__.py
@@ -2,6 +2,7 @@
from __future__ import annotations
+from .auth_context import AuthContext as AuthContext
from .managed_auth import ManagedAuth as ManagedAuth
from .login_response import LoginResponse as LoginResponse
from .connection_list_params import ConnectionListParams as ConnectionListParams
diff --git a/src/kernel/types/auth/auth_context.py b/src/kernel/types/auth/auth_context.py
new file mode 100644
index 00000000..650d63c5
--- /dev/null
+++ b/src/kernel/types/auth/auth_context.py
@@ -0,0 +1,98 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from typing import Optional
+from typing_extensions import Literal
+
+from ..._models import BaseModel
+
+__all__ = [
+ "AuthContext",
+ "Authentication",
+ "Authorization",
+ "AuthorizationCredentialScope",
+ "AuthorizationEffectiveScope",
+ "Organization",
+ "Principal",
+]
+
+
+class Authentication(BaseModel):
+ credential_id: Optional[str] = None
+ """
+ The API key ID when authenticated with an API key; null for session credentials.
+ """
+
+ method: Literal["api_key", "jwt"]
+ """The credential format used to authenticate the request."""
+
+ source: Literal["api_key", "oauth", "dashboard"]
+ """The source classification resolved by authentication middleware."""
+
+
+class AuthorizationCredentialScope(BaseModel):
+ """A scope within the authenticated organization.
+
+ A null project_id represents organization-wide scope.
+ """
+
+ project_id: Optional[str] = None
+ """The Kernel project ID, or null when the scope is organization-wide."""
+
+
+class AuthorizationEffectiveScope(BaseModel):
+ """A scope within the authenticated organization.
+
+ A null project_id represents organization-wide scope.
+ """
+
+ project_id: Optional[str] = None
+ """The Kernel project ID, or null when the scope is organization-wide."""
+
+
+class Authorization(BaseModel):
+ """The credential's maximum scope and the effective scope selected for this request.
+
+ Future permission data can be added without changing scope semantics.
+ """
+
+ credential_scope: AuthorizationCredentialScope
+ """A scope within the authenticated organization.
+
+ A null project_id represents organization-wide scope.
+ """
+
+ effective_scope: AuthorizationEffectiveScope
+ """A scope within the authenticated organization.
+
+ A null project_id represents organization-wide scope.
+ """
+
+
+class Organization(BaseModel):
+ id: str
+ """The authenticated Kernel organization ID."""
+
+
+class Principal(BaseModel):
+ id: str
+ """The API key ID for API-key principals or user ID for user principals."""
+
+ type: Literal["api_key", "user"]
+ """The kind of principal authenticated for the request."""
+
+
+class AuthContext(BaseModel):
+ """The identity and authorization context resolved for the current request."""
+
+ authentication: Authentication
+
+ authorization: Authorization
+ """The credential's maximum scope and the effective scope selected for this
+ request.
+
+ Future permission data can be added without changing scope semantics.
+ """
+
+ organization: Organization
+
+ principal: Principal
diff --git a/tests/api_resources/auth/test_context.py b/tests/api_resources/auth/test_context.py
new file mode 100644
index 00000000..18ed5c58
--- /dev/null
+++ b/tests/api_resources/auth/test_context.py
@@ -0,0 +1,80 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from __future__ import annotations
+
+import os
+from typing import Any, cast
+
+import pytest
+
+from kernel import Kernel, AsyncKernel
+from tests.utils import assert_matches_type
+from kernel.types.auth import AuthContext
+
+base_url = os.environ.get("TEST_API_BASE_URL", "http://127.0.0.1:4010")
+
+
+class TestContext:
+ parametrize = pytest.mark.parametrize("client", [False, True], indirect=True, ids=["loose", "strict"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ def test_method_retrieve(self, client: Kernel) -> None:
+ context = client.auth.context.retrieve()
+ assert_matches_type(AuthContext, context, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ def test_raw_response_retrieve(self, client: Kernel) -> None:
+ response = client.auth.context.with_raw_response.retrieve()
+
+ assert response.is_closed is True
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+ context = response.parse()
+ assert_matches_type(AuthContext, context, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ def test_streaming_response_retrieve(self, client: Kernel) -> None:
+ with client.auth.context.with_streaming_response.retrieve() as response:
+ assert not response.is_closed
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+
+ context = response.parse()
+ assert_matches_type(AuthContext, context, path=["response"])
+
+ assert cast(Any, response.is_closed) is True
+
+
+class TestAsyncContext:
+ parametrize = pytest.mark.parametrize(
+ "async_client", [False, True, {"http_client": "aiohttp"}], indirect=True, ids=["loose", "strict", "aiohttp"]
+ )
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ async def test_method_retrieve(self, async_client: AsyncKernel) -> None:
+ context = await async_client.auth.context.retrieve()
+ assert_matches_type(AuthContext, context, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ async def test_raw_response_retrieve(self, async_client: AsyncKernel) -> None:
+ response = await async_client.auth.context.with_raw_response.retrieve()
+
+ assert response.is_closed is True
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+ context = await response.parse()
+ assert_matches_type(AuthContext, context, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ async def test_streaming_response_retrieve(self, async_client: AsyncKernel) -> None:
+ async with async_client.auth.context.with_streaming_response.retrieve() as response:
+ assert not response.is_closed
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+
+ context = await response.parse()
+ assert_matches_type(AuthContext, context, path=["response"])
+
+ assert cast(Any, response.is_closed) is True
From bfa28a3a2d61c356bfbe932f9fc4901bba6f5a1f Mon Sep 17 00:00:00 2001
From: "stainless-app[bot]"
<142633134+stainless-app[bot]@users.noreply.github.com>
Date: Sat, 8 Aug 2026 02:52:26 +0000
Subject: [PATCH 171/366] release: 0.87.0
---
.release-please-manifest.json | 2 +-
CHANGELOG.md | 11 +++++++++++
pyproject.toml | 2 +-
src/kernel/_version.py | 2 +-
4 files changed, 14 insertions(+), 3 deletions(-)
diff --git a/.release-please-manifest.json b/.release-please-manifest.json
index 3da9036b..ae449647 100644
--- a/.release-please-manifest.json
+++ b/.release-please-manifest.json
@@ -1,3 +1,3 @@
{
- ".": "0.86.1"
+ ".": "0.87.0"
}
\ No newline at end of file
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 3731eada..a45c90eb 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,16 @@
# Changelog
+## 0.87.0 (2026-08-08)
+
+Full Changelog: [v0.86.1...v0.87.0](https://github.com/kernel/kernel-python-sdk/compare/v0.86.1...v0.87.0)
+
+### Features
+
+* Add audit logs plan paywall to the dashboard ([18fdd7b](https://github.com/kernel/kernel-python-sdk/commit/18fdd7b7e37c0346805e43e7cf4b41f0e75ff29e))
+* Expose authenticated request context ([9da2570](https://github.com/kernel/kernel-python-sdk/commit/9da25705b9d0c5deca086bc78a41436f2d123837))
+* Expose profile save behavior in browser responses ([b211a4f](https://github.com/kernel/kernel-python-sdk/commit/b211a4f83b6a3e9eb4b9e41e8493057f57be6cbf))
+* Profiles, file I/O, replays: ungate for Free (packaging PR 3) ([6135e12](https://github.com/kernel/kernel-python-sdk/commit/6135e1288bc08b74532c76f65c5f96851ff98793))
+
## 0.86.1 (2026-08-06)
Full Changelog: [v0.86.0...v0.86.1](https://github.com/kernel/kernel-python-sdk/compare/v0.86.0...v0.86.1)
diff --git a/pyproject.toml b/pyproject.toml
index d4029ac3..505189ab 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -1,6 +1,6 @@
[project]
name = "kernel"
-version = "0.86.1"
+version = "0.87.0"
description = "The official Python library for the kernel API"
dynamic = ["readme"]
license = "Apache-2.0"
diff --git a/src/kernel/_version.py b/src/kernel/_version.py
index f6e23035..6c0e6685 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.86.1" # x-release-please-version
+__version__ = "0.87.0" # x-release-please-version
From b136c5037dff30a8d126e90c77527fcb31fdc5ce Mon Sep 17 00:00:00 2001
From: rgarcia <72655+rgarcia@users.noreply.github.com>
Date: Sat, 8 Aug 2026 13:22:16 +0000
Subject: [PATCH 172/366] Test direct routing stream cancellation
---
tests/test_browser_routing.py | 54 ++++++++++++++++++++++++++++++++++-
1 file changed, 53 insertions(+), 1 deletion(-)
diff --git a/tests/test_browser_routing.py b/tests/test_browser_routing.py
index ce9a726f..9b37908e 100644
--- a/tests/test_browser_routing.py
+++ b/tests/test_browser_routing.py
@@ -1,7 +1,9 @@
from __future__ import annotations
import os
-from typing import Any, cast
+import asyncio
+from typing import Any, AsyncIterator, cast
+from typing_extensions import override
import httpx
import respx
@@ -119,6 +121,56 @@ def test_telemetry_stream_routes_directly_to_vm(monkeypatch: pytest.MonkeyPatch)
assert request.headers.get("Authorization") is None
+@pytest.mark.asyncio
+async def test_async_telemetry_stream_cancellation_survives_direct_routing(
+ monkeypatch: pytest.MonkeyPatch,
+) -> None:
+ monkeypatch.setenv("KERNEL_BROWSER_ROUTING_SUBRESOURCES", "telemetry/stream")
+ read_started = asyncio.Event()
+ read_stopped = asyncio.Event()
+
+ class BlockingSSEStream(httpx.AsyncByteStream):
+ @override
+ async def __aiter__(self) -> AsyncIterator[bytes]:
+ read_started.set()
+ try:
+ await asyncio.Event().wait()
+ finally:
+ read_stopped.set()
+ yield b""
+
+ @override
+ async def aclose(self) -> None:
+ read_stopped.set()
+
+ async def handle_request(request: httpx.Request) -> httpx.Response:
+ assert request.url.path == "/browser/kernel/telemetry/stream"
+ return httpx.Response(
+ 200,
+ headers={"content-type": "text/event-stream"},
+ stream=BlockingSSEStream(),
+ )
+
+ http_client = httpx.AsyncClient(transport=httpx.MockTransport(handle_request))
+ async with AsyncKernel(
+ base_url=base_url,
+ api_key=api_key,
+ http_client=http_client,
+ _strict_response_validation=True,
+ ) as client:
+ route = browser_route_from_browser(_fake_browser())
+ assert route is not None
+ client.browser_route_cache.set(route)
+ stream = await client.browsers.telemetry.stream("sess-1")
+ consumer = asyncio.create_task(stream.__anext__())
+ await asyncio.wait_for(read_started.wait(), timeout=1)
+
+ consumer.cancel()
+ with pytest.raises(asyncio.CancelledError):
+ await asyncio.wait_for(consumer, timeout=1)
+ await asyncio.wait_for(read_stopped.wait(), timeout=1)
+
+
@respx.mock
def test_browser_request_params_cannot_override_target_url_or_jwt() -> None:
route = respx.get("http://browser-session.test/browser/kernel/curl/raw").mock(
From 5ae6c3e0b6a54a884b9fc6d3d4658d970a7aa459 Mon Sep 17 00:00:00 2001
From: "kernel-internal[bot]"
<260533166+kernel-internal[bot]@users.noreply.github.com>
Date: Sat, 8 Aug 2026 19:56:33 +0000
Subject: [PATCH 173/366] feat: Forward egress deploy owner attribution
Stainless-Generated-From: 00e953c7c4e06216815d4e9f8e53ab7596262ca7
---
api.md | 4 +
src/kernel/resources/auth/connections.py | 91 +++++----
src/kernel/resources/browsers/browsers.py | 65 +++++-
src/kernel/types/__init__.py | 4 +
src/kernel/types/auth/__init__.py | 2 +
.../types/auth/connection_create_params.py | 33 ++--
.../types/auth/connection_login_params.py | 31 ++-
.../types/auth/connection_update_params.py | 33 ++--
src/kernel/types/auth/managed_auth.py | 24 ++-
.../types/auth/managed_auth_browser_config.py | 118 +++++++++++
.../auth/managed_auth_browser_config_param.py | 120 +++++++++++
src/kernel/types/browser_create_params.py | 25 ++-
src/kernel/types/browser_create_response.py | 9 +-
src/kernel/types/browser_list_response.py | 9 +-
.../types/browser_pool_acquire_response.py | 9 +-
src/kernel/types/browser_proxy.py | 30 +++
src/kernel/types/browser_proxy_config.py | 34 ++++
.../types/browser_proxy_config_param.py | 35 ++++
src/kernel/types/browser_proxy_mode.py | 7 +
src/kernel/types/browser_retrieve_response.py | 9 +-
src/kernel/types/browser_update_params.py | 17 +-
src/kernel/types/browser_update_response.py | 9 +-
.../invocation_list_browsers_response.py | 9 +-
tests/api_resources/auth/test_connections.py | 186 ++++++++++++++++++
tests/api_resources/test_browsers.py | 20 ++
25 files changed, 808 insertions(+), 125 deletions(-)
create mode 100644 src/kernel/types/auth/managed_auth_browser_config.py
create mode 100644 src/kernel/types/auth/managed_auth_browser_config_param.py
create mode 100644 src/kernel/types/browser_proxy.py
create mode 100644 src/kernel/types/browser_proxy_config.py
create mode 100644 src/kernel/types/browser_proxy_config_param.py
create mode 100644 src/kernel/types/browser_proxy_mode.py
diff --git a/api.md b/api.md
index 7de716a0..65ac4c06 100644
--- a/api.md
+++ b/api.md
@@ -81,6 +81,9 @@ Types:
```python
from kernel.types import (
BrowserPoolRef,
+ BrowserProxy,
+ BrowserProxyConfig,
+ BrowserProxyMode,
BrowserUsage,
Profile,
Tags,
@@ -314,6 +317,7 @@ Types:
from kernel.types.auth import (
LoginResponse,
ManagedAuth,
+ ManagedAuthBrowserConfig,
ManagedAuthCreateRequest,
ManagedAuthTimelineEvent,
ManagedAuthUpdateRequest,
diff --git a/src/kernel/resources/auth/connections.py b/src/kernel/resources/auth/connections.py
index f5f4529a..a84d42af 100644
--- a/src/kernel/resources/auth/connections.py
+++ b/src/kernel/resources/auth/connections.py
@@ -33,6 +33,7 @@
from ...types.auth.submit_fields_response import SubmitFieldsResponse
from ...types.auth.connection_follow_response import ConnectionFollowResponse
from ...types.auth.managed_auth_timeline_event import ManagedAuthTimelineEvent
+from ...types.auth.managed_auth_browser_config_param import ManagedAuthBrowserConfigParam
__all__ = ["ConnectionsResource", "AsyncConnectionsResource"]
@@ -66,6 +67,7 @@ def create(
profile_name: str,
allowed_domains: SequenceNotStr[str] | Omit = omit,
auto_reauth: bool | Omit = omit,
+ browser: ManagedAuthBrowserConfigParam | Omit = omit,
browser_telemetry: Optional[connection_create_params.BrowserTelemetry] | Omit = omit,
credential: connection_create_params.Credential | Omit = omit,
health_check_interval: int | Omit = omit,
@@ -120,9 +122,11 @@ def create(
false, expired sessions are marked as `NEEDS_AUTH` instead of attempting
re-auth. Defaults to true.
- browser_telemetry: Browser telemetry configuration used by this connection's browser sessions by
- default. Uses the exact create-browser configuration. Can be overridden
- per-login.
+ browser: Default browser configuration for login, reauthentication, and health-check
+ sessions.
+
+ browser_telemetry: Deprecated. Use browser.telemetry. Retained during migration for existing
+ clients.
credential:
Reference to credentials for the auth connection. Use one of:
@@ -144,10 +148,7 @@ def create(
login_url: Optional login page URL to skip discovery
- proxy: Proxy selection. Provide either id or name. The proxy must be in the same
- project as the resource referencing it. When selecting by name, the name must
- match exactly one active proxy in the project. Ambiguous names return a 400; use
- id for stable references.
+ proxy: Deprecated. Use browser.proxy. Retained during migration for existing clients.
record_session: Whether to record browser sessions for this connection by default. Useful for
debugging. Can be overridden per-login. Defaults to false.
@@ -171,6 +172,7 @@ def create(
"profile_name": profile_name,
"allowed_domains": allowed_domains,
"auto_reauth": auto_reauth,
+ "browser": browser,
"browser_telemetry": browser_telemetry,
"credential": credential,
"health_check_interval": health_check_interval,
@@ -229,6 +231,7 @@ def update(
*,
allowed_domains: SequenceNotStr[str] | Omit = omit,
auto_reauth: bool | Omit = omit,
+ browser: ManagedAuthBrowserConfigParam | Omit = omit,
browser_telemetry: Optional[connection_update_params.BrowserTelemetry] | Omit = omit,
credential: connection_update_params.Credential | Omit = omit,
health_check_interval: int | Omit = omit,
@@ -260,9 +263,11 @@ def update(
when `health_checks` is false. When false, expired sessions detected by a health
check are marked as `NEEDS_AUTH` instead of attempting re-auth.
- browser_telemetry: Browser telemetry configuration used by future browser sessions for this
- connection. Uses the exact create-browser configuration. Set enabled to false to
- disable telemetry.
+ browser: Browser configuration updates for future login, reauthentication, and
+ health-check sessions. Omitted properties remain unchanged.
+
+ browser_telemetry: Deprecated. Use browser.telemetry. Retained during migration for existing
+ clients.
credential:
Reference to credentials for the auth connection. Use one of:
@@ -280,10 +285,7 @@ def update(
login_url: Login page URL. Set to empty string to clear.
- proxy: Proxy selection. Provide either id or name. The proxy must be in the same
- project as the resource referencing it. When selecting by name, the name must
- match exactly one active proxy in the project. Ambiguous names return a 400; use
- id for stable references.
+ proxy: Deprecated. Use browser.proxy. Retained during migration for existing clients.
record_session: Whether to record browser sessions for this connection by default
@@ -305,6 +307,7 @@ def update(
{
"allowed_domains": allowed_domains,
"auto_reauth": auto_reauth,
+ "browser": browser,
"browser_telemetry": browser_telemetry,
"credential": credential,
"health_check_interval": health_check_interval,
@@ -464,6 +467,7 @@ def login(
self,
id: str,
*,
+ browser: ManagedAuthBrowserConfigParam | Omit = omit,
browser_telemetry: Optional[connection_login_params.BrowserTelemetry] | Omit = omit,
proxy: connection_login_params.Proxy | Omit = omit,
record_session: bool | Omit = omit,
@@ -481,14 +485,13 @@ def login(
credentials are stored.
Args:
- browser_telemetry: Override the connection's default browser telemetry configuration for this
- login. When omitted, the connection's browser_telemetry default is used. Uses
- the exact create-browser configuration.
+ browser: Browser configuration override for this login. Omitted properties inherit the
+ connection defaults.
+
+ browser_telemetry: Deprecated. Use browser.telemetry. Retained during migration for existing
+ clients.
- proxy: Proxy selection. Provide either id or name. The proxy must be in the same
- project as the resource referencing it. When selecting by name, the name must
- match exactly one active proxy in the project. Ambiguous names return a 400; use
- id for stable references.
+ proxy: Deprecated. Use browser.proxy. Retained during migration for existing clients.
record_session: Override the connection's default for recording this login's browser session.
When omitted, the connection's record_session default is used.
@@ -507,6 +510,7 @@ def login(
path_template("/auth/connections/{id}/login", id=id),
body=maybe_transform(
{
+ "browser": browser,
"browser_telemetry": browser_telemetry,
"proxy": proxy,
"record_session": record_session,
@@ -675,6 +679,7 @@ async def create(
profile_name: str,
allowed_domains: SequenceNotStr[str] | Omit = omit,
auto_reauth: bool | Omit = omit,
+ browser: ManagedAuthBrowserConfigParam | Omit = omit,
browser_telemetry: Optional[connection_create_params.BrowserTelemetry] | Omit = omit,
credential: connection_create_params.Credential | Omit = omit,
health_check_interval: int | Omit = omit,
@@ -729,9 +734,11 @@ async def create(
false, expired sessions are marked as `NEEDS_AUTH` instead of attempting
re-auth. Defaults to true.
- browser_telemetry: Browser telemetry configuration used by this connection's browser sessions by
- default. Uses the exact create-browser configuration. Can be overridden
- per-login.
+ browser: Default browser configuration for login, reauthentication, and health-check
+ sessions.
+
+ browser_telemetry: Deprecated. Use browser.telemetry. Retained during migration for existing
+ clients.
credential:
Reference to credentials for the auth connection. Use one of:
@@ -753,10 +760,7 @@ async def create(
login_url: Optional login page URL to skip discovery
- proxy: Proxy selection. Provide either id or name. The proxy must be in the same
- project as the resource referencing it. When selecting by name, the name must
- match exactly one active proxy in the project. Ambiguous names return a 400; use
- id for stable references.
+ proxy: Deprecated. Use browser.proxy. Retained during migration for existing clients.
record_session: Whether to record browser sessions for this connection by default. Useful for
debugging. Can be overridden per-login. Defaults to false.
@@ -780,6 +784,7 @@ async def create(
"profile_name": profile_name,
"allowed_domains": allowed_domains,
"auto_reauth": auto_reauth,
+ "browser": browser,
"browser_telemetry": browser_telemetry,
"credential": credential,
"health_check_interval": health_check_interval,
@@ -838,6 +843,7 @@ async def update(
*,
allowed_domains: SequenceNotStr[str] | Omit = omit,
auto_reauth: bool | Omit = omit,
+ browser: ManagedAuthBrowserConfigParam | Omit = omit,
browser_telemetry: Optional[connection_update_params.BrowserTelemetry] | Omit = omit,
credential: connection_update_params.Credential | Omit = omit,
health_check_interval: int | Omit = omit,
@@ -869,9 +875,11 @@ async def update(
when `health_checks` is false. When false, expired sessions detected by a health
check are marked as `NEEDS_AUTH` instead of attempting re-auth.
- browser_telemetry: Browser telemetry configuration used by future browser sessions for this
- connection. Uses the exact create-browser configuration. Set enabled to false to
- disable telemetry.
+ browser: Browser configuration updates for future login, reauthentication, and
+ health-check sessions. Omitted properties remain unchanged.
+
+ browser_telemetry: Deprecated. Use browser.telemetry. Retained during migration for existing
+ clients.
credential:
Reference to credentials for the auth connection. Use one of:
@@ -889,10 +897,7 @@ async def update(
login_url: Login page URL. Set to empty string to clear.
- proxy: Proxy selection. Provide either id or name. The proxy must be in the same
- project as the resource referencing it. When selecting by name, the name must
- match exactly one active proxy in the project. Ambiguous names return a 400; use
- id for stable references.
+ proxy: Deprecated. Use browser.proxy. Retained during migration for existing clients.
record_session: Whether to record browser sessions for this connection by default
@@ -914,6 +919,7 @@ async def update(
{
"allowed_domains": allowed_domains,
"auto_reauth": auto_reauth,
+ "browser": browser,
"browser_telemetry": browser_telemetry,
"credential": credential,
"health_check_interval": health_check_interval,
@@ -1073,6 +1079,7 @@ async def login(
self,
id: str,
*,
+ browser: ManagedAuthBrowserConfigParam | Omit = omit,
browser_telemetry: Optional[connection_login_params.BrowserTelemetry] | Omit = omit,
proxy: connection_login_params.Proxy | Omit = omit,
record_session: bool | Omit = omit,
@@ -1090,14 +1097,13 @@ async def login(
credentials are stored.
Args:
- browser_telemetry: Override the connection's default browser telemetry configuration for this
- login. When omitted, the connection's browser_telemetry default is used. Uses
- the exact create-browser configuration.
+ browser: Browser configuration override for this login. Omitted properties inherit the
+ connection defaults.
+
+ browser_telemetry: Deprecated. Use browser.telemetry. Retained during migration for existing
+ clients.
- proxy: Proxy selection. Provide either id or name. The proxy must be in the same
- project as the resource referencing it. When selecting by name, the name must
- match exactly one active proxy in the project. Ambiguous names return a 400; use
- id for stable references.
+ proxy: Deprecated. Use browser.proxy. Retained during migration for existing clients.
record_session: Override the connection's default for recording this login's browser session.
When omitted, the connection's record_session default is used.
@@ -1116,6 +1122,7 @@ async def login(
path_template("/auth/connections/{id}/login", id=id),
body=await async_maybe_transform(
{
+ "browser": browser,
"browser_telemetry": browser_telemetry,
"proxy": proxy,
"record_session": record_session,
diff --git a/src/kernel/resources/browsers/browsers.py b/src/kernel/resources/browsers/browsers.py
index 553a98dd..19f48988 100644
--- a/src/kernel/resources/browsers/browsers.py
+++ b/src/kernel/resources/browsers/browsers.py
@@ -97,6 +97,7 @@
from ...types.browser_create_response import BrowserCreateResponse
from ...types.browser_update_response import BrowserUpdateResponse
from ...types.browser_retrieve_response import BrowserRetrieveResponse
+from ...types.browser_proxy_config_param import BrowserProxyConfigParam
from ...types.shared_params.browser_profile import BrowserProfile
from ...types.shared_params.browser_viewport import BrowserViewport
from ...types.shared_params.browser_extension import BrowserExtension
@@ -172,6 +173,7 @@ def create(
kiosk_mode: bool | Omit = omit,
name: str | Omit = omit,
profile: BrowserProfile | Omit = omit,
+ proxy: BrowserProxyConfigParam | Omit = omit,
proxy_id: str | Omit = omit,
start_url: str | Omit = omit,
stealth: bool | Omit = omit,
@@ -216,15 +218,26 @@ def create(
specified, the matching profile will be loaded into the browser session.
Profiles must be created beforehand.
+ proxy: Proxy configuration for the browser session. Cannot be combined with proxy_id.
+ Omit to use the browser default: stealth browsers use Kernel's default stealth
+ proxy, while non-stealth browsers use direct egress. Set mode to direct to force
+ direct egress regardless of stealth. Set mode to default to explicitly use the
+ browser default: Kernel's default stealth proxy when stealth=true, or direct
+ egress when stealth=false. Select id or name to use that proxy regardless of
+ stealth. Proxy selection does not change stealth or CAPTCHA solver behavior.
+
proxy_id: Optional proxy to associate to the browser session. Must reference a proxy in
- the same project as the browser session.
+ the same project as the browser session. Deprecated in favor of proxy.
start_url: Optional URL to open when the browser session is created. Navigation is
best-effort, so navigation failures do not prevent the session from being
created.
- stealth: If true, launches the browser in stealth mode to reduce detection by anti-bot
- mechanisms.
+ stealth: If true, launches the browser in stealth mode and enables the CAPTCHA solver.
+ Defaults to false. When proxy is omitted, stealth browsers use Kernel's default
+ stealth proxy and non-stealth browsers use direct egress. An explicit proxy
+ configuration changes only egress; it does not enable or disable stealth or the
+ CAPTCHA solver.
tags: Optional user-defined key-value tags for the browser session, used to find and
group sessions later. Can be changed later via PATCH /browsers/{id_or_name}. Up
@@ -275,6 +288,7 @@ def create(
"kiosk_mode": kiosk_mode,
"name": name,
"profile": profile,
+ "proxy": proxy,
"proxy_id": proxy_id,
"start_url": start_url,
"stealth": stealth,
@@ -340,6 +354,7 @@ def update(
disable_default_proxy: bool | Omit = omit,
name: Optional[str] | Omit = omit,
profile: BrowserProfile | Omit = omit,
+ proxy: BrowserProxyConfigParam | Omit = omit,
proxy_id: Optional[str] | Omit = omit,
tags: Optional[TagsParam] | Omit = omit,
telemetry: Optional[browser_update_params.Telemetry] | Omit = omit,
@@ -356,7 +371,7 @@ def update(
Args:
disable_default_proxy: If true, stealth browsers connect directly instead of using the default stealth
- proxy.
+ proxy. Deprecated in favor of proxy.mode.
name: Human-readable name for the browser session. Omit to leave unchanged, set to an
empty string to clear the name. When set, must be unique among active sessions
@@ -365,8 +380,15 @@ def update(
profile: Profile to load into the browser session. Only allowed if the session does not
already have a profile loaded.
+ proxy: Proxy configuration to apply. Omit to leave the current configuration unchanged.
+ Cannot be combined with proxy_id or disable_default_proxy. Set mode to direct to
+ switch to direct egress regardless of stealth. Set mode to default to restore
+ the browser default after using a selected proxy: Kernel's default stealth proxy
+ for a stealth browser, or direct egress for a non-stealth browser. Updating
+ proxy does not change stealth or CAPTCHA solver behavior.
+
proxy_id: ID of the proxy to use. Omit to leave unchanged, set to empty string to remove
- proxy.
+ proxy. Deprecated in favor of proxy.
tags: User-defined key-value tags for the browser session. Omit to leave unchanged.
Provide a map to replace the entire tag set (full replace, not a merge). Set to
@@ -397,6 +419,7 @@ def update(
"disable_default_proxy": disable_default_proxy,
"name": name,
"profile": profile,
+ "proxy": proxy,
"proxy_id": proxy_id,
"tags": tags,
"telemetry": telemetry,
@@ -750,6 +773,7 @@ async def create(
kiosk_mode: bool | Omit = omit,
name: str | Omit = omit,
profile: BrowserProfile | Omit = omit,
+ proxy: BrowserProxyConfigParam | Omit = omit,
proxy_id: str | Omit = omit,
start_url: str | Omit = omit,
stealth: bool | Omit = omit,
@@ -794,15 +818,26 @@ async def create(
specified, the matching profile will be loaded into the browser session.
Profiles must be created beforehand.
+ proxy: Proxy configuration for the browser session. Cannot be combined with proxy_id.
+ Omit to use the browser default: stealth browsers use Kernel's default stealth
+ proxy, while non-stealth browsers use direct egress. Set mode to direct to force
+ direct egress regardless of stealth. Set mode to default to explicitly use the
+ browser default: Kernel's default stealth proxy when stealth=true, or direct
+ egress when stealth=false. Select id or name to use that proxy regardless of
+ stealth. Proxy selection does not change stealth or CAPTCHA solver behavior.
+
proxy_id: Optional proxy to associate to the browser session. Must reference a proxy in
- the same project as the browser session.
+ the same project as the browser session. Deprecated in favor of proxy.
start_url: Optional URL to open when the browser session is created. Navigation is
best-effort, so navigation failures do not prevent the session from being
created.
- stealth: If true, launches the browser in stealth mode to reduce detection by anti-bot
- mechanisms.
+ stealth: If true, launches the browser in stealth mode and enables the CAPTCHA solver.
+ Defaults to false. When proxy is omitted, stealth browsers use Kernel's default
+ stealth proxy and non-stealth browsers use direct egress. An explicit proxy
+ configuration changes only egress; it does not enable or disable stealth or the
+ CAPTCHA solver.
tags: Optional user-defined key-value tags for the browser session, used to find and
group sessions later. Can be changed later via PATCH /browsers/{id_or_name}. Up
@@ -853,6 +888,7 @@ async def create(
"kiosk_mode": kiosk_mode,
"name": name,
"profile": profile,
+ "proxy": proxy,
"proxy_id": proxy_id,
"start_url": start_url,
"stealth": stealth,
@@ -918,6 +954,7 @@ async def update(
disable_default_proxy: bool | Omit = omit,
name: Optional[str] | Omit = omit,
profile: BrowserProfile | Omit = omit,
+ proxy: BrowserProxyConfigParam | Omit = omit,
proxy_id: Optional[str] | Omit = omit,
tags: Optional[TagsParam] | Omit = omit,
telemetry: Optional[browser_update_params.Telemetry] | Omit = omit,
@@ -934,7 +971,7 @@ async def update(
Args:
disable_default_proxy: If true, stealth browsers connect directly instead of using the default stealth
- proxy.
+ proxy. Deprecated in favor of proxy.mode.
name: Human-readable name for the browser session. Omit to leave unchanged, set to an
empty string to clear the name. When set, must be unique among active sessions
@@ -943,8 +980,15 @@ async def update(
profile: Profile to load into the browser session. Only allowed if the session does not
already have a profile loaded.
+ proxy: Proxy configuration to apply. Omit to leave the current configuration unchanged.
+ Cannot be combined with proxy_id or disable_default_proxy. Set mode to direct to
+ switch to direct egress regardless of stealth. Set mode to default to restore
+ the browser default after using a selected proxy: Kernel's default stealth proxy
+ for a stealth browser, or direct egress for a non-stealth browser. Updating
+ proxy does not change stealth or CAPTCHA solver behavior.
+
proxy_id: ID of the proxy to use. Omit to leave unchanged, set to empty string to remove
- proxy.
+ proxy. Deprecated in favor of proxy.
tags: User-defined key-value tags for the browser session. Omit to leave unchanged.
Provide a map to replace the entire tag set (full replace, not a merge). Set to
@@ -975,6 +1019,7 @@ async def update(
"disable_default_proxy": disable_default_proxy,
"name": name,
"profile": profile,
+ "proxy": proxy,
"proxy_id": proxy_id,
"tags": tags,
"telemetry": telemetry,
diff --git a/src/kernel/types/__init__.py b/src/kernel/types/__init__.py
index ab58edbc..e86ac9c4 100644
--- a/src/kernel/types/__init__.py
+++ b/src/kernel/types/__init__.py
@@ -22,6 +22,7 @@
from .credential import Credential as Credential
from .tags_param import TagsParam as TagsParam
from .browser_pool import BrowserPool as BrowserPool
+from .browser_proxy import BrowserProxy as BrowserProxy
from .browser_usage import BrowserUsage as BrowserUsage
from .app_list_params import AppListParams as AppListParams
from .audit_log_entry import AuditLogEntry as AuditLogEntry
@@ -29,6 +30,7 @@
from .browser_pool_ref import BrowserPoolRef as BrowserPoolRef
from .app_list_response import AppListResponse as AppListResponse
from .proxy_list_params import ProxyListParams as ProxyListParams
+from .browser_proxy_mode import BrowserProxyMode as BrowserProxyMode
from .proxy_check_params import ProxyCheckParams as ProxyCheckParams
from .api_key_list_params import APIKeyListParams as APIKeyListParams
from .browser_curl_params import BrowserCurlParams as BrowserCurlParams
@@ -39,6 +41,7 @@
from .proxy_create_params import ProxyCreateParams as ProxyCreateParams
from .proxy_list_response import ProxyListResponse as ProxyListResponse
from .proxy_update_params import ProxyUpdateParams as ProxyUpdateParams
+from .browser_proxy_config import BrowserProxyConfig as BrowserProxyConfig
from .proxy_check_response import ProxyCheckResponse as ProxyCheckResponse
from .api_key_create_params import APIKeyCreateParams as APIKeyCreateParams
from .api_key_rotate_params import APIKeyRotateParams as APIKeyRotateParams
@@ -85,6 +88,7 @@
from .browser_pool_create_params import BrowserPoolCreateParams as BrowserPoolCreateParams
from .browser_pool_delete_params import BrowserPoolDeleteParams as BrowserPoolDeleteParams
from .browser_pool_update_params import BrowserPoolUpdateParams as BrowserPoolUpdateParams
+from .browser_proxy_config_param import BrowserProxyConfigParam as BrowserProxyConfigParam
from .deployment_create_response import DeploymentCreateResponse as DeploymentCreateResponse
from .deployment_follow_response import DeploymentFollowResponse as DeploymentFollowResponse
from .invocation_create_response import InvocationCreateResponse as InvocationCreateResponse
diff --git a/src/kernel/types/auth/__init__.py b/src/kernel/types/auth/__init__.py
index 2d0cf1b0..ced593b0 100644
--- a/src/kernel/types/auth/__init__.py
+++ b/src/kernel/types/auth/__init__.py
@@ -13,4 +13,6 @@
from .connection_update_params import ConnectionUpdateParams as ConnectionUpdateParams
from .connection_follow_response import ConnectionFollowResponse as ConnectionFollowResponse
from .connection_timeline_params import ConnectionTimelineParams as ConnectionTimelineParams
+from .managed_auth_browser_config import ManagedAuthBrowserConfig as ManagedAuthBrowserConfig
from .managed_auth_timeline_event import ManagedAuthTimelineEvent as ManagedAuthTimelineEvent
+from .managed_auth_browser_config_param import ManagedAuthBrowserConfigParam as ManagedAuthBrowserConfigParam
diff --git a/src/kernel/types/auth/connection_create_params.py b/src/kernel/types/auth/connection_create_params.py
index 407857be..23d53cb2 100644
--- a/src/kernel/types/auth/connection_create_params.py
+++ b/src/kernel/types/auth/connection_create_params.py
@@ -6,6 +6,7 @@
from typing_extensions import Required, TypedDict
from ..._types import SequenceNotStr
+from .managed_auth_browser_config_param import ManagedAuthBrowserConfigParam
from ..browsers.browser_telemetry_categories_config_param import BrowserTelemetryCategoriesConfigParam
__all__ = [
@@ -62,11 +63,16 @@ class ConnectionCreateParams(TypedDict, total=False):
re-auth. Defaults to true.
"""
- browser_telemetry: Optional[BrowserTelemetry]
+ browser: ManagedAuthBrowserConfigParam
+ """
+ Default browser configuration for login, reauthentication, and health-check
+ sessions.
"""
- Browser telemetry configuration used by this connection's browser sessions by
- default. Uses the exact create-browser configuration. Can be overridden
- per-login.
+
+ browser_telemetry: Optional[BrowserTelemetry]
+ """Deprecated.
+
+ Use browser.telemetry. Retained during migration for existing clients.
"""
credential: Credential
@@ -98,13 +104,7 @@ class ConnectionCreateParams(TypedDict, total=False):
"""Optional login page URL to skip discovery"""
proxy: Proxy
- """Proxy selection.
-
- Provide either id or name. The proxy must be in the same project as the resource
- referencing it. When selecting by name, the name must match exactly one active
- proxy in the project. Ambiguous names return a 400; use id for stable
- references.
- """
+ """Deprecated. Use browser.proxy. Retained during migration for existing clients."""
record_session: bool
"""Whether to record browser sessions for this connection by default.
@@ -164,8 +164,9 @@ class BrowserTelemetryExport(TypedDict, total=False):
class BrowserTelemetry(TypedDict, total=False):
- """
- Browser telemetry configuration used by this connection's browser sessions by default. Uses the exact create-browser configuration. Can be overridden per-login.
+ """Deprecated.
+
+ Use browser.telemetry. Retained during migration for existing clients.
"""
browser: BrowserTelemetryCategoriesConfigParam
@@ -225,11 +226,7 @@ class Credential(TypedDict, total=False):
class Proxy(TypedDict, total=False):
- """Proxy selection.
-
- Provide either id or name. The proxy must be in the same project as the resource referencing it.
- When selecting by name, the name must match exactly one active proxy in the project. Ambiguous names return a 400; use id for stable references.
- """
+ """Deprecated. Use browser.proxy. Retained during migration for existing clients."""
id: str
"""Proxy ID"""
diff --git a/src/kernel/types/auth/connection_login_params.py b/src/kernel/types/auth/connection_login_params.py
index 117d3c7d..c26b020f 100644
--- a/src/kernel/types/auth/connection_login_params.py
+++ b/src/kernel/types/auth/connection_login_params.py
@@ -5,6 +5,7 @@
from typing import Optional
from typing_extensions import TypedDict
+from .managed_auth_browser_config_param import ManagedAuthBrowserConfigParam
from ..browsers.browser_telemetry_categories_config_param import BrowserTelemetryCategoriesConfigParam
__all__ = [
@@ -18,23 +19,21 @@
class ConnectionLoginParams(TypedDict, total=False):
- browser_telemetry: Optional[BrowserTelemetry]
- """Override the connection's default browser telemetry configuration for this
- login.
+ browser: ManagedAuthBrowserConfigParam
+ """Browser configuration override for this login.
- When omitted, the connection's browser_telemetry default is used. Uses the exact
- create-browser configuration.
+ Omitted properties inherit the connection defaults.
"""
- proxy: Proxy
- """Proxy selection.
+ browser_telemetry: Optional[BrowserTelemetry]
+ """Deprecated.
- Provide either id or name. The proxy must be in the same project as the resource
- referencing it. When selecting by name, the name must match exactly one active
- proxy in the project. Ambiguous names return a 400; use id for stable
- references.
+ Use browser.telemetry. Retained during migration for existing clients.
"""
+ proxy: Proxy
+ """Deprecated. Use browser.proxy. Retained during migration for existing clients."""
+
record_session: bool
"""Override the connection's default for recording this login's browser session.
@@ -87,9 +86,9 @@ class BrowserTelemetryExport(TypedDict, total=False):
class BrowserTelemetry(TypedDict, total=False):
- """Override the connection's default browser telemetry configuration for this login.
+ """Deprecated.
- When omitted, the connection's browser_telemetry default is used. Uses the exact create-browser configuration.
+ Use browser.telemetry. Retained during migration for existing clients.
"""
browser: BrowserTelemetryCategoriesConfigParam
@@ -127,11 +126,7 @@ class BrowserTelemetry(TypedDict, total=False):
class Proxy(TypedDict, total=False):
- """Proxy selection.
-
- Provide either id or name. The proxy must be in the same project as the resource referencing it.
- When selecting by name, the name must match exactly one active proxy in the project. Ambiguous names return a 400; use id for stable references.
- """
+ """Deprecated. Use browser.proxy. Retained during migration for existing clients."""
id: str
"""Proxy ID"""
diff --git a/src/kernel/types/auth/connection_update_params.py b/src/kernel/types/auth/connection_update_params.py
index f92a40c7..ad3fbe44 100644
--- a/src/kernel/types/auth/connection_update_params.py
+++ b/src/kernel/types/auth/connection_update_params.py
@@ -6,6 +6,7 @@
from typing_extensions import TypedDict
from ..._types import SequenceNotStr
+from .managed_auth_browser_config_param import ManagedAuthBrowserConfigParam
from ..browsers.browser_telemetry_categories_config_param import BrowserTelemetryCategoriesConfigParam
__all__ = [
@@ -35,11 +36,16 @@ class ConnectionUpdateParams(TypedDict, total=False):
re-auth.
"""
- browser_telemetry: Optional[BrowserTelemetry]
+ browser: ManagedAuthBrowserConfigParam
+ """
+ Browser configuration updates for future login, reauthentication, and
+ health-check sessions. Omitted properties remain unchanged.
"""
- Browser telemetry configuration used by future browser sessions for this
- connection. Uses the exact create-browser configuration. Set enabled to false to
- disable telemetry.
+
+ browser_telemetry: Optional[BrowserTelemetry]
+ """Deprecated.
+
+ Use browser.telemetry. Retained during migration for existing clients.
"""
credential: Credential
@@ -65,13 +71,7 @@ class ConnectionUpdateParams(TypedDict, total=False):
"""Login page URL. Set to empty string to clear."""
proxy: Proxy
- """Proxy selection.
-
- Provide either id or name. The proxy must be in the same project as the resource
- referencing it. When selecting by name, the name must match exactly one active
- proxy in the project. Ambiguous names return a 400; use id for stable
- references.
- """
+ """Deprecated. Use browser.proxy. Retained during migration for existing clients."""
record_session: bool
"""Whether to record browser sessions for this connection by default"""
@@ -125,8 +125,9 @@ class BrowserTelemetryExport(TypedDict, total=False):
class BrowserTelemetry(TypedDict, total=False):
- """
- Browser telemetry configuration used by future browser sessions for this connection. Uses the exact create-browser configuration. Set enabled to false to disable telemetry.
+ """Deprecated.
+
+ Use browser.telemetry. Retained during migration for existing clients.
"""
browser: BrowserTelemetryCategoriesConfigParam
@@ -186,11 +187,7 @@ class Credential(TypedDict, total=False):
class Proxy(TypedDict, total=False):
- """Proxy selection.
-
- Provide either id or name. The proxy must be in the same project as the resource referencing it.
- When selecting by name, the name must match exactly one active proxy in the project. Ambiguous names return a 400; use id for stable references.
- """
+ """Deprecated. Use browser.proxy. Retained during migration for existing clients."""
id: str
"""Proxy ID"""
diff --git a/src/kernel/types/auth/managed_auth.py b/src/kernel/types/auth/managed_auth.py
index e3bf7b9c..f1bb2198 100644
--- a/src/kernel/types/auth/managed_auth.py
+++ b/src/kernel/types/auth/managed_auth.py
@@ -5,6 +5,7 @@
from typing_extensions import Literal
from ..._models import BaseModel
+from .managed_auth_browser_config import ManagedAuthBrowserConfig
from ..browsers.browser_telemetry_categories_config import BrowserTelemetryCategoriesConfig
__all__ = [
@@ -68,8 +69,9 @@ class BrowserTelemetryExport(BaseModel):
class BrowserTelemetry(BaseModel):
- """
- Browser telemetry configuration used by this connection's browser sessions by default. The exact create-browser configuration is preserved and can be overridden per-login.
+ """Deprecated.
+
+ Use browser.telemetry. Retained during migration for existing clients.
"""
browser: Optional[BrowserTelemetryCategoriesConfig] = None
@@ -324,6 +326,12 @@ class ManagedAuth(BaseModel):
re-auth.
"""
+ browser: Optional[ManagedAuthBrowserConfig] = None
+ """
+ Default browser configuration for login, reauthentication, and health-check
+ sessions.
+ """
+
browser_session_id: Optional[str] = None
"""
ID of the underlying browser session driving the current flow (present when flow
@@ -332,10 +340,9 @@ class ManagedAuth(BaseModel):
"""
browser_telemetry: Optional[BrowserTelemetry] = None
- """
- Browser telemetry configuration used by this connection's browser sessions by
- default. The exact create-browser configuration is preserved and can be
- overridden per-login.
+ """Deprecated.
+
+ Use browser.telemetry. Retained during migration for existing clients.
"""
can_reauth: Optional[bool] = None
@@ -513,7 +520,10 @@ class ManagedAuth(BaseModel):
"""URL where the browser landed after successful login"""
proxy_id: Optional[str] = None
- """ID of the proxy associated with this connection, if any."""
+ """Deprecated.
+
+ Read browser.proxy instead. Retained during migration for existing clients.
+ """
sign_in_options: Optional[List[SignInOption]] = None
"""
diff --git a/src/kernel/types/auth/managed_auth_browser_config.py b/src/kernel/types/auth/managed_auth_browser_config.py
new file mode 100644
index 00000000..a4d63de7
--- /dev/null
+++ b/src/kernel/types/auth/managed_auth_browser_config.py
@@ -0,0 +1,118 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from typing import Optional
+
+from ..._models import BaseModel
+from ..browser_proxy_config import BrowserProxyConfig
+from ..browsers.browser_telemetry_categories_config import BrowserTelemetryCategoriesConfig
+
+__all__ = [
+ "ManagedAuthBrowserConfig",
+ "Telemetry",
+ "TelemetryExport",
+ "TelemetryExportOtlp",
+ "TelemetryExportOtlpDestination",
+]
+
+
+class TelemetryExportOtlpDestination(BaseModel):
+ """OTLP destination to export this session's captured telemetry to.
+
+ Provide either id or name. Requires telemetry capture to be enabled.
+ """
+
+ id: Optional[str] = None
+ """OTLP destination ID"""
+
+ name: Optional[str] = None
+ """OTLP destination name"""
+
+
+class TelemetryExportOtlp(BaseModel):
+ """
+ Export captured telemetry over OTLP to one of the org's configured destinations.
+ """
+
+ destination: Optional[TelemetryExportOtlpDestination] = None
+ """OTLP destination to export this session's captured telemetry to.
+
+ Provide either id or name. Requires telemetry capture to be enabled.
+ """
+
+ enabled: Optional[bool] = None
+ """Whether to export captured telemetry over OTLP.
+
+ Setting destination implies enabled=true, so this only needs to be set
+ explicitly to disable export (enabled=false with a destination is rejected).
+ """
+
+
+class TelemetryExport(BaseModel):
+ """Where to export this session's captured telemetry.
+
+ Omit to capture without exporting.
+ """
+
+ otlp: Optional[TelemetryExportOtlp] = None
+ """
+ Export captured telemetry over OTLP to one of the org's configured destinations.
+ """
+
+
+class Telemetry(BaseModel):
+ """Browser telemetry configuration using the same semantics as browser create."""
+
+ browser: Optional[BrowserTelemetryCategoriesConfig] = None
+ """Per-category capture flags.
+
+ The operational categories (control, connection, system, captcha) are captured
+ whenever telemetry is enabled; set one to enabled=false to opt out. The CDP
+ categories (console, network, page, interaction) and screenshot are off by
+ default; set enabled=true to opt in. On create, provided categories layer onto
+ the default set. On update, provided categories merge onto the session's current
+ config; when no telemetry is active this falls back to the default set (matching
+ create). If browser is omitted or empty, the default set is used. A browser
+ config that disables every category stops capture on update and starts no
+ capture on create.
+ """
+
+ enabled: Optional[bool] = None
+ """Request shortcut for browser telemetry capture.
+
+ True enables capture; with no browser category settings it captures the default
+ set (control, connection, system, captcha), and any browser category settings
+ are layered onto that default set. On update, enabled=true resolves the config
+ fresh from the default set plus any provided categories, replacing the session's
+ current selection rather than merging onto it; omit enabled to merge categories
+ onto the current selection instead. False stops capture on update and starts no
+ capture on create. enabled=false cannot be combined with browser category
+ settings.
+ """
+
+ export: Optional[TelemetryExport] = None
+ """Where to export this session's captured telemetry.
+
+ Omit to capture without exporting.
+ """
+
+
+class ManagedAuthBrowserConfig(BaseModel):
+ """
+ Browser configuration applied to browser sessions created for a managed auth connection. Managed auth controls the profile, headless mode, timeout, start URL, kiosk mode, and viewport.
+ """
+
+ proxy: Optional[BrowserProxyConfig] = None
+ """Proxy configuration for managed auth browser sessions.
+
+ Omit on create to derive the default from stealth, or on update and login to
+ preserve or inherit the connection default.
+ """
+
+ stealth: Optional[bool] = None
+ """Whether managed auth browser sessions use stealth mode.
+
+ Defaults to true when omitted.
+ """
+
+ telemetry: Optional[Telemetry] = None
+ """Browser telemetry configuration using the same semantics as browser create."""
diff --git a/src/kernel/types/auth/managed_auth_browser_config_param.py b/src/kernel/types/auth/managed_auth_browser_config_param.py
new file mode 100644
index 00000000..7f8650e0
--- /dev/null
+++ b/src/kernel/types/auth/managed_auth_browser_config_param.py
@@ -0,0 +1,120 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from __future__ import annotations
+
+from typing import Optional
+from typing_extensions import TypedDict
+
+from ..browser_proxy_config_param import BrowserProxyConfigParam
+from ..browsers.browser_telemetry_categories_config_param import BrowserTelemetryCategoriesConfigParam
+
+__all__ = [
+ "ManagedAuthBrowserConfigParam",
+ "Telemetry",
+ "TelemetryExport",
+ "TelemetryExportOtlp",
+ "TelemetryExportOtlpDestination",
+]
+
+
+class TelemetryExportOtlpDestination(TypedDict, total=False):
+ """OTLP destination to export this session's captured telemetry to.
+
+ Provide either id or name. Requires telemetry capture to be enabled.
+ """
+
+ id: str
+ """OTLP destination ID"""
+
+ name: str
+ """OTLP destination name"""
+
+
+class TelemetryExportOtlp(TypedDict, total=False):
+ """
+ Export captured telemetry over OTLP to one of the org's configured destinations.
+ """
+
+ destination: TelemetryExportOtlpDestination
+ """OTLP destination to export this session's captured telemetry to.
+
+ Provide either id or name. Requires telemetry capture to be enabled.
+ """
+
+ enabled: bool
+ """Whether to export captured telemetry over OTLP.
+
+ Setting destination implies enabled=true, so this only needs to be set
+ explicitly to disable export (enabled=false with a destination is rejected).
+ """
+
+
+class TelemetryExport(TypedDict, total=False):
+ """Where to export this session's captured telemetry.
+
+ Omit to capture without exporting.
+ """
+
+ otlp: TelemetryExportOtlp
+ """
+ Export captured telemetry over OTLP to one of the org's configured destinations.
+ """
+
+
+class Telemetry(TypedDict, total=False):
+ """Browser telemetry configuration using the same semantics as browser create."""
+
+ browser: BrowserTelemetryCategoriesConfigParam
+ """Per-category capture flags.
+
+ The operational categories (control, connection, system, captcha) are captured
+ whenever telemetry is enabled; set one to enabled=false to opt out. The CDP
+ categories (console, network, page, interaction) and screenshot are off by
+ default; set enabled=true to opt in. On create, provided categories layer onto
+ the default set. On update, provided categories merge onto the session's current
+ config; when no telemetry is active this falls back to the default set (matching
+ create). If browser is omitted or empty, the default set is used. A browser
+ config that disables every category stops capture on update and starts no
+ capture on create.
+ """
+
+ enabled: bool
+ """Request shortcut for browser telemetry capture.
+
+ True enables capture; with no browser category settings it captures the default
+ set (control, connection, system, captcha), and any browser category settings
+ are layered onto that default set. On update, enabled=true resolves the config
+ fresh from the default set plus any provided categories, replacing the session's
+ current selection rather than merging onto it; omit enabled to merge categories
+ onto the current selection instead. False stops capture on update and starts no
+ capture on create. enabled=false cannot be combined with browser category
+ settings.
+ """
+
+ export: TelemetryExport
+ """Where to export this session's captured telemetry.
+
+ Omit to capture without exporting.
+ """
+
+
+class ManagedAuthBrowserConfigParam(TypedDict, total=False):
+ """
+ Browser configuration applied to browser sessions created for a managed auth connection. Managed auth controls the profile, headless mode, timeout, start URL, kiosk mode, and viewport.
+ """
+
+ proxy: BrowserProxyConfigParam
+ """Proxy configuration for managed auth browser sessions.
+
+ Omit on create to derive the default from stealth, or on update and login to
+ preserve or inherit the connection default.
+ """
+
+ stealth: bool
+ """Whether managed auth browser sessions use stealth mode.
+
+ Defaults to true when omitted.
+ """
+
+ telemetry: Optional[Telemetry]
+ """Browser telemetry configuration using the same semantics as browser create."""
diff --git a/src/kernel/types/browser_create_params.py b/src/kernel/types/browser_create_params.py
index f09a29cf..262383c5 100644
--- a/src/kernel/types/browser_create_params.py
+++ b/src/kernel/types/browser_create_params.py
@@ -6,6 +6,7 @@
from typing_extensions import TypedDict
from .tags_param import TagsParam
+from .browser_proxy_config_param import BrowserProxyConfigParam
from .shared_params.browser_profile import BrowserProfile
from .shared_params.browser_viewport import BrowserViewport
from .shared_params.browser_extension import BrowserExtension
@@ -70,10 +71,23 @@ class BrowserCreateParams(TypedDict, total=False):
into the browser session. Profiles must be created beforehand.
"""
+ proxy: BrowserProxyConfigParam
+ """Proxy configuration for the browser session.
+
+ Cannot be combined with proxy_id. Omit to use the browser default: stealth
+ browsers use Kernel's default stealth proxy, while non-stealth browsers use
+ direct egress. Set mode to direct to force direct egress regardless of stealth.
+ Set mode to default to explicitly use the browser default: Kernel's default
+ stealth proxy when stealth=true, or direct egress when stealth=false. Select id
+ or name to use that proxy regardless of stealth. Proxy selection does not change
+ stealth or CAPTCHA solver behavior.
+ """
+
proxy_id: str
"""Optional proxy to associate to the browser session.
- Must reference a proxy in the same project as the browser session.
+ Must reference a proxy in the same project as the browser session. Deprecated in
+ favor of proxy.
"""
start_url: str
@@ -84,9 +98,12 @@ class BrowserCreateParams(TypedDict, total=False):
"""
stealth: bool
- """
- If true, launches the browser in stealth mode to reduce detection by anti-bot
- mechanisms.
+ """If true, launches the browser in stealth mode and enables the CAPTCHA solver.
+
+ Defaults to false. When proxy is omitted, stealth browsers use Kernel's default
+ stealth proxy and non-stealth browsers use direct egress. An explicit proxy
+ configuration changes only egress; it does not enable or disable stealth or the
+ CAPTCHA solver.
"""
tags: TagsParam
diff --git a/src/kernel/types/browser_create_response.py b/src/kernel/types/browser_create_response.py
index 0dbf867c..8618e7e5 100644
--- a/src/kernel/types/browser_create_response.py
+++ b/src/kernel/types/browser_create_response.py
@@ -6,6 +6,7 @@
from .tags import Tags
from .profile import Profile
from .._models import BaseModel
+from .browser_proxy import BrowserProxy
from .browser_usage import BrowserUsage
from .browser_pool_ref import BrowserPoolRef
from .shared.browser_viewport import BrowserViewport
@@ -79,8 +80,14 @@ class BrowserCreateResponse(BaseModel):
when the session ends. Omitted when no profile is attached.
"""
+ proxy: Optional[BrowserProxy] = None
+ """Resolved proxy configuration for this browser session."""
+
proxy_id: Optional[str] = None
- """ID of the proxy associated with this browser session, if any."""
+ """ID of the proxy associated with this browser session, if any.
+
+ Deprecated in favor of proxy.
+ """
start_url: Optional[str] = None
"""URL the session was asked to navigate to on creation, if any.
diff --git a/src/kernel/types/browser_list_response.py b/src/kernel/types/browser_list_response.py
index f3215ed5..8f4917a8 100644
--- a/src/kernel/types/browser_list_response.py
+++ b/src/kernel/types/browser_list_response.py
@@ -6,6 +6,7 @@
from .tags import Tags
from .profile import Profile
from .._models import BaseModel
+from .browser_proxy import BrowserProxy
from .browser_usage import BrowserUsage
from .browser_pool_ref import BrowserPoolRef
from .shared.browser_viewport import BrowserViewport
@@ -79,8 +80,14 @@ class BrowserListResponse(BaseModel):
when the session ends. Omitted when no profile is attached.
"""
+ proxy: Optional[BrowserProxy] = None
+ """Resolved proxy configuration for this browser session."""
+
proxy_id: Optional[str] = None
- """ID of the proxy associated with this browser session, if any."""
+ """ID of the proxy associated with this browser session, if any.
+
+ Deprecated in favor of proxy.
+ """
start_url: Optional[str] = None
"""URL the session was asked to navigate to on creation, if any.
diff --git a/src/kernel/types/browser_pool_acquire_response.py b/src/kernel/types/browser_pool_acquire_response.py
index 1dabc492..7d849958 100644
--- a/src/kernel/types/browser_pool_acquire_response.py
+++ b/src/kernel/types/browser_pool_acquire_response.py
@@ -6,6 +6,7 @@
from .tags import Tags
from .profile import Profile
from .._models import BaseModel
+from .browser_proxy import BrowserProxy
from .browser_usage import BrowserUsage
from .browser_pool_ref import BrowserPoolRef
from .shared.browser_viewport import BrowserViewport
@@ -79,8 +80,14 @@ class BrowserPoolAcquireResponse(BaseModel):
when the session ends. Omitted when no profile is attached.
"""
+ proxy: Optional[BrowserProxy] = None
+ """Resolved proxy configuration for this browser session."""
+
proxy_id: Optional[str] = None
- """ID of the proxy associated with this browser session, if any."""
+ """ID of the proxy associated with this browser session, if any.
+
+ Deprecated in favor of proxy.
+ """
start_url: Optional[str] = None
"""URL the session was asked to navigate to on creation, if any.
diff --git a/src/kernel/types/browser_proxy.py b/src/kernel/types/browser_proxy.py
new file mode 100644
index 00000000..20078429
--- /dev/null
+++ b/src/kernel/types/browser_proxy.py
@@ -0,0 +1,30 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from typing import Optional
+
+from .._models import BaseModel
+from .browser_proxy_mode import BrowserProxyMode
+
+__all__ = ["BrowserProxy"]
+
+
+class BrowserProxy(BaseModel):
+ """Resolved proxy configuration for a browser session.
+
+ Selected proxies are returned by stable ID.
+ """
+
+ id: Optional[str] = None
+ """Selected proxy ID."""
+
+ mode: Optional[BrowserProxyMode] = None
+ """Proxy egress mode.
+
+ direct forces no proxy regardless of stealth. default uses the browser's
+ stealth-derived default: Kernel's default stealth proxy when stealth=true, or
+ direct egress when stealth=false. default is primarily useful on browser update
+ to restore the browser default after selected-proxy egress.
+ """
+
+ name: Optional[str] = None
+ """Selected proxy name."""
diff --git a/src/kernel/types/browser_proxy_config.py b/src/kernel/types/browser_proxy_config.py
new file mode 100644
index 00000000..6db5d5ec
--- /dev/null
+++ b/src/kernel/types/browser_proxy_config.py
@@ -0,0 +1,34 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from typing import Optional
+
+from .._models import BaseModel
+from .browser_proxy_mode import BrowserProxyMode
+
+__all__ = ["BrowserProxyConfig"]
+
+
+class BrowserProxyConfig(BaseModel):
+ """Browser proxy configuration.
+
+ Provide exactly one of mode, id, or name; an empty object is invalid.
+ Set mode to direct for no proxy regardless of stealth. Set mode to default to use the browser's stealth-derived default: Kernel's default stealth proxy when stealth=true, or direct egress when stealth=false.
+ Select id or name to use that proxy regardless of stealth. The selected proxy must be in the same project as the browser. Names must match exactly one active proxy; use id for stable references.
+ Proxy configuration changes only egress and does not change stealth or CAPTCHA solver behavior. A stealth browser using mode=direct still runs in stealth mode with the CAPTCHA solver enabled.
+ When proxy is omitted on browser creation, stealth browsers use Kernel's default stealth proxy and non-stealth browsers use direct egress. When omitted on update, the current configuration is unchanged.
+ """
+
+ id: Optional[str] = None
+ """Proxy ID."""
+
+ mode: Optional[BrowserProxyMode] = None
+ """Proxy egress mode.
+
+ direct forces no proxy regardless of stealth. default uses the browser's
+ stealth-derived default: Kernel's default stealth proxy when stealth=true, or
+ direct egress when stealth=false. default is primarily useful on browser update
+ to restore the browser default after selected-proxy egress.
+ """
+
+ name: Optional[str] = None
+ """Proxy name. Must match exactly one active proxy in the project."""
diff --git a/src/kernel/types/browser_proxy_config_param.py b/src/kernel/types/browser_proxy_config_param.py
new file mode 100644
index 00000000..0fc83157
--- /dev/null
+++ b/src/kernel/types/browser_proxy_config_param.py
@@ -0,0 +1,35 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from __future__ import annotations
+
+from typing_extensions import TypedDict
+
+from .browser_proxy_mode import BrowserProxyMode
+
+__all__ = ["BrowserProxyConfigParam"]
+
+
+class BrowserProxyConfigParam(TypedDict, total=False):
+ """Browser proxy configuration.
+
+ Provide exactly one of mode, id, or name; an empty object is invalid.
+ Set mode to direct for no proxy regardless of stealth. Set mode to default to use the browser's stealth-derived default: Kernel's default stealth proxy when stealth=true, or direct egress when stealth=false.
+ Select id or name to use that proxy regardless of stealth. The selected proxy must be in the same project as the browser. Names must match exactly one active proxy; use id for stable references.
+ Proxy configuration changes only egress and does not change stealth or CAPTCHA solver behavior. A stealth browser using mode=direct still runs in stealth mode with the CAPTCHA solver enabled.
+ When proxy is omitted on browser creation, stealth browsers use Kernel's default stealth proxy and non-stealth browsers use direct egress. When omitted on update, the current configuration is unchanged.
+ """
+
+ id: str
+ """Proxy ID."""
+
+ mode: BrowserProxyMode
+ """Proxy egress mode.
+
+ direct forces no proxy regardless of stealth. default uses the browser's
+ stealth-derived default: Kernel's default stealth proxy when stealth=true, or
+ direct egress when stealth=false. default is primarily useful on browser update
+ to restore the browser default after selected-proxy egress.
+ """
+
+ name: str
+ """Proxy name. Must match exactly one active proxy in the project."""
diff --git a/src/kernel/types/browser_proxy_mode.py b/src/kernel/types/browser_proxy_mode.py
new file mode 100644
index 00000000..c8b51c1b
--- /dev/null
+++ b/src/kernel/types/browser_proxy_mode.py
@@ -0,0 +1,7 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from typing_extensions import Literal, TypeAlias
+
+__all__ = ["BrowserProxyMode"]
+
+BrowserProxyMode: TypeAlias = Literal["direct", "default"]
diff --git a/src/kernel/types/browser_retrieve_response.py b/src/kernel/types/browser_retrieve_response.py
index 5563ab98..3afc6082 100644
--- a/src/kernel/types/browser_retrieve_response.py
+++ b/src/kernel/types/browser_retrieve_response.py
@@ -6,6 +6,7 @@
from .tags import Tags
from .profile import Profile
from .._models import BaseModel
+from .browser_proxy import BrowserProxy
from .browser_usage import BrowserUsage
from .browser_pool_ref import BrowserPoolRef
from .shared.browser_viewport import BrowserViewport
@@ -79,8 +80,14 @@ class BrowserRetrieveResponse(BaseModel):
when the session ends. Omitted when no profile is attached.
"""
+ proxy: Optional[BrowserProxy] = None
+ """Resolved proxy configuration for this browser session."""
+
proxy_id: Optional[str] = None
- """ID of the proxy associated with this browser session, if any."""
+ """ID of the proxy associated with this browser session, if any.
+
+ Deprecated in favor of proxy.
+ """
start_url: Optional[str] = None
"""URL the session was asked to navigate to on creation, if any.
diff --git a/src/kernel/types/browser_update_params.py b/src/kernel/types/browser_update_params.py
index 01b19c8a..a9ea675c 100644
--- a/src/kernel/types/browser_update_params.py
+++ b/src/kernel/types/browser_update_params.py
@@ -6,6 +6,7 @@
from typing_extensions import TypedDict
from .tags_param import TagsParam
+from .browser_proxy_config_param import BrowserProxyConfigParam
from .shared_params.browser_profile import BrowserProfile
from .shared_params.browser_viewport import BrowserViewport
from .browsers.browser_telemetry_categories_config_param import BrowserTelemetryCategoriesConfigParam
@@ -24,7 +25,7 @@ class BrowserUpdateParams(TypedDict, total=False):
disable_default_proxy: bool
"""
If true, stealth browsers connect directly instead of using the default stealth
- proxy.
+ proxy. Deprecated in favor of proxy.mode.
"""
name: Optional[str]
@@ -40,10 +41,22 @@ class BrowserUpdateParams(TypedDict, total=False):
Only allowed if the session does not already have a profile loaded.
"""
+ proxy: BrowserProxyConfigParam
+ """Proxy configuration to apply.
+
+ Omit to leave the current configuration unchanged. Cannot be combined with
+ proxy_id or disable_default_proxy. Set mode to direct to switch to direct egress
+ regardless of stealth. Set mode to default to restore the browser default after
+ using a selected proxy: Kernel's default stealth proxy for a stealth browser, or
+ direct egress for a non-stealth browser. Updating proxy does not change stealth
+ or CAPTCHA solver behavior.
+ """
+
proxy_id: Optional[str]
"""ID of the proxy to use.
- Omit to leave unchanged, set to empty string to remove proxy.
+ Omit to leave unchanged, set to empty string to remove proxy. Deprecated in
+ favor of proxy.
"""
tags: Optional[TagsParam]
diff --git a/src/kernel/types/browser_update_response.py b/src/kernel/types/browser_update_response.py
index e4602bd5..d60fe900 100644
--- a/src/kernel/types/browser_update_response.py
+++ b/src/kernel/types/browser_update_response.py
@@ -6,6 +6,7 @@
from .tags import Tags
from .profile import Profile
from .._models import BaseModel
+from .browser_proxy import BrowserProxy
from .browser_usage import BrowserUsage
from .browser_pool_ref import BrowserPoolRef
from .shared.browser_viewport import BrowserViewport
@@ -79,8 +80,14 @@ class BrowserUpdateResponse(BaseModel):
when the session ends. Omitted when no profile is attached.
"""
+ proxy: Optional[BrowserProxy] = None
+ """Resolved proxy configuration for this browser session."""
+
proxy_id: Optional[str] = None
- """ID of the proxy associated with this browser session, if any."""
+ """ID of the proxy associated with this browser session, if any.
+
+ Deprecated in favor of proxy.
+ """
start_url: Optional[str] = None
"""URL the session was asked to navigate to on creation, if any.
diff --git a/src/kernel/types/invocation_list_browsers_response.py b/src/kernel/types/invocation_list_browsers_response.py
index 9abb0fa6..5cfe7e89 100644
--- a/src/kernel/types/invocation_list_browsers_response.py
+++ b/src/kernel/types/invocation_list_browsers_response.py
@@ -6,6 +6,7 @@
from .tags import Tags
from .profile import Profile
from .._models import BaseModel
+from .browser_proxy import BrowserProxy
from .browser_usage import BrowserUsage
from .browser_pool_ref import BrowserPoolRef
from .shared.browser_viewport import BrowserViewport
@@ -79,8 +80,14 @@ class Browser(BaseModel):
when the session ends. Omitted when no profile is attached.
"""
+ proxy: Optional[BrowserProxy] = None
+ """Resolved proxy configuration for this browser session."""
+
proxy_id: Optional[str] = None
- """ID of the proxy associated with this browser session, if any."""
+ """ID of the proxy associated with this browser session, if any.
+
+ Deprecated in favor of proxy.
+ """
start_url: Optional[str] = None
"""URL the session was asked to navigate to on creation, if any.
diff --git a/tests/api_resources/auth/test_connections.py b/tests/api_resources/auth/test_connections.py
index 321e84e4..f026a2cd 100644
--- a/tests/api_resources/auth/test_connections.py
+++ b/tests/api_resources/auth/test_connections.py
@@ -40,6 +40,37 @@ def test_method_create_with_all_params(self, client: Kernel) -> None:
profile_name="user-123",
allowed_domains=["login.netflix.com", "auth.netflix.com"],
auto_reauth=True,
+ browser={
+ "proxy": {
+ "id": "x",
+ "mode": "direct",
+ "name": "x",
+ },
+ "stealth": False,
+ "telemetry": {
+ "browser": {
+ "captcha": {"enabled": True},
+ "connection": {"enabled": True},
+ "console": {"enabled": True},
+ "control": {"enabled": True},
+ "interaction": {"enabled": True},
+ "network": {"enabled": True},
+ "page": {"enabled": True},
+ "screenshot": {"enabled": True},
+ "system": {"enabled": True},
+ },
+ "enabled": True,
+ "export": {
+ "otlp": {
+ "destination": {
+ "id": "id",
+ "name": "name",
+ },
+ "enabled": True,
+ }
+ },
+ },
+ },
browser_telemetry={
"browser": {
"captcha": {"enabled": True},
@@ -166,6 +197,37 @@ def test_method_update_with_all_params(self, client: Kernel) -> None:
id="id",
allowed_domains=["login.netflix.com", "auth.netflix.com"],
auto_reauth=True,
+ browser={
+ "proxy": {
+ "id": "x",
+ "mode": "direct",
+ "name": "x",
+ },
+ "stealth": False,
+ "telemetry": {
+ "browser": {
+ "captcha": {"enabled": True},
+ "connection": {"enabled": True},
+ "console": {"enabled": True},
+ "control": {"enabled": True},
+ "interaction": {"enabled": True},
+ "network": {"enabled": True},
+ "page": {"enabled": True},
+ "screenshot": {"enabled": True},
+ "system": {"enabled": True},
+ },
+ "enabled": True,
+ "export": {
+ "otlp": {
+ "destination": {
+ "id": "id",
+ "name": "name",
+ },
+ "enabled": True,
+ }
+ },
+ },
+ },
browser_telemetry={
"browser": {
"captcha": {"enabled": True},
@@ -377,6 +439,37 @@ def test_method_login(self, client: Kernel) -> None:
def test_method_login_with_all_params(self, client: Kernel) -> None:
connection = client.auth.connections.login(
id="id",
+ browser={
+ "proxy": {
+ "id": "x",
+ "mode": "direct",
+ "name": "x",
+ },
+ "stealth": False,
+ "telemetry": {
+ "browser": {
+ "captcha": {"enabled": True},
+ "connection": {"enabled": True},
+ "console": {"enabled": True},
+ "control": {"enabled": True},
+ "interaction": {"enabled": True},
+ "network": {"enabled": True},
+ "page": {"enabled": True},
+ "screenshot": {"enabled": True},
+ "system": {"enabled": True},
+ },
+ "enabled": True,
+ "export": {
+ "otlp": {
+ "destination": {
+ "id": "id",
+ "name": "name",
+ },
+ "enabled": True,
+ }
+ },
+ },
+ },
browser_telemetry={
"browser": {
"captcha": {"enabled": True},
@@ -581,6 +674,37 @@ async def test_method_create_with_all_params(self, async_client: AsyncKernel) ->
profile_name="user-123",
allowed_domains=["login.netflix.com", "auth.netflix.com"],
auto_reauth=True,
+ browser={
+ "proxy": {
+ "id": "x",
+ "mode": "direct",
+ "name": "x",
+ },
+ "stealth": False,
+ "telemetry": {
+ "browser": {
+ "captcha": {"enabled": True},
+ "connection": {"enabled": True},
+ "console": {"enabled": True},
+ "control": {"enabled": True},
+ "interaction": {"enabled": True},
+ "network": {"enabled": True},
+ "page": {"enabled": True},
+ "screenshot": {"enabled": True},
+ "system": {"enabled": True},
+ },
+ "enabled": True,
+ "export": {
+ "otlp": {
+ "destination": {
+ "id": "id",
+ "name": "name",
+ },
+ "enabled": True,
+ }
+ },
+ },
+ },
browser_telemetry={
"browser": {
"captcha": {"enabled": True},
@@ -707,6 +831,37 @@ async def test_method_update_with_all_params(self, async_client: AsyncKernel) ->
id="id",
allowed_domains=["login.netflix.com", "auth.netflix.com"],
auto_reauth=True,
+ browser={
+ "proxy": {
+ "id": "x",
+ "mode": "direct",
+ "name": "x",
+ },
+ "stealth": False,
+ "telemetry": {
+ "browser": {
+ "captcha": {"enabled": True},
+ "connection": {"enabled": True},
+ "console": {"enabled": True},
+ "control": {"enabled": True},
+ "interaction": {"enabled": True},
+ "network": {"enabled": True},
+ "page": {"enabled": True},
+ "screenshot": {"enabled": True},
+ "system": {"enabled": True},
+ },
+ "enabled": True,
+ "export": {
+ "otlp": {
+ "destination": {
+ "id": "id",
+ "name": "name",
+ },
+ "enabled": True,
+ }
+ },
+ },
+ },
browser_telemetry={
"browser": {
"captcha": {"enabled": True},
@@ -918,6 +1073,37 @@ async def test_method_login(self, async_client: AsyncKernel) -> None:
async def test_method_login_with_all_params(self, async_client: AsyncKernel) -> None:
connection = await async_client.auth.connections.login(
id="id",
+ browser={
+ "proxy": {
+ "id": "x",
+ "mode": "direct",
+ "name": "x",
+ },
+ "stealth": False,
+ "telemetry": {
+ "browser": {
+ "captcha": {"enabled": True},
+ "connection": {"enabled": True},
+ "console": {"enabled": True},
+ "control": {"enabled": True},
+ "interaction": {"enabled": True},
+ "network": {"enabled": True},
+ "page": {"enabled": True},
+ "screenshot": {"enabled": True},
+ "system": {"enabled": True},
+ },
+ "enabled": True,
+ "export": {
+ "otlp": {
+ "destination": {
+ "id": "id",
+ "name": "name",
+ },
+ "enabled": True,
+ }
+ },
+ },
+ },
browser_telemetry={
"browser": {
"captcha": {"enabled": True},
diff --git a/tests/api_resources/test_browsers.py b/tests/api_resources/test_browsers.py
index a8f93efc..8b0a6182 100644
--- a/tests/api_resources/test_browsers.py
+++ b/tests/api_resources/test_browsers.py
@@ -51,6 +51,11 @@ def test_method_create_with_all_params(self, client: Kernel) -> None:
"name": "name",
"save_changes": True,
},
+ proxy={
+ "id": "x",
+ "mode": "direct",
+ "name": "x",
+ },
proxy_id="proxy_id",
start_url="https://example.com",
stealth=True,
@@ -183,6 +188,11 @@ def test_method_update_with_all_params(self, client: Kernel) -> None:
"name": "name",
"save_changes": True,
},
+ proxy={
+ "id": "x",
+ "mode": "direct",
+ "name": "x",
+ },
proxy_id="proxy_id",
tags={
"team": "backend",
@@ -496,6 +506,11 @@ async def test_method_create_with_all_params(self, async_client: AsyncKernel) ->
"name": "name",
"save_changes": True,
},
+ proxy={
+ "id": "x",
+ "mode": "direct",
+ "name": "x",
+ },
proxy_id="proxy_id",
start_url="https://example.com",
stealth=True,
@@ -628,6 +643,11 @@ async def test_method_update_with_all_params(self, async_client: AsyncKernel) ->
"name": "name",
"save_changes": True,
},
+ proxy={
+ "id": "x",
+ "mode": "direct",
+ "name": "x",
+ },
proxy_id="proxy_id",
tags={
"team": "backend",
From bacb44f61f32e82367c264bab9120595f93f5cbf Mon Sep 17 00:00:00 2001
From: "kernel-internal[bot]"
<260533166+kernel-internal[bot]@users.noreply.github.com>
Date: Sun, 9 Aug 2026 16:32:50 +0000
Subject: [PATCH 174/366] feat: Persist CUA-TS reauth blockers before failure
Stainless-Generated-From: 207ec8076e1bcc7c24257506b0d58a18837505bb
---
src/kernel/types/auth/managed_auth.py | 3 +++
1 file changed, 3 insertions(+)
diff --git a/src/kernel/types/auth/managed_auth.py b/src/kernel/types/auth/managed_auth.py
index f1bb2198..3625cbbc 100644
--- a/src/kernel/types/auth/managed_auth.py
+++ b/src/kernel/types/auth/managed_auth.py
@@ -369,6 +369,7 @@ class ManagedAuth(BaseModel):
"requires_totp_without_secret",
"requires_sms_code",
"requires_email_code",
+ "requires_customer_input",
]
] = None
"""
@@ -401,6 +402,8 @@ class ManagedAuth(BaseModel):
automatically
- `requires_email_code` — flow needs an email code that cannot be received
automatically
+ - `requires_customer_input` — flow needs another field or choice that is
+ unavailable during unattended re-authentication
"""
choices: Optional[List[Choice]] = None
From 2afe161bedb272fe458ae2f0adcd3df140cb151d Mon Sep 17 00:00:00 2001
From: "kernel-internal[bot]"
<260533166+kernel-internal[bot]@users.noreply.github.com>
Date: Mon, 10 Aug 2026 16:33:45 +0000
Subject: [PATCH 175/366] feat: Managed auth: free-plan connections (cap 3),
credentials, 6h floor (packaging PR 5)
Stainless-Generated-From: 27294af8c209bd0f75875350803cd3ec10f081a9
---
src/kernel/resources/auth/connections.py | 14 ++++++++------
src/kernel/types/auth/connection_create_params.py | 5 +++--
src/kernel/types/auth/managed_auth.py | 5 +++--
3 files changed, 14 insertions(+), 10 deletions(-)
diff --git a/src/kernel/resources/auth/connections.py b/src/kernel/resources/auth/connections.py
index a84d42af..56785ea2 100644
--- a/src/kernel/resources/auth/connections.py
+++ b/src/kernel/resources/auth/connections.py
@@ -137,9 +137,10 @@ def create(
health_check_interval: Interval in seconds between automatic health checks. When set, the system
periodically verifies the authentication status and triggers re-authentication
- if needed. Maximum is 86400 (24 hours). Default is 3600 (1 hour). The minimum
- depends on your plan: Enterprise: 300 (5 minutes), Startup: 1200 (20 minutes),
- Hobbyist: 3600 (1 hour).
+ if needed. Maximum is 86400 (24 hours). Default is 3600 (1 hour) or your plan
+ minimum, whichever is larger. The minimum depends on your plan: Enterprise: 300
+ (5 minutes), Startup: 1200 (20 minutes), Hobbyist: 3600 (1 hour), Free: 21600 (6
+ hours).
health_checks: Whether to enable periodic health checks. When false, the system will not
automatically verify authentication status, and `auto_reauth` has no effect on
@@ -749,9 +750,10 @@ async def create(
health_check_interval: Interval in seconds between automatic health checks. When set, the system
periodically verifies the authentication status and triggers re-authentication
- if needed. Maximum is 86400 (24 hours). Default is 3600 (1 hour). The minimum
- depends on your plan: Enterprise: 300 (5 minutes), Startup: 1200 (20 minutes),
- Hobbyist: 3600 (1 hour).
+ if needed. Maximum is 86400 (24 hours). Default is 3600 (1 hour) or your plan
+ minimum, whichever is larger. The minimum depends on your plan: Enterprise: 300
+ (5 minutes), Startup: 1200 (20 minutes), Hobbyist: 3600 (1 hour), Free: 21600 (6
+ hours).
health_checks: Whether to enable periodic health checks. When false, the system will not
automatically verify authentication status, and `auto_reauth` has no effect on
diff --git a/src/kernel/types/auth/connection_create_params.py b/src/kernel/types/auth/connection_create_params.py
index 23d53cb2..2fdc808b 100644
--- a/src/kernel/types/auth/connection_create_params.py
+++ b/src/kernel/types/auth/connection_create_params.py
@@ -88,8 +88,9 @@ class ConnectionCreateParams(TypedDict, total=False):
When set, the system periodically verifies the authentication status and
triggers re-authentication if needed. Maximum is 86400 (24 hours). Default is
- 3600 (1 hour). The minimum depends on your plan: Enterprise: 300 (5 minutes),
- Startup: 1200 (20 minutes), Hobbyist: 3600 (1 hour).
+ 3600 (1 hour) or your plan minimum, whichever is larger. The minimum depends on
+ your plan: Enterprise: 300 (5 minutes), Startup: 1200 (20 minutes), Hobbyist:
+ 3600 (1 hour), Free: 21600 (6 hours).
"""
health_checks: bool
diff --git a/src/kernel/types/auth/managed_auth.py b/src/kernel/types/auth/managed_auth.py
index 3625cbbc..9db58514 100644
--- a/src/kernel/types/auth/managed_auth.py
+++ b/src/kernel/types/auth/managed_auth.py
@@ -469,8 +469,9 @@ class ManagedAuth(BaseModel):
When set, the system periodically verifies the authentication status and
triggers re-authentication if needed. Maximum is 86400 (24 hours). Default is
- 3600 (1 hour). The minimum depends on your plan: Enterprise: 300 (5 minutes),
- Startup: 1200 (20 minutes), Hobbyist: 3600 (1 hour).
+ 3600 (1 hour) or your plan minimum, whichever is larger. The minimum depends on
+ your plan: Enterprise: 300 (5 minutes), Startup: 1200 (20 minutes), Hobbyist:
+ 3600 (1 hour), Free: 21600 (6 hours).
"""
health_checks: Optional[bool] = None
From b2d8ebd981d4acb1ef4c2b40a9f2728d005bcd70 Mon Sep 17 00:00:00 2001
From: rgarcia <72655+rgarcia@users.noreply.github.com>
Date: Mon, 10 Aug 2026 19:10:46 +0000
Subject: [PATCH 176/366] Assert stream cancellation reaches transport
---
tests/test_browser_routing.py | 19 ++++++++++---------
1 file changed, 10 insertions(+), 9 deletions(-)
diff --git a/tests/test_browser_routing.py b/tests/test_browser_routing.py
index 9b37908e..cd502fa1 100644
--- a/tests/test_browser_routing.py
+++ b/tests/test_browser_routing.py
@@ -127,24 +127,25 @@ async def test_async_telemetry_stream_cancellation_survives_direct_routing(
) -> None:
monkeypatch.setenv("KERNEL_BROWSER_ROUTING_SUBRESOURCES", "telemetry/stream")
read_started = asyncio.Event()
- read_stopped = asyncio.Event()
+ transport_cancelled = asyncio.Event()
+ chunks: asyncio.Queue[bytes] = asyncio.Queue()
class BlockingSSEStream(httpx.AsyncByteStream):
@override
async def __aiter__(self) -> AsyncIterator[bytes]:
read_started.set()
try:
- await asyncio.Event().wait()
- finally:
- read_stopped.set()
- yield b""
+ while True:
+ yield await chunks.get()
+ except asyncio.CancelledError:
+ transport_cancelled.set()
+ raise
@override
async def aclose(self) -> None:
- read_stopped.set()
+ pass
- async def handle_request(request: httpx.Request) -> httpx.Response:
- assert request.url.path == "/browser/kernel/telemetry/stream"
+ async def handle_request(_request: httpx.Request) -> httpx.Response:
return httpx.Response(
200,
headers={"content-type": "text/event-stream"},
@@ -168,7 +169,7 @@ async def handle_request(request: httpx.Request) -> httpx.Response:
consumer.cancel()
with pytest.raises(asyncio.CancelledError):
await asyncio.wait_for(consumer, timeout=1)
- await asyncio.wait_for(read_stopped.wait(), timeout=1)
+ await asyncio.wait_for(transport_cancelled.wait(), timeout=1)
@respx.mock
From d281b8f7bb61c0ab13e9a7afeec69fcc5975f2c0 Mon Sep 17 00:00:00 2001
From: rgarcia <72655+rgarcia@users.noreply.github.com>
Date: Mon, 10 Aug 2026 19:31:08 +0000
Subject: [PATCH 177/366] Clarify stream cancellation test scope
---
tests/test_browser_routing.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/tests/test_browser_routing.py b/tests/test_browser_routing.py
index cd502fa1..dbb8b895 100644
--- a/tests/test_browser_routing.py
+++ b/tests/test_browser_routing.py
@@ -122,7 +122,7 @@ def test_telemetry_stream_routes_directly_to_vm(monkeypatch: pytest.MonkeyPatch)
@pytest.mark.asyncio
-async def test_async_telemetry_stream_cancellation_survives_direct_routing(
+async def test_async_telemetry_stream_cancellation_reaches_transport(
monkeypatch: pytest.MonkeyPatch,
) -> None:
monkeypatch.setenv("KERNEL_BROWSER_ROUTING_SUBRESOURCES", "telemetry/stream")
From 4280b89feaa1c61949a02832550fc19252ad982a Mon Sep 17 00:00:00 2001
From: "kernel-internal[bot]"
<260533166+kernel-internal[bot]@users.noreply.github.com>
Date: Mon, 10 Aug 2026 19:58:28 +0000
Subject: [PATCH 178/366] release: 0.88.0
---
.release-please-manifest.json | 2 +-
CHANGELOG.md | 15 +++++++++++++++
pyproject.toml | 2 +-
src/kernel/_version.py | 2 +-
4 files changed, 18 insertions(+), 3 deletions(-)
diff --git a/.release-please-manifest.json b/.release-please-manifest.json
index ae449647..d80a91e2 100644
--- a/.release-please-manifest.json
+++ b/.release-please-manifest.json
@@ -1,3 +1,3 @@
{
- ".": "0.87.0"
+ ".": "0.88.0"
}
\ No newline at end of file
diff --git a/CHANGELOG.md b/CHANGELOG.md
index a45c90eb..94d0a32d 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,20 @@
# Changelog
+## [0.88.0](https://github.com/kernel/kernel-python-sdk/compare/v0.87.0...v0.88.0) (2026-08-10)
+
+
+### Features
+
+* Forward egress deploy owner attribution ([5ae6c3e](https://github.com/kernel/kernel-python-sdk/commit/5ae6c3e0b6a54a884b9fc6d3d4658d970a7aa459))
+* Managed auth: free-plan connections (cap 3), credentials, 6h floor (packaging PR 5) ([2afe161](https://github.com/kernel/kernel-python-sdk/commit/2afe161bedb272fe458ae2f0adcd3df140cb151d))
+* Persist CUA-TS reauth blockers before failure ([bacb44f](https://github.com/kernel/kernel-python-sdk/commit/bacb44f61f32e82367c264bab9120595f93f5cbf))
+
+
+### Chores
+
+* reconcile stlc generation history ([7ee80ac](https://github.com/kernel/kernel-python-sdk/commit/7ee80ac1a20d52b576cb8d08f533e8886a19982d))
+* reconcile stlc generation history ([1ec288f](https://github.com/kernel/kernel-python-sdk/commit/1ec288fb183b8b5eb3a18e487053c354d3246334))
+
## 0.87.0 (2026-08-08)
Full Changelog: [v0.86.1...v0.87.0](https://github.com/kernel/kernel-python-sdk/compare/v0.86.1...v0.87.0)
diff --git a/pyproject.toml b/pyproject.toml
index 505189ab..db2cb8e0 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -1,6 +1,6 @@
[project]
name = "kernel"
-version = "0.87.0"
+version = "0.88.0"
description = "The official Python library for the kernel API"
dynamic = ["readme"]
license = "Apache-2.0"
diff --git a/src/kernel/_version.py b/src/kernel/_version.py
index 6c0e6685..892c18cb 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.87.0" # x-release-please-version
+__version__ = "0.88.0" # x-release-please-version
From 18db1edf5e0dd980bf55fbe294c8cda33ba4dcf4 Mon Sep 17 00:00:00 2001
From: masnwilliams <43387599+masnwilliams@users.noreply.github.com>
Date: Mon, 10 Aug 2026 20:01:09 +0000
Subject: [PATCH 179/366] docs: correct 0.88.0 release notes
---
CHANGELOG.md | 12 ++----------
1 file changed, 2 insertions(+), 10 deletions(-)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 94d0a32d..9137ac6f 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,18 +2,10 @@
## [0.88.0](https://github.com/kernel/kernel-python-sdk/compare/v0.87.0...v0.88.0) (2026-08-10)
-
### Features
-* Forward egress deploy owner attribution ([5ae6c3e](https://github.com/kernel/kernel-python-sdk/commit/5ae6c3e0b6a54a884b9fc6d3d4658d970a7aa459))
-* Managed auth: free-plan connections (cap 3), credentials, 6h floor (packaging PR 5) ([2afe161](https://github.com/kernel/kernel-python-sdk/commit/2afe161bedb272fe458ae2f0adcd3df140cb151d))
-* Persist CUA-TS reauth blockers before failure ([bacb44f](https://github.com/kernel/kernel-python-sdk/commit/bacb44f61f32e82367c264bab9120595f93f5cbf))
-
-
-### Chores
-
-* reconcile stlc generation history ([7ee80ac](https://github.com/kernel/kernel-python-sdk/commit/7ee80ac1a20d52b576cb8d08f533e8886a19982d))
-* reconcile stlc generation history ([1ec288f](https://github.com/kernel/kernel-python-sdk/commit/1ec288fb183b8b5eb3a18e487053c354d3246334))
+* Add typed browser proxy configuration using `mode`, `id`, or `name`
+* Add nested managed-auth browser configuration and per-login browser overrides
## 0.87.0 (2026-08-08)
From d980cbe052ea090a8ad2ed1dc51b72e7c6bc1b16 Mon Sep 17 00:00:00 2001
From: sjmiller609 <7516283+sjmiller609@users.noreply.github.com>
Date: Mon, 10 Aug 2026 20:23:42 +0000
Subject: [PATCH 180/366] ci: use one SDK release PR
---
.github/workflows/release-please.yml | 76 ++++++++++++++++++++++++++--
.github/workflows/stlc-promote.yml | 71 +++++++++++++-------------
release-please-config.json | 1 +
3 files changed, 110 insertions(+), 38 deletions(-)
diff --git a/.github/workflows/release-please.yml b/.github/workflows/release-please.yml
index 3f6bf798..4737ca42 100644
--- a/.github/workflows/release-please.yml
+++ b/.github/workflows/release-please.yml
@@ -1,12 +1,21 @@
name: Release Please
+
+# `next` accumulates validated SDK changes in one versioned PR to `main`.
+# Merging that PR creates the GitHub release; the package publishing workflow
+# runs from the release event.
on:
push:
branches:
+ - next
- main
permissions:
contents: read
+concurrency:
+ group: release-please
+ cancel-in-progress: false
+
jobs:
release-please:
if: github.repository == 'kernel/kernel-python-sdk'
@@ -26,7 +35,68 @@ jobs:
permission-pull-requests: write
permission-workflows: write
- - uses: googleapis/release-please-action@5c625bfb5d1ff62eadeeb3772007f7f66fdcf071 # v4.4.1
- id: release
+ - name: Set up Node
+ uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
+ with:
+ node-version: '18.20.2'
+
+ - name: Set up pnpm
+ uses: pnpm/action-setup@f40ffcd9367d9f12939873eb1018b921a783ffaa # v4
with:
- token: ${{ steps.release-token.outputs.token }}
+ version: '9.11.0'
+ run_install: false
+
+ - name: Build pinned release tooling
+ id: tooling
+ env:
+ RELEASE_PLEASE_DIR: ${{ runner.temp }}/release-please
+ RELEASE_PLEASE_SHA: a116e1e520e0f87824acf46a2e79c91d41e819d7
+ run: |
+ set -euo pipefail
+ rm -rf "$RELEASE_PLEASE_DIR"
+ git init "$RELEASE_PLEASE_DIR"
+ git -C "$RELEASE_PLEASE_DIR" remote add origin https://github.com/stainless-api/release-please.git
+ git -C "$RELEASE_PLEASE_DIR" fetch --depth=1 origin "$RELEASE_PLEASE_SHA"
+ git -C "$RELEASE_PLEASE_DIR" checkout --detach FETCH_HEAD
+ pnpm --dir "$RELEASE_PLEASE_DIR" install --frozen-lockfile
+ pnpm --dir "$RELEASE_PLEASE_DIR" build
+ echo "cli=$RELEASE_PLEASE_DIR/build/src/bin/release-please.js" >> "$GITHUB_OUTPUT"
+
+ - name: Open or update the release PR
+ if: github.ref_name == 'next'
+ env:
+ GH_TOKEN: ${{ steps.release-token.outputs.token }}
+ RELEASE_PLEASE: ${{ steps.tooling.outputs.cli }}
+ run: |
+ set -euo pipefail
+ node "$RELEASE_PLEASE" release-pr \
+ --repo-url "$GITHUB_REPOSITORY" \
+ --token "$GH_TOKEN" \
+ --target-branch main \
+ --changes-branch next
+
+ - name: Remove the legacy promotion PR
+ if: github.ref_name == 'next'
+ env:
+ GH_TOKEN: ${{ steps.release-token.outputs.token }}
+ run: |
+ set -euo pipefail
+ legacy=$(gh pr list --repo "$GITHUB_REPOSITORY" --head stainless/release \
+ --state open --json number --jq '.[].number')
+ for pr in $legacy; do
+ gh pr close "$pr" --repo "$GITHUB_REPOSITORY" \
+ --comment "Superseded by the versioned release PR from next to main."
+ done
+ gh api -X DELETE "repos/$GITHUB_REPOSITORY/git/refs/heads/stainless/release" >/dev/null 2>&1 || true
+
+ - name: Create the GitHub release
+ if: github.ref_name == 'main'
+ env:
+ GH_TOKEN: ${{ steps.release-token.outputs.token }}
+ RELEASE_PLEASE: ${{ steps.tooling.outputs.cli }}
+ run: |
+ set -euo pipefail
+ node "$RELEASE_PLEASE" github-release \
+ --repo-url "$GITHUB_REPOSITORY" \
+ --token "$GH_TOKEN" \
+ --target-branch main
diff --git a/.github/workflows/stlc-promote.yml b/.github/workflows/stlc-promote.yml
index 5123a90a..d917658c 100644
--- a/.github/workflows/stlc-promote.yml
+++ b/.github/workflows/stlc-promote.yml
@@ -1,8 +1,9 @@
-name: Promote SDKs
+name: Promote SDK changes
-# Production requires pull requests, so staging is promoted through a merge-
-# commit PR. Never squash or rebase this cross-repo PR: preserving the incoming
-# commits keeps production and staging on one ancestry chain.
+# Staging is the generator's integration history. Production `next` is the
+# developer-facing queue for the next release. This workflow combines the
+# latest released state with validated staging changes, then advances `next`.
+# Release automation maintains the single versioned PR from `next` to `main`.
on:
push:
branches: [main]
@@ -35,49 +36,49 @@ jobs:
repositories: kernel-python-sdk
permission-contents: write
permission-workflows: write
- permission-pull-requests: write
- - name: Fetch production main
+ - name: Fetch production branches
+ id: production
env:
GH_TOKEN: ${{ steps.production-token.outputs.token }}
PRODUCTION_REPO: kernel/kernel-python-sdk
run: |
- git remote add production "https://x-access-token:${GH_TOKEN}@github.com/${PRODUCTION_REPO}.git"
+ set -euo pipefail
+ git remote add production \
+ "https://x-access-token:${GH_TOKEN}@github.com/${PRODUCTION_REPO}.git"
git fetch production main
-
- - name: Check whether production already has staging's content
- id: diff
- run: |
- MERGED=$(git merge-tree --write-tree production/main origin/main) || MERGED=conflict
- PRODUCTION_TREE=$(git rev-parse 'production/main^{tree}')
- if [ "$MERGED" = "$PRODUCTION_TREE" ]; then
- echo "Production already contains staging's content. Nothing to promote."
- echo "synced=true" >> "$GITHUB_OUTPUT"
+ if git ls-remote --exit-code --heads production next >/dev/null 2>&1; then
+ git fetch production next
+ echo "has_next=true" >> "$GITHUB_OUTPUT"
else
- echo "synced=false" >> "$GITHUB_OUTPUT"
+ echo "has_next=false" >> "$GITHUB_OUTPUT"
fi
- - name: Push the production release branch
- if: steps.diff.outputs.synced == 'false'
+ - name: Prepare the next release branch
env:
+ APP_SLUG: ${{ steps.production-token.outputs.app-slug }}
GH_TOKEN: ${{ steps.production-token.outputs.token }}
- PRODUCTION_REPO: kernel/kernel-python-sdk
- run: git push production origin/main:refs/heads/stainless/release --force
+ HAS_NEXT: ${{ steps.production.outputs.has_next }}
+ run: |
+ set -euo pipefail
+ bot_id=$(gh api "/users/${APP_SLUG}[bot]" --jq .id)
+ git config user.name "${APP_SLUG}[bot]"
+ git config user.email "${bot_id}+${APP_SLUG}[bot]@users.noreply.github.com"
+
+ git checkout -B stlc/promote-next origin/main
+ if ! git merge-base --is-ancestor production/main HEAD; then
+ git merge --no-edit production/main
+ fi
- - name: Open or update the promote PR
- if: steps.diff.outputs.synced == 'false'
+ if [ "$HAS_NEXT" = "true" ] && ! git merge-base --is-ancestor production/next HEAD; then
+ echo "::error title=Promotion blocked::production next has commits that are not present in staging or production main."
+ exit 1
+ fi
+
+ - name: Update the pending release
env:
GH_TOKEN: ${{ steps.production-token.outputs.token }}
- PRODUCTION_REPO: kernel/kernel-python-sdk
run: |
- body=$(mktemp)
- git log --oneline production/main..origin/main > "$body"
- existing=$(gh pr list --repo "$PRODUCTION_REPO" --head stainless/release --state open --json number --jq 'if length == 0 then "" else .[0].number end')
- if [ -z "$existing" ]; then
- gh pr create --repo "$PRODUCTION_REPO" --base main --head stainless/release --title "Release SDK updates" --body-file "$body"
- else
- gh pr edit "$existing" --repo "$PRODUCTION_REPO" --title "Release SDK updates" --body-file "$body"
- fi
- if ! gh pr merge stainless/release --repo "$PRODUCTION_REPO" --merge --auto; then
- echo "::warning title=Manual promotion required::Merge the promote PR with a merge commit."
- fi
+ set -euo pipefail
+ git push production HEAD:refs/heads/next
+ echo "Updated production next; the versioned release PR will be opened or refreshed."
diff --git a/release-please-config.json b/release-please-config.json
index bdba3b86..6ffb6a57 100644
--- a/release-please-config.json
+++ b/release-please-config.json
@@ -10,6 +10,7 @@
"bump-minor-pre-major": true,
"bump-patch-for-minor-pre-major": false,
"pull-request-header": "Automated Release PR",
+ "pull-request-footer": "Merge this pull request with a merge commit. Merging creates the GitHub release and publishes the package.",
"pull-request-title-pattern": "release: ${version}",
"changelog-sections": [
{
From 65c00dc5404e9c886352b6831e54c1326f2557e6 Mon Sep 17 00:00:00 2001
From: sjmiller609 <7516283+sjmiller609@users.noreply.github.com>
Date: Mon, 10 Aug 2026 21:08:51 +0000
Subject: [PATCH 181/366] Preserve pending SDK release ancestry
---
.github/workflows/stlc-promote.yml | 15 +++++++++++----
1 file changed, 11 insertions(+), 4 deletions(-)
diff --git a/.github/workflows/stlc-promote.yml b/.github/workflows/stlc-promote.yml
index d917658c..f625307d 100644
--- a/.github/workflows/stlc-promote.yml
+++ b/.github/workflows/stlc-promote.yml
@@ -65,14 +65,21 @@ jobs:
git config user.name "${APP_SLUG}[bot]"
git config user.email "${bot_id}+${APP_SLUG}[bot]@users.noreply.github.com"
- git checkout -B stlc/promote-next origin/main
+ if [ "$HAS_NEXT" = "true" ]; then
+ git checkout -B stlc/promote-next production/next
+ else
+ git checkout -B stlc/promote-next production/main
+ fi
+
if ! git merge-base --is-ancestor production/main HEAD; then
git merge --no-edit production/main
fi
+ if ! git merge-base --is-ancestor origin/main HEAD; then
+ git merge --no-edit origin/main
+ fi
- if [ "$HAS_NEXT" = "true" ] && ! git merge-base --is-ancestor production/next HEAD; then
- echo "::error title=Promotion blocked::production next has commits that are not present in staging or production main."
- exit 1
+ if [ "$HAS_NEXT" = "true" ]; then
+ git merge-base --is-ancestor production/next HEAD
fi
- name: Update the pending release
From 0bd6a7f0e857efb66257c428da50eeba24dee175 Mon Sep 17 00:00:00 2001
From: sjmiller609 <7516283+sjmiller609@users.noreply.github.com>
Date: Mon, 10 Aug 2026 21:34:49 +0000
Subject: [PATCH 182/366] Open recovery PRs for promotion conflicts
---
.github/workflows/stlc-promote.yml | 44 ++++++++++++++++++++++++++++--
1 file changed, 42 insertions(+), 2 deletions(-)
diff --git a/.github/workflows/stlc-promote.yml b/.github/workflows/stlc-promote.yml
index f625307d..e63aaf9e 100644
--- a/.github/workflows/stlc-promote.yml
+++ b/.github/workflows/stlc-promote.yml
@@ -35,6 +35,7 @@ jobs:
owner: kernel
repositories: kernel-python-sdk
permission-contents: write
+ permission-pull-requests: write
permission-workflows: write
- name: Fetch production branches
@@ -59,12 +60,49 @@ jobs:
APP_SLUG: ${{ steps.production-token.outputs.app-slug }}
GH_TOKEN: ${{ steps.production-token.outputs.token }}
HAS_NEXT: ${{ steps.production.outputs.has_next }}
+ PRODUCTION_REPO: kernel/kernel-python-sdk
run: |
set -euo pipefail
bot_id=$(gh api "/users/${APP_SLUG}[bot]" --jq .id)
git config user.name "${APP_SLUG}[bot]"
git config user.email "${bot_id}+${APP_SLUG}[bot]@users.noreply.github.com"
+ open_conflict_pr() {
+ source_ref=$1
+ source_name=$2
+ advance_next=$3
+ conflict_branch=stlc/promotion-conflict
+
+ git merge --abort
+ existing=$(gh pr list --repo "$PRODUCTION_REPO" --base next \
+ --head "$conflict_branch" --state open --json url --jq '.[0].url // ""')
+ if [ -n "$existing" ]; then
+ echo "::error title=SDK promotion blocked::Resolve the existing recovery PR: $existing"
+ exit 1
+ fi
+
+ if [ "$advance_next" = "true" ]; then
+ git push production HEAD:refs/heads/next
+ fi
+ git push production "$source_ref:refs/heads/$conflict_branch" --force
+
+ body=$(mktemp)
+ printf '%s\n' \
+ '## SDK promotion conflict' \
+ '' \
+ "The automated promotion could not merge $source_name into the pending next release." \
+ '' \
+ 'Resolve the conflicts on this branch, validate the SDK, mark this PR ready, and merge it with a merge commit.' \
+ '' \
+ 'After merging, rerun the staging Promote SDK changes workflow to include any newer generated changes.' \
+ > "$body"
+ recovery_url=$(gh pr create --repo "$PRODUCTION_REPO" --draft \
+ --base next --head "$conflict_branch" \
+ --title 'chore: resolve SDK promotion conflict' --body-file "$body")
+ echo "::error title=SDK promotion conflict::Resolve the recovery PR: $recovery_url"
+ exit 1
+ }
+
if [ "$HAS_NEXT" = "true" ]; then
git checkout -B stlc/promote-next production/next
else
@@ -72,10 +110,12 @@ jobs:
fi
if ! git merge-base --is-ancestor production/main HEAD; then
- git merge --no-edit production/main
+ git merge --no-edit production/main || \
+ open_conflict_pr production/main 'production main' false
fi
if ! git merge-base --is-ancestor origin/main HEAD; then
- git merge --no-edit origin/main
+ git merge --no-edit origin/main || \
+ open_conflict_pr origin/main 'validated staging changes' true
fi
if [ "$HAS_NEXT" = "true" ]; then
From e639638e52a2abcacc2bf1d0d81da09ccb023142 Mon Sep 17 00:00:00 2001
From: sjmiller609 <7516283+sjmiller609@users.noreply.github.com>
Date: Mon, 10 Aug 2026 21:59:45 +0000
Subject: [PATCH 183/366] Preserve conflict handler error checks
---
.github/workflows/stlc-promote.yml | 6 ++++--
1 file changed, 4 insertions(+), 2 deletions(-)
diff --git a/.github/workflows/stlc-promote.yml b/.github/workflows/stlc-promote.yml
index e63aaf9e..3bcd88c7 100644
--- a/.github/workflows/stlc-promote.yml
+++ b/.github/workflows/stlc-promote.yml
@@ -110,12 +110,14 @@ jobs:
fi
if ! git merge-base --is-ancestor production/main HEAD; then
- git merge --no-edit production/main || \
+ if ! git merge --no-edit production/main; then
open_conflict_pr production/main 'production main' false
+ fi
fi
if ! git merge-base --is-ancestor origin/main HEAD; then
- git merge --no-edit origin/main || \
+ if ! git merge --no-edit origin/main; then
open_conflict_pr origin/main 'validated staging changes' true
+ fi
fi
if [ "$HAS_NEXT" = "true" ]; then
From d7c92107ee02cd222a605880ec0f30ef7dcca926 Mon Sep 17 00:00:00 2001
From: tnsardesai <18272584+tnsardesai@users.noreply.github.com>
Date: Tue, 11 Aug 2026 19:26:09 +0000
Subject: [PATCH 184/366] test: add region to the fake browser fixture
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
The API spec gains region as a required field on browser responses
(kernel/kernel#3270); strict response validation rejects the fixture
without it. Safe against the current spec — models allow extra keys.
---
tests/test_browser_routing.py | 1 +
1 file changed, 1 insertion(+)
diff --git a/tests/test_browser_routing.py b/tests/test_browser_routing.py
index dbb8b895..84c5c541 100644
--- a/tests/test_browser_routing.py
+++ b/tests/test_browser_routing.py
@@ -32,6 +32,7 @@ def _fake_browser() -> dict[str, object]:
"headless": True,
"stealth": False,
"timeout_seconds": 60,
+ "region": "us-east",
}
From 9ff1b445f8f2bb3f9bb24f613e22c8b940fc1d5a Mon Sep 17 00:00:00 2001
From: "kernel-internal[bot]"
<260533166+kernel-internal[bot]@users.noreply.github.com>
Date: Tue, 11 Aug 2026 23:49:40 +0000
Subject: [PATCH 185/366] feat: Expose plan-derived auth limits on GET
/org/limits
Stainless-Generated-From: f793e2f1d478198a0f75f553437b8b554b75368f
---
src/kernel/resources/organization/limits.py | 12 ++----------
src/kernel/types/organization/org_limits.py | 21 +++++++++++++++++++++
2 files changed, 23 insertions(+), 10 deletions(-)
diff --git a/src/kernel/resources/organization/limits.py b/src/kernel/resources/organization/limits.py
index d30bfbfa..d1bb6ecb 100644
--- a/src/kernel/resources/organization/limits.py
+++ b/src/kernel/resources/organization/limits.py
@@ -55,11 +55,7 @@ def retrieve(
extra_body: Body | None = None,
timeout: float | httpx.Timeout | None | NotGiven = not_given,
) -> OrgLimits:
- """
- Get the organization's concurrency limit — the maximum browsers running at once
- across on-demand sessions and browser pool reservations — and the default
- per-project concurrency cap applied to projects without an explicit override.
- """
+ """Get the organization's effective limits and managed auth usage."""
return self._get(
"/org/limits",
options=make_request_options(
@@ -142,11 +138,7 @@ async def retrieve(
extra_body: Body | None = None,
timeout: float | httpx.Timeout | None | NotGiven = not_given,
) -> OrgLimits:
- """
- Get the organization's concurrency limit — the maximum browsers running at once
- across on-demand sessions and browser pool reservations — and the default
- per-project concurrency cap applied to projects without an explicit override.
- """
+ """Get the organization's effective limits and managed auth usage."""
return await self._get(
"/org/limits",
options=make_request_options(
diff --git a/src/kernel/types/organization/org_limits.py b/src/kernel/types/organization/org_limits.py
index 3d614996..bdc3cb4a 100644
--- a/src/kernel/types/organization/org_limits.py
+++ b/src/kernel/types/organization/org_limits.py
@@ -8,6 +8,27 @@
class OrgLimits(BaseModel):
+ auth_connections_used: int
+ """
+ The organization's current non-deleted managed auth connections, counted
+ org-wide across every project. Compare against max_auth_connections to show
+ remaining capacity before a create is rejected with 403 insufficient_plan.
+ """
+
+ max_auth_connections: Optional[int] = None
+ """Maximum managed auth connections the organization's plan allows.
+
+ Null means unlimited. Counted org-wide, so it cannot be multiplied across
+ projects.
+ """
+
+ min_health_check_interval_seconds: int
+ """
+ Smallest health_check_interval the organization's plan accepts on a managed auth
+ connection. Requests below this are rejected with 400. Existing connections
+ stored below the floor are grandfathered until edited.
+ """
+
default_project_max_concurrent_sessions: Optional[int] = None
"""
Default maximum concurrent browsers applied to every project that has no
From fdb020d437b3b410b2d5758691f2818d5e744580 Mon Sep 17 00:00:00 2001
From: "kernel-internal[bot]"
<260533166+kernel-internal[bot]@users.noreply.github.com>
Date: Wed, 12 Aug 2026 01:28:14 +0000
Subject: [PATCH 186/366] feat: Add region as a first-class API field with plan
and flag gating
Stainless-Generated-From: 91d0b1968ff1d553bd8eb0700df9b6c8d9167bce
---
src/kernel/resources/browser_pools.py | 21 +++++++++++++++++++
src/kernel/resources/browsers/browsers.py | 20 ++++++++++++++++++
src/kernel/types/browser_create_params.py | 9 +++++++-
src/kernel/types/browser_create_response.py | 4 ++++
src/kernel/types/browser_list_params.py | 3 +++
src/kernel/types/browser_list_response.py | 4 ++++
src/kernel/types/browser_pool.py | 4 ++++
.../types/browser_pool_acquire_response.py | 4 ++++
.../types/browser_pool_create_params.py | 9 +++++++-
src/kernel/types/browser_pool_list_params.py | 5 ++++-
src/kernel/types/browser_retrieve_response.py | 4 ++++
src/kernel/types/browser_update_response.py | 4 ++++
.../invocation_list_browsers_response.py | 4 ++++
tests/api_resources/test_browser_pools.py | 4 ++++
tests/api_resources/test_browsers.py | 4 ++++
15 files changed, 100 insertions(+), 3 deletions(-)
diff --git a/src/kernel/resources/browser_pools.py b/src/kernel/resources/browser_pools.py
index d71fc272..18eafcfe 100644
--- a/src/kernel/resources/browser_pools.py
+++ b/src/kernel/resources/browser_pools.py
@@ -3,6 +3,7 @@
from __future__ import annotations
from typing import Dict, Iterable, Optional
+from typing_extensions import Literal
import httpx
@@ -70,6 +71,7 @@ def create(
profile: browser_pool_create_params.Profile | Omit = omit,
proxy_id: str | Omit = omit,
refresh_on_profile_update: bool | Omit = omit,
+ region: Literal["us-east", "eu-west"] | Omit = omit,
start_url: str | Omit = omit,
stealth: bool | Omit = omit,
telemetry: Optional[browser_pool_create_params.Telemetry] | Omit = omit,
@@ -126,6 +128,10 @@ def create(
pool browsers pick up the latest profile data. When a profile is provided during
creation, this defaults to true. Requires a profile to be set on the pool.
+ region: Geographic region for the browser pool. It is fixed once the pool is created.
+ Region selection requires a Start-Up or Enterprise plan, defaults to us-east
+ when omitted on create.
+
start_url: Optional URL to navigate to when a new browser is warmed into the pool.
Best-effort: failures to navigate do not fail pool fill. Only applied to
newly-warmed browsers; browsers reused via release/acquire keep whatever URL the
@@ -180,6 +186,7 @@ def create(
"profile": profile,
"proxy_id": proxy_id,
"refresh_on_profile_update": refresh_on_profile_update,
+ "region": region,
"start_url": start_url,
"stealth": stealth,
"telemetry": telemetry,
@@ -386,6 +393,7 @@ def list(
name: str | Omit = omit,
offset: int | Omit = omit,
query: str | Omit = omit,
+ region: Literal["us-east", "eu-west"] | 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,
@@ -409,6 +417,8 @@ def list(
query: Case-insensitive substring match against browser pool name. IDs match by exact
value.
+ region: Filter pools by geographic region. Omit to list pools in all regions.
+
extra_headers: Send extra headers
extra_query: Add additional query parameters to the request
@@ -431,6 +441,7 @@ def list(
"name": name,
"offset": offset,
"query": query,
+ "region": region,
},
browser_pool_list_params.BrowserPoolListParams,
),
@@ -678,6 +689,7 @@ async def create(
profile: browser_pool_create_params.Profile | Omit = omit,
proxy_id: str | Omit = omit,
refresh_on_profile_update: bool | Omit = omit,
+ region: Literal["us-east", "eu-west"] | Omit = omit,
start_url: str | Omit = omit,
stealth: bool | Omit = omit,
telemetry: Optional[browser_pool_create_params.Telemetry] | Omit = omit,
@@ -734,6 +746,10 @@ async def create(
pool browsers pick up the latest profile data. When a profile is provided during
creation, this defaults to true. Requires a profile to be set on the pool.
+ region: Geographic region for the browser pool. It is fixed once the pool is created.
+ Region selection requires a Start-Up or Enterprise plan, defaults to us-east
+ when omitted on create.
+
start_url: Optional URL to navigate to when a new browser is warmed into the pool.
Best-effort: failures to navigate do not fail pool fill. Only applied to
newly-warmed browsers; browsers reused via release/acquire keep whatever URL the
@@ -788,6 +804,7 @@ async def create(
"profile": profile,
"proxy_id": proxy_id,
"refresh_on_profile_update": refresh_on_profile_update,
+ "region": region,
"start_url": start_url,
"stealth": stealth,
"telemetry": telemetry,
@@ -994,6 +1011,7 @@ def list(
name: str | Omit = omit,
offset: int | Omit = omit,
query: str | Omit = omit,
+ region: Literal["us-east", "eu-west"] | 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,
@@ -1017,6 +1035,8 @@ def list(
query: Case-insensitive substring match against browser pool name. IDs match by exact
value.
+ region: Filter pools by geographic region. Omit to list pools in all regions.
+
extra_headers: Send extra headers
extra_query: Add additional query parameters to the request
@@ -1039,6 +1059,7 @@ def list(
"name": name,
"offset": offset,
"query": query,
+ "region": region,
},
browser_pool_list_params.BrowserPoolListParams,
),
diff --git a/src/kernel/resources/browsers/browsers.py b/src/kernel/resources/browsers/browsers.py
index 19f48988..35c9d612 100644
--- a/src/kernel/resources/browsers/browsers.py
+++ b/src/kernel/resources/browsers/browsers.py
@@ -175,6 +175,7 @@ def create(
profile: BrowserProfile | Omit = omit,
proxy: BrowserProxyConfigParam | Omit = omit,
proxy_id: str | Omit = omit,
+ region: Literal["us-east", "eu-west"] | Omit = omit,
start_url: str | Omit = omit,
stealth: bool | Omit = omit,
tags: TagsParam | Omit = omit,
@@ -229,6 +230,10 @@ def create(
proxy_id: Optional proxy to associate to the browser session. Must reference a proxy in
the same project as the browser session. Deprecated in favor of proxy.
+ region: Geographic region for the browser session. It is fixed once the session is
+ created. Region selection requires a Start-Up or Enterprise plan, defaults to
+ us-east when omitted on create.
+
start_url: Optional URL to open when the browser session is created. Navigation is
best-effort, so navigation failures do not prevent the session from being
created.
@@ -290,6 +295,7 @@ def create(
"profile": profile,
"proxy": proxy,
"proxy_id": proxy_id,
+ "region": region,
"start_url": start_url,
"stealth": stealth,
"tags": tags,
@@ -440,6 +446,7 @@ def list(
limit: int | Omit = omit,
offset: int | Omit = omit,
query: str | Omit = omit,
+ region: Literal["us-east", "eu-west"] | Omit = omit,
status: Literal["active", "deleted", "all"] | Omit = omit,
tags: Dict[str, str] | Omit = omit,
# Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
@@ -464,6 +471,8 @@ def list(
query: Search browsers by name, session ID, profile name or ID, proxy ID, or pool name.
+ region: Filter sessions by geographic region. Omit to list sessions in all regions.
+
status: Filter sessions by status. "active" returns only active sessions (default),
"deleted" returns only soft-deleted sessions, "all" returns both.
@@ -493,6 +502,7 @@ def list(
"limit": limit,
"offset": offset,
"query": query,
+ "region": region,
"status": status,
"tags": tags,
},
@@ -775,6 +785,7 @@ async def create(
profile: BrowserProfile | Omit = omit,
proxy: BrowserProxyConfigParam | Omit = omit,
proxy_id: str | Omit = omit,
+ region: Literal["us-east", "eu-west"] | Omit = omit,
start_url: str | Omit = omit,
stealth: bool | Omit = omit,
tags: TagsParam | Omit = omit,
@@ -829,6 +840,10 @@ async def create(
proxy_id: Optional proxy to associate to the browser session. Must reference a proxy in
the same project as the browser session. Deprecated in favor of proxy.
+ region: Geographic region for the browser session. It is fixed once the session is
+ created. Region selection requires a Start-Up or Enterprise plan, defaults to
+ us-east when omitted on create.
+
start_url: Optional URL to open when the browser session is created. Navigation is
best-effort, so navigation failures do not prevent the session from being
created.
@@ -890,6 +905,7 @@ async def create(
"profile": profile,
"proxy": proxy,
"proxy_id": proxy_id,
+ "region": region,
"start_url": start_url,
"stealth": stealth,
"tags": tags,
@@ -1040,6 +1056,7 @@ def list(
limit: int | Omit = omit,
offset: int | Omit = omit,
query: str | Omit = omit,
+ region: Literal["us-east", "eu-west"] | Omit = omit,
status: Literal["active", "deleted", "all"] | Omit = omit,
tags: Dict[str, str] | Omit = omit,
# Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
@@ -1064,6 +1081,8 @@ def list(
query: Search browsers by name, session ID, profile name or ID, proxy ID, or pool name.
+ region: Filter sessions by geographic region. Omit to list sessions in all regions.
+
status: Filter sessions by status. "active" returns only active sessions (default),
"deleted" returns only soft-deleted sessions, "all" returns both.
@@ -1093,6 +1112,7 @@ def list(
"limit": limit,
"offset": offset,
"query": query,
+ "region": region,
"status": status,
"tags": tags,
},
diff --git a/src/kernel/types/browser_create_params.py b/src/kernel/types/browser_create_params.py
index 262383c5..e30878ec 100644
--- a/src/kernel/types/browser_create_params.py
+++ b/src/kernel/types/browser_create_params.py
@@ -3,7 +3,7 @@
from __future__ import annotations
from typing import Dict, Iterable, Optional
-from typing_extensions import TypedDict
+from typing_extensions import Literal, TypedDict
from .tags_param import TagsParam
from .browser_proxy_config_param import BrowserProxyConfigParam
@@ -90,6 +90,13 @@ class BrowserCreateParams(TypedDict, total=False):
favor of proxy.
"""
+ region: Literal["us-east", "eu-west"]
+ """Geographic region for the browser session.
+
+ It is fixed once the session is created. Region selection requires a Start-Up or
+ Enterprise plan, defaults to us-east when omitted on create.
+ """
+
start_url: str
"""Optional URL to open when the browser session is created.
diff --git a/src/kernel/types/browser_create_response.py b/src/kernel/types/browser_create_response.py
index 8618e7e5..429151df 100644
--- a/src/kernel/types/browser_create_response.py
+++ b/src/kernel/types/browser_create_response.py
@@ -2,6 +2,7 @@
from typing import Dict, Optional
from datetime import datetime
+from typing_extensions import Literal
from .tags import Tags
from .profile import Profile
@@ -25,6 +26,9 @@ class BrowserCreateResponse(BaseModel):
headless: bool
"""Whether the browser session is running in headless mode."""
+ region: Literal["us-east", "eu-west"]
+ """Geographic region of the browser session. Fixed once the session is created."""
+
session_id: str
"""Unique identifier for the browser session"""
diff --git a/src/kernel/types/browser_list_params.py b/src/kernel/types/browser_list_params.py
index 57de2521..8b829a8d 100644
--- a/src/kernel/types/browser_list_params.py
+++ b/src/kernel/types/browser_list_params.py
@@ -27,6 +27,9 @@ class BrowserListParams(TypedDict, total=False):
Search browsers by name, session ID, profile name or ID, proxy ID, or pool name.
"""
+ region: Literal["us-east", "eu-west"]
+ """Filter sessions by geographic region. Omit to list sessions in all regions."""
+
status: Literal["active", "deleted", "all"]
"""Filter sessions by status.
diff --git a/src/kernel/types/browser_list_response.py b/src/kernel/types/browser_list_response.py
index 8f4917a8..42d261a3 100644
--- a/src/kernel/types/browser_list_response.py
+++ b/src/kernel/types/browser_list_response.py
@@ -2,6 +2,7 @@
from typing import Dict, Optional
from datetime import datetime
+from typing_extensions import Literal
from .tags import Tags
from .profile import Profile
@@ -25,6 +26,9 @@ class BrowserListResponse(BaseModel):
headless: bool
"""Whether the browser session is running in headless mode."""
+ region: Literal["us-east", "eu-west"]
+ """Geographic region of the browser session. Fixed once the session is created."""
+
session_id: str
"""Unique identifier for the browser session"""
diff --git a/src/kernel/types/browser_pool.py b/src/kernel/types/browser_pool.py
index 344ffd77..6f5a2052 100644
--- a/src/kernel/types/browser_pool.py
+++ b/src/kernel/types/browser_pool.py
@@ -2,6 +2,7 @@
from typing import Dict, List, Optional
from datetime import datetime
+from typing_extensions import Literal
from .._models import BaseModel
from .shared.browser_viewport import BrowserViewport
@@ -166,6 +167,9 @@ class BrowserPool(BaseModel):
(echoed as sent on create).
"""
+ region: Literal["us-east", "eu-west"]
+ """Geographic region of the browser pool. Fixed once the pool is created."""
+
name: Optional[str] = None
"""Browser pool name, if set"""
diff --git a/src/kernel/types/browser_pool_acquire_response.py b/src/kernel/types/browser_pool_acquire_response.py
index 7d849958..7096b9f6 100644
--- a/src/kernel/types/browser_pool_acquire_response.py
+++ b/src/kernel/types/browser_pool_acquire_response.py
@@ -2,6 +2,7 @@
from typing import Dict, Optional
from datetime import datetime
+from typing_extensions import Literal
from .tags import Tags
from .profile import Profile
@@ -25,6 +26,9 @@ class BrowserPoolAcquireResponse(BaseModel):
headless: bool
"""Whether the browser session is running in headless mode."""
+ region: Literal["us-east", "eu-west"]
+ """Geographic region of the browser session. Fixed once the session is created."""
+
session_id: str
"""Unique identifier for the browser session"""
diff --git a/src/kernel/types/browser_pool_create_params.py b/src/kernel/types/browser_pool_create_params.py
index fb6aa63b..123fb172 100644
--- a/src/kernel/types/browser_pool_create_params.py
+++ b/src/kernel/types/browser_pool_create_params.py
@@ -3,7 +3,7 @@
from __future__ import annotations
from typing import Dict, Iterable, Optional
-from typing_extensions import Required, TypedDict
+from typing_extensions import Literal, Required, TypedDict
from .shared_params.browser_viewport import BrowserViewport
from .shared_params.browser_extension import BrowserExtension
@@ -83,6 +83,13 @@ class BrowserPoolCreateParams(TypedDict, total=False):
creation, this defaults to true. Requires a profile to be set on the pool.
"""
+ region: Literal["us-east", "eu-west"]
+ """Geographic region for the browser pool.
+
+ It is fixed once the pool is created. Region selection requires a Start-Up or
+ Enterprise plan, defaults to us-east when omitted on create.
+ """
+
start_url: str
"""Optional URL to navigate to when a new browser is warmed into the pool.
diff --git a/src/kernel/types/browser_pool_list_params.py b/src/kernel/types/browser_pool_list_params.py
index 75fea5db..36f2f8ca 100644
--- a/src/kernel/types/browser_pool_list_params.py
+++ b/src/kernel/types/browser_pool_list_params.py
@@ -2,7 +2,7 @@
from __future__ import annotations
-from typing_extensions import TypedDict
+from typing_extensions import Literal, TypedDict
__all__ = ["BrowserPoolListParams"]
@@ -27,3 +27,6 @@ class BrowserPoolListParams(TypedDict, total=False):
IDs match by exact value.
"""
+
+ region: Literal["us-east", "eu-west"]
+ """Filter pools by geographic region. Omit to list pools in all regions."""
diff --git a/src/kernel/types/browser_retrieve_response.py b/src/kernel/types/browser_retrieve_response.py
index 3afc6082..e7459a0c 100644
--- a/src/kernel/types/browser_retrieve_response.py
+++ b/src/kernel/types/browser_retrieve_response.py
@@ -2,6 +2,7 @@
from typing import Dict, Optional
from datetime import datetime
+from typing_extensions import Literal
from .tags import Tags
from .profile import Profile
@@ -25,6 +26,9 @@ class BrowserRetrieveResponse(BaseModel):
headless: bool
"""Whether the browser session is running in headless mode."""
+ region: Literal["us-east", "eu-west"]
+ """Geographic region of the browser session. Fixed once the session is created."""
+
session_id: str
"""Unique identifier for the browser session"""
diff --git a/src/kernel/types/browser_update_response.py b/src/kernel/types/browser_update_response.py
index d60fe900..4cfe18d4 100644
--- a/src/kernel/types/browser_update_response.py
+++ b/src/kernel/types/browser_update_response.py
@@ -2,6 +2,7 @@
from typing import Dict, Optional
from datetime import datetime
+from typing_extensions import Literal
from .tags import Tags
from .profile import Profile
@@ -25,6 +26,9 @@ class BrowserUpdateResponse(BaseModel):
headless: bool
"""Whether the browser session is running in headless mode."""
+ region: Literal["us-east", "eu-west"]
+ """Geographic region of the browser session. Fixed once the session is created."""
+
session_id: str
"""Unique identifier for the browser session"""
diff --git a/src/kernel/types/invocation_list_browsers_response.py b/src/kernel/types/invocation_list_browsers_response.py
index 5cfe7e89..caedb6ea 100644
--- a/src/kernel/types/invocation_list_browsers_response.py
+++ b/src/kernel/types/invocation_list_browsers_response.py
@@ -2,6 +2,7 @@
from typing import Dict, List, Optional
from datetime import datetime
+from typing_extensions import Literal
from .tags import Tags
from .profile import Profile
@@ -25,6 +26,9 @@ class Browser(BaseModel):
headless: bool
"""Whether the browser session is running in headless mode."""
+ region: Literal["us-east", "eu-west"]
+ """Geographic region of the browser session. Fixed once the session is created."""
+
session_id: str
"""Unique identifier for the browser session"""
diff --git a/tests/api_resources/test_browser_pools.py b/tests/api_resources/test_browser_pools.py
index a85d5be7..8922c801 100644
--- a/tests/api_resources/test_browser_pools.py
+++ b/tests/api_resources/test_browser_pools.py
@@ -51,6 +51,7 @@ def test_method_create_with_all_params(self, client: Kernel) -> None:
},
proxy_id="proxy_id",
refresh_on_profile_update=True,
+ region="us-east",
start_url="https://example.com",
stealth=True,
telemetry={
@@ -267,6 +268,7 @@ def test_method_list_with_all_params(self, client: Kernel) -> None:
name="name",
offset=0,
query="query",
+ region="us-east",
)
assert_matches_type(SyncOffsetPagination[BrowserPool], browser_pool, path=["response"])
@@ -557,6 +559,7 @@ async def test_method_create_with_all_params(self, async_client: AsyncKernel) ->
},
proxy_id="proxy_id",
refresh_on_profile_update=True,
+ region="us-east",
start_url="https://example.com",
stealth=True,
telemetry={
@@ -773,6 +776,7 @@ async def test_method_list_with_all_params(self, async_client: AsyncKernel) -> N
name="name",
offset=0,
query="query",
+ region="us-east",
)
assert_matches_type(AsyncOffsetPagination[BrowserPool], browser_pool, path=["response"])
diff --git a/tests/api_resources/test_browsers.py b/tests/api_resources/test_browsers.py
index 8b0a6182..3fd4c6dc 100644
--- a/tests/api_resources/test_browsers.py
+++ b/tests/api_resources/test_browsers.py
@@ -57,6 +57,7 @@ def test_method_create_with_all_params(self, client: Kernel) -> None:
"name": "x",
},
proxy_id="proxy_id",
+ region="us-east",
start_url="https://example.com",
stealth=True,
tags={
@@ -278,6 +279,7 @@ def test_method_list_with_all_params(self, client: Kernel) -> None:
limit=1,
offset=0,
query="query",
+ region="us-east",
status="active",
tags={"foo": "string"},
)
@@ -512,6 +514,7 @@ async def test_method_create_with_all_params(self, async_client: AsyncKernel) ->
"name": "x",
},
proxy_id="proxy_id",
+ region="us-east",
start_url="https://example.com",
stealth=True,
tags={
@@ -733,6 +736,7 @@ async def test_method_list_with_all_params(self, async_client: AsyncKernel) -> N
limit=1,
offset=0,
query="query",
+ region="us-east",
status="active",
tags={"foo": "string"},
)
From b30c0e31182f09247bbf32823c256e27725fdbfc Mon Sep 17 00:00:00 2001
From: "kernel-internal[bot]"
<260533166+kernel-internal[bot]@users.noreply.github.com>
Date: Wed, 12 Aug 2026 16:04:42 +0000
Subject: [PATCH 187/366] feat: Add typed network config with private_hosts to
browsers and pools
Stainless-Generated-From: d10c7305e30fe0869a9027e670f3b6ca1121870c
---
README.md | 4 +-
api.md | 1 +
src/kernel/resources/browser_pools.py | 25 ++++++++++++
src/kernel/resources/browsers/browsers.py | 9 +++++
src/kernel/types/__init__.py | 2 +
src/kernel/types/browser_create_params.py | 7 ++++
src/kernel/types/browser_create_response.py | 7 ++++
src/kernel/types/browser_list_response.py | 7 ++++
src/kernel/types/browser_network_config.py | 37 ++++++++++++++++++
.../types/browser_network_config_param.py | 39 +++++++++++++++++++
src/kernel/types/browser_pool.py | 7 ++++
.../types/browser_pool_acquire_response.py | 7 ++++
.../types/browser_pool_create_params.py | 4 ++
.../types/browser_pool_update_params.py | 10 +++++
src/kernel/types/browser_retrieve_response.py | 7 ++++
src/kernel/types/browser_update_response.py | 7 ++++
.../invocation_list_browsers_response.py | 7 ++++
tests/api_resources/test_browser_pools.py | 4 ++
tests/api_resources/test_browsers.py | 2 +
19 files changed, 191 insertions(+), 2 deletions(-)
create mode 100644 src/kernel/types/browser_network_config.py
create mode 100644 src/kernel/types/browser_network_config_param.py
diff --git a/README.md b/README.md
index 2e8ed560..35a3bc41 100644
--- a/README.md
+++ b/README.md
@@ -203,9 +203,9 @@ from kernel import Kernel
client = Kernel()
browser = client.browsers.create(
- profile={},
+ network={},
)
-print(browser.profile)
+print(browser.network)
```
## File uploads
diff --git a/api.md b/api.md
index 65ac4c06..b991db92 100644
--- a/api.md
+++ b/api.md
@@ -80,6 +80,7 @@ Types:
```python
from kernel.types import (
+ BrowserNetworkConfig,
BrowserPoolRef,
BrowserProxy,
BrowserProxyConfig,
diff --git a/src/kernel/resources/browser_pools.py b/src/kernel/resources/browser_pools.py
index 18eafcfe..99bda961 100644
--- a/src/kernel/resources/browser_pools.py
+++ b/src/kernel/resources/browser_pools.py
@@ -29,6 +29,7 @@
from .._base_client import AsyncPaginator, make_request_options
from ..types.tags_param import TagsParam
from ..types.browser_pool import BrowserPool
+from ..types.browser_network_config_param import BrowserNetworkConfigParam
from ..types.browser_pool_acquire_response import BrowserPoolAcquireResponse
from ..types.shared_params.browser_viewport import BrowserViewport
from ..types.shared_params.browser_extension import BrowserExtension
@@ -68,6 +69,7 @@ def create(
headless: bool | Omit = omit,
kiosk_mode: bool | Omit = omit,
name: str | Omit = omit,
+ network: BrowserNetworkConfigParam | Omit = omit,
profile: browser_pool_create_params.Profile | Omit = omit,
proxy_id: str | Omit = omit,
refresh_on_profile_update: bool | Omit = omit,
@@ -115,6 +117,8 @@ def create(
name: Optional name for the browser pool. Must be unique within the project.
+ network: Network configuration applied to browsers in this pool.
+
profile: Profile configuration for browsers in a pool. Provide either id or name.
Profiles must be created beforehand. Unlike single browser sessions, pools load
the profile read-only and never persist changes back to it, so save_changes is
@@ -183,6 +187,7 @@ def create(
"headless": headless,
"kiosk_mode": kiosk_mode,
"name": name,
+ "network": network,
"profile": profile,
"proxy_id": proxy_id,
"refresh_on_profile_update": refresh_on_profile_update,
@@ -245,6 +250,7 @@ def update(
headless: bool | Omit = omit,
kiosk_mode: bool | Omit = omit,
name: str | Omit = omit,
+ network: BrowserNetworkConfigParam | Omit = omit,
profile: browser_pool_update_params.Profile | Omit = omit,
proxy_id: str | Omit = omit,
refresh_on_profile_update: bool | Omit = omit,
@@ -298,6 +304,12 @@ def update(
name: If provided, replaces the pool name. Empty string is a no-op; the pool name
cannot be cleared or reset to empty once assigned.
+ network: If provided, replaces the pool's network configuration. Omit to leave the
+ existing configuration unchanged; an empty object ({}) removes it, while
+ network: {private_hosts: []} sets an explicit empty list. Only applied to
+ browsers created in the pool after the update; browsers already in the pool keep
+ their configuration until discarded (see discard_all_idle).
+
profile: Profile configuration for browsers in a pool. Provide either id or name.
Profiles must be created beforehand. Unlike single browser sessions, pools load
the profile read-only and never persist changes back to it, so save_changes is
@@ -368,6 +380,7 @@ def update(
"headless": headless,
"kiosk_mode": kiosk_mode,
"name": name,
+ "network": network,
"profile": profile,
"proxy_id": proxy_id,
"refresh_on_profile_update": refresh_on_profile_update,
@@ -686,6 +699,7 @@ async def create(
headless: bool | Omit = omit,
kiosk_mode: bool | Omit = omit,
name: str | Omit = omit,
+ network: BrowserNetworkConfigParam | Omit = omit,
profile: browser_pool_create_params.Profile | Omit = omit,
proxy_id: str | Omit = omit,
refresh_on_profile_update: bool | Omit = omit,
@@ -733,6 +747,8 @@ async def create(
name: Optional name for the browser pool. Must be unique within the project.
+ network: Network configuration applied to browsers in this pool.
+
profile: Profile configuration for browsers in a pool. Provide either id or name.
Profiles must be created beforehand. Unlike single browser sessions, pools load
the profile read-only and never persist changes back to it, so save_changes is
@@ -801,6 +817,7 @@ async def create(
"headless": headless,
"kiosk_mode": kiosk_mode,
"name": name,
+ "network": network,
"profile": profile,
"proxy_id": proxy_id,
"refresh_on_profile_update": refresh_on_profile_update,
@@ -863,6 +880,7 @@ async def update(
headless: bool | Omit = omit,
kiosk_mode: bool | Omit = omit,
name: str | Omit = omit,
+ network: BrowserNetworkConfigParam | Omit = omit,
profile: browser_pool_update_params.Profile | Omit = omit,
proxy_id: str | Omit = omit,
refresh_on_profile_update: bool | Omit = omit,
@@ -916,6 +934,12 @@ async def update(
name: If provided, replaces the pool name. Empty string is a no-op; the pool name
cannot be cleared or reset to empty once assigned.
+ network: If provided, replaces the pool's network configuration. Omit to leave the
+ existing configuration unchanged; an empty object ({}) removes it, while
+ network: {private_hosts: []} sets an explicit empty list. Only applied to
+ browsers created in the pool after the update; browsers already in the pool keep
+ their configuration until discarded (see discard_all_idle).
+
profile: Profile configuration for browsers in a pool. Provide either id or name.
Profiles must be created beforehand. Unlike single browser sessions, pools load
the profile read-only and never persist changes back to it, so save_changes is
@@ -986,6 +1010,7 @@ async def update(
"headless": headless,
"kiosk_mode": kiosk_mode,
"name": name,
+ "network": network,
"profile": profile,
"proxy_id": proxy_id,
"refresh_on_profile_update": refresh_on_profile_update,
diff --git a/src/kernel/resources/browsers/browsers.py b/src/kernel/resources/browsers/browsers.py
index 35c9d612..7e46a361 100644
--- a/src/kernel/resources/browsers/browsers.py
+++ b/src/kernel/resources/browsers/browsers.py
@@ -98,6 +98,7 @@
from ...types.browser_update_response import BrowserUpdateResponse
from ...types.browser_retrieve_response import BrowserRetrieveResponse
from ...types.browser_proxy_config_param import BrowserProxyConfigParam
+from ...types.browser_network_config_param import BrowserNetworkConfigParam
from ...types.shared_params.browser_profile import BrowserProfile
from ...types.shared_params.browser_viewport import BrowserViewport
from ...types.shared_params.browser_extension import BrowserExtension
@@ -172,6 +173,7 @@ def create(
invocation_id: str | Omit = omit,
kiosk_mode: bool | Omit = omit,
name: str | Omit = omit,
+ network: BrowserNetworkConfigParam | Omit = omit,
profile: BrowserProfile | Omit = omit,
proxy: BrowserProxyConfigParam | Omit = omit,
proxy_id: str | Omit = omit,
@@ -215,6 +217,8 @@ def create(
the dashboard. Must be unique among active sessions within the project. Can be
changed later via PATCH /browsers/{id_or_name}.
+ network: Network configuration for the browser session. Cannot be changed after creation.
+
profile: Profile selection for the browser session. Provide either id or name. If
specified, the matching profile will be loaded into the browser session.
Profiles must be created beforehand.
@@ -292,6 +296,7 @@ def create(
"invocation_id": invocation_id,
"kiosk_mode": kiosk_mode,
"name": name,
+ "network": network,
"profile": profile,
"proxy": proxy,
"proxy_id": proxy_id,
@@ -782,6 +787,7 @@ async def create(
invocation_id: str | Omit = omit,
kiosk_mode: bool | Omit = omit,
name: str | Omit = omit,
+ network: BrowserNetworkConfigParam | Omit = omit,
profile: BrowserProfile | Omit = omit,
proxy: BrowserProxyConfigParam | Omit = omit,
proxy_id: str | Omit = omit,
@@ -825,6 +831,8 @@ async def create(
the dashboard. Must be unique among active sessions within the project. Can be
changed later via PATCH /browsers/{id_or_name}.
+ network: Network configuration for the browser session. Cannot be changed after creation.
+
profile: Profile selection for the browser session. Provide either id or name. If
specified, the matching profile will be loaded into the browser session.
Profiles must be created beforehand.
@@ -902,6 +910,7 @@ async def create(
"invocation_id": invocation_id,
"kiosk_mode": kiosk_mode,
"name": name,
+ "network": network,
"profile": profile,
"proxy": proxy,
"proxy_id": proxy_id,
diff --git a/src/kernel/types/__init__.py b/src/kernel/types/__init__.py
index e86ac9c4..13c85213 100644
--- a/src/kernel/types/__init__.py
+++ b/src/kernel/types/__init__.py
@@ -58,6 +58,7 @@
from .project_update_params import ProjectUpdateParams as ProjectUpdateParams
from .proxy_create_response import ProxyCreateResponse as ProxyCreateResponse
from .proxy_update_response import ProxyUpdateResponse as ProxyUpdateResponse
+from .browser_network_config import BrowserNetworkConfig as BrowserNetworkConfig
from .credential_list_params import CredentialListParams as CredentialListParams
from .deployment_list_params import DeploymentListParams as DeploymentListParams
from .deployment_state_event import DeploymentStateEvent as DeploymentStateEvent
@@ -96,6 +97,7 @@
from .invocation_update_response import InvocationUpdateResponse as InvocationUpdateResponse
from .browser_pool_acquire_params import BrowserPoolAcquireParams as BrowserPoolAcquireParams
from .browser_pool_release_params import BrowserPoolReleaseParams as BrowserPoolReleaseParams
+from .browser_network_config_param import BrowserNetworkConfigParam as BrowserNetworkConfigParam
from .deployment_retrieve_response import DeploymentRetrieveResponse as DeploymentRetrieveResponse
from .invocation_retrieve_response import InvocationRetrieveResponse as InvocationRetrieveResponse
from .audit_log_export_chunk_params import AuditLogExportChunkParams as AuditLogExportChunkParams
diff --git a/src/kernel/types/browser_create_params.py b/src/kernel/types/browser_create_params.py
index e30878ec..0c280602 100644
--- a/src/kernel/types/browser_create_params.py
+++ b/src/kernel/types/browser_create_params.py
@@ -7,6 +7,7 @@
from .tags_param import TagsParam
from .browser_proxy_config_param import BrowserProxyConfigParam
+from .browser_network_config_param import BrowserNetworkConfigParam
from .shared_params.browser_profile import BrowserProfile
from .shared_params.browser_viewport import BrowserViewport
from .shared_params.browser_extension import BrowserExtension
@@ -64,6 +65,12 @@ class BrowserCreateParams(TypedDict, total=False):
changed later via PATCH /browsers/{id_or_name}.
"""
+ network: BrowserNetworkConfigParam
+ """Network configuration for the browser session.
+
+ Cannot be changed after creation.
+ """
+
profile: BrowserProfile
"""Profile selection for the browser session.
diff --git a/src/kernel/types/browser_create_response.py b/src/kernel/types/browser_create_response.py
index 429151df..f8d625e9 100644
--- a/src/kernel/types/browser_create_response.py
+++ b/src/kernel/types/browser_create_response.py
@@ -10,6 +10,7 @@
from .browser_proxy import BrowserProxy
from .browser_usage import BrowserUsage
from .browser_pool_ref import BrowserPoolRef
+from .browser_network_config import BrowserNetworkConfig
from .shared.browser_viewport import BrowserViewport
from .browsers.browser_telemetry_config import BrowserTelemetryConfig
@@ -72,6 +73,12 @@ class BrowserCreateResponse(BaseModel):
name: Optional[str] = None
"""Human-readable name of the browser session, if one was set at creation."""
+ network: Optional[BrowserNetworkConfig] = None
+ """Network configuration the session was created with, if any.
+
+ Omitted when the session has no network configuration.
+ """
+
pool: Optional[BrowserPoolRef] = None
"""Browser pool this session was acquired from, if any."""
diff --git a/src/kernel/types/browser_list_response.py b/src/kernel/types/browser_list_response.py
index 42d261a3..108bbef2 100644
--- a/src/kernel/types/browser_list_response.py
+++ b/src/kernel/types/browser_list_response.py
@@ -10,6 +10,7 @@
from .browser_proxy import BrowserProxy
from .browser_usage import BrowserUsage
from .browser_pool_ref import BrowserPoolRef
+from .browser_network_config import BrowserNetworkConfig
from .shared.browser_viewport import BrowserViewport
from .browsers.browser_telemetry_config import BrowserTelemetryConfig
@@ -72,6 +73,12 @@ class BrowserListResponse(BaseModel):
name: Optional[str] = None
"""Human-readable name of the browser session, if one was set at creation."""
+ network: Optional[BrowserNetworkConfig] = None
+ """Network configuration the session was created with, if any.
+
+ Omitted when the session has no network configuration.
+ """
+
pool: Optional[BrowserPoolRef] = None
"""Browser pool this session was acquired from, if any."""
diff --git a/src/kernel/types/browser_network_config.py b/src/kernel/types/browser_network_config.py
new file mode 100644
index 00000000..e1941816
--- /dev/null
+++ b/src/kernel/types/browser_network_config.py
@@ -0,0 +1,37 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from typing import List, Optional
+
+from .._models import BaseModel
+
+__all__ = ["BrowserNetworkConfig"]
+
+
+class BrowserNetworkConfig(BaseModel):
+ """Network configuration for a browser session or browser pool."""
+
+ private_hosts: Optional[List[str]] = None
+ """
+ Destinations the browser reaches directly through the session's own network
+ instead of through Kernel-managed egress — for private hosts reachable over a
+ VPN or tunnel the session has joined (e.g. a Tailscale tailnet). By default,
+ private IP ranges already route directly: RFC1918 (10.0.0.0/8, 172.16.0.0/12,
+ 192.168.0.0/16), CGNAT/Tailscale (100.64.0.0/10), and IPv6 ULA (fc00::/7). An
+ explicitly supplied list replaces those defaults with exactly the entries given,
+ and an empty list ([]) disables them so all traffic uses Kernel-managed egress;
+ omit private_hosts to keep the defaults. Entries are hostname patterns
+ ("_.example.ts.net", "preview.internal") or IP/CIDR literals ("100.64.0.0/10",
+ "10.1.30.63"). IP and CIDR entries only match URLs written with a literal IP
+ address; they never match hostnames that resolve into the range, so private DNS
+ names need a hostname entry even when they resolve inside the default ranges.
+ CIDRs must be in canonical masked form (host bits zero), and only the private
+ ranges listed above are accepted; public, loopback, link-local, and unspecified
+ ranges are rejected. Exact IPv6 addresses must be bracketed ("[fd00::1]"); IPv6
+ CIDR ranges are unbracketed ("fd00::/8"). Wildcards are limited to one leading
+ "_." over a suffix with at least two labels that is not a public suffix (so
+ "_.co.uk" or "_.ts.net" are rejected, while "\\**.example.ts.net" is accepted).
+ Hostname and IP entries may carry a port; CIDR ranges may not. Hostname entries
+ are not resolved during validation, so callers must ensure they identify private
+ destinations. Not related to a proxy's bypass_hosts, which selects between
+ upstream-proxy and Kernel-managed direct egress and cannot reach into a VPN.
+ """
diff --git a/src/kernel/types/browser_network_config_param.py b/src/kernel/types/browser_network_config_param.py
new file mode 100644
index 00000000..49f7ea64
--- /dev/null
+++ b/src/kernel/types/browser_network_config_param.py
@@ -0,0 +1,39 @@
+# 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__ = ["BrowserNetworkConfigParam"]
+
+
+class BrowserNetworkConfigParam(TypedDict, total=False):
+ """Network configuration for a browser session or browser pool."""
+
+ private_hosts: SequenceNotStr[str]
+ """
+ Destinations the browser reaches directly through the session's own network
+ instead of through Kernel-managed egress — for private hosts reachable over a
+ VPN or tunnel the session has joined (e.g. a Tailscale tailnet). By default,
+ private IP ranges already route directly: RFC1918 (10.0.0.0/8, 172.16.0.0/12,
+ 192.168.0.0/16), CGNAT/Tailscale (100.64.0.0/10), and IPv6 ULA (fc00::/7). An
+ explicitly supplied list replaces those defaults with exactly the entries given,
+ and an empty list ([]) disables them so all traffic uses Kernel-managed egress;
+ omit private_hosts to keep the defaults. Entries are hostname patterns
+ ("_.example.ts.net", "preview.internal") or IP/CIDR literals ("100.64.0.0/10",
+ "10.1.30.63"). IP and CIDR entries only match URLs written with a literal IP
+ address; they never match hostnames that resolve into the range, so private DNS
+ names need a hostname entry even when they resolve inside the default ranges.
+ CIDRs must be in canonical masked form (host bits zero), and only the private
+ ranges listed above are accepted; public, loopback, link-local, and unspecified
+ ranges are rejected. Exact IPv6 addresses must be bracketed ("[fd00::1]"); IPv6
+ CIDR ranges are unbracketed ("fd00::/8"). Wildcards are limited to one leading
+ "_." over a suffix with at least two labels that is not a public suffix (so
+ "_.co.uk" or "_.ts.net" are rejected, while "\\**.example.ts.net" is accepted).
+ Hostname and IP entries may carry a port; CIDR ranges may not. Hostname entries
+ are not resolved during validation, so callers must ensure they identify private
+ destinations. Not related to a proxy's bypass_hosts, which selects between
+ upstream-proxy and Kernel-managed direct egress and cannot reach into a VPN.
+ """
diff --git a/src/kernel/types/browser_pool.py b/src/kernel/types/browser_pool.py
index 6f5a2052..4504fc5d 100644
--- a/src/kernel/types/browser_pool.py
+++ b/src/kernel/types/browser_pool.py
@@ -5,6 +5,7 @@
from typing_extensions import Literal
from .._models import BaseModel
+from .browser_network_config import BrowserNetworkConfig
from .shared.browser_viewport import BrowserViewport
from .shared.browser_extension import BrowserExtension
from .browsers.browser_telemetry_config import BrowserTelemetryConfig
@@ -75,6 +76,12 @@ class BrowserPoolConfig(BaseModel):
name: Optional[str] = None
"""Optional name for the browser pool. Must be unique within the project."""
+ network: Optional[BrowserNetworkConfig] = None
+ """Network configuration applied to browsers in this pool, if any.
+
+ Omitted when the pool has no network configuration.
+ """
+
profile: Optional[BrowserPoolConfigProfile] = None
"""Profile configuration for browsers in a pool.
diff --git a/src/kernel/types/browser_pool_acquire_response.py b/src/kernel/types/browser_pool_acquire_response.py
index 7096b9f6..720dc569 100644
--- a/src/kernel/types/browser_pool_acquire_response.py
+++ b/src/kernel/types/browser_pool_acquire_response.py
@@ -10,6 +10,7 @@
from .browser_proxy import BrowserProxy
from .browser_usage import BrowserUsage
from .browser_pool_ref import BrowserPoolRef
+from .browser_network_config import BrowserNetworkConfig
from .shared.browser_viewport import BrowserViewport
from .browsers.browser_telemetry_config import BrowserTelemetryConfig
@@ -72,6 +73,12 @@ class BrowserPoolAcquireResponse(BaseModel):
name: Optional[str] = None
"""Human-readable name of the browser session, if one was set at creation."""
+ network: Optional[BrowserNetworkConfig] = None
+ """Network configuration the session was created with, if any.
+
+ Omitted when the session has no network configuration.
+ """
+
pool: Optional[BrowserPoolRef] = None
"""Browser pool this session was acquired from, if any."""
diff --git a/src/kernel/types/browser_pool_create_params.py b/src/kernel/types/browser_pool_create_params.py
index 123fb172..474e4243 100644
--- a/src/kernel/types/browser_pool_create_params.py
+++ b/src/kernel/types/browser_pool_create_params.py
@@ -5,6 +5,7 @@
from typing import Dict, Iterable, Optional
from typing_extensions import Literal, Required, TypedDict
+from .browser_network_config_param import BrowserNetworkConfigParam
from .shared_params.browser_viewport import BrowserViewport
from .shared_params.browser_extension import BrowserExtension
from .browsers.browser_telemetry_categories_config_param import BrowserTelemetryCategoriesConfigParam
@@ -61,6 +62,9 @@ class BrowserPoolCreateParams(TypedDict, total=False):
name: str
"""Optional name for the browser pool. Must be unique within the project."""
+ network: BrowserNetworkConfigParam
+ """Network configuration applied to browsers in this pool."""
+
profile: Profile
"""Profile configuration for browsers in a pool.
diff --git a/src/kernel/types/browser_pool_update_params.py b/src/kernel/types/browser_pool_update_params.py
index 069181c3..36693003 100644
--- a/src/kernel/types/browser_pool_update_params.py
+++ b/src/kernel/types/browser_pool_update_params.py
@@ -5,6 +5,7 @@
from typing import Dict, Iterable, Optional
from typing_extensions import TypedDict
+from .browser_network_config_param import BrowserNetworkConfigParam
from .shared_params.browser_viewport import BrowserViewport
from .shared_params.browser_extension import BrowserExtension
from .browsers.browser_telemetry_categories_config_param import BrowserTelemetryCategoriesConfigParam
@@ -67,6 +68,15 @@ class BrowserPoolUpdateParams(TypedDict, total=False):
assigned.
"""
+ network: BrowserNetworkConfigParam
+ """If provided, replaces the pool's network configuration.
+
+ Omit to leave the existing configuration unchanged; an empty object ({}) removes
+ it, while network: {private_hosts: []} sets an explicit empty list. Only applied
+ to browsers created in the pool after the update; browsers already in the pool
+ keep their configuration until discarded (see discard_all_idle).
+ """
+
profile: Profile
"""Profile configuration for browsers in a pool.
diff --git a/src/kernel/types/browser_retrieve_response.py b/src/kernel/types/browser_retrieve_response.py
index e7459a0c..8e39a607 100644
--- a/src/kernel/types/browser_retrieve_response.py
+++ b/src/kernel/types/browser_retrieve_response.py
@@ -10,6 +10,7 @@
from .browser_proxy import BrowserProxy
from .browser_usage import BrowserUsage
from .browser_pool_ref import BrowserPoolRef
+from .browser_network_config import BrowserNetworkConfig
from .shared.browser_viewport import BrowserViewport
from .browsers.browser_telemetry_config import BrowserTelemetryConfig
@@ -72,6 +73,12 @@ class BrowserRetrieveResponse(BaseModel):
name: Optional[str] = None
"""Human-readable name of the browser session, if one was set at creation."""
+ network: Optional[BrowserNetworkConfig] = None
+ """Network configuration the session was created with, if any.
+
+ Omitted when the session has no network configuration.
+ """
+
pool: Optional[BrowserPoolRef] = None
"""Browser pool this session was acquired from, if any."""
diff --git a/src/kernel/types/browser_update_response.py b/src/kernel/types/browser_update_response.py
index 4cfe18d4..c04f43f8 100644
--- a/src/kernel/types/browser_update_response.py
+++ b/src/kernel/types/browser_update_response.py
@@ -10,6 +10,7 @@
from .browser_proxy import BrowserProxy
from .browser_usage import BrowserUsage
from .browser_pool_ref import BrowserPoolRef
+from .browser_network_config import BrowserNetworkConfig
from .shared.browser_viewport import BrowserViewport
from .browsers.browser_telemetry_config import BrowserTelemetryConfig
@@ -72,6 +73,12 @@ class BrowserUpdateResponse(BaseModel):
name: Optional[str] = None
"""Human-readable name of the browser session, if one was set at creation."""
+ network: Optional[BrowserNetworkConfig] = None
+ """Network configuration the session was created with, if any.
+
+ Omitted when the session has no network configuration.
+ """
+
pool: Optional[BrowserPoolRef] = None
"""Browser pool this session was acquired from, if any."""
diff --git a/src/kernel/types/invocation_list_browsers_response.py b/src/kernel/types/invocation_list_browsers_response.py
index caedb6ea..bcf8dd40 100644
--- a/src/kernel/types/invocation_list_browsers_response.py
+++ b/src/kernel/types/invocation_list_browsers_response.py
@@ -10,6 +10,7 @@
from .browser_proxy import BrowserProxy
from .browser_usage import BrowserUsage
from .browser_pool_ref import BrowserPoolRef
+from .browser_network_config import BrowserNetworkConfig
from .shared.browser_viewport import BrowserViewport
from .browsers.browser_telemetry_config import BrowserTelemetryConfig
@@ -72,6 +73,12 @@ class Browser(BaseModel):
name: Optional[str] = None
"""Human-readable name of the browser session, if one was set at creation."""
+ network: Optional[BrowserNetworkConfig] = None
+ """Network configuration the session was created with, if any.
+
+ Omitted when the session has no network configuration.
+ """
+
pool: Optional[BrowserPoolRef] = None
"""Browser pool this session was acquired from, if any."""
diff --git a/tests/api_resources/test_browser_pools.py b/tests/api_resources/test_browser_pools.py
index 8922c801..e411df27 100644
--- a/tests/api_resources/test_browser_pools.py
+++ b/tests/api_resources/test_browser_pools.py
@@ -45,6 +45,7 @@ def test_method_create_with_all_params(self, client: Kernel) -> None:
headless=False,
kiosk_mode=True,
name="my-pool",
+ network={"private_hosts": ["*.example.ts.net", "100.64.0.0/10"]},
profile={
"id": "id",
"name": "name",
@@ -179,6 +180,7 @@ def test_method_update_with_all_params(self, client: Kernel) -> None:
headless=False,
kiosk_mode=True,
name="my-pool",
+ network={"private_hosts": ["*.example.ts.net", "100.64.0.0/10"]},
profile={
"id": "id",
"name": "name",
@@ -553,6 +555,7 @@ async def test_method_create_with_all_params(self, async_client: AsyncKernel) ->
headless=False,
kiosk_mode=True,
name="my-pool",
+ network={"private_hosts": ["*.example.ts.net", "100.64.0.0/10"]},
profile={
"id": "id",
"name": "name",
@@ -687,6 +690,7 @@ async def test_method_update_with_all_params(self, async_client: AsyncKernel) ->
headless=False,
kiosk_mode=True,
name="my-pool",
+ network={"private_hosts": ["*.example.ts.net", "100.64.0.0/10"]},
profile={
"id": "id",
"name": "name",
diff --git a/tests/api_resources/test_browsers.py b/tests/api_resources/test_browsers.py
index 3fd4c6dc..ae017186 100644
--- a/tests/api_resources/test_browsers.py
+++ b/tests/api_resources/test_browsers.py
@@ -46,6 +46,7 @@ def test_method_create_with_all_params(self, client: Kernel) -> None:
invocation_id="rr33xuugxj9h0bkf1rdt2bet",
kiosk_mode=True,
name="checkout-flow-1",
+ network={"private_hosts": ["*.example.ts.net", "100.64.0.0/10"]},
profile={
"id": "id",
"name": "name",
@@ -503,6 +504,7 @@ async def test_method_create_with_all_params(self, async_client: AsyncKernel) ->
invocation_id="rr33xuugxj9h0bkf1rdt2bet",
kiosk_mode=True,
name="checkout-flow-1",
+ network={"private_hosts": ["*.example.ts.net", "100.64.0.0/10"]},
profile={
"id": "id",
"name": "name",
From f202d649655977511bf180a998c7aac8fa940914 Mon Sep 17 00:00:00 2001
From: "kernel-internal[bot]"
<260533166+kernel-internal[bot]@users.noreply.github.com>
Date: Wed, 12 Aug 2026 16:09:00 +0000
Subject: [PATCH 188/366] release: 0.89.0
---
.release-please-manifest.json | 2 +-
CHANGELOG.md | 9 +++++++++
pyproject.toml | 2 +-
src/kernel/_version.py | 2 +-
4 files changed, 12 insertions(+), 3 deletions(-)
diff --git a/.release-please-manifest.json b/.release-please-manifest.json
index d80a91e2..6af24e3d 100644
--- a/.release-please-manifest.json
+++ b/.release-please-manifest.json
@@ -1,3 +1,3 @@
{
- ".": "0.88.0"
+ ".": "0.89.0"
}
\ No newline at end of file
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 9137ac6f..92fb5212 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,14 @@
# Changelog
+## [0.89.0](https://github.com/kernel/kernel-python-sdk/compare/v0.88.0...v0.89.0) (2026-08-12)
+
+
+### Features
+
+* Add region as a first-class API field with plan and flag gating ([fdb020d](https://github.com/kernel/kernel-python-sdk/commit/fdb020d437b3b410b2d5758691f2818d5e744580))
+* Add typed network config with private_hosts to browsers and pools ([b30c0e3](https://github.com/kernel/kernel-python-sdk/commit/b30c0e31182f09247bbf32823c256e27725fdbfc))
+* Expose plan-derived auth limits on GET /org/limits ([9ff1b44](https://github.com/kernel/kernel-python-sdk/commit/9ff1b445f8f2bb3f9bb24f613e22c8b940fc1d5a))
+
## [0.88.0](https://github.com/kernel/kernel-python-sdk/compare/v0.87.0...v0.88.0) (2026-08-10)
### Features
diff --git a/pyproject.toml b/pyproject.toml
index db2cb8e0..57df3c54 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -1,6 +1,6 @@
[project]
name = "kernel"
-version = "0.88.0"
+version = "0.89.0"
description = "The official Python library for the kernel API"
dynamic = ["readme"]
license = "Apache-2.0"
diff --git a/src/kernel/_version.py b/src/kernel/_version.py
index 892c18cb..e4082c56 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.88.0" # x-release-please-version
+__version__ = "0.89.0" # x-release-please-version
From 67970b2b8cc0312b81d10e6fec6c3a016c79d347 Mon Sep 17 00:00:00 2001
From: "kernel-internal[bot]"
<260533166+kernel-internal[bot]@users.noreply.github.com>
Date: Wed, 12 Aug 2026 17:15:11 +0000
Subject: [PATCH 189/366] feat: Support project selection by ID or name
Stainless-Generated-From: edf469a448508ce7f57fbabeded1c15af42c3d8c
---
api.md | 10 ++--
src/kernel/_client.py | 14 ++++++
src/kernel/resources/projects/limits.py | 32 ++++++------
src/kernel/resources/projects/projects.py | 56 ++++++++++-----------
src/kernel/types/project_create_params.py | 2 +-
src/kernel/types/project_update_params.py | 2 +-
tests/api_resources/projects/test_limits.py | 40 +++++++--------
tests/api_resources/test_projects.py | 56 ++++++++++-----------
8 files changed, 113 insertions(+), 99 deletions(-)
diff --git a/api.md b/api.md
index b991db92..d442a3d0 100644
--- a/api.md
+++ b/api.md
@@ -432,10 +432,10 @@ from kernel.types import CreateProjectRequest, Project, UpdateProjectRequest
Methods:
- client.projects.create(\*\*params) -> Project
-- client.projects.retrieve(id) -> Project
-- client.projects.update(id, \*\*params) -> Project
+- client.projects.retrieve(id_or_name) -> Project
+- client.projects.update(id_or_name, \*\*params) -> Project
- client.projects.list(\*\*params) -> SyncOffsetPagination[Project]
-- client.projects.delete(id) -> None
+- client.projects.delete(id_or_name) -> None
## Limits
@@ -447,8 +447,8 @@ from kernel.types.projects import ProjectLimits, UpdateProjectLimitsRequest
Methods:
-- client.projects.limits.retrieve(id) -> ProjectLimits
-- client.projects.limits.update(id, \*\*params) -> ProjectLimits
+- client.projects.limits.retrieve(id_or_name) -> ProjectLimits
+- client.projects.limits.update(id_or_name, \*\*params) -> ProjectLimits
# Organization
diff --git a/src/kernel/_client.py b/src/kernel/_client.py
index 62bd2014..eb4d1af1 100644
--- a/src/kernel/_client.py
+++ b/src/kernel/_client.py
@@ -103,6 +103,7 @@ class Kernel(SyncAPIClient):
browser_route_cache: BrowserRouteCache
project_id: str | None
+ project: str | None
_environment: Literal["production", "development"] | NotGiven
_browser_routing: BrowserRoutingConfig
@@ -112,6 +113,7 @@ def __init__(
*,
api_key: str | None = None,
project_id: str | None = None,
+ project: str | None = None,
environment: Literal["production", "development"] | NotGiven = not_given,
base_url: str | httpx.URL | None | NotGiven = not_given,
timeout: float | Timeout | None | NotGiven = not_given,
@@ -147,6 +149,8 @@ def __init__(
self.project_id = project_id
+ self.project = project
+
self._environment = environment
base_url_env = os.environ.get("KERNEL_BASE_URL")
@@ -328,6 +332,7 @@ def default_headers(self) -> dict[str, str | Omit]:
**super().default_headers,
"X-Stainless-Async": "false",
"X-Kernel-Project-Id": self.project_id if self.project_id is not None else Omit(),
+ "X-Kernel-Project": self.project if self.project is not None else Omit(),
**self._custom_headers,
}
@@ -367,6 +372,7 @@ def copy(
*,
api_key: str | None = None,
project_id: str | None = None,
+ project: str | None = None,
environment: Literal["production", "development"] | None = None,
base_url: str | httpx.URL | None = None,
timeout: float | Timeout | None | NotGiven = not_given,
@@ -404,6 +410,7 @@ def copy(
return self.__class__(
api_key=api_key or self.api_key,
project_id=project_id or self.project_id,
+ project=project or self.project,
base_url=base_url or self.base_url,
environment=environment or self._environment,
timeout=self.timeout if isinstance(timeout, NotGiven) else timeout,
@@ -459,6 +466,7 @@ class AsyncKernel(AsyncAPIClient):
browser_route_cache: BrowserRouteCache
project_id: str | None
+ project: str | None
_environment: Literal["production", "development"] | NotGiven
_browser_routing: BrowserRoutingConfig
@@ -468,6 +476,7 @@ def __init__(
*,
api_key: str | None = None,
project_id: str | None = None,
+ project: str | None = None,
environment: Literal["production", "development"] | NotGiven = not_given,
base_url: str | httpx.URL | None | NotGiven = not_given,
timeout: float | Timeout | None | NotGiven = not_given,
@@ -503,6 +512,8 @@ def __init__(
self.project_id = project_id
+ self.project = project
+
self._environment = environment
base_url_env = os.environ.get("KERNEL_BASE_URL")
@@ -684,6 +695,7 @@ def default_headers(self) -> dict[str, str | Omit]:
**super().default_headers,
"X-Stainless-Async": f"async:{get_async_library()}",
"X-Kernel-Project-Id": self.project_id if self.project_id is not None else Omit(),
+ "X-Kernel-Project": self.project if self.project is not None else Omit(),
**self._custom_headers,
}
@@ -723,6 +735,7 @@ def copy(
*,
api_key: str | None = None,
project_id: str | None = None,
+ project: str | None = None,
environment: Literal["production", "development"] | None = None,
base_url: str | httpx.URL | None = None,
timeout: float | Timeout | None | NotGiven = not_given,
@@ -760,6 +773,7 @@ def copy(
return self.__class__(
api_key=api_key or self.api_key,
project_id=project_id or self.project_id,
+ project=project or self.project,
base_url=base_url or self.base_url,
environment=environment or self._environment,
timeout=self.timeout if isinstance(timeout, NotGiven) else timeout,
diff --git a/src/kernel/resources/projects/limits.py b/src/kernel/resources/projects/limits.py
index 87227768..ef95b4d0 100644
--- a/src/kernel/resources/projects/limits.py
+++ b/src/kernel/resources/projects/limits.py
@@ -51,7 +51,7 @@ def with_streaming_response(self) -> LimitsResourceWithStreamingResponse:
def retrieve(
self,
- id: str,
+ id_or_name: str,
*,
# Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
# The extra values given here take precedence over values defined on the client or passed to this method.
@@ -74,10 +74,10 @@ def retrieve(
timeout: Override the client-level default timeout for this request, in seconds
"""
- if not id:
- raise ValueError(f"Expected a non-empty value for `id` but received {id!r}")
+ if not id_or_name:
+ raise ValueError(f"Expected a non-empty value for `id_or_name` but received {id_or_name!r}")
return self._get(
- path_template("/org/projects/{id}/limits", id=id),
+ path_template("/org/projects/{id_or_name}/limits", id_or_name=id_or_name),
options=make_request_options(
extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
),
@@ -86,7 +86,7 @@ def retrieve(
def update(
self,
- id: str,
+ id_or_name: str,
*,
max_concurrent_invocations: Optional[int] | Omit = omit,
max_concurrent_sessions: Optional[int] | Omit = omit,
@@ -123,10 +123,10 @@ def update(
timeout: Override the client-level default timeout for this request, in seconds
"""
- if not id:
- raise ValueError(f"Expected a non-empty value for `id` but received {id!r}")
+ if not id_or_name:
+ raise ValueError(f"Expected a non-empty value for `id_or_name` but received {id_or_name!r}")
return self._patch(
- path_template("/org/projects/{id}/limits", id=id),
+ path_template("/org/projects/{id_or_name}/limits", id_or_name=id_or_name),
body=maybe_transform(
{
"max_concurrent_invocations": max_concurrent_invocations,
@@ -170,7 +170,7 @@ def with_streaming_response(self) -> AsyncLimitsResourceWithStreamingResponse:
async def retrieve(
self,
- id: str,
+ id_or_name: str,
*,
# Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
# The extra values given here take precedence over values defined on the client or passed to this method.
@@ -193,10 +193,10 @@ async def retrieve(
timeout: Override the client-level default timeout for this request, in seconds
"""
- if not id:
- raise ValueError(f"Expected a non-empty value for `id` but received {id!r}")
+ if not id_or_name:
+ raise ValueError(f"Expected a non-empty value for `id_or_name` but received {id_or_name!r}")
return await self._get(
- path_template("/org/projects/{id}/limits", id=id),
+ path_template("/org/projects/{id_or_name}/limits", id_or_name=id_or_name),
options=make_request_options(
extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
),
@@ -205,7 +205,7 @@ async def retrieve(
async def update(
self,
- id: str,
+ id_or_name: str,
*,
max_concurrent_invocations: Optional[int] | Omit = omit,
max_concurrent_sessions: Optional[int] | Omit = omit,
@@ -242,10 +242,10 @@ async def update(
timeout: Override the client-level default timeout for this request, in seconds
"""
- if not id:
- raise ValueError(f"Expected a non-empty value for `id` but received {id!r}")
+ if not 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(
- path_template("/org/projects/{id}/limits", id=id),
+ path_template("/org/projects/{id_or_name}/limits", id_or_name=id_or_name),
body=await async_maybe_transform(
{
"max_concurrent_invocations": max_concurrent_invocations,
diff --git a/src/kernel/resources/projects/projects.py b/src/kernel/resources/projects/projects.py
index 2c3d3569..ca519a75 100644
--- a/src/kernel/resources/projects/projects.py
+++ b/src/kernel/resources/projects/projects.py
@@ -82,7 +82,7 @@ def create(
Create a new project within the authenticated organization.
Args:
- name: Project name (1-255 Unicode code points)
+ name: Project name (1-255 Unicode code points; cannot contain `/` or `%`)
extra_headers: Send extra headers
@@ -103,7 +103,7 @@ def create(
def retrieve(
self,
- id: str,
+ id_or_name: str,
*,
# Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
# The extra values given here take precedence over values defined on the client or passed to this method.
@@ -125,10 +125,10 @@ def retrieve(
timeout: Override the client-level default timeout for this request, in seconds
"""
- if not id:
- raise ValueError(f"Expected a non-empty value for `id` but received {id!r}")
+ if not id_or_name:
+ raise ValueError(f"Expected a non-empty value for `id_or_name` but received {id_or_name!r}")
return self._get(
- path_template("/org/projects/{id}", id=id),
+ path_template("/org/projects/{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
),
@@ -137,7 +137,7 @@ def retrieve(
def update(
self,
- id: str,
+ id_or_name: str,
*,
name: str | Omit = omit,
status: Literal["active", "archived"] | Omit = omit,
@@ -152,7 +152,7 @@ def update(
Update a project's name or status.
Args:
- name: New project name (1-255 Unicode code points)
+ name: New project name (1-255 Unicode code points; cannot contain `/` or `%`)
status: New project status
@@ -164,10 +164,10 @@ def update(
timeout: Override the client-level default timeout for this request, in seconds
"""
- if not id:
- raise ValueError(f"Expected a non-empty value for `id` but received {id!r}")
+ if not id_or_name:
+ raise ValueError(f"Expected a non-empty value for `id_or_name` but received {id_or_name!r}")
return self._patch(
- path_template("/org/projects/{id}", id=id),
+ path_template("/org/projects/{id_or_name}", id_or_name=id_or_name),
body=maybe_transform(
{
"name": name,
@@ -239,7 +239,7 @@ def list(
def delete(
self,
- id: str,
+ id_or_name: str,
*,
# Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
# The extra values given here take precedence over values defined on the client or passed to this method.
@@ -261,11 +261,11 @@ def delete(
timeout: Override the client-level default timeout for this request, in seconds
"""
- if not id:
- raise ValueError(f"Expected a non-empty value for `id` but received {id!r}")
+ if not id_or_name:
+ 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(
- path_template("/org/projects/{id}", id=id),
+ path_template("/org/projects/{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
),
@@ -323,7 +323,7 @@ async def create(
Create a new project within the authenticated organization.
Args:
- name: Project name (1-255 Unicode code points)
+ name: Project name (1-255 Unicode code points; cannot contain `/` or `%`)
extra_headers: Send extra headers
@@ -344,7 +344,7 @@ async def create(
async def retrieve(
self,
- id: str,
+ id_or_name: str,
*,
# Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
# The extra values given here take precedence over values defined on the client or passed to this method.
@@ -366,10 +366,10 @@ async def retrieve(
timeout: Override the client-level default timeout for this request, in seconds
"""
- if not id:
- raise ValueError(f"Expected a non-empty value for `id` but received {id!r}")
+ if not id_or_name:
+ raise ValueError(f"Expected a non-empty value for `id_or_name` but received {id_or_name!r}")
return await self._get(
- path_template("/org/projects/{id}", id=id),
+ path_template("/org/projects/{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
),
@@ -378,7 +378,7 @@ async def retrieve(
async def update(
self,
- id: str,
+ id_or_name: str,
*,
name: str | Omit = omit,
status: Literal["active", "archived"] | Omit = omit,
@@ -393,7 +393,7 @@ async def update(
Update a project's name or status.
Args:
- name: New project name (1-255 Unicode code points)
+ name: New project name (1-255 Unicode code points; cannot contain `/` or `%`)
status: New project status
@@ -405,10 +405,10 @@ async def update(
timeout: Override the client-level default timeout for this request, in seconds
"""
- if not id:
- raise ValueError(f"Expected a non-empty value for `id` but received {id!r}")
+ if not 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(
- path_template("/org/projects/{id}", id=id),
+ path_template("/org/projects/{id_or_name}", id_or_name=id_or_name),
body=await async_maybe_transform(
{
"name": name,
@@ -480,7 +480,7 @@ def list(
async def delete(
self,
- id: str,
+ id_or_name: str,
*,
# Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
# The extra values given here take precedence over values defined on the client or passed to this method.
@@ -502,11 +502,11 @@ async def delete(
timeout: Override the client-level default timeout for this request, in seconds
"""
- if not id:
- raise ValueError(f"Expected a non-empty value for `id` but received {id!r}")
+ if not id_or_name:
+ 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(
- path_template("/org/projects/{id}", id=id),
+ path_template("/org/projects/{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/types/project_create_params.py b/src/kernel/types/project_create_params.py
index 9c51c570..664e89bc 100644
--- a/src/kernel/types/project_create_params.py
+++ b/src/kernel/types/project_create_params.py
@@ -9,4 +9,4 @@
class ProjectCreateParams(TypedDict, total=False):
name: Required[str]
- """Project name (1-255 Unicode code points)"""
+ """Project name (1-255 Unicode code points; cannot contain `/` or `%`)"""
diff --git a/src/kernel/types/project_update_params.py b/src/kernel/types/project_update_params.py
index 7b4359ff..3d93cef4 100644
--- a/src/kernel/types/project_update_params.py
+++ b/src/kernel/types/project_update_params.py
@@ -9,7 +9,7 @@
class ProjectUpdateParams(TypedDict, total=False):
name: str
- """New project name (1-255 Unicode code points)"""
+ """New project name (1-255 Unicode code points; cannot contain `/` or `%`)"""
status: Literal["active", "archived"]
"""New project status"""
diff --git a/tests/api_resources/projects/test_limits.py b/tests/api_resources/projects/test_limits.py
index a42a0bde..e8791c30 100644
--- a/tests/api_resources/projects/test_limits.py
+++ b/tests/api_resources/projects/test_limits.py
@@ -21,7 +21,7 @@ class TestLimits:
@parametrize
def test_method_retrieve(self, client: Kernel) -> None:
limit = client.projects.limits.retrieve(
- "id",
+ "id_or_name",
)
assert_matches_type(ProjectLimits, limit, path=["response"])
@@ -29,7 +29,7 @@ def test_method_retrieve(self, client: Kernel) -> None:
@parametrize
def test_raw_response_retrieve(self, client: Kernel) -> None:
response = client.projects.limits.with_raw_response.retrieve(
- "id",
+ "id_or_name",
)
assert response.is_closed is True
@@ -41,7 +41,7 @@ def test_raw_response_retrieve(self, client: Kernel) -> None:
@parametrize
def test_streaming_response_retrieve(self, client: Kernel) -> None:
with client.projects.limits.with_streaming_response.retrieve(
- "id",
+ "id_or_name",
) as response:
assert not response.is_closed
assert response.http_request.headers.get("X-Stainless-Lang") == "python"
@@ -54,7 +54,7 @@ def test_streaming_response_retrieve(self, client: Kernel) -> None:
@pytest.mark.skip(reason="Mock server tests are disabled")
@parametrize
def test_path_params_retrieve(self, client: Kernel) -> None:
- with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"):
+ with pytest.raises(ValueError, match=r"Expected a non-empty value for `id_or_name` but received ''"):
client.projects.limits.with_raw_response.retrieve(
"",
)
@@ -63,7 +63,7 @@ def test_path_params_retrieve(self, client: Kernel) -> None:
@parametrize
def test_method_update(self, client: Kernel) -> None:
limit = client.projects.limits.update(
- id="id",
+ id_or_name="id_or_name",
)
assert_matches_type(ProjectLimits, limit, path=["response"])
@@ -71,7 +71,7 @@ def test_method_update(self, client: Kernel) -> None:
@parametrize
def test_method_update_with_all_params(self, client: Kernel) -> None:
limit = client.projects.limits.update(
- id="id",
+ id_or_name="id_or_name",
max_concurrent_invocations=0,
max_concurrent_sessions=0,
max_pooled_sessions=0,
@@ -82,7 +82,7 @@ def test_method_update_with_all_params(self, client: Kernel) -> None:
@parametrize
def test_raw_response_update(self, client: Kernel) -> None:
response = client.projects.limits.with_raw_response.update(
- id="id",
+ id_or_name="id_or_name",
)
assert response.is_closed is True
@@ -94,7 +94,7 @@ def test_raw_response_update(self, client: Kernel) -> None:
@parametrize
def test_streaming_response_update(self, client: Kernel) -> None:
with client.projects.limits.with_streaming_response.update(
- id="id",
+ id_or_name="id_or_name",
) as response:
assert not response.is_closed
assert response.http_request.headers.get("X-Stainless-Lang") == "python"
@@ -107,9 +107,9 @@ def test_streaming_response_update(self, client: Kernel) -> None:
@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 ''"):
+ with pytest.raises(ValueError, match=r"Expected a non-empty value for `id_or_name` but received ''"):
client.projects.limits.with_raw_response.update(
- id="",
+ id_or_name="",
)
@@ -122,7 +122,7 @@ class TestAsyncLimits:
@parametrize
async def test_method_retrieve(self, async_client: AsyncKernel) -> None:
limit = await async_client.projects.limits.retrieve(
- "id",
+ "id_or_name",
)
assert_matches_type(ProjectLimits, limit, path=["response"])
@@ -130,7 +130,7 @@ async def test_method_retrieve(self, async_client: AsyncKernel) -> None:
@parametrize
async def test_raw_response_retrieve(self, async_client: AsyncKernel) -> None:
response = await async_client.projects.limits.with_raw_response.retrieve(
- "id",
+ "id_or_name",
)
assert response.is_closed is True
@@ -142,7 +142,7 @@ async def test_raw_response_retrieve(self, async_client: AsyncKernel) -> None:
@parametrize
async def test_streaming_response_retrieve(self, async_client: AsyncKernel) -> None:
async with async_client.projects.limits.with_streaming_response.retrieve(
- "id",
+ "id_or_name",
) as response:
assert not response.is_closed
assert response.http_request.headers.get("X-Stainless-Lang") == "python"
@@ -155,7 +155,7 @@ async def test_streaming_response_retrieve(self, async_client: AsyncKernel) -> N
@pytest.mark.skip(reason="Mock server tests are disabled")
@parametrize
async def test_path_params_retrieve(self, async_client: AsyncKernel) -> None:
- with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"):
+ with pytest.raises(ValueError, match=r"Expected a non-empty value for `id_or_name` but received ''"):
await async_client.projects.limits.with_raw_response.retrieve(
"",
)
@@ -164,7 +164,7 @@ async def test_path_params_retrieve(self, async_client: AsyncKernel) -> None:
@parametrize
async def test_method_update(self, async_client: AsyncKernel) -> None:
limit = await async_client.projects.limits.update(
- id="id",
+ id_or_name="id_or_name",
)
assert_matches_type(ProjectLimits, limit, path=["response"])
@@ -172,7 +172,7 @@ async def test_method_update(self, async_client: AsyncKernel) -> None:
@parametrize
async def test_method_update_with_all_params(self, async_client: AsyncKernel) -> None:
limit = await async_client.projects.limits.update(
- id="id",
+ id_or_name="id_or_name",
max_concurrent_invocations=0,
max_concurrent_sessions=0,
max_pooled_sessions=0,
@@ -183,7 +183,7 @@ async def test_method_update_with_all_params(self, async_client: AsyncKernel) ->
@parametrize
async def test_raw_response_update(self, async_client: AsyncKernel) -> None:
response = await async_client.projects.limits.with_raw_response.update(
- id="id",
+ id_or_name="id_or_name",
)
assert response.is_closed is True
@@ -195,7 +195,7 @@ async def test_raw_response_update(self, async_client: AsyncKernel) -> None:
@parametrize
async def test_streaming_response_update(self, async_client: AsyncKernel) -> None:
async with async_client.projects.limits.with_streaming_response.update(
- id="id",
+ id_or_name="id_or_name",
) as response:
assert not response.is_closed
assert response.http_request.headers.get("X-Stainless-Lang") == "python"
@@ -208,7 +208,7 @@ async def test_streaming_response_update(self, async_client: AsyncKernel) -> Non
@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 ''"):
+ with pytest.raises(ValueError, match=r"Expected a non-empty value for `id_or_name` but received ''"):
await async_client.projects.limits.with_raw_response.update(
- id="",
+ id_or_name="",
)
diff --git a/tests/api_resources/test_projects.py b/tests/api_resources/test_projects.py
index e8d98c29..6eaf3b62 100644
--- a/tests/api_resources/test_projects.py
+++ b/tests/api_resources/test_projects.py
@@ -56,7 +56,7 @@ def test_streaming_response_create(self, client: Kernel) -> None:
@parametrize
def test_method_retrieve(self, client: Kernel) -> None:
project = client.projects.retrieve(
- "id",
+ "id_or_name",
)
assert_matches_type(Project, project, path=["response"])
@@ -64,7 +64,7 @@ def test_method_retrieve(self, client: Kernel) -> None:
@parametrize
def test_raw_response_retrieve(self, client: Kernel) -> None:
response = client.projects.with_raw_response.retrieve(
- "id",
+ "id_or_name",
)
assert response.is_closed is True
@@ -76,7 +76,7 @@ def test_raw_response_retrieve(self, client: Kernel) -> None:
@parametrize
def test_streaming_response_retrieve(self, client: Kernel) -> None:
with client.projects.with_streaming_response.retrieve(
- "id",
+ "id_or_name",
) as response:
assert not response.is_closed
assert response.http_request.headers.get("X-Stainless-Lang") == "python"
@@ -89,7 +89,7 @@ def test_streaming_response_retrieve(self, client: Kernel) -> None:
@pytest.mark.skip(reason="Mock server tests are disabled")
@parametrize
def test_path_params_retrieve(self, client: Kernel) -> None:
- with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"):
+ with pytest.raises(ValueError, match=r"Expected a non-empty value for `id_or_name` but received ''"):
client.projects.with_raw_response.retrieve(
"",
)
@@ -98,7 +98,7 @@ def test_path_params_retrieve(self, client: Kernel) -> None:
@parametrize
def test_method_update(self, client: Kernel) -> None:
project = client.projects.update(
- id="id",
+ id_or_name="id_or_name",
)
assert_matches_type(Project, project, path=["response"])
@@ -106,7 +106,7 @@ def test_method_update(self, client: Kernel) -> None:
@parametrize
def test_method_update_with_all_params(self, client: Kernel) -> None:
project = client.projects.update(
- id="id",
+ id_or_name="id_or_name",
name="x",
status="active",
)
@@ -116,7 +116,7 @@ def test_method_update_with_all_params(self, client: Kernel) -> None:
@parametrize
def test_raw_response_update(self, client: Kernel) -> None:
response = client.projects.with_raw_response.update(
- id="id",
+ id_or_name="id_or_name",
)
assert response.is_closed is True
@@ -128,7 +128,7 @@ def test_raw_response_update(self, client: Kernel) -> None:
@parametrize
def test_streaming_response_update(self, client: Kernel) -> None:
with client.projects.with_streaming_response.update(
- id="id",
+ id_or_name="id_or_name",
) as response:
assert not response.is_closed
assert response.http_request.headers.get("X-Stainless-Lang") == "python"
@@ -141,9 +141,9 @@ def test_streaming_response_update(self, client: Kernel) -> None:
@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 ''"):
+ with pytest.raises(ValueError, match=r"Expected a non-empty value for `id_or_name` but received ''"):
client.projects.with_raw_response.update(
- id="",
+ id_or_name="",
)
@pytest.mark.skip(reason="Mock server tests are disabled")
@@ -189,7 +189,7 @@ def test_streaming_response_list(self, client: Kernel) -> None:
@parametrize
def test_method_delete(self, client: Kernel) -> None:
project = client.projects.delete(
- "id",
+ "id_or_name",
)
assert project is None
@@ -197,7 +197,7 @@ def test_method_delete(self, client: Kernel) -> None:
@parametrize
def test_raw_response_delete(self, client: Kernel) -> None:
response = client.projects.with_raw_response.delete(
- "id",
+ "id_or_name",
)
assert response.is_closed is True
@@ -209,7 +209,7 @@ def test_raw_response_delete(self, client: Kernel) -> None:
@parametrize
def test_streaming_response_delete(self, client: Kernel) -> None:
with client.projects.with_streaming_response.delete(
- "id",
+ "id_or_name",
) as response:
assert not response.is_closed
assert response.http_request.headers.get("X-Stainless-Lang") == "python"
@@ -222,7 +222,7 @@ def test_streaming_response_delete(self, client: Kernel) -> None:
@pytest.mark.skip(reason="Mock server tests are disabled")
@parametrize
def test_path_params_delete(self, client: Kernel) -> None:
- with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"):
+ with pytest.raises(ValueError, match=r"Expected a non-empty value for `id_or_name` but received ''"):
client.projects.with_raw_response.delete(
"",
)
@@ -271,7 +271,7 @@ async def test_streaming_response_create(self, async_client: AsyncKernel) -> Non
@parametrize
async def test_method_retrieve(self, async_client: AsyncKernel) -> None:
project = await async_client.projects.retrieve(
- "id",
+ "id_or_name",
)
assert_matches_type(Project, project, path=["response"])
@@ -279,7 +279,7 @@ async def test_method_retrieve(self, async_client: AsyncKernel) -> None:
@parametrize
async def test_raw_response_retrieve(self, async_client: AsyncKernel) -> None:
response = await async_client.projects.with_raw_response.retrieve(
- "id",
+ "id_or_name",
)
assert response.is_closed is True
@@ -291,7 +291,7 @@ async def test_raw_response_retrieve(self, async_client: AsyncKernel) -> None:
@parametrize
async def test_streaming_response_retrieve(self, async_client: AsyncKernel) -> None:
async with async_client.projects.with_streaming_response.retrieve(
- "id",
+ "id_or_name",
) as response:
assert not response.is_closed
assert response.http_request.headers.get("X-Stainless-Lang") == "python"
@@ -304,7 +304,7 @@ async def test_streaming_response_retrieve(self, async_client: AsyncKernel) -> N
@pytest.mark.skip(reason="Mock server tests are disabled")
@parametrize
async def test_path_params_retrieve(self, async_client: AsyncKernel) -> None:
- with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"):
+ with pytest.raises(ValueError, match=r"Expected a non-empty value for `id_or_name` but received ''"):
await async_client.projects.with_raw_response.retrieve(
"",
)
@@ -313,7 +313,7 @@ async def test_path_params_retrieve(self, async_client: AsyncKernel) -> None:
@parametrize
async def test_method_update(self, async_client: AsyncKernel) -> None:
project = await async_client.projects.update(
- id="id",
+ id_or_name="id_or_name",
)
assert_matches_type(Project, project, path=["response"])
@@ -321,7 +321,7 @@ async def test_method_update(self, async_client: AsyncKernel) -> None:
@parametrize
async def test_method_update_with_all_params(self, async_client: AsyncKernel) -> None:
project = await async_client.projects.update(
- id="id",
+ id_or_name="id_or_name",
name="x",
status="active",
)
@@ -331,7 +331,7 @@ async def test_method_update_with_all_params(self, async_client: AsyncKernel) ->
@parametrize
async def test_raw_response_update(self, async_client: AsyncKernel) -> None:
response = await async_client.projects.with_raw_response.update(
- id="id",
+ id_or_name="id_or_name",
)
assert response.is_closed is True
@@ -343,7 +343,7 @@ async def test_raw_response_update(self, async_client: AsyncKernel) -> None:
@parametrize
async def test_streaming_response_update(self, async_client: AsyncKernel) -> None:
async with async_client.projects.with_streaming_response.update(
- id="id",
+ id_or_name="id_or_name",
) as response:
assert not response.is_closed
assert response.http_request.headers.get("X-Stainless-Lang") == "python"
@@ -356,9 +356,9 @@ async def test_streaming_response_update(self, async_client: AsyncKernel) -> Non
@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 ''"):
+ with pytest.raises(ValueError, match=r"Expected a non-empty value for `id_or_name` but received ''"):
await async_client.projects.with_raw_response.update(
- id="",
+ id_or_name="",
)
@pytest.mark.skip(reason="Mock server tests are disabled")
@@ -404,7 +404,7 @@ async def test_streaming_response_list(self, async_client: AsyncKernel) -> None:
@parametrize
async def test_method_delete(self, async_client: AsyncKernel) -> None:
project = await async_client.projects.delete(
- "id",
+ "id_or_name",
)
assert project is None
@@ -412,7 +412,7 @@ async def test_method_delete(self, async_client: AsyncKernel) -> None:
@parametrize
async def test_raw_response_delete(self, async_client: AsyncKernel) -> None:
response = await async_client.projects.with_raw_response.delete(
- "id",
+ "id_or_name",
)
assert response.is_closed is True
@@ -424,7 +424,7 @@ async def test_raw_response_delete(self, async_client: AsyncKernel) -> None:
@parametrize
async def test_streaming_response_delete(self, async_client: AsyncKernel) -> None:
async with async_client.projects.with_streaming_response.delete(
- "id",
+ "id_or_name",
) as response:
assert not response.is_closed
assert response.http_request.headers.get("X-Stainless-Lang") == "python"
@@ -437,7 +437,7 @@ async def test_streaming_response_delete(self, async_client: AsyncKernel) -> Non
@pytest.mark.skip(reason="Mock server tests are disabled")
@parametrize
async def test_path_params_delete(self, async_client: AsyncKernel) -> None:
- with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"):
+ with pytest.raises(ValueError, match=r"Expected a non-empty value for `id_or_name` but received ''"):
await async_client.projects.with_raw_response.delete(
"",
)
From 3bb6956e8021a955cfcbd12fd93d2a3a6b648ed7 Mon Sep 17 00:00:00 2001
From: "kernel-internal[bot]"
<260533166+kernel-internal[bot]@users.noreply.github.com>
Date: Wed, 12 Aug 2026 23:31:18 +0000
Subject: [PATCH 190/366] feat: Preserve canonical managed auth input metadata
Stainless-Generated-From: 99ae9e331b3ab1c83b6142638f9272eda6a5c06b
---
.../types/auth/connection_follow_response.py | 18 ++++++++++++++++++
src/kernel/types/auth/managed_auth.py | 18 ++++++++++++++++++
2 files changed, 36 insertions(+)
diff --git a/src/kernel/types/auth/connection_follow_response.py b/src/kernel/types/auth/connection_follow_response.py
index 7a0ecd05..c411c1ba 100644
--- a/src/kernel/types/auth/connection_follow_response.py
+++ b/src/kernel/types/auth/connection_follow_response.py
@@ -35,9 +35,24 @@ class ManagedAuthStateEventChoice(BaseModel):
]
"""Choice type."""
+ context: Optional[str] = None
+ """Context captured for a choice."""
+
description: Optional[str] = None
"""Additional context for the choice."""
+ display_text: Optional[str] = None
+ """Display text captured for a choice."""
+
+ masked_destination: Optional[str] = None
+ """Masked phone number or email address shown for an MFA choice."""
+
+ mfa_type: Optional[Literal["sms", "call", "email", "totp", "push", "password", "passkey", "switch", "other"]] = None
+ """Semantic MFA method.
+
+ Choice id remains the stable identity of the exact option selected.
+ """
+
observed_selector: Optional[str] = None
"""Selector for the visible choice, when available."""
@@ -88,6 +103,9 @@ class ManagedAuthStateEventField(BaseModel):
type: Literal["identifier", "password", "code", "totp_code", "totp_secret", "text"]
"""Managed-auth field type."""
+ hint: Optional[str] = None
+ """Context shown near the field, including a masked code destination."""
+
label: Optional[str] = None
"""Human-readable label shown to the user."""
diff --git a/src/kernel/types/auth/managed_auth.py b/src/kernel/types/auth/managed_auth.py
index 9db58514..ea5be1c6 100644
--- a/src/kernel/types/auth/managed_auth.py
+++ b/src/kernel/types/auth/managed_auth.py
@@ -122,9 +122,24 @@ class Choice(BaseModel):
]
"""Choice type."""
+ context: Optional[str] = None
+ """Context captured for a choice."""
+
description: Optional[str] = None
"""Additional context for the choice."""
+ display_text: Optional[str] = None
+ """Display text captured for a choice."""
+
+ masked_destination: Optional[str] = None
+ """Masked phone number or email address shown for an MFA choice."""
+
+ mfa_type: Optional[Literal["sms", "call", "email", "totp", "push", "password", "passkey", "switch", "other"]] = None
+ """Semantic MFA method.
+
+ Choice id remains the stable identity of the exact option selected.
+ """
+
observed_selector: Optional[str] = None
"""Selector for the visible choice, when available."""
@@ -197,6 +212,9 @@ class Field(BaseModel):
type: Literal["identifier", "password", "code", "totp_code", "totp_secret", "text"]
"""Managed-auth field type."""
+ hint: Optional[str] = None
+ """Context shown near the field, including a masked code destination."""
+
label: Optional[str] = None
"""Human-readable label shown to the user."""
From eeb74dff42b450ac48d17082636cd6f383105748 Mon Sep 17 00:00:00 2001
From: "kernel-internal[bot]"
<260533166+kernel-internal[bot]@users.noreply.github.com>
Date: Wed, 12 Aug 2026 23:35:49 +0000
Subject: [PATCH 191/366] release: 0.90.0
---
.release-please-manifest.json | 2 +-
CHANGELOG.md | 8 ++++++++
pyproject.toml | 2 +-
src/kernel/_version.py | 2 +-
4 files changed, 11 insertions(+), 3 deletions(-)
diff --git a/.release-please-manifest.json b/.release-please-manifest.json
index 6af24e3d..f2863752 100644
--- a/.release-please-manifest.json
+++ b/.release-please-manifest.json
@@ -1,3 +1,3 @@
{
- ".": "0.89.0"
+ ".": "0.90.0"
}
\ No newline at end of file
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 92fb5212..f6add314 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,13 @@
# Changelog
+## [0.90.0](https://github.com/kernel/kernel-python-sdk/compare/v0.89.0...v0.90.0) (2026-08-12)
+
+
+### Features
+
+* Preserve canonical managed auth input metadata ([3bb6956](https://github.com/kernel/kernel-python-sdk/commit/3bb6956e8021a955cfcbd12fd93d2a3a6b648ed7))
+* Support project selection by ID or name ([67970b2](https://github.com/kernel/kernel-python-sdk/commit/67970b2b8cc0312b81d10e6fec6c3a016c79d347))
+
## [0.89.0](https://github.com/kernel/kernel-python-sdk/compare/v0.88.0...v0.89.0) (2026-08-12)
diff --git a/pyproject.toml b/pyproject.toml
index 57df3c54..521e38c9 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -1,6 +1,6 @@
[project]
name = "kernel"
-version = "0.89.0"
+version = "0.90.0"
description = "The official Python library for the kernel API"
dynamic = ["readme"]
license = "Apache-2.0"
diff --git a/src/kernel/_version.py b/src/kernel/_version.py
index e4082c56..8933bc49 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.89.0" # x-release-please-version
+__version__ = "0.90.0" # x-release-please-version
From 0d5a4c29be2e2bc0a216436cbda4bc8602784d02 Mon Sep 17 00:00:00 2001
From: "kernel-internal[bot]"
<260533166+kernel-internal[bot]@users.noreply.github.com>
Date: Thu, 13 Aug 2026 20:48:05 +0000
Subject: [PATCH 192/366] feat: Require a name on credential providers and
backfill unnamed rows
Stainless-Generated-From: 0b9c98f90d25c32e13ea19b634f735d1709c371d
---
src/kernel/resources/credential_providers.py | 12 ++++++++----
.../types/credential_provider_create_params.py | 5 ++++-
.../types/credential_provider_update_params.py | 5 ++++-
3 files changed, 16 insertions(+), 6 deletions(-)
diff --git a/src/kernel/resources/credential_providers.py b/src/kernel/resources/credential_providers.py
index de079a8c..6a003f99 100644
--- a/src/kernel/resources/credential_providers.py
+++ b/src/kernel/resources/credential_providers.py
@@ -73,7 +73,8 @@ def create(
Args:
token: Service account token for the provider (e.g., 1Password service account token)
- name: Human-readable name for this provider instance (unique per org)
+ name: Human-readable name for this provider instance (unique per org). Surrounding
+ whitespace is trimmed and the trimmed value must be non-empty.
provider_type: Type of credential provider
@@ -163,7 +164,8 @@ def update(
enabled: Whether the provider is enabled for credential lookups
- name: Human-readable name for this provider instance
+ name: Human-readable name for this provider instance. Surrounding whitespace is
+ trimmed and the trimmed value must be non-empty.
priority: Priority order for credential lookups (lower numbers are checked first)
@@ -392,7 +394,8 @@ async def create(
Args:
token: Service account token for the provider (e.g., 1Password service account token)
- name: Human-readable name for this provider instance (unique per org)
+ name: Human-readable name for this provider instance (unique per org). Surrounding
+ whitespace is trimmed and the trimmed value must be non-empty.
provider_type: Type of credential provider
@@ -482,7 +485,8 @@ async def update(
enabled: Whether the provider is enabled for credential lookups
- name: Human-readable name for this provider instance
+ name: Human-readable name for this provider instance. Surrounding whitespace is
+ trimmed and the trimmed value must be non-empty.
priority: Priority order for credential lookups (lower numbers are checked first)
diff --git a/src/kernel/types/credential_provider_create_params.py b/src/kernel/types/credential_provider_create_params.py
index ea531c7d..7076ff72 100644
--- a/src/kernel/types/credential_provider_create_params.py
+++ b/src/kernel/types/credential_provider_create_params.py
@@ -12,7 +12,10 @@ class CredentialProviderCreateParams(TypedDict, total=False):
"""Service account token for the provider (e.g., 1Password service account token)"""
name: Required[str]
- """Human-readable name for this provider instance (unique per org)"""
+ """Human-readable name for this provider instance (unique per org).
+
+ Surrounding whitespace is trimmed and the trimmed value must be non-empty.
+ """
provider_type: Required[Literal["onepassword"]]
"""Type of credential provider"""
diff --git a/src/kernel/types/credential_provider_update_params.py b/src/kernel/types/credential_provider_update_params.py
index b2dda02b..272a7661 100644
--- a/src/kernel/types/credential_provider_update_params.py
+++ b/src/kernel/types/credential_provider_update_params.py
@@ -18,7 +18,10 @@ class CredentialProviderUpdateParams(TypedDict, total=False):
"""Whether the provider is enabled for credential lookups"""
name: str
- """Human-readable name for this provider instance"""
+ """Human-readable name for this provider instance.
+
+ Surrounding whitespace is trimmed and the trimmed value must be non-empty.
+ """
priority: int
"""Priority order for credential lookups (lower numbers are checked first)"""
From ccf4127272adc749f0da75b7e2458e4a9e183423 Mon Sep 17 00:00:00 2001
From: "kernel-internal[bot]"
<260533166+kernel-internal[bot]@users.noreply.github.com>
Date: Fri, 14 Aug 2026 17:49:41 +0000
Subject: [PATCH 193/366] feat: Expose configurable browser memory
Stainless-Generated-From: 32f5b5d0edf73007529398a1c89d0f9ef15e060a
---
api.md | 2 ++
src/kernel/resources/browsers/browsers.py | 10 ++++++++++
src/kernel/types/__init__.py | 2 ++
src/kernel/types/browser_create_params.py | 4 ++++
src/kernel/types/browser_create_response.py | 4 ++++
src/kernel/types/browser_list_response.py | 4 ++++
src/kernel/types/browser_memory.py | 7 +++++++
src/kernel/types/browser_memory_request.py | 7 +++++++
src/kernel/types/browser_pool_acquire_response.py | 4 ++++
src/kernel/types/browser_retrieve_response.py | 4 ++++
src/kernel/types/browser_update_response.py | 4 ++++
src/kernel/types/invocation_list_browsers_response.py | 4 ++++
tests/api_resources/test_browsers.py | 2 ++
tests/test_browser_routing.py | 1 +
14 files changed, 59 insertions(+)
create mode 100644 src/kernel/types/browser_memory.py
create mode 100644 src/kernel/types/browser_memory_request.py
diff --git a/api.md b/api.md
index d442a3d0..52d4e4c3 100644
--- a/api.md
+++ b/api.md
@@ -80,6 +80,8 @@ Types:
```python
from kernel.types import (
+ BrowserMemory,
+ BrowserMemoryRequest,
BrowserNetworkConfig,
BrowserPoolRef,
BrowserProxy,
diff --git a/src/kernel/resources/browsers/browsers.py b/src/kernel/resources/browsers/browsers.py
index 7e46a361..bf80a931 100644
--- a/src/kernel/resources/browsers/browsers.py
+++ b/src/kernel/resources/browsers/browsers.py
@@ -25,6 +25,7 @@
AsyncFsResourceWithStreamingResponse,
)
from ...types import (
+ BrowserMemoryRequest,
browser_curl_params,
browser_list_params,
browser_create_params,
@@ -94,6 +95,7 @@
async_stream_via_browser_route,
async_request_via_browser_route,
)
+from ...types.browser_memory_request import BrowserMemoryRequest
from ...types.browser_create_response import BrowserCreateResponse
from ...types.browser_update_response import BrowserUpdateResponse
from ...types.browser_retrieve_response import BrowserRetrieveResponse
@@ -172,6 +174,7 @@ def create(
headless: bool | Omit = omit,
invocation_id: str | Omit = omit,
kiosk_mode: bool | Omit = omit,
+ memory: BrowserMemoryRequest | Omit = omit,
name: str | Omit = omit,
network: BrowserNetworkConfigParam | Omit = omit,
profile: BrowserProfile | Omit = omit,
@@ -213,6 +216,8 @@ def create(
kiosk_mode: If true, launches the browser in kiosk mode to hide address bar and tabs in live
view.
+ memory: Memory for a headful, non-GPU browser session. Defaults to 8GiB.
+
name: Optional human-readable name for the browser session, used to find it later in
the dashboard. Must be unique among active sessions within the project. Can be
changed later via PATCH /browsers/{id_or_name}.
@@ -295,6 +300,7 @@ def create(
"headless": headless,
"invocation_id": invocation_id,
"kiosk_mode": kiosk_mode,
+ "memory": memory,
"name": name,
"network": network,
"profile": profile,
@@ -786,6 +792,7 @@ async def create(
headless: bool | Omit = omit,
invocation_id: str | Omit = omit,
kiosk_mode: bool | Omit = omit,
+ memory: BrowserMemoryRequest | Omit = omit,
name: str | Omit = omit,
network: BrowserNetworkConfigParam | Omit = omit,
profile: BrowserProfile | Omit = omit,
@@ -827,6 +834,8 @@ async def create(
kiosk_mode: If true, launches the browser in kiosk mode to hide address bar and tabs in live
view.
+ memory: Memory for a headful, non-GPU browser session. Defaults to 8GiB.
+
name: Optional human-readable name for the browser session, used to find it later in
the dashboard. Must be unique among active sessions within the project. Can be
changed later via PATCH /browsers/{id_or_name}.
@@ -909,6 +918,7 @@ async def create(
"headless": headless,
"invocation_id": invocation_id,
"kiosk_mode": kiosk_mode,
+ "memory": memory,
"name": name,
"network": network,
"profile": profile,
diff --git a/src/kernel/types/__init__.py b/src/kernel/types/__init__.py
index 13c85213..6373f24c 100644
--- a/src/kernel/types/__init__.py
+++ b/src/kernel/types/__init__.py
@@ -24,6 +24,7 @@
from .browser_pool import BrowserPool as BrowserPool
from .browser_proxy import BrowserProxy as BrowserProxy
from .browser_usage import BrowserUsage as BrowserUsage
+from .browser_memory import BrowserMemory as BrowserMemory
from .app_list_params import AppListParams as AppListParams
from .audit_log_entry import AuditLogEntry as AuditLogEntry
from .created_api_key import CreatedAPIKey as CreatedAPIKey
@@ -58,6 +59,7 @@
from .project_update_params import ProjectUpdateParams as ProjectUpdateParams
from .proxy_create_response import ProxyCreateResponse as ProxyCreateResponse
from .proxy_update_response import ProxyUpdateResponse as ProxyUpdateResponse
+from .browser_memory_request import BrowserMemoryRequest as BrowserMemoryRequest
from .browser_network_config import BrowserNetworkConfig as BrowserNetworkConfig
from .credential_list_params import CredentialListParams as CredentialListParams
from .deployment_list_params import DeploymentListParams as DeploymentListParams
diff --git a/src/kernel/types/browser_create_params.py b/src/kernel/types/browser_create_params.py
index 0c280602..2899fb7c 100644
--- a/src/kernel/types/browser_create_params.py
+++ b/src/kernel/types/browser_create_params.py
@@ -6,6 +6,7 @@
from typing_extensions import Literal, TypedDict
from .tags_param import TagsParam
+from .browser_memory_request import BrowserMemoryRequest
from .browser_proxy_config_param import BrowserProxyConfigParam
from .browser_network_config_param import BrowserNetworkConfigParam
from .shared_params.browser_profile import BrowserProfile
@@ -58,6 +59,9 @@ class BrowserCreateParams(TypedDict, total=False):
view.
"""
+ memory: BrowserMemoryRequest
+ """Memory for a headful, non-GPU browser session. Defaults to 8GiB."""
+
name: str
"""
Optional human-readable name for the browser session, used to find it later in
diff --git a/src/kernel/types/browser_create_response.py b/src/kernel/types/browser_create_response.py
index f8d625e9..dcc96f43 100644
--- a/src/kernel/types/browser_create_response.py
+++ b/src/kernel/types/browser_create_response.py
@@ -9,6 +9,7 @@
from .._models import BaseModel
from .browser_proxy import BrowserProxy
from .browser_usage import BrowserUsage
+from .browser_memory import BrowserMemory
from .browser_pool_ref import BrowserPoolRef
from .browser_network_config import BrowserNetworkConfig
from .shared.browser_viewport import BrowserViewport
@@ -27,6 +28,9 @@ class BrowserCreateResponse(BaseModel):
headless: bool
"""Whether the browser session is running in headless mode."""
+ memory: BrowserMemory
+ """Memory allocated to the browser session."""
+
region: Literal["us-east", "eu-west"]
"""Geographic region of the browser session. Fixed once the session is created."""
diff --git a/src/kernel/types/browser_list_response.py b/src/kernel/types/browser_list_response.py
index 108bbef2..0ef81b9a 100644
--- a/src/kernel/types/browser_list_response.py
+++ b/src/kernel/types/browser_list_response.py
@@ -9,6 +9,7 @@
from .._models import BaseModel
from .browser_proxy import BrowserProxy
from .browser_usage import BrowserUsage
+from .browser_memory import BrowserMemory
from .browser_pool_ref import BrowserPoolRef
from .browser_network_config import BrowserNetworkConfig
from .shared.browser_viewport import BrowserViewport
@@ -27,6 +28,9 @@ class BrowserListResponse(BaseModel):
headless: bool
"""Whether the browser session is running in headless mode."""
+ memory: BrowserMemory
+ """Memory allocated to the browser session."""
+
region: Literal["us-east", "eu-west"]
"""Geographic region of the browser session. Fixed once the session is created."""
diff --git a/src/kernel/types/browser_memory.py b/src/kernel/types/browser_memory.py
new file mode 100644
index 00000000..b86a12aa
--- /dev/null
+++ b/src/kernel/types/browser_memory.py
@@ -0,0 +1,7 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from typing_extensions import Literal, TypeAlias
+
+__all__ = ["BrowserMemory"]
+
+BrowserMemory: TypeAlias = Literal["1GiB", "2GiB", "6GiB", "8GiB", "16GiB"]
diff --git a/src/kernel/types/browser_memory_request.py b/src/kernel/types/browser_memory_request.py
new file mode 100644
index 00000000..f2363b71
--- /dev/null
+++ b/src/kernel/types/browser_memory_request.py
@@ -0,0 +1,7 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from typing_extensions import Literal, TypeAlias
+
+__all__ = ["BrowserMemoryRequest"]
+
+BrowserMemoryRequest: TypeAlias = Literal["8GiB", "16GiB"]
diff --git a/src/kernel/types/browser_pool_acquire_response.py b/src/kernel/types/browser_pool_acquire_response.py
index 720dc569..d9638105 100644
--- a/src/kernel/types/browser_pool_acquire_response.py
+++ b/src/kernel/types/browser_pool_acquire_response.py
@@ -9,6 +9,7 @@
from .._models import BaseModel
from .browser_proxy import BrowserProxy
from .browser_usage import BrowserUsage
+from .browser_memory import BrowserMemory
from .browser_pool_ref import BrowserPoolRef
from .browser_network_config import BrowserNetworkConfig
from .shared.browser_viewport import BrowserViewport
@@ -27,6 +28,9 @@ class BrowserPoolAcquireResponse(BaseModel):
headless: bool
"""Whether the browser session is running in headless mode."""
+ memory: BrowserMemory
+ """Memory allocated to the browser session."""
+
region: Literal["us-east", "eu-west"]
"""Geographic region of the browser session. Fixed once the session is created."""
diff --git a/src/kernel/types/browser_retrieve_response.py b/src/kernel/types/browser_retrieve_response.py
index 8e39a607..8292024b 100644
--- a/src/kernel/types/browser_retrieve_response.py
+++ b/src/kernel/types/browser_retrieve_response.py
@@ -9,6 +9,7 @@
from .._models import BaseModel
from .browser_proxy import BrowserProxy
from .browser_usage import BrowserUsage
+from .browser_memory import BrowserMemory
from .browser_pool_ref import BrowserPoolRef
from .browser_network_config import BrowserNetworkConfig
from .shared.browser_viewport import BrowserViewport
@@ -27,6 +28,9 @@ class BrowserRetrieveResponse(BaseModel):
headless: bool
"""Whether the browser session is running in headless mode."""
+ memory: BrowserMemory
+ """Memory allocated to the browser session."""
+
region: Literal["us-east", "eu-west"]
"""Geographic region of the browser session. Fixed once the session is created."""
diff --git a/src/kernel/types/browser_update_response.py b/src/kernel/types/browser_update_response.py
index c04f43f8..7dd7de88 100644
--- a/src/kernel/types/browser_update_response.py
+++ b/src/kernel/types/browser_update_response.py
@@ -9,6 +9,7 @@
from .._models import BaseModel
from .browser_proxy import BrowserProxy
from .browser_usage import BrowserUsage
+from .browser_memory import BrowserMemory
from .browser_pool_ref import BrowserPoolRef
from .browser_network_config import BrowserNetworkConfig
from .shared.browser_viewport import BrowserViewport
@@ -27,6 +28,9 @@ class BrowserUpdateResponse(BaseModel):
headless: bool
"""Whether the browser session is running in headless mode."""
+ memory: BrowserMemory
+ """Memory allocated to the browser session."""
+
region: Literal["us-east", "eu-west"]
"""Geographic region of the browser session. Fixed once the session is created."""
diff --git a/src/kernel/types/invocation_list_browsers_response.py b/src/kernel/types/invocation_list_browsers_response.py
index bcf8dd40..db32954d 100644
--- a/src/kernel/types/invocation_list_browsers_response.py
+++ b/src/kernel/types/invocation_list_browsers_response.py
@@ -9,6 +9,7 @@
from .._models import BaseModel
from .browser_proxy import BrowserProxy
from .browser_usage import BrowserUsage
+from .browser_memory import BrowserMemory
from .browser_pool_ref import BrowserPoolRef
from .browser_network_config import BrowserNetworkConfig
from .shared.browser_viewport import BrowserViewport
@@ -27,6 +28,9 @@ class Browser(BaseModel):
headless: bool
"""Whether the browser session is running in headless mode."""
+ memory: BrowserMemory
+ """Memory allocated to the browser session."""
+
region: Literal["us-east", "eu-west"]
"""Geographic region of the browser session. Fixed once the session is created."""
diff --git a/tests/api_resources/test_browsers.py b/tests/api_resources/test_browsers.py
index ae017186..1a352038 100644
--- a/tests/api_resources/test_browsers.py
+++ b/tests/api_resources/test_browsers.py
@@ -45,6 +45,7 @@ def test_method_create_with_all_params(self, client: Kernel) -> None:
headless=False,
invocation_id="rr33xuugxj9h0bkf1rdt2bet",
kiosk_mode=True,
+ memory="8GiB",
name="checkout-flow-1",
network={"private_hosts": ["*.example.ts.net", "100.64.0.0/10"]},
profile={
@@ -503,6 +504,7 @@ async def test_method_create_with_all_params(self, async_client: AsyncKernel) ->
headless=False,
invocation_id="rr33xuugxj9h0bkf1rdt2bet",
kiosk_mode=True,
+ memory="8GiB",
name="checkout-flow-1",
network={"private_hosts": ["*.example.ts.net", "100.64.0.0/10"]},
profile={
diff --git a/tests/test_browser_routing.py b/tests/test_browser_routing.py
index 84c5c541..3538c221 100644
--- a/tests/test_browser_routing.py
+++ b/tests/test_browser_routing.py
@@ -31,6 +31,7 @@ def _fake_browser() -> dict[str, object]:
"created_at": "2020-01-01T00:00:00Z",
"headless": True,
"stealth": False,
+ "memory": "8GiB",
"timeout_seconds": 60,
"region": "us-east",
}
From 8479adff62631c996a4a9129fd8639c16d96b92b Mon Sep 17 00:00:00 2001
From: "kernel-internal[bot]"
<260533166+kernel-internal[bot]@users.noreply.github.com>
Date: Fri, 14 Aug 2026 18:08:26 +0000
Subject: [PATCH 194/366] feat: Add customer-facing OTLP destination CRUD API
Stainless-Generated-From: fc9d70b47afce5656d62cc298600a5e249ea0d63
---
.stats.yml | 2 +-
api.md | 18 +
src/kernel/_client.py | 38 +
src/kernel/resources/__init__.py | 14 +
src/kernel/resources/browsers/browsers.py | 24 +-
src/kernel/resources/browsers/telemetry.py | 8 +-
src/kernel/resources/telemetry/__init__.py | 33 +
.../resources/telemetry/destinations.py | 676 ++++++++++++++++++
src/kernel/resources/telemetry/telemetry.py | 120 ++++
src/kernel/types/telemetry/__init__.py | 8 +
.../telemetry/destination_create_params.py | 39 +
.../telemetry/destination_list_params.py | 27 +
.../telemetry/destination_update_params.py | 31 +
.../types/telemetry/otlp_destination.py | 41 ++
tests/api_resources/telemetry/__init__.py | 1 +
.../telemetry/test_destinations.py | 477 ++++++++++++
16 files changed, 1548 insertions(+), 9 deletions(-)
create mode 100644 src/kernel/resources/telemetry/__init__.py
create mode 100644 src/kernel/resources/telemetry/destinations.py
create mode 100644 src/kernel/resources/telemetry/telemetry.py
create mode 100644 src/kernel/types/telemetry/__init__.py
create mode 100644 src/kernel/types/telemetry/destination_create_params.py
create mode 100644 src/kernel/types/telemetry/destination_list_params.py
create mode 100644 src/kernel/types/telemetry/destination_update_params.py
create mode 100644 src/kernel/types/telemetry/otlp_destination.py
create mode 100644 tests/api_resources/telemetry/__init__.py
create mode 100644 tests/api_resources/telemetry/test_destinations.py
diff --git a/.stats.yml b/.stats.yml
index 50996d49..98f97da5 100644
--- a/.stats.yml
+++ b/.stats.yml
@@ -1 +1 @@
-configured_endpoints: 134
+configured_endpoints: 139
diff --git a/api.md b/api.md
index 52d4e4c3..62ade185 100644
--- a/api.md
+++ b/api.md
@@ -342,6 +342,24 @@ Methods:
- client.auth.connections.submit(id, \*\*params) -> SubmitFieldsResponse
- client.auth.connections.timeline(id, \*\*params) -> SyncOffsetPagination[ManagedAuthTimelineEvent]
+# Telemetry
+
+## Destinations
+
+Types:
+
+```python
+from kernel.types.telemetry import OtlpDestination
+```
+
+Methods:
+
+- client.telemetry.destinations.create(\*\*params) -> OtlpDestination
+- client.telemetry.destinations.retrieve(id_or_name) -> OtlpDestination
+- client.telemetry.destinations.update(id_or_name, \*\*params) -> OtlpDestination
+- client.telemetry.destinations.list(\*\*params) -> SyncOffsetPagination[OtlpDestination]
+- client.telemetry.destinations.delete(id_or_name) -> None
+
# Proxies
Types:
diff --git a/src/kernel/_client.py b/src/kernel/_client.py
index eb4d1af1..497cc5d5 100644
--- a/src/kernel/_client.py
+++ b/src/kernel/_client.py
@@ -54,6 +54,7 @@
browsers,
profiles,
projects,
+ telemetry,
audit_logs,
extensions,
credentials,
@@ -75,6 +76,7 @@
from .resources.browser_pools import BrowserPoolsResource, AsyncBrowserPoolsResource
from .resources.browsers.browsers import BrowsersResource, AsyncBrowsersResource
from .resources.projects.projects import ProjectsResource, AsyncProjectsResource
+ from .resources.telemetry.telemetry import TelemetryResource, AsyncTelemetryResource
from .resources.credential_providers import CredentialProvidersResource, AsyncCredentialProvidersResource
from .resources.audit_logs.audit_logs import AuditLogsResource, AsyncAuditLogsResource
from .resources.organization.organization import OrganizationResource, AsyncOrganizationResource
@@ -240,6 +242,12 @@ def auth(self) -> AuthResource:
return AuthResource(self)
+ @cached_property
+ def telemetry(self) -> TelemetryResource:
+ from .resources.telemetry import TelemetryResource
+
+ return TelemetryResource(self)
+
@cached_property
def proxies(self) -> ProxiesResource:
"""Create and manage proxy configurations for routing browser traffic."""
@@ -603,6 +611,12 @@ def auth(self) -> AsyncAuthResource:
return AsyncAuthResource(self)
+ @cached_property
+ def telemetry(self) -> AsyncTelemetryResource:
+ from .resources.telemetry import AsyncTelemetryResource
+
+ return AsyncTelemetryResource(self)
+
@cached_property
def proxies(self) -> AsyncProxiesResource:
"""Create and manage proxy configurations for routing browser traffic."""
@@ -870,6 +884,12 @@ def auth(self) -> auth.AuthResourceWithRawResponse:
return AuthResourceWithRawResponse(self._client.auth)
+ @cached_property
+ def telemetry(self) -> telemetry.TelemetryResourceWithRawResponse:
+ from .resources.telemetry import TelemetryResourceWithRawResponse
+
+ return TelemetryResourceWithRawResponse(self._client.telemetry)
+
@cached_property
def proxies(self) -> proxies.ProxiesResourceWithRawResponse:
"""Create and manage proxy configurations for routing browser traffic."""
@@ -984,6 +1004,12 @@ def auth(self) -> auth.AsyncAuthResourceWithRawResponse:
return AsyncAuthResourceWithRawResponse(self._client.auth)
+ @cached_property
+ def telemetry(self) -> telemetry.AsyncTelemetryResourceWithRawResponse:
+ from .resources.telemetry import AsyncTelemetryResourceWithRawResponse
+
+ return AsyncTelemetryResourceWithRawResponse(self._client.telemetry)
+
@cached_property
def proxies(self) -> proxies.AsyncProxiesResourceWithRawResponse:
"""Create and manage proxy configurations for routing browser traffic."""
@@ -1098,6 +1124,12 @@ def auth(self) -> auth.AuthResourceWithStreamingResponse:
return AuthResourceWithStreamingResponse(self._client.auth)
+ @cached_property
+ def telemetry(self) -> telemetry.TelemetryResourceWithStreamingResponse:
+ from .resources.telemetry import TelemetryResourceWithStreamingResponse
+
+ return TelemetryResourceWithStreamingResponse(self._client.telemetry)
+
@cached_property
def proxies(self) -> proxies.ProxiesResourceWithStreamingResponse:
"""Create and manage proxy configurations for routing browser traffic."""
@@ -1212,6 +1244,12 @@ def auth(self) -> auth.AsyncAuthResourceWithStreamingResponse:
return AsyncAuthResourceWithStreamingResponse(self._client.auth)
+ @cached_property
+ def telemetry(self) -> telemetry.AsyncTelemetryResourceWithStreamingResponse:
+ from .resources.telemetry import AsyncTelemetryResourceWithStreamingResponse
+
+ return AsyncTelemetryResourceWithStreamingResponse(self._client.telemetry)
+
@cached_property
def proxies(self) -> proxies.AsyncProxiesResourceWithStreamingResponse:
"""Create and manage proxy configurations for routing browser traffic."""
diff --git a/src/kernel/resources/__init__.py b/src/kernel/resources/__init__.py
index 586c499a..10f1a88b 100644
--- a/src/kernel/resources/__init__.py
+++ b/src/kernel/resources/__init__.py
@@ -56,6 +56,14 @@
ProjectsResourceWithStreamingResponse,
AsyncProjectsResourceWithStreamingResponse,
)
+from .telemetry import (
+ TelemetryResource,
+ AsyncTelemetryResource,
+ TelemetryResourceWithRawResponse,
+ AsyncTelemetryResourceWithRawResponse,
+ TelemetryResourceWithStreamingResponse,
+ AsyncTelemetryResourceWithStreamingResponse,
+)
from .audit_logs import (
AuditLogsResource,
AsyncAuditLogsResource,
@@ -158,6 +166,12 @@
"AsyncAuthResourceWithRawResponse",
"AuthResourceWithStreamingResponse",
"AsyncAuthResourceWithStreamingResponse",
+ "TelemetryResource",
+ "AsyncTelemetryResource",
+ "TelemetryResourceWithRawResponse",
+ "AsyncTelemetryResourceWithRawResponse",
+ "TelemetryResourceWithStreamingResponse",
+ "AsyncTelemetryResourceWithStreamingResponse",
"ProxiesResource",
"AsyncProxiesResource",
"ProxiesResourceWithRawResponse",
diff --git a/src/kernel/resources/browsers/browsers.py b/src/kernel/resources/browsers/browsers.py
index bf80a931..8078b952 100644
--- a/src/kernel/resources/browsers/browsers.py
+++ b/src/kernel/resources/browsers/browsers.py
@@ -113,7 +113,9 @@ class BrowsersResource(SyncAPIResource):
@cached_property
def telemetry(self) -> TelemetryResource:
- """Stream live telemetry events from a browser session."""
+ """
+ Stream live telemetry events from a browser session, and manage the destinations sessions export them to.
+ """
return TelemetryResource(self._client)
@cached_property
@@ -731,7 +733,9 @@ class AsyncBrowsersResource(AsyncAPIResource):
@cached_property
def telemetry(self) -> AsyncTelemetryResource:
- """Stream live telemetry events from a browser session."""
+ """
+ Stream live telemetry events from a browser session, and manage the destinations sessions export them to.
+ """
return AsyncTelemetryResource(self._client)
@cached_property
@@ -1372,7 +1376,9 @@ def __init__(self, browsers: BrowsersResource) -> None:
@cached_property
def telemetry(self) -> TelemetryResourceWithRawResponse:
- """Stream live telemetry events from a browser session."""
+ """
+ Stream live telemetry events from a browser session, and manage the destinations sessions export them to.
+ """
return TelemetryResourceWithRawResponse(self._browsers.telemetry)
@cached_property
@@ -1434,7 +1440,9 @@ def __init__(self, browsers: AsyncBrowsersResource) -> None:
@cached_property
def telemetry(self) -> AsyncTelemetryResourceWithRawResponse:
- """Stream live telemetry events from a browser session."""
+ """
+ Stream live telemetry events from a browser session, and manage the destinations sessions export them to.
+ """
return AsyncTelemetryResourceWithRawResponse(self._browsers.telemetry)
@cached_property
@@ -1496,7 +1504,9 @@ def __init__(self, browsers: BrowsersResource) -> None:
@cached_property
def telemetry(self) -> TelemetryResourceWithStreamingResponse:
- """Stream live telemetry events from a browser session."""
+ """
+ Stream live telemetry events from a browser session, and manage the destinations sessions export them to.
+ """
return TelemetryResourceWithStreamingResponse(self._browsers.telemetry)
@cached_property
@@ -1558,7 +1568,9 @@ def __init__(self, browsers: AsyncBrowsersResource) -> None:
@cached_property
def telemetry(self) -> AsyncTelemetryResourceWithStreamingResponse:
- """Stream live telemetry events from a browser session."""
+ """
+ Stream live telemetry events from a browser session, and manage the destinations sessions export them to.
+ """
return AsyncTelemetryResourceWithStreamingResponse(self._browsers.telemetry)
@cached_property
diff --git a/src/kernel/resources/browsers/telemetry.py b/src/kernel/resources/browsers/telemetry.py
index 4c7769e7..5ed61557 100644
--- a/src/kernel/resources/browsers/telemetry.py
+++ b/src/kernel/resources/browsers/telemetry.py
@@ -28,7 +28,9 @@
class TelemetryResource(SyncAPIResource):
- """Stream live telemetry events from a browser session."""
+ """
+ Stream live telemetry events from a browser session, and manage the destinations sessions export them to.
+ """
@cached_property
def with_raw_response(self) -> TelemetryResourceWithRawResponse:
@@ -202,7 +204,9 @@ def stream(
class AsyncTelemetryResource(AsyncAPIResource):
- """Stream live telemetry events from a browser session."""
+ """
+ Stream live telemetry events from a browser session, and manage the destinations sessions export them to.
+ """
@cached_property
def with_raw_response(self) -> AsyncTelemetryResourceWithRawResponse:
diff --git a/src/kernel/resources/telemetry/__init__.py b/src/kernel/resources/telemetry/__init__.py
new file mode 100644
index 00000000..77034151
--- /dev/null
+++ b/src/kernel/resources/telemetry/__init__.py
@@ -0,0 +1,33 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from .telemetry import (
+ TelemetryResource,
+ AsyncTelemetryResource,
+ TelemetryResourceWithRawResponse,
+ AsyncTelemetryResourceWithRawResponse,
+ TelemetryResourceWithStreamingResponse,
+ AsyncTelemetryResourceWithStreamingResponse,
+)
+from .destinations import (
+ DestinationsResource,
+ AsyncDestinationsResource,
+ DestinationsResourceWithRawResponse,
+ AsyncDestinationsResourceWithRawResponse,
+ DestinationsResourceWithStreamingResponse,
+ AsyncDestinationsResourceWithStreamingResponse,
+)
+
+__all__ = [
+ "DestinationsResource",
+ "AsyncDestinationsResource",
+ "DestinationsResourceWithRawResponse",
+ "AsyncDestinationsResourceWithRawResponse",
+ "DestinationsResourceWithStreamingResponse",
+ "AsyncDestinationsResourceWithStreamingResponse",
+ "TelemetryResource",
+ "AsyncTelemetryResource",
+ "TelemetryResourceWithRawResponse",
+ "AsyncTelemetryResourceWithRawResponse",
+ "TelemetryResourceWithStreamingResponse",
+ "AsyncTelemetryResourceWithStreamingResponse",
+]
diff --git a/src/kernel/resources/telemetry/destinations.py b/src/kernel/resources/telemetry/destinations.py
new file mode 100644
index 00000000..09442435
--- /dev/null
+++ b/src/kernel/resources/telemetry/destinations.py
@@ -0,0 +1,676 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from __future__ import annotations
+
+from typing import Dict, Optional
+
+import httpx
+
+from ..._types import Body, Omit, Query, Headers, NoneType, NotGiven, omit, not_given
+from ..._utils import path_template, maybe_transform, async_maybe_transform
+from ..._compat import cached_property
+from ..._resource import SyncAPIResource, AsyncAPIResource
+from ..._response import (
+ to_raw_response_wrapper,
+ to_streamed_response_wrapper,
+ async_to_raw_response_wrapper,
+ async_to_streamed_response_wrapper,
+)
+from ...pagination import SyncOffsetPagination, AsyncOffsetPagination
+from ..._base_client import AsyncPaginator, make_request_options
+from ...types.telemetry import destination_list_params, destination_create_params, destination_update_params
+from ...types.telemetry.otlp_destination import OtlpDestination
+
+__all__ = ["DestinationsResource", "AsyncDestinationsResource"]
+
+
+class DestinationsResource(SyncAPIResource):
+ """
+ Stream live telemetry events from a browser session, and manage the destinations sessions export them to.
+ """
+
+ @cached_property
+ def with_raw_response(self) -> DestinationsResourceWithRawResponse:
+ """
+ This property can be used as a prefix for any HTTP method call to return
+ the raw response object instead of the parsed content.
+
+ For more information, see https://www.github.com/kernel/kernel-python-sdk#accessing-raw-response-data-eg-headers
+ """
+ return DestinationsResourceWithRawResponse(self)
+
+ @cached_property
+ def with_streaming_response(self) -> DestinationsResourceWithStreamingResponse:
+ """
+ An alternative to `.with_raw_response` that doesn't eagerly read the response body.
+
+ For more information, see https://www.github.com/kernel/kernel-python-sdk#with_streaming_response
+ """
+ return DestinationsResourceWithStreamingResponse(self)
+
+ def create(
+ self,
+ *,
+ endpoint: str,
+ name: str,
+ description: str | Omit = omit,
+ headers: Dict[str, str] | Omit = omit,
+ # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
+ # The extra values given here take precedence over values defined on the client or passed to this method.
+ extra_headers: Headers | None = None,
+ extra_query: Query | None = None,
+ extra_body: Body | None = None,
+ timeout: float | httpx.Timeout | None | NotGiven = not_given,
+ ) -> OtlpDestination:
+ """Create an OTLP export destination in the resolved project.
+
+ Names must be unique
+ within the project.
+
+ Args:
+ endpoint: Base endpoint of the OTLP/HTTP collector, without a signal path. Kernel appends
+ the signal path itself, so pass `https://api.honeycomb.io` rather than
+ `https://api.honeycomb.io/v1/logs`. If your provider's docs give you a
+ signal-specific URL, drop the trailing `/v1/logs`, `/v1/traces`, or
+ `/v1/metrics` — an endpoint that already carries one is rejected.
+
+ Must be http or https, must resolve to a public address, and must carry no query
+ string or fragment. Examples: `https://api.honeycomb.io`,
+ `https://otlp-gateway-prod-us-east-0.grafana.net/otlp`,
+ `https://otlp.datadoghq.com` (Datadog's OTLP intake for US1, not its logs
+ intake).
+
+ name: Unique within the project.
+
+ headers: Headers sent with each export request, typically an ingestion key. Encrypted at
+ rest and returned redacted. Names and values must be valid HTTP header tokens,
+ and the names and values together cannot exceed 8192 bytes. Names are matched
+ case-insensitively and stored canonicalized, so supplying two spellings of one
+ header is rejected.
+
+ 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
+ """
+ return self._post(
+ "/telemetry/destinations",
+ body=maybe_transform(
+ {
+ "endpoint": endpoint,
+ "name": name,
+ "description": description,
+ "headers": headers,
+ },
+ destination_create_params.DestinationCreateParams,
+ ),
+ options=make_request_options(
+ extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
+ ),
+ cast_to=OtlpDestination,
+ )
+
+ def retrieve(
+ self,
+ id_or_name: str,
+ *,
+ # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
+ # The extra values given here take precedence over values defined on the client or passed to this method.
+ extra_headers: Headers | None = None,
+ extra_query: Query | None = None,
+ extra_body: Body | None = None,
+ timeout: float | httpx.Timeout | None | NotGiven = not_given,
+ ) -> OtlpDestination:
+ """
+ Retrieve a single OTLP destination in the resolved project by its ID or name.
+
+ Args:
+ extra_headers: Send extra headers
+
+ extra_query: Add additional query parameters to the request
+
+ extra_body: Add additional JSON properties to the request
+
+ timeout: Override the client-level default timeout for this request, in seconds
+ """
+ if not id_or_name:
+ raise ValueError(f"Expected a non-empty value for `id_or_name` but received {id_or_name!r}")
+ return self._get(
+ path_template("/telemetry/destinations/{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
+ ),
+ cast_to=OtlpDestination,
+ )
+
+ def update(
+ self,
+ id_or_name: str,
+ *,
+ description: str | Omit = omit,
+ endpoint: str | Omit = omit,
+ headers: Dict[str, Optional[str]] | Omit = omit,
+ name: str | Omit = omit,
+ # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
+ # The extra values given here take precedence over values defined on the client or passed to this method.
+ extra_headers: Headers | None = None,
+ extra_query: Query | None = None,
+ extra_body: Body | None = None,
+ timeout: float | httpx.Timeout | None | NotGiven = not_given,
+ ) -> OtlpDestination:
+ """Update an OTLP destination.
+
+ Sessions already exporting to it pick up the new
+ values without restarting, which makes this the way to rotate credentials
+ without interrupting export.
+
+ Names must be unique within the project. Renaming is refused with a 409 while a
+ managed auth connection selects this destination by name, since that connection
+ resolves the name on every login. Every other field, including `headers`, stays
+ editable.
+
+ Args:
+ endpoint: Base endpoint of the OTLP/HTTP collector, without a signal path. Same rules as
+ on create.
+
+ headers: Edits stored headers key by key rather than replacing the map. A string value
+ adds or replaces that header, `null` deletes it, and any key you omit is left as
+ it is. Names are matched case-insensitively, so `authorization` replaces a
+ stored `Authorization` rather than adding a second entry. This is the credential
+ rotation path; sessions already exporting pick up the new values without
+ restarting. Names and values must be valid HTTP header tokens, and the names and
+ values together cannot exceed 8192 bytes.
+
+ extra_headers: Send extra headers
+
+ extra_query: Add additional query parameters to the request
+
+ extra_body: Add additional JSON properties to the request
+
+ timeout: Override the client-level default timeout for this request, in seconds
+ """
+ if not id_or_name:
+ raise ValueError(f"Expected a non-empty value for `id_or_name` but received {id_or_name!r}")
+ return self._patch(
+ path_template("/telemetry/destinations/{id_or_name}", id_or_name=id_or_name),
+ body=maybe_transform(
+ {
+ "description": description,
+ "endpoint": endpoint,
+ "headers": headers,
+ "name": name,
+ },
+ destination_update_params.DestinationUpdateParams,
+ ),
+ options=make_request_options(
+ extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
+ ),
+ cast_to=OtlpDestination,
+ )
+
+ def list(
+ self,
+ *,
+ limit: int | Omit = omit,
+ name: str | Omit = omit,
+ offset: int | Omit = omit,
+ query: str | Omit = omit,
+ # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
+ # The extra values given here take precedence over values defined on the client or passed to this method.
+ extra_headers: Headers | None = None,
+ extra_query: Query | None = None,
+ extra_body: Body | None = None,
+ timeout: float | httpx.Timeout | None | NotGiven = not_given,
+ ) -> SyncOffsetPagination[OtlpDestination]:
+ """
+ List OTLP export destinations in the resolved project.
+
+ Args:
+ limit: Limit the number of destinations to return.
+
+ name: Exact-match filter on destination name using the database collation. In
+ production, matching is case- and accent-insensitive.
+
+ offset: Offset the number of destinations to return.
+
+ query: Case-insensitive substring match against destination name or endpoint. IDs match
+ by exact value.
+
+ 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
+ """
+ return self._get_api_list(
+ "/telemetry/destinations",
+ page=SyncOffsetPagination[OtlpDestination],
+ options=make_request_options(
+ extra_headers=extra_headers,
+ extra_query=extra_query,
+ extra_body=extra_body,
+ timeout=timeout,
+ query=maybe_transform(
+ {
+ "limit": limit,
+ "name": name,
+ "offset": offset,
+ "query": query,
+ },
+ destination_list_params.DestinationListParams,
+ ),
+ ),
+ model=OtlpDestination,
+ )
+
+ def delete(
+ self,
+ id_or_name: str,
+ *,
+ # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
+ # The extra values given here take precedence over values defined on the client or passed to this method.
+ extra_headers: Headers | None = None,
+ extra_query: Query | None = None,
+ extra_body: Body | None = None,
+ timeout: float | httpx.Timeout | None | NotGiven = not_given,
+ ) -> None:
+ """Delete an OTLP destination.
+
+ Sessions bound to it are still exporting, so the
+ delete is refused with a 409 while any exist; either wait for those sessions to
+ end or delete them first. It is refused the same way while a managed auth
+ connection still selects it, because that connection re-resolves the destination
+ on every login, and while a managed auth login using it is still in progress.
+
+ Args:
+ extra_headers: Send extra headers
+
+ extra_query: Add additional query parameters to the request
+
+ extra_body: Add additional JSON properties to the request
+
+ timeout: Override the client-level default timeout for this request, in seconds
+ """
+ if not id_or_name:
+ raise ValueError(f"Expected a non-empty value for `id_or_name` but received {id_or_name!r}")
+ extra_headers = {"Accept": "*/*", **(extra_headers or {})}
+ return self._delete(
+ path_template("/telemetry/destinations/{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
+ ),
+ cast_to=NoneType,
+ )
+
+
+class AsyncDestinationsResource(AsyncAPIResource):
+ """
+ Stream live telemetry events from a browser session, and manage the destinations sessions export them to.
+ """
+
+ @cached_property
+ def with_raw_response(self) -> AsyncDestinationsResourceWithRawResponse:
+ """
+ This property can be used as a prefix for any HTTP method call to return
+ the raw response object instead of the parsed content.
+
+ For more information, see https://www.github.com/kernel/kernel-python-sdk#accessing-raw-response-data-eg-headers
+ """
+ return AsyncDestinationsResourceWithRawResponse(self)
+
+ @cached_property
+ def with_streaming_response(self) -> AsyncDestinationsResourceWithStreamingResponse:
+ """
+ An alternative to `.with_raw_response` that doesn't eagerly read the response body.
+
+ For more information, see https://www.github.com/kernel/kernel-python-sdk#with_streaming_response
+ """
+ return AsyncDestinationsResourceWithStreamingResponse(self)
+
+ async def create(
+ self,
+ *,
+ endpoint: str,
+ name: str,
+ description: str | Omit = omit,
+ headers: Dict[str, str] | Omit = omit,
+ # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
+ # The extra values given here take precedence over values defined on the client or passed to this method.
+ extra_headers: Headers | None = None,
+ extra_query: Query | None = None,
+ extra_body: Body | None = None,
+ timeout: float | httpx.Timeout | None | NotGiven = not_given,
+ ) -> OtlpDestination:
+ """Create an OTLP export destination in the resolved project.
+
+ Names must be unique
+ within the project.
+
+ Args:
+ endpoint: Base endpoint of the OTLP/HTTP collector, without a signal path. Kernel appends
+ the signal path itself, so pass `https://api.honeycomb.io` rather than
+ `https://api.honeycomb.io/v1/logs`. If your provider's docs give you a
+ signal-specific URL, drop the trailing `/v1/logs`, `/v1/traces`, or
+ `/v1/metrics` — an endpoint that already carries one is rejected.
+
+ Must be http or https, must resolve to a public address, and must carry no query
+ string or fragment. Examples: `https://api.honeycomb.io`,
+ `https://otlp-gateway-prod-us-east-0.grafana.net/otlp`,
+ `https://otlp.datadoghq.com` (Datadog's OTLP intake for US1, not its logs
+ intake).
+
+ name: Unique within the project.
+
+ headers: Headers sent with each export request, typically an ingestion key. Encrypted at
+ rest and returned redacted. Names and values must be valid HTTP header tokens,
+ and the names and values together cannot exceed 8192 bytes. Names are matched
+ case-insensitively and stored canonicalized, so supplying two spellings of one
+ header is rejected.
+
+ 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
+ """
+ return await self._post(
+ "/telemetry/destinations",
+ body=await async_maybe_transform(
+ {
+ "endpoint": endpoint,
+ "name": name,
+ "description": description,
+ "headers": headers,
+ },
+ destination_create_params.DestinationCreateParams,
+ ),
+ options=make_request_options(
+ extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
+ ),
+ cast_to=OtlpDestination,
+ )
+
+ async def retrieve(
+ self,
+ id_or_name: str,
+ *,
+ # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
+ # The extra values given here take precedence over values defined on the client or passed to this method.
+ extra_headers: Headers | None = None,
+ extra_query: Query | None = None,
+ extra_body: Body | None = None,
+ timeout: float | httpx.Timeout | None | NotGiven = not_given,
+ ) -> OtlpDestination:
+ """
+ Retrieve a single OTLP destination in the resolved project by its ID or name.
+
+ Args:
+ extra_headers: Send extra headers
+
+ extra_query: Add additional query parameters to the request
+
+ extra_body: Add additional JSON properties to the request
+
+ timeout: Override the client-level default timeout for this request, in seconds
+ """
+ if not id_or_name:
+ raise ValueError(f"Expected a non-empty value for `id_or_name` but received {id_or_name!r}")
+ return await self._get(
+ path_template("/telemetry/destinations/{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
+ ),
+ cast_to=OtlpDestination,
+ )
+
+ async def update(
+ self,
+ id_or_name: str,
+ *,
+ description: str | Omit = omit,
+ endpoint: str | Omit = omit,
+ headers: Dict[str, Optional[str]] | Omit = omit,
+ name: str | Omit = omit,
+ # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
+ # The extra values given here take precedence over values defined on the client or passed to this method.
+ extra_headers: Headers | None = None,
+ extra_query: Query | None = None,
+ extra_body: Body | None = None,
+ timeout: float | httpx.Timeout | None | NotGiven = not_given,
+ ) -> OtlpDestination:
+ """Update an OTLP destination.
+
+ Sessions already exporting to it pick up the new
+ values without restarting, which makes this the way to rotate credentials
+ without interrupting export.
+
+ Names must be unique within the project. Renaming is refused with a 409 while a
+ managed auth connection selects this destination by name, since that connection
+ resolves the name on every login. Every other field, including `headers`, stays
+ editable.
+
+ Args:
+ endpoint: Base endpoint of the OTLP/HTTP collector, without a signal path. Same rules as
+ on create.
+
+ headers: Edits stored headers key by key rather than replacing the map. A string value
+ adds or replaces that header, `null` deletes it, and any key you omit is left as
+ it is. Names are matched case-insensitively, so `authorization` replaces a
+ stored `Authorization` rather than adding a second entry. This is the credential
+ rotation path; sessions already exporting pick up the new values without
+ restarting. Names and values must be valid HTTP header tokens, and the names and
+ values together cannot exceed 8192 bytes.
+
+ extra_headers: Send extra headers
+
+ extra_query: Add additional query parameters to the request
+
+ extra_body: Add additional JSON properties to the request
+
+ timeout: Override the client-level default timeout for this request, in seconds
+ """
+ if not id_or_name:
+ raise ValueError(f"Expected a non-empty value for `id_or_name` but received {id_or_name!r}")
+ return await self._patch(
+ path_template("/telemetry/destinations/{id_or_name}", id_or_name=id_or_name),
+ body=await async_maybe_transform(
+ {
+ "description": description,
+ "endpoint": endpoint,
+ "headers": headers,
+ "name": name,
+ },
+ destination_update_params.DestinationUpdateParams,
+ ),
+ options=make_request_options(
+ extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
+ ),
+ cast_to=OtlpDestination,
+ )
+
+ def list(
+ self,
+ *,
+ limit: int | Omit = omit,
+ name: str | Omit = omit,
+ offset: int | Omit = omit,
+ query: str | Omit = omit,
+ # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
+ # The extra values given here take precedence over values defined on the client or passed to this method.
+ extra_headers: Headers | None = None,
+ extra_query: Query | None = None,
+ extra_body: Body | None = None,
+ timeout: float | httpx.Timeout | None | NotGiven = not_given,
+ ) -> AsyncPaginator[OtlpDestination, AsyncOffsetPagination[OtlpDestination]]:
+ """
+ List OTLP export destinations in the resolved project.
+
+ Args:
+ limit: Limit the number of destinations to return.
+
+ name: Exact-match filter on destination name using the database collation. In
+ production, matching is case- and accent-insensitive.
+
+ offset: Offset the number of destinations to return.
+
+ query: Case-insensitive substring match against destination name or endpoint. IDs match
+ by exact value.
+
+ 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
+ """
+ return self._get_api_list(
+ "/telemetry/destinations",
+ page=AsyncOffsetPagination[OtlpDestination],
+ options=make_request_options(
+ extra_headers=extra_headers,
+ extra_query=extra_query,
+ extra_body=extra_body,
+ timeout=timeout,
+ query=maybe_transform(
+ {
+ "limit": limit,
+ "name": name,
+ "offset": offset,
+ "query": query,
+ },
+ destination_list_params.DestinationListParams,
+ ),
+ ),
+ model=OtlpDestination,
+ )
+
+ async def delete(
+ self,
+ id_or_name: str,
+ *,
+ # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
+ # The extra values given here take precedence over values defined on the client or passed to this method.
+ extra_headers: Headers | None = None,
+ extra_query: Query | None = None,
+ extra_body: Body | None = None,
+ timeout: float | httpx.Timeout | None | NotGiven = not_given,
+ ) -> None:
+ """Delete an OTLP destination.
+
+ Sessions bound to it are still exporting, so the
+ delete is refused with a 409 while any exist; either wait for those sessions to
+ end or delete them first. It is refused the same way while a managed auth
+ connection still selects it, because that connection re-resolves the destination
+ on every login, and while a managed auth login using it is still in progress.
+
+ Args:
+ extra_headers: Send extra headers
+
+ extra_query: Add additional query parameters to the request
+
+ extra_body: Add additional JSON properties to the request
+
+ timeout: Override the client-level default timeout for this request, in seconds
+ """
+ if not id_or_name:
+ raise ValueError(f"Expected a non-empty value for `id_or_name` but received {id_or_name!r}")
+ extra_headers = {"Accept": "*/*", **(extra_headers or {})}
+ return await self._delete(
+ path_template("/telemetry/destinations/{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
+ ),
+ cast_to=NoneType,
+ )
+
+
+class DestinationsResourceWithRawResponse:
+ def __init__(self, destinations: DestinationsResource) -> None:
+ self._destinations = destinations
+
+ self.create = to_raw_response_wrapper(
+ destinations.create,
+ )
+ self.retrieve = to_raw_response_wrapper(
+ destinations.retrieve,
+ )
+ self.update = to_raw_response_wrapper(
+ destinations.update,
+ )
+ self.list = to_raw_response_wrapper(
+ destinations.list,
+ )
+ self.delete = to_raw_response_wrapper(
+ destinations.delete,
+ )
+
+
+class AsyncDestinationsResourceWithRawResponse:
+ def __init__(self, destinations: AsyncDestinationsResource) -> None:
+ self._destinations = destinations
+
+ self.create = async_to_raw_response_wrapper(
+ destinations.create,
+ )
+ self.retrieve = async_to_raw_response_wrapper(
+ destinations.retrieve,
+ )
+ self.update = async_to_raw_response_wrapper(
+ destinations.update,
+ )
+ self.list = async_to_raw_response_wrapper(
+ destinations.list,
+ )
+ self.delete = async_to_raw_response_wrapper(
+ destinations.delete,
+ )
+
+
+class DestinationsResourceWithStreamingResponse:
+ def __init__(self, destinations: DestinationsResource) -> None:
+ self._destinations = destinations
+
+ self.create = to_streamed_response_wrapper(
+ destinations.create,
+ )
+ self.retrieve = to_streamed_response_wrapper(
+ destinations.retrieve,
+ )
+ self.update = to_streamed_response_wrapper(
+ destinations.update,
+ )
+ self.list = to_streamed_response_wrapper(
+ destinations.list,
+ )
+ self.delete = to_streamed_response_wrapper(
+ destinations.delete,
+ )
+
+
+class AsyncDestinationsResourceWithStreamingResponse:
+ def __init__(self, destinations: AsyncDestinationsResource) -> None:
+ self._destinations = destinations
+
+ self.create = async_to_streamed_response_wrapper(
+ destinations.create,
+ )
+ self.retrieve = async_to_streamed_response_wrapper(
+ destinations.retrieve,
+ )
+ self.update = async_to_streamed_response_wrapper(
+ destinations.update,
+ )
+ self.list = async_to_streamed_response_wrapper(
+ destinations.list,
+ )
+ self.delete = async_to_streamed_response_wrapper(
+ destinations.delete,
+ )
diff --git a/src/kernel/resources/telemetry/telemetry.py b/src/kernel/resources/telemetry/telemetry.py
new file mode 100644
index 00000000..afdb7434
--- /dev/null
+++ b/src/kernel/resources/telemetry/telemetry.py
@@ -0,0 +1,120 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from __future__ import annotations
+
+from ..._compat import cached_property
+from ..._resource import SyncAPIResource, AsyncAPIResource
+from .destinations import (
+ DestinationsResource,
+ AsyncDestinationsResource,
+ DestinationsResourceWithRawResponse,
+ AsyncDestinationsResourceWithRawResponse,
+ DestinationsResourceWithStreamingResponse,
+ AsyncDestinationsResourceWithStreamingResponse,
+)
+
+__all__ = ["TelemetryResource", "AsyncTelemetryResource"]
+
+
+class TelemetryResource(SyncAPIResource):
+ @cached_property
+ def destinations(self) -> DestinationsResource:
+ """
+ Stream live telemetry events from a browser session, and manage the destinations sessions export them to.
+ """
+ return DestinationsResource(self._client)
+
+ @cached_property
+ def with_raw_response(self) -> TelemetryResourceWithRawResponse:
+ """
+ This property can be used as a prefix for any HTTP method call to return
+ the raw response object instead of the parsed content.
+
+ For more information, see https://www.github.com/kernel/kernel-python-sdk#accessing-raw-response-data-eg-headers
+ """
+ return TelemetryResourceWithRawResponse(self)
+
+ @cached_property
+ def with_streaming_response(self) -> TelemetryResourceWithStreamingResponse:
+ """
+ An alternative to `.with_raw_response` that doesn't eagerly read the response body.
+
+ For more information, see https://www.github.com/kernel/kernel-python-sdk#with_streaming_response
+ """
+ return TelemetryResourceWithStreamingResponse(self)
+
+
+class AsyncTelemetryResource(AsyncAPIResource):
+ @cached_property
+ def destinations(self) -> AsyncDestinationsResource:
+ """
+ Stream live telemetry events from a browser session, and manage the destinations sessions export them to.
+ """
+ return AsyncDestinationsResource(self._client)
+
+ @cached_property
+ def with_raw_response(self) -> AsyncTelemetryResourceWithRawResponse:
+ """
+ This property can be used as a prefix for any HTTP method call to return
+ the raw response object instead of the parsed content.
+
+ For more information, see https://www.github.com/kernel/kernel-python-sdk#accessing-raw-response-data-eg-headers
+ """
+ return AsyncTelemetryResourceWithRawResponse(self)
+
+ @cached_property
+ def with_streaming_response(self) -> AsyncTelemetryResourceWithStreamingResponse:
+ """
+ An alternative to `.with_raw_response` that doesn't eagerly read the response body.
+
+ For more information, see https://www.github.com/kernel/kernel-python-sdk#with_streaming_response
+ """
+ return AsyncTelemetryResourceWithStreamingResponse(self)
+
+
+class TelemetryResourceWithRawResponse:
+ def __init__(self, telemetry: TelemetryResource) -> None:
+ self._telemetry = telemetry
+
+ @cached_property
+ def destinations(self) -> DestinationsResourceWithRawResponse:
+ """
+ Stream live telemetry events from a browser session, and manage the destinations sessions export them to.
+ """
+ return DestinationsResourceWithRawResponse(self._telemetry.destinations)
+
+
+class AsyncTelemetryResourceWithRawResponse:
+ def __init__(self, telemetry: AsyncTelemetryResource) -> None:
+ self._telemetry = telemetry
+
+ @cached_property
+ def destinations(self) -> AsyncDestinationsResourceWithRawResponse:
+ """
+ Stream live telemetry events from a browser session, and manage the destinations sessions export them to.
+ """
+ return AsyncDestinationsResourceWithRawResponse(self._telemetry.destinations)
+
+
+class TelemetryResourceWithStreamingResponse:
+ def __init__(self, telemetry: TelemetryResource) -> None:
+ self._telemetry = telemetry
+
+ @cached_property
+ def destinations(self) -> DestinationsResourceWithStreamingResponse:
+ """
+ Stream live telemetry events from a browser session, and manage the destinations sessions export them to.
+ """
+ return DestinationsResourceWithStreamingResponse(self._telemetry.destinations)
+
+
+class AsyncTelemetryResourceWithStreamingResponse:
+ def __init__(self, telemetry: AsyncTelemetryResource) -> None:
+ self._telemetry = telemetry
+
+ @cached_property
+ def destinations(self) -> AsyncDestinationsResourceWithStreamingResponse:
+ """
+ Stream live telemetry events from a browser session, and manage the destinations sessions export them to.
+ """
+ return AsyncDestinationsResourceWithStreamingResponse(self._telemetry.destinations)
diff --git a/src/kernel/types/telemetry/__init__.py b/src/kernel/types/telemetry/__init__.py
new file mode 100644
index 00000000..e0a465df
--- /dev/null
+++ b/src/kernel/types/telemetry/__init__.py
@@ -0,0 +1,8 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from __future__ import annotations
+
+from .otlp_destination import OtlpDestination as OtlpDestination
+from .destination_list_params import DestinationListParams as DestinationListParams
+from .destination_create_params import DestinationCreateParams as DestinationCreateParams
+from .destination_update_params import DestinationUpdateParams as DestinationUpdateParams
diff --git a/src/kernel/types/telemetry/destination_create_params.py b/src/kernel/types/telemetry/destination_create_params.py
new file mode 100644
index 00000000..882f7cc6
--- /dev/null
+++ b/src/kernel/types/telemetry/destination_create_params.py
@@ -0,0 +1,39 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from __future__ import annotations
+
+from typing import Dict
+from typing_extensions import Required, TypedDict
+
+__all__ = ["DestinationCreateParams"]
+
+
+class DestinationCreateParams(TypedDict, total=False):
+ endpoint: Required[str]
+ """Base endpoint of the OTLP/HTTP collector, without a signal path.
+
+ Kernel appends the signal path itself, so pass `https://api.honeycomb.io` rather
+ than `https://api.honeycomb.io/v1/logs`. If your provider's docs give you a
+ signal-specific URL, drop the trailing `/v1/logs`, `/v1/traces`, or
+ `/v1/metrics` — an endpoint that already carries one is rejected.
+
+ Must be http or https, must resolve to a public address, and must carry no query
+ string or fragment. Examples: `https://api.honeycomb.io`,
+ `https://otlp-gateway-prod-us-east-0.grafana.net/otlp`,
+ `https://otlp.datadoghq.com` (Datadog's OTLP intake for US1, not its logs
+ intake).
+ """
+
+ name: Required[str]
+ """Unique within the project."""
+
+ description: str
+
+ headers: Dict[str, str]
+ """Headers sent with each export request, typically an ingestion key.
+
+ Encrypted at rest and returned redacted. Names and values must be valid HTTP
+ header tokens, and the names and values together cannot exceed 8192 bytes. Names
+ are matched case-insensitively and stored canonicalized, so supplying two
+ spellings of one header is rejected.
+ """
diff --git a/src/kernel/types/telemetry/destination_list_params.py b/src/kernel/types/telemetry/destination_list_params.py
new file mode 100644
index 00000000..fa7a1dbe
--- /dev/null
+++ b/src/kernel/types/telemetry/destination_list_params.py
@@ -0,0 +1,27 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from __future__ import annotations
+
+from typing_extensions import TypedDict
+
+__all__ = ["DestinationListParams"]
+
+
+class DestinationListParams(TypedDict, total=False):
+ limit: int
+ """Limit the number of destinations to return."""
+
+ name: str
+ """Exact-match filter on destination name using the database collation.
+
+ In production, matching is case- and accent-insensitive.
+ """
+
+ offset: int
+ """Offset the number of destinations to return."""
+
+ query: str
+ """Case-insensitive substring match against destination name or endpoint.
+
+ IDs match by exact value.
+ """
diff --git a/src/kernel/types/telemetry/destination_update_params.py b/src/kernel/types/telemetry/destination_update_params.py
new file mode 100644
index 00000000..266b8029
--- /dev/null
+++ b/src/kernel/types/telemetry/destination_update_params.py
@@ -0,0 +1,31 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from __future__ import annotations
+
+from typing import Dict, Optional
+from typing_extensions import TypedDict
+
+__all__ = ["DestinationUpdateParams"]
+
+
+class DestinationUpdateParams(TypedDict, total=False):
+ description: str
+
+ endpoint: str
+ """Base endpoint of the OTLP/HTTP collector, without a signal path.
+
+ Same rules as on create.
+ """
+
+ headers: Dict[str, Optional[str]]
+ """Edits stored headers key by key rather than replacing the map.
+
+ A string value adds or replaces that header, `null` deletes it, and any key you
+ omit is left as it is. Names are matched case-insensitively, so `authorization`
+ replaces a stored `Authorization` rather than adding a second entry. This is the
+ credential rotation path; sessions already exporting pick up the new values
+ without restarting. Names and values must be valid HTTP header tokens, and the
+ names and values together cannot exceed 8192 bytes.
+ """
+
+ name: str
diff --git a/src/kernel/types/telemetry/otlp_destination.py b/src/kernel/types/telemetry/otlp_destination.py
new file mode 100644
index 00000000..c6e6f2bc
--- /dev/null
+++ b/src/kernel/types/telemetry/otlp_destination.py
@@ -0,0 +1,41 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from typing import Dict, Optional
+from datetime import datetime
+
+from ..._models import BaseModel
+
+__all__ = ["OtlpDestination"]
+
+
+class OtlpDestination(BaseModel):
+ """An OTLP endpoint to export browser session telemetry to.
+
+ Reference one from `telemetry.export.otlp.destination` when creating a browser to export that session's captured telemetry to it.
+ """
+
+ id: str
+
+ created_at: datetime
+
+ endpoint: str
+ """OTLP/HTTP endpoint telemetry is sent to."""
+
+ headers: Dict[str, str]
+ """Headers sent with each export request.
+
+ Names are returned in canonical form (`Authorization`, not `authorization`).
+ Values are returned redacted as empty strings, so the keys are visible but the
+ credentials are not.
+ """
+
+ name: str
+ """Unique within the project.
+
+ Usable in place of the ID when selecting a destination, so it cannot be shaped
+ like an ID.
+ """
+
+ updated_at: datetime
+
+ description: Optional[str] = None
diff --git a/tests/api_resources/telemetry/__init__.py b/tests/api_resources/telemetry/__init__.py
new file mode 100644
index 00000000..fd8019a9
--- /dev/null
+++ b/tests/api_resources/telemetry/__init__.py
@@ -0,0 +1 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
diff --git a/tests/api_resources/telemetry/test_destinations.py b/tests/api_resources/telemetry/test_destinations.py
new file mode 100644
index 00000000..ddf06c0d
--- /dev/null
+++ b/tests/api_resources/telemetry/test_destinations.py
@@ -0,0 +1,477 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from __future__ import annotations
+
+import os
+from typing import Any, cast
+
+import pytest
+
+from kernel import Kernel, AsyncKernel
+from tests.utils import assert_matches_type
+from kernel.pagination import SyncOffsetPagination, AsyncOffsetPagination
+from kernel.types.telemetry import (
+ OtlpDestination,
+)
+
+base_url = os.environ.get("TEST_API_BASE_URL", "http://127.0.0.1:4010")
+
+
+class TestDestinations:
+ parametrize = pytest.mark.parametrize("client", [False, True], indirect=True, ids=["loose", "strict"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ def test_method_create(self, client: Kernel) -> None:
+ destination = client.telemetry.destinations.create(
+ endpoint="x",
+ name="name",
+ )
+ assert_matches_type(OtlpDestination, destination, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ def test_method_create_with_all_params(self, client: Kernel) -> None:
+ destination = client.telemetry.destinations.create(
+ endpoint="x",
+ name="name",
+ description="description",
+ headers={"foo": "string"},
+ )
+ assert_matches_type(OtlpDestination, destination, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ def test_raw_response_create(self, client: Kernel) -> None:
+ response = client.telemetry.destinations.with_raw_response.create(
+ endpoint="x",
+ name="name",
+ )
+
+ assert response.is_closed is True
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+ destination = response.parse()
+ assert_matches_type(OtlpDestination, destination, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ def test_streaming_response_create(self, client: Kernel) -> None:
+ with client.telemetry.destinations.with_streaming_response.create(
+ endpoint="x",
+ name="name",
+ ) as response:
+ assert not response.is_closed
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+
+ destination = response.parse()
+ assert_matches_type(OtlpDestination, destination, path=["response"])
+
+ assert cast(Any, response.is_closed) is True
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ def test_method_retrieve(self, client: Kernel) -> None:
+ destination = client.telemetry.destinations.retrieve(
+ "id_or_name",
+ )
+ assert_matches_type(OtlpDestination, destination, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ def test_raw_response_retrieve(self, client: Kernel) -> None:
+ response = client.telemetry.destinations.with_raw_response.retrieve(
+ "id_or_name",
+ )
+
+ assert response.is_closed is True
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+ destination = response.parse()
+ assert_matches_type(OtlpDestination, destination, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ def test_streaming_response_retrieve(self, client: Kernel) -> None:
+ with client.telemetry.destinations.with_streaming_response.retrieve(
+ "id_or_name",
+ ) as response:
+ assert not response.is_closed
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+
+ destination = response.parse()
+ assert_matches_type(OtlpDestination, destination, path=["response"])
+
+ assert cast(Any, response.is_closed) is True
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ def test_path_params_retrieve(self, client: Kernel) -> None:
+ with pytest.raises(ValueError, match=r"Expected a non-empty value for `id_or_name` but received ''"):
+ client.telemetry.destinations.with_raw_response.retrieve(
+ "",
+ )
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ def test_method_update(self, client: Kernel) -> None:
+ destination = client.telemetry.destinations.update(
+ id_or_name="id_or_name",
+ )
+ assert_matches_type(OtlpDestination, destination, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ def test_method_update_with_all_params(self, client: Kernel) -> None:
+ destination = client.telemetry.destinations.update(
+ id_or_name="id_or_name",
+ description="description",
+ endpoint="x",
+ headers={"foo": "string"},
+ name="name",
+ )
+ assert_matches_type(OtlpDestination, destination, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ def test_raw_response_update(self, client: Kernel) -> None:
+ response = client.telemetry.destinations.with_raw_response.update(
+ id_or_name="id_or_name",
+ )
+
+ assert response.is_closed is True
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+ destination = response.parse()
+ assert_matches_type(OtlpDestination, destination, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ def test_streaming_response_update(self, client: Kernel) -> None:
+ with client.telemetry.destinations.with_streaming_response.update(
+ id_or_name="id_or_name",
+ ) as response:
+ assert not response.is_closed
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+
+ destination = response.parse()
+ assert_matches_type(OtlpDestination, destination, 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_or_name` but received ''"):
+ client.telemetry.destinations.with_raw_response.update(
+ id_or_name="",
+ )
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ def test_method_list(self, client: Kernel) -> None:
+ destination = client.telemetry.destinations.list()
+ assert_matches_type(SyncOffsetPagination[OtlpDestination], destination, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ def test_method_list_with_all_params(self, client: Kernel) -> None:
+ destination = client.telemetry.destinations.list(
+ limit=1,
+ name="name",
+ offset=0,
+ query="query",
+ )
+ assert_matches_type(SyncOffsetPagination[OtlpDestination], destination, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ def test_raw_response_list(self, client: Kernel) -> None:
+ response = client.telemetry.destinations.with_raw_response.list()
+
+ assert response.is_closed is True
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+ destination = response.parse()
+ assert_matches_type(SyncOffsetPagination[OtlpDestination], destination, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ def test_streaming_response_list(self, client: Kernel) -> None:
+ with client.telemetry.destinations.with_streaming_response.list() as response:
+ assert not response.is_closed
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+
+ destination = response.parse()
+ assert_matches_type(SyncOffsetPagination[OtlpDestination], destination, path=["response"])
+
+ assert cast(Any, response.is_closed) is True
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ def test_method_delete(self, client: Kernel) -> None:
+ destination = client.telemetry.destinations.delete(
+ "id_or_name",
+ )
+ assert destination is None
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ def test_raw_response_delete(self, client: Kernel) -> None:
+ response = client.telemetry.destinations.with_raw_response.delete(
+ "id_or_name",
+ )
+
+ assert response.is_closed is True
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+ destination = response.parse()
+ assert destination is None
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ def test_streaming_response_delete(self, client: Kernel) -> None:
+ with client.telemetry.destinations.with_streaming_response.delete(
+ "id_or_name",
+ ) as response:
+ assert not response.is_closed
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+
+ destination = response.parse()
+ assert destination is None
+
+ assert cast(Any, response.is_closed) is True
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ def test_path_params_delete(self, client: Kernel) -> None:
+ with pytest.raises(ValueError, match=r"Expected a non-empty value for `id_or_name` but received ''"):
+ client.telemetry.destinations.with_raw_response.delete(
+ "",
+ )
+
+
+class TestAsyncDestinations:
+ parametrize = pytest.mark.parametrize(
+ "async_client", [False, True, {"http_client": "aiohttp"}], indirect=True, ids=["loose", "strict", "aiohttp"]
+ )
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ async def test_method_create(self, async_client: AsyncKernel) -> None:
+ destination = await async_client.telemetry.destinations.create(
+ endpoint="x",
+ name="name",
+ )
+ assert_matches_type(OtlpDestination, destination, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ async def test_method_create_with_all_params(self, async_client: AsyncKernel) -> None:
+ destination = await async_client.telemetry.destinations.create(
+ endpoint="x",
+ name="name",
+ description="description",
+ headers={"foo": "string"},
+ )
+ assert_matches_type(OtlpDestination, destination, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ async def test_raw_response_create(self, async_client: AsyncKernel) -> None:
+ response = await async_client.telemetry.destinations.with_raw_response.create(
+ endpoint="x",
+ name="name",
+ )
+
+ assert response.is_closed is True
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+ destination = await response.parse()
+ assert_matches_type(OtlpDestination, destination, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ async def test_streaming_response_create(self, async_client: AsyncKernel) -> None:
+ async with async_client.telemetry.destinations.with_streaming_response.create(
+ endpoint="x",
+ name="name",
+ ) as response:
+ assert not response.is_closed
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+
+ destination = await response.parse()
+ assert_matches_type(OtlpDestination, destination, path=["response"])
+
+ assert cast(Any, response.is_closed) is True
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ async def test_method_retrieve(self, async_client: AsyncKernel) -> None:
+ destination = await async_client.telemetry.destinations.retrieve(
+ "id_or_name",
+ )
+ assert_matches_type(OtlpDestination, destination, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ async def test_raw_response_retrieve(self, async_client: AsyncKernel) -> None:
+ response = await async_client.telemetry.destinations.with_raw_response.retrieve(
+ "id_or_name",
+ )
+
+ assert response.is_closed is True
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+ destination = await response.parse()
+ assert_matches_type(OtlpDestination, destination, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ async def test_streaming_response_retrieve(self, async_client: AsyncKernel) -> None:
+ async with async_client.telemetry.destinations.with_streaming_response.retrieve(
+ "id_or_name",
+ ) as response:
+ assert not response.is_closed
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+
+ destination = await response.parse()
+ assert_matches_type(OtlpDestination, destination, 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_retrieve(self, async_client: AsyncKernel) -> None:
+ with pytest.raises(ValueError, match=r"Expected a non-empty value for `id_or_name` but received ''"):
+ await async_client.telemetry.destinations.with_raw_response.retrieve(
+ "",
+ )
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ async def test_method_update(self, async_client: AsyncKernel) -> None:
+ destination = await async_client.telemetry.destinations.update(
+ id_or_name="id_or_name",
+ )
+ assert_matches_type(OtlpDestination, destination, 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:
+ destination = await async_client.telemetry.destinations.update(
+ id_or_name="id_or_name",
+ description="description",
+ endpoint="x",
+ headers={"foo": "string"},
+ name="name",
+ )
+ assert_matches_type(OtlpDestination, destination, 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.telemetry.destinations.with_raw_response.update(
+ id_or_name="id_or_name",
+ )
+
+ assert response.is_closed is True
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+ destination = await response.parse()
+ assert_matches_type(OtlpDestination, destination, 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.telemetry.destinations.with_streaming_response.update(
+ id_or_name="id_or_name",
+ ) as response:
+ assert not response.is_closed
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+
+ destination = await response.parse()
+ assert_matches_type(OtlpDestination, destination, 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_or_name` but received ''"):
+ await async_client.telemetry.destinations.with_raw_response.update(
+ id_or_name="",
+ )
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ async def test_method_list(self, async_client: AsyncKernel) -> None:
+ destination = await async_client.telemetry.destinations.list()
+ assert_matches_type(AsyncOffsetPagination[OtlpDestination], destination, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ async def test_method_list_with_all_params(self, async_client: AsyncKernel) -> None:
+ destination = await async_client.telemetry.destinations.list(
+ limit=1,
+ name="name",
+ offset=0,
+ query="query",
+ )
+ assert_matches_type(AsyncOffsetPagination[OtlpDestination], destination, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ async def test_raw_response_list(self, async_client: AsyncKernel) -> None:
+ response = await async_client.telemetry.destinations.with_raw_response.list()
+
+ assert response.is_closed is True
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+ destination = await response.parse()
+ assert_matches_type(AsyncOffsetPagination[OtlpDestination], destination, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ async def test_streaming_response_list(self, async_client: AsyncKernel) -> None:
+ async with async_client.telemetry.destinations.with_streaming_response.list() as response:
+ assert not response.is_closed
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+
+ destination = await response.parse()
+ assert_matches_type(AsyncOffsetPagination[OtlpDestination], destination, path=["response"])
+
+ assert cast(Any, response.is_closed) is True
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ async def test_method_delete(self, async_client: AsyncKernel) -> None:
+ destination = await async_client.telemetry.destinations.delete(
+ "id_or_name",
+ )
+ assert destination is None
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ async def test_raw_response_delete(self, async_client: AsyncKernel) -> None:
+ response = await async_client.telemetry.destinations.with_raw_response.delete(
+ "id_or_name",
+ )
+
+ assert response.is_closed is True
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+ destination = await response.parse()
+ assert destination is None
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ async def test_streaming_response_delete(self, async_client: AsyncKernel) -> None:
+ async with async_client.telemetry.destinations.with_streaming_response.delete(
+ "id_or_name",
+ ) as response:
+ assert not response.is_closed
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+
+ destination = await response.parse()
+ assert destination is None
+
+ assert cast(Any, response.is_closed) is True
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ async def test_path_params_delete(self, async_client: AsyncKernel) -> None:
+ with pytest.raises(ValueError, match=r"Expected a non-empty value for `id_or_name` but received ''"):
+ await async_client.telemetry.destinations.with_raw_response.delete(
+ "",
+ )
From db06ca8018bf8baf7ff65ddf917415cd66da726a Mon Sep 17 00:00:00 2001
From: "kernel-internal[bot]"
<260533166+kernel-internal[bot]@users.noreply.github.com>
Date: Fri, 14 Aug 2026 18:12:22 +0000
Subject: [PATCH 195/366] release: 0.91.0
---
.release-please-manifest.json | 2 +-
CHANGELOG.md | 9 +++++++++
pyproject.toml | 2 +-
src/kernel/_version.py | 2 +-
4 files changed, 12 insertions(+), 3 deletions(-)
diff --git a/.release-please-manifest.json b/.release-please-manifest.json
index f2863752..29c535cb 100644
--- a/.release-please-manifest.json
+++ b/.release-please-manifest.json
@@ -1,3 +1,3 @@
{
- ".": "0.90.0"
+ ".": "0.91.0"
}
\ No newline at end of file
diff --git a/CHANGELOG.md b/CHANGELOG.md
index f6add314..4b641f09 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,14 @@
# Changelog
+## [0.91.0](https://github.com/kernel/kernel-python-sdk/compare/v0.90.0...v0.91.0) (2026-08-14)
+
+
+### Features
+
+* Add customer-facing OTLP destination CRUD API ([8479adf](https://github.com/kernel/kernel-python-sdk/commit/8479adff62631c996a4a9129fd8639c16d96b92b))
+* Expose configurable browser memory ([ccf4127](https://github.com/kernel/kernel-python-sdk/commit/ccf4127272adc749f0da75b7e2458e4a9e183423))
+* Require a name on credential providers and backfill unnamed rows ([0d5a4c2](https://github.com/kernel/kernel-python-sdk/commit/0d5a4c29be2e2bc0a216436cbda4bc8602784d02))
+
## [0.90.0](https://github.com/kernel/kernel-python-sdk/compare/v0.89.0...v0.90.0) (2026-08-12)
diff --git a/pyproject.toml b/pyproject.toml
index 521e38c9..1c078ba3 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -1,6 +1,6 @@
[project]
name = "kernel"
-version = "0.90.0"
+version = "0.91.0"
description = "The official Python library for the kernel API"
dynamic = ["readme"]
license = "Apache-2.0"
diff --git a/src/kernel/_version.py b/src/kernel/_version.py
index 8933bc49..06739f24 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.90.0" # x-release-please-version
+__version__ = "0.91.0" # x-release-please-version
From 5b74e460198b174e69f0bf690b01f216e89b3dd0 Mon Sep 17 00:00:00 2001
From: "kernel-internal[bot]"
<260533166+kernel-internal[bot]@users.noreply.github.com>
Date: Mon, 17 Aug 2026 20:38:21 +0000
Subject: [PATCH 196/366] feat: chore(stlc): seal custom-code tracking files
Stainless-Generated-From: 6d79f6bf702d5e653e96eb43611b7593c0c3620b
---
.stats.yml | 2 +-
api.md | 12 ++
src/kernel/resources/organization/__init__.py | 14 ++
.../resources/organization/entitlements.py | 147 +++++++++++++
.../resources/organization/organization.py | 38 ++++
src/kernel/types/organization/__init__.py | 1 +
.../types/organization/org_entitlements.py | 199 ++++++++++++++++++
.../organization/test_entitlements.py | 80 +++++++
8 files changed, 492 insertions(+), 1 deletion(-)
create mode 100644 src/kernel/resources/organization/entitlements.py
create mode 100644 src/kernel/types/organization/org_entitlements.py
create mode 100644 tests/api_resources/organization/test_entitlements.py
diff --git a/.stats.yml b/.stats.yml
index 98f97da5..f9d1086b 100644
--- a/.stats.yml
+++ b/.stats.yml
@@ -1 +1 @@
-configured_endpoints: 139
+configured_endpoints: 140
diff --git a/api.md b/api.md
index 62ade185..fda90ce0 100644
--- a/api.md
+++ b/api.md
@@ -472,6 +472,18 @@ Methods:
# Organization
+## Entitlements
+
+Types:
+
+```python
+from kernel.types.organization import OrgEntitlements
+```
+
+Methods:
+
+- client.organization.entitlements.retrieve() -> OrgEntitlements
+
## Limits
Types:
diff --git a/src/kernel/resources/organization/__init__.py b/src/kernel/resources/organization/__init__.py
index 68c28d60..ae6da711 100644
--- a/src/kernel/resources/organization/__init__.py
+++ b/src/kernel/resources/organization/__init__.py
@@ -8,6 +8,14 @@
LimitsResourceWithStreamingResponse,
AsyncLimitsResourceWithStreamingResponse,
)
+from .entitlements import (
+ EntitlementsResource,
+ AsyncEntitlementsResource,
+ EntitlementsResourceWithRawResponse,
+ AsyncEntitlementsResourceWithRawResponse,
+ EntitlementsResourceWithStreamingResponse,
+ AsyncEntitlementsResourceWithStreamingResponse,
+)
from .organization import (
OrganizationResource,
AsyncOrganizationResource,
@@ -18,6 +26,12 @@
)
__all__ = [
+ "EntitlementsResource",
+ "AsyncEntitlementsResource",
+ "EntitlementsResourceWithRawResponse",
+ "AsyncEntitlementsResourceWithRawResponse",
+ "EntitlementsResourceWithStreamingResponse",
+ "AsyncEntitlementsResourceWithStreamingResponse",
"LimitsResource",
"AsyncLimitsResource",
"LimitsResourceWithRawResponse",
diff --git a/src/kernel/resources/organization/entitlements.py b/src/kernel/resources/organization/entitlements.py
new file mode 100644
index 00000000..05dc90f8
--- /dev/null
+++ b/src/kernel/resources/organization/entitlements.py
@@ -0,0 +1,147 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from __future__ import annotations
+
+import httpx
+
+from ..._types import Body, Query, Headers, NotGiven, not_given
+from ..._compat import cached_property
+from ..._resource import SyncAPIResource, AsyncAPIResource
+from ..._response import (
+ to_raw_response_wrapper,
+ to_streamed_response_wrapper,
+ async_to_raw_response_wrapper,
+ async_to_streamed_response_wrapper,
+)
+from ..._base_client import make_request_options
+from ...types.organization.org_entitlements import OrgEntitlements
+
+__all__ = ["EntitlementsResource", "AsyncEntitlementsResource"]
+
+
+class EntitlementsResource(SyncAPIResource):
+ """Read and manage organization-level limits."""
+
+ @cached_property
+ def with_raw_response(self) -> EntitlementsResourceWithRawResponse:
+ """
+ This property can be used as a prefix for any HTTP method call to return
+ the raw response object instead of the parsed content.
+
+ For more information, see https://www.github.com/kernel/kernel-python-sdk#accessing-raw-response-data-eg-headers
+ """
+ return EntitlementsResourceWithRawResponse(self)
+
+ @cached_property
+ def with_streaming_response(self) -> EntitlementsResourceWithStreamingResponse:
+ """
+ An alternative to `.with_raw_response` that doesn't eagerly read the response body.
+
+ For more information, see https://www.github.com/kernel/kernel-python-sdk#with_streaming_response
+ """
+ return EntitlementsResourceWithStreamingResponse(self)
+
+ def retrieve(
+ self,
+ *,
+ # 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,
+ ) -> OrgEntitlements:
+ """
+ Get the authenticated organization's effective feature access and constraints
+ after applying its plan, active trial treatment, plan status, and
+ organization-specific overrides. Null constraint values mean unlimited.
+ """
+ return self._get(
+ "/org/entitlements",
+ options=make_request_options(
+ extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
+ ),
+ cast_to=OrgEntitlements,
+ )
+
+
+class AsyncEntitlementsResource(AsyncAPIResource):
+ """Read and manage organization-level limits."""
+
+ @cached_property
+ def with_raw_response(self) -> AsyncEntitlementsResourceWithRawResponse:
+ """
+ This property can be used as a prefix for any HTTP method call to return
+ the raw response object instead of the parsed content.
+
+ For more information, see https://www.github.com/kernel/kernel-python-sdk#accessing-raw-response-data-eg-headers
+ """
+ return AsyncEntitlementsResourceWithRawResponse(self)
+
+ @cached_property
+ def with_streaming_response(self) -> AsyncEntitlementsResourceWithStreamingResponse:
+ """
+ An alternative to `.with_raw_response` that doesn't eagerly read the response body.
+
+ For more information, see https://www.github.com/kernel/kernel-python-sdk#with_streaming_response
+ """
+ return AsyncEntitlementsResourceWithStreamingResponse(self)
+
+ async def retrieve(
+ self,
+ *,
+ # 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,
+ ) -> OrgEntitlements:
+ """
+ Get the authenticated organization's effective feature access and constraints
+ after applying its plan, active trial treatment, plan status, and
+ organization-specific overrides. Null constraint values mean unlimited.
+ """
+ return await self._get(
+ "/org/entitlements",
+ options=make_request_options(
+ extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
+ ),
+ cast_to=OrgEntitlements,
+ )
+
+
+class EntitlementsResourceWithRawResponse:
+ def __init__(self, entitlements: EntitlementsResource) -> None:
+ self._entitlements = entitlements
+
+ self.retrieve = to_raw_response_wrapper(
+ entitlements.retrieve,
+ )
+
+
+class AsyncEntitlementsResourceWithRawResponse:
+ def __init__(self, entitlements: AsyncEntitlementsResource) -> None:
+ self._entitlements = entitlements
+
+ self.retrieve = async_to_raw_response_wrapper(
+ entitlements.retrieve,
+ )
+
+
+class EntitlementsResourceWithStreamingResponse:
+ def __init__(self, entitlements: EntitlementsResource) -> None:
+ self._entitlements = entitlements
+
+ self.retrieve = to_streamed_response_wrapper(
+ entitlements.retrieve,
+ )
+
+
+class AsyncEntitlementsResourceWithStreamingResponse:
+ def __init__(self, entitlements: AsyncEntitlementsResource) -> None:
+ self._entitlements = entitlements
+
+ self.retrieve = async_to_streamed_response_wrapper(
+ entitlements.retrieve,
+ )
diff --git a/src/kernel/resources/organization/organization.py b/src/kernel/resources/organization/organization.py
index 21163122..78ad5624 100644
--- a/src/kernel/resources/organization/organization.py
+++ b/src/kernel/resources/organization/organization.py
@@ -12,11 +12,24 @@
)
from ..._compat import cached_property
from ..._resource import SyncAPIResource, AsyncAPIResource
+from .entitlements import (
+ EntitlementsResource,
+ AsyncEntitlementsResource,
+ EntitlementsResourceWithRawResponse,
+ AsyncEntitlementsResourceWithRawResponse,
+ EntitlementsResourceWithStreamingResponse,
+ AsyncEntitlementsResourceWithStreamingResponse,
+)
__all__ = ["OrganizationResource", "AsyncOrganizationResource"]
class OrganizationResource(SyncAPIResource):
+ @cached_property
+ def entitlements(self) -> EntitlementsResource:
+ """Read and manage organization-level limits."""
+ return EntitlementsResource(self._client)
+
@cached_property
def limits(self) -> LimitsResource:
"""Read and manage organization-level limits."""
@@ -43,6 +56,11 @@ def with_streaming_response(self) -> OrganizationResourceWithStreamingResponse:
class AsyncOrganizationResource(AsyncAPIResource):
+ @cached_property
+ def entitlements(self) -> AsyncEntitlementsResource:
+ """Read and manage organization-level limits."""
+ return AsyncEntitlementsResource(self._client)
+
@cached_property
def limits(self) -> AsyncLimitsResource:
"""Read and manage organization-level limits."""
@@ -72,6 +90,11 @@ class OrganizationResourceWithRawResponse:
def __init__(self, organization: OrganizationResource) -> None:
self._organization = organization
+ @cached_property
+ def entitlements(self) -> EntitlementsResourceWithRawResponse:
+ """Read and manage organization-level limits."""
+ return EntitlementsResourceWithRawResponse(self._organization.entitlements)
+
@cached_property
def limits(self) -> LimitsResourceWithRawResponse:
"""Read and manage organization-level limits."""
@@ -82,6 +105,11 @@ class AsyncOrganizationResourceWithRawResponse:
def __init__(self, organization: AsyncOrganizationResource) -> None:
self._organization = organization
+ @cached_property
+ def entitlements(self) -> AsyncEntitlementsResourceWithRawResponse:
+ """Read and manage organization-level limits."""
+ return AsyncEntitlementsResourceWithRawResponse(self._organization.entitlements)
+
@cached_property
def limits(self) -> AsyncLimitsResourceWithRawResponse:
"""Read and manage organization-level limits."""
@@ -92,6 +120,11 @@ class OrganizationResourceWithStreamingResponse:
def __init__(self, organization: OrganizationResource) -> None:
self._organization = organization
+ @cached_property
+ def entitlements(self) -> EntitlementsResourceWithStreamingResponse:
+ """Read and manage organization-level limits."""
+ return EntitlementsResourceWithStreamingResponse(self._organization.entitlements)
+
@cached_property
def limits(self) -> LimitsResourceWithStreamingResponse:
"""Read and manage organization-level limits."""
@@ -102,6 +135,11 @@ class AsyncOrganizationResourceWithStreamingResponse:
def __init__(self, organization: AsyncOrganizationResource) -> None:
self._organization = organization
+ @cached_property
+ def entitlements(self) -> AsyncEntitlementsResourceWithStreamingResponse:
+ """Read and manage organization-level limits."""
+ return AsyncEntitlementsResourceWithStreamingResponse(self._organization.entitlements)
+
@cached_property
def limits(self) -> AsyncLimitsResourceWithStreamingResponse:
"""Read and manage organization-level limits."""
diff --git a/src/kernel/types/organization/__init__.py b/src/kernel/types/organization/__init__.py
index d2411f9a..415d132a 100644
--- a/src/kernel/types/organization/__init__.py
+++ b/src/kernel/types/organization/__init__.py
@@ -3,4 +3,5 @@
from __future__ import annotations
from .org_limits import OrgLimits as OrgLimits
+from .org_entitlements import OrgEntitlements as OrgEntitlements
from .limit_update_params import LimitUpdateParams as LimitUpdateParams
diff --git a/src/kernel/types/organization/org_entitlements.py b/src/kernel/types/organization/org_entitlements.py
new file mode 100644
index 00000000..c940cbef
--- /dev/null
+++ b/src/kernel/types/organization/org_entitlements.py
@@ -0,0 +1,199 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from typing import Optional
+from datetime import datetime
+from typing_extensions import Literal
+
+from ..._models import BaseModel
+
+__all__ = [
+ "OrgEntitlements",
+ "Features",
+ "FeaturesBrowserExtensions",
+ "FeaturesBrowserPools",
+ "FeaturesBrowserReplays",
+ "FeaturesCredentialProviders",
+ "FeaturesCredentials",
+ "FeaturesCustomProxies",
+ "FeaturesFileIo",
+ "FeaturesGPU",
+ "FeaturesManagedAuth",
+ "FeaturesManagedProxies",
+ "FeaturesProfiles",
+ "FeaturesProxyBypassHosts",
+ "Limits",
+ "Plan",
+]
+
+
+class FeaturesBrowserExtensions(BaseModel):
+ enabled: bool
+ """Whether browser extensions are available."""
+
+ max_stored_per_org: Optional[int] = None
+ """Maximum active custom extensions the organization may store.
+
+ Null means unlimited. Loading stored extensions into a browser is not
+ plan-limited.
+ """
+
+
+class FeaturesBrowserPools(BaseModel):
+ enabled: bool
+ """Whether the organization is entitled to use this feature."""
+
+
+class FeaturesBrowserReplays(BaseModel):
+ enabled: bool
+ """Whether browser replay recording is available."""
+
+ retention_days: int
+ """Number of days browser replays are retained, matching the replay reaper policy."""
+
+
+class FeaturesCredentialProviders(BaseModel):
+ enabled: bool
+ """Whether the organization is entitled to use this feature."""
+
+
+class FeaturesCredentials(BaseModel):
+ enabled: bool
+ """Whether the organization is entitled to use this feature."""
+
+
+class FeaturesCustomProxies(BaseModel):
+ enabled: bool
+ """Whether the organization is entitled to use this feature."""
+
+
+class FeaturesFileIo(BaseModel):
+ enabled: bool
+ """Whether the organization is entitled to use this feature."""
+
+
+class FeaturesGPU(BaseModel):
+ enabled: bool
+ """Whether the organization is entitled to use this feature."""
+
+
+class FeaturesManagedAuth(BaseModel):
+ enabled: bool
+ """Whether managed auth is available."""
+
+ health_check_interval_default_seconds: int
+ """
+ Effective interval in seconds used when a connection is created without an
+ explicit health-check interval.
+ """
+
+ health_check_interval_max_seconds: int
+ """Maximum accepted managed auth health-check interval in seconds."""
+
+ health_check_interval_min_seconds: int
+ """Minimum accepted managed auth health-check interval in seconds."""
+
+ max_connections: Optional[int] = None
+ """Maximum active managed auth connections in the organization.
+
+ Null means unlimited.
+ """
+
+
+class FeaturesManagedProxies(BaseModel):
+ enabled: bool
+ """Whether the organization is entitled to use this feature."""
+
+
+class FeaturesProfiles(BaseModel):
+ enabled: bool
+ """Whether the organization is entitled to use this feature."""
+
+
+class FeaturesProxyBypassHosts(BaseModel):
+ enabled: bool
+ """Whether the organization is entitled to use this feature."""
+
+
+class Features(BaseModel):
+ browser_extensions: FeaturesBrowserExtensions
+
+ browser_pools: FeaturesBrowserPools
+
+ browser_replays: FeaturesBrowserReplays
+
+ credential_providers: FeaturesCredentialProviders
+
+ credentials: FeaturesCredentials
+
+ custom_proxies: FeaturesCustomProxies
+
+ file_io: FeaturesFileIo
+
+ gpu: FeaturesGPU
+
+ managed_auth: FeaturesManagedAuth
+
+ managed_proxies: FeaturesManagedProxies
+
+ profiles: FeaturesProfiles
+
+ proxy_bypass_hosts: FeaturesProxyBypassHosts
+
+
+class Limits(BaseModel):
+ default_max_concurrent_invocations_per_app: int
+ """
+ Effective org-level default concurrent invocation ceiling for apps without an
+ app-specific override. App-specific overrides are not represented here.
+ """
+
+ max_concurrent_browsers: int
+ """
+ Effective organization-wide ceiling shared by on-demand browsers and browser
+ pool reservations.
+ """
+
+ max_concurrent_invocations: int
+ """Effective organization-wide concurrent app invocation ceiling."""
+
+
+class Plan(BaseModel):
+ id: Literal["FREE", "HOBBYIST", "START_UP", "ENTERPRISE"]
+ """The organization's contractual plan identifier.
+
+ Use the resolved feature and limit values, not this field, for access decisions.
+ """
+
+ effective_id: Literal["FREE", "HOBBYIST", "START_UP", "ENTERPRISE"]
+ """The plan used to resolve plan-based access.
+
+ Active trials resolve to START_UP regardless of the contractual plan.
+ """
+
+ is_trialing: bool
+ """Whether the organization is currently within its trial period."""
+
+ status: Optional[Literal["NEEDS_PAYMENT_METHOD", "ACTIVE", "CANCELED", "UNPAID"]] = None
+ """
+ Current billing status of the contractual plan, or null when no billing status
+ is recorded. Status-sensitive feature values already account for it.
+ """
+
+ trial_ends_at: Optional[datetime] = None
+ """Configured trial end timestamp, or null when the organization has no trial.
+
+ A past timestamp may be returned when is_trialing is false.
+ """
+
+
+class OrgEntitlements(BaseModel):
+ """Effective feature access and constraints for the authenticated organization.
+
+ Values already include trial treatment, plan status, and organization-specific overrides; consumers should use these resolved values instead of comparing plan IDs.
+ """
+
+ features: Features
+
+ limits: Limits
+
+ plan: Plan
diff --git a/tests/api_resources/organization/test_entitlements.py b/tests/api_resources/organization/test_entitlements.py
new file mode 100644
index 00000000..d788bc66
--- /dev/null
+++ b/tests/api_resources/organization/test_entitlements.py
@@ -0,0 +1,80 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from __future__ import annotations
+
+import os
+from typing import Any, cast
+
+import pytest
+
+from kernel import Kernel, AsyncKernel
+from tests.utils import assert_matches_type
+from kernel.types.organization import OrgEntitlements
+
+base_url = os.environ.get("TEST_API_BASE_URL", "http://127.0.0.1:4010")
+
+
+class TestEntitlements:
+ parametrize = pytest.mark.parametrize("client", [False, True], indirect=True, ids=["loose", "strict"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ def test_method_retrieve(self, client: Kernel) -> None:
+ entitlement = client.organization.entitlements.retrieve()
+ assert_matches_type(OrgEntitlements, entitlement, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ def test_raw_response_retrieve(self, client: Kernel) -> None:
+ response = client.organization.entitlements.with_raw_response.retrieve()
+
+ assert response.is_closed is True
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+ entitlement = response.parse()
+ assert_matches_type(OrgEntitlements, entitlement, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ def test_streaming_response_retrieve(self, client: Kernel) -> None:
+ with client.organization.entitlements.with_streaming_response.retrieve() as response:
+ assert not response.is_closed
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+
+ entitlement = response.parse()
+ assert_matches_type(OrgEntitlements, entitlement, path=["response"])
+
+ assert cast(Any, response.is_closed) is True
+
+
+class TestAsyncEntitlements:
+ parametrize = pytest.mark.parametrize(
+ "async_client", [False, True, {"http_client": "aiohttp"}], indirect=True, ids=["loose", "strict", "aiohttp"]
+ )
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ async def test_method_retrieve(self, async_client: AsyncKernel) -> None:
+ entitlement = await async_client.organization.entitlements.retrieve()
+ assert_matches_type(OrgEntitlements, entitlement, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ async def test_raw_response_retrieve(self, async_client: AsyncKernel) -> None:
+ response = await async_client.organization.entitlements.with_raw_response.retrieve()
+
+ assert response.is_closed is True
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+ entitlement = await response.parse()
+ assert_matches_type(OrgEntitlements, entitlement, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ async def test_streaming_response_retrieve(self, async_client: AsyncKernel) -> None:
+ async with async_client.organization.entitlements.with_streaming_response.retrieve() as response:
+ assert not response.is_closed
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+
+ entitlement = await response.parse()
+ assert_matches_type(OrgEntitlements, entitlement, path=["response"])
+
+ assert cast(Any, response.is_closed) is True
From 15ece029ba208c6affaf0bdbf920bf3094a37a56 Mon Sep 17 00:00:00 2001
From: "kernel-internal[bot]"
<260533166+kernel-internal[bot]@users.noreply.github.com>
Date: Mon, 17 Aug 2026 20:42:27 +0000
Subject: [PATCH 197/366] release: 0.92.0
---
.release-please-manifest.json | 2 +-
CHANGELOG.md | 7 +++++++
pyproject.toml | 2 +-
src/kernel/_version.py | 2 +-
4 files changed, 10 insertions(+), 3 deletions(-)
diff --git a/.release-please-manifest.json b/.release-please-manifest.json
index 29c535cb..0114ce37 100644
--- a/.release-please-manifest.json
+++ b/.release-please-manifest.json
@@ -1,3 +1,3 @@
{
- ".": "0.91.0"
+ ".": "0.92.0"
}
\ No newline at end of file
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 4b641f09..fb983168 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,12 @@
# Changelog
+## [0.92.0](https://github.com/kernel/kernel-python-sdk/compare/v0.91.0...v0.92.0) (2026-08-17)
+
+
+### Features
+
+* chore(stlc): seal custom-code tracking files ([5b74e46](https://github.com/kernel/kernel-python-sdk/commit/5b74e460198b174e69f0bf690b01f216e89b3dd0))
+
## [0.91.0](https://github.com/kernel/kernel-python-sdk/compare/v0.90.0...v0.91.0) (2026-08-14)
diff --git a/pyproject.toml b/pyproject.toml
index 1c078ba3..dbb578c4 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -1,6 +1,6 @@
[project]
name = "kernel"
-version = "0.91.0"
+version = "0.92.0"
description = "The official Python library for the kernel API"
dynamic = ["readme"]
license = "Apache-2.0"
diff --git a/src/kernel/_version.py b/src/kernel/_version.py
index 06739f24..ce129655 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.91.0" # x-release-please-version
+__version__ = "0.92.0" # x-release-please-version
From cad8abe03b1a4375c666a3d6574d80b3fdf7a945 Mon Sep 17 00:00:00 2001
From: tnsardesai <18272584+tnsardesai@users.noreply.github.com>
Date: Tue, 18 Aug 2026 01:40:33 +0000
Subject: [PATCH 198/366] Route computer and playwright calls directly to the
VM
Add computer and playwright to the default browser routing allowlist
so screenshot and execute traffic can skip the control plane once
metro-api records activity on the kernel proxy.
---
src/kernel/lib/browser_routing/routing.py | 2 +-
tests/test_browser_routing.py | 86 ++++++++++++++++++++++-
2 files changed, 84 insertions(+), 4 deletions(-)
diff --git a/src/kernel/lib/browser_routing/routing.py b/src/kernel/lib/browser_routing/routing.py
index 7f28d726..11cd7647 100644
--- a/src/kernel/lib/browser_routing/routing.py
+++ b/src/kernel/lib/browser_routing/routing.py
@@ -44,7 +44,7 @@ def browser_routing_config_from_env() -> BrowserRoutingConfig:
# Path prefixes eligible for direct-to-VM routing. "telemetry/stream" is
# the live SSE endpoint (VM); "telemetry/events" is a historical read
# served by the control plane (S2) and must NOT be here.
- return BrowserRoutingConfig(subresources=("curl", "telemetry/stream"))
+ return BrowserRoutingConfig(subresources=("curl", "telemetry/stream", "computer", "playwright"))
if raw.strip() == "":
return BrowserRoutingConfig()
diff --git a/tests/test_browser_routing.py b/tests/test_browser_routing.py
index 3538c221..51f23a1b 100644
--- a/tests/test_browser_routing.py
+++ b/tests/test_browser_routing.py
@@ -392,7 +392,12 @@ def test_browser_route_from_browser_requires_base_url_and_jwt() -> None:
def test_browser_routing_config_from_env_defaults_to_curl(monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.delenv("KERNEL_BROWSER_ROUTING_SUBRESOURCES", raising=False)
- assert browser_routing_config_from_env().subresources == ("curl", "telemetry/stream")
+ assert browser_routing_config_from_env().subresources == (
+ "curl",
+ "telemetry/stream",
+ "computer",
+ "playwright",
+ )
def test_direct_vm_routing_allowlist_segment_boundary() -> None:
@@ -401,13 +406,16 @@ def test_direct_vm_routing_allowlist_segment_boundary() -> None:
# stream-prefixed-but-different path is not matched.
from kernel.lib.browser_routing.routing import _matches_direct_vm_prefix
- prefixes = ("curl", "telemetry/stream")
+ prefixes = ("curl", "telemetry/stream", "computer", "playwright")
assert _matches_direct_vm_prefix("telemetry/stream", prefixes) is True
assert _matches_direct_vm_prefix("telemetry/stream/x", prefixes) is True
assert _matches_direct_vm_prefix("telemetry/events", prefixes) is False
assert _matches_direct_vm_prefix("telemetry/streaming-config", prefixes) is False
assert _matches_direct_vm_prefix("telemetry", prefixes) is False
assert _matches_direct_vm_prefix("curl/raw", prefixes) is True
+ assert _matches_direct_vm_prefix("computer/screenshot", prefixes) is True
+ assert _matches_direct_vm_prefix("playwright/execute", prefixes) is True
+ assert _matches_direct_vm_prefix("process/exec", prefixes) is False
assert _matches_direct_vm_prefix("fs/read", prefixes) is False
@@ -427,7 +435,7 @@ def test_rewrite_direct_vm_options_keeps_telemetry_events_on_control_plane() ->
cache.set(
BrowserRoute(session_id="sess-1", base_url="http://browser-session.test/browser/kernel", jwt="token-abc")
)
- config = BrowserRoutingConfig(subresources=("curl", "telemetry/stream"))
+ config = BrowserRoutingConfig(subresources=("curl", "telemetry/stream", "computer", "playwright"))
events = rewrite_direct_vm_options(
FinalRequestOptions(method="get", url="/browsers/sess-1/telemetry/events"), cache=cache, config=config
@@ -439,7 +447,79 @@ def test_rewrite_direct_vm_options_keeps_telemetry_events_on_control_plane() ->
)
assert str(stream.url).startswith("http://browser-session.test/browser/kernel/telemetry/stream")
+ screenshot = rewrite_direct_vm_options(
+ FinalRequestOptions(method="post", url="/browsers/sess-1/computer/screenshot"), cache=cache, config=config
+ )
+ assert str(screenshot.url).startswith("http://browser-session.test/browser/kernel/computer/screenshot")
+
+ execute = rewrite_direct_vm_options(
+ FinalRequestOptions(method="post", url="/browsers/sess-1/playwright/execute"), cache=cache, config=config
+ )
+ assert str(execute.url).startswith("http://browser-session.test/browser/kernel/playwright/execute")
+
+ process = rewrite_direct_vm_options(
+ FinalRequestOptions(method="post", url="/browsers/sess-1/process/exec"), cache=cache, config=config
+ )
+ assert process.url == "/browsers/sess-1/process/exec"
+
+ fs_read = rewrite_direct_vm_options(
+ FinalRequestOptions(method="get", url="/browsers/sess-1/fs/read_file"), cache=cache, config=config
+ )
+ assert fs_read.url == "/browsers/sess-1/fs/read_file"
+
def test_browser_routing_config_from_env_empty_string_disables_routing(monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.setenv("KERNEL_BROWSER_ROUTING_SUBRESOURCES", "")
assert browser_routing_config_from_env().subresources == ()
+
+
+@respx.mock
+def test_computer_screenshot_and_playwright_execute_route_to_vm_by_default(
+ monkeypatch: pytest.MonkeyPatch,
+) -> None:
+ monkeypatch.delenv("KERNEL_BROWSER_ROUTING_SUBRESOURCES", raising=False)
+ screenshot = respx.post("http://browser-session.test/browser/kernel/computer/screenshot").mock(
+ return_value=httpx.Response(200, content=b"png", headers={"content-type": "image/png"})
+ )
+ execute = respx.post("http://browser-session.test/browser/kernel/playwright/execute").mock(
+ return_value=httpx.Response(200, json={"success": True})
+ )
+ with Kernel(base_url=base_url, api_key=api_key, _strict_response_validation=True) as client:
+ _cache_browser(client)
+ client.browsers.computer.capture_screenshot("sess-1")
+ out = client.browsers.playwright.execute("sess-1", code="return 1")
+
+ assert screenshot.called
+ screenshot_req = cast(httpx.Request, cast(Any, screenshot.calls[0]).request)
+ assert screenshot_req.url.params.get("jwt") == "token-abc"
+ assert screenshot_req.headers.get("Authorization") is None
+ assert execute.called
+ execute_req = cast(httpx.Request, cast(Any, execute.calls[0]).request)
+ assert execute_req.url.params.get("jwt") == "token-abc"
+ assert execute_req.headers.get("Authorization") is None
+ assert out.success is True
+
+
+@respx.mock
+def test_process_fs_and_telemetry_events_stay_on_api_origin_by_default(
+ monkeypatch: pytest.MonkeyPatch,
+) -> None:
+ monkeypatch.delenv("KERNEL_BROWSER_ROUTING_SUBRESOURCES", raising=False)
+ process = respx.post(f"{base_url}/browsers/sess-1/process/exec").mock(
+ return_value=httpx.Response(200, json={"exit_code": 0, "stdout_b64": "", "stderr_b64": ""})
+ )
+ fs_read = respx.get(f"{base_url}/browsers/sess-1/fs/read_file").mock(
+ return_value=httpx.Response(200, content=b"x", headers={"content-type": "application/octet-stream"})
+ )
+ events = respx.get(f"{base_url}/browsers/sess-1/telemetry/events").mock(
+ return_value=httpx.Response(200, json=[])
+ )
+ with Kernel(base_url=base_url, api_key=api_key, _strict_response_validation=True) as client:
+ _cache_browser(client)
+ client.browsers.process.exec("sess-1", command="echo")
+ client.browsers.fs.read_file("sess-1", path="/tmp/x")
+ client.browsers.telemetry.events("sess-1")
+
+ assert process.called
+ assert fs_read.called
+ assert events.called
From b32fc1d18419d02119759c1cd0533350b533a8a9 Mon Sep 17 00:00:00 2001
From: tnsardesai <18272584+tnsardesai@users.noreply.github.com>
Date: Tue, 18 Aug 2026 20:44:23 +0000
Subject: [PATCH 199/366] Rename default-allowlist test to match the new list
The default now includes computer and playwright, not just curl.
---
tests/test_browser_routing.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/tests/test_browser_routing.py b/tests/test_browser_routing.py
index 51f23a1b..4921a5be 100644
--- a/tests/test_browser_routing.py
+++ b/tests/test_browser_routing.py
@@ -390,7 +390,7 @@ def test_browser_route_from_browser_requires_base_url_and_jwt() -> None:
assert browser_route_from_browser({**_fake_browser(), "cdp_ws_url": None}) is None
-def test_browser_routing_config_from_env_defaults_to_curl(monkeypatch: pytest.MonkeyPatch) -> None:
+def test_browser_routing_config_from_env_defaults_to_curl_telemetry_computer_and_playwright(monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.delenv("KERNEL_BROWSER_ROUTING_SUBRESOURCES", raising=False)
assert browser_routing_config_from_env().subresources == (
"curl",
From 1b28d622902bb71cd3c0303e0a9fd0066918e773 Mon Sep 17 00:00:00 2001
From: tnsardesai <18272584+tnsardesai@users.noreply.github.com>
Date: Tue, 18 Aug 2026 21:02:51 +0000
Subject: [PATCH 200/366] Shorten default-allowlist test name
The asserted list can grow; the test name should not.
---
tests/test_browser_routing.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/tests/test_browser_routing.py b/tests/test_browser_routing.py
index 4921a5be..0f43f716 100644
--- a/tests/test_browser_routing.py
+++ b/tests/test_browser_routing.py
@@ -390,7 +390,7 @@ def test_browser_route_from_browser_requires_base_url_and_jwt() -> None:
assert browser_route_from_browser({**_fake_browser(), "cdp_ws_url": None}) is None
-def test_browser_routing_config_from_env_defaults_to_curl_telemetry_computer_and_playwright(monkeypatch: pytest.MonkeyPatch) -> None:
+def test_browser_routing_config_from_env_defaults(monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.delenv("KERNEL_BROWSER_ROUTING_SUBRESOURCES", raising=False)
assert browser_routing_config_from_env().subresources == (
"curl",
From 3522fcb9e70854ad2900585f5b37222e7f0e1b26 Mon Sep 17 00:00:00 2001
From: "kernel-internal[bot]"
<260533166+kernel-internal[bot]@users.noreply.github.com>
Date: Tue, 18 Aug 2026 21:04:16 +0000
Subject: [PATCH 201/366] feat: Harden canonical managed auth interactions
Stainless-Generated-From: d1e34365f0a909af7b481fcda9c54a08297b67ea
---
src/kernel/types/auth/connection_follow_response.py | 9 +++------
src/kernel/types/auth/managed_auth.py | 9 +++------
2 files changed, 6 insertions(+), 12 deletions(-)
diff --git a/src/kernel/types/auth/connection_follow_response.py b/src/kernel/types/auth/connection_follow_response.py
index c411c1ba..2119f54c 100644
--- a/src/kernel/types/auth/connection_follow_response.py
+++ b/src/kernel/types/auth/connection_follow_response.py
@@ -97,6 +97,9 @@ class ManagedAuthStateEventField(BaseModel):
id: str
"""Stable field identifier for canonical submit."""
+ reason: Literal["missing", "rejected"]
+ """Why the field requires user input."""
+
ref: str
"""Credential reference name to store the submitted value under."""
@@ -112,12 +115,6 @@ class ManagedAuthStateEventField(BaseModel):
observed_selector: Optional[str] = None
"""Selector for the visible field, when available."""
- replace_existing: Optional[bool] = None
- """
- Whether the submitted value must replace an existing credential after explicit
- rejection.
- """
-
required: Optional[bool] = None
"""Whether this field is required."""
diff --git a/src/kernel/types/auth/managed_auth.py b/src/kernel/types/auth/managed_auth.py
index ea5be1c6..d0551c9c 100644
--- a/src/kernel/types/auth/managed_auth.py
+++ b/src/kernel/types/auth/managed_auth.py
@@ -206,6 +206,9 @@ class Field(BaseModel):
id: str
"""Stable field identifier for canonical submit."""
+ reason: Literal["missing", "rejected"]
+ """Why the field requires user input."""
+
ref: str
"""Credential reference name to store the submitted value under."""
@@ -221,12 +224,6 @@ class Field(BaseModel):
observed_selector: Optional[str] = None
"""Selector for the visible field, when available."""
- replace_existing: Optional[bool] = None
- """
- Whether the submitted value must replace an existing credential after explicit
- rejection.
- """
-
required: Optional[bool] = None
"""Whether this field is required."""
From 63fb0606910b17a99f7a584e425fbd77e2d562e5 Mon Sep 17 00:00:00 2001
From: "kernel-internal[bot]"
<260533166+kernel-internal[bot]@users.noreply.github.com>
Date: Wed, 19 Aug 2026 18:49:04 +0000
Subject: [PATCH 202/366] feat: Bind managed auth submissions to interactions
Stainless-Generated-From: 53fd8808daedcb207b0d72e49a72d5deb67817f4
---
src/kernel/resources/auth/connections.py | 10 ++++++++++
src/kernel/types/auth/connection_follow_response.py | 7 +++++++
src/kernel/types/auth/connection_submit_params.py | 6 ++++++
src/kernel/types/auth/managed_auth.py | 7 +++++++
tests/api_resources/auth/test_connections.py | 2 ++
5 files changed, 32 insertions(+)
diff --git a/src/kernel/resources/auth/connections.py b/src/kernel/resources/auth/connections.py
index 56785ea2..ccc3d890 100644
--- a/src/kernel/resources/auth/connections.py
+++ b/src/kernel/resources/auth/connections.py
@@ -530,6 +530,7 @@ def submit(
*,
field_values: Dict[str, str] | Omit = omit,
fields: Dict[str, str] | Omit = omit,
+ interaction_id: str | Omit = omit,
mfa_option_id: str | Omit = omit,
selected_choice_id: str | Omit = omit,
sign_in_option_id: str | Omit = omit,
@@ -552,6 +553,9 @@ def submit(
fields: Map of field name to value
+ interaction_id: Opaque interaction ID returned with canonical fields and choices. Required for
+ canonical submissions.
+
mfa_option_id: The MFA method type to select (when mfa_options were returned)
selected_choice_id: Canonical choice ID selected by the user.
@@ -580,6 +584,7 @@ def submit(
{
"field_values": field_values,
"fields": fields,
+ "interaction_id": interaction_id,
"mfa_option_id": mfa_option_id,
"selected_choice_id": selected_choice_id,
"sign_in_option_id": sign_in_option_id,
@@ -1143,6 +1148,7 @@ async def submit(
*,
field_values: Dict[str, str] | Omit = omit,
fields: Dict[str, str] | Omit = omit,
+ interaction_id: str | Omit = omit,
mfa_option_id: str | Omit = omit,
selected_choice_id: str | Omit = omit,
sign_in_option_id: str | Omit = omit,
@@ -1165,6 +1171,9 @@ async def submit(
fields: Map of field name to value
+ interaction_id: Opaque interaction ID returned with canonical fields and choices. Required for
+ canonical submissions.
+
mfa_option_id: The MFA method type to select (when mfa_options were returned)
selected_choice_id: Canonical choice ID selected by the user.
@@ -1193,6 +1202,7 @@ async def submit(
{
"field_values": field_values,
"fields": fields,
+ "interaction_id": interaction_id,
"mfa_option_id": mfa_option_id,
"selected_choice_id": selected_choice_id,
"sign_in_option_id": sign_in_option_id,
diff --git a/src/kernel/types/auth/connection_follow_response.py b/src/kernel/types/auth/connection_follow_response.py
index 2119f54c..ff6dcf0b 100644
--- a/src/kernel/types/auth/connection_follow_response.py
+++ b/src/kernel/types/auth/connection_follow_response.py
@@ -221,6 +221,13 @@ class ManagedAuthStateEvent(BaseModel):
hosted_url: Optional[str] = None
"""URL to redirect user to for hosted login."""
+ interaction_id: Optional[str] = None
+ """Opaque identifier for the current canonical interaction.
+
+ Required when submitting fields or choices and changes for each new actionable
+ pause.
+ """
+
live_view_url: Optional[str] = None
"""Browser live view URL for debugging."""
diff --git a/src/kernel/types/auth/connection_submit_params.py b/src/kernel/types/auth/connection_submit_params.py
index 19861ed8..04421fef 100644
--- a/src/kernel/types/auth/connection_submit_params.py
+++ b/src/kernel/types/auth/connection_submit_params.py
@@ -15,6 +15,12 @@ class ConnectionSubmitParams(TypedDict, total=False):
fields: Dict[str, str]
"""Map of field name to value"""
+ interaction_id: str
+ """Opaque interaction ID returned with canonical fields and choices.
+
+ Required for canonical submissions.
+ """
+
mfa_option_id: str
"""The MFA method type to select (when mfa_options were returned)"""
diff --git a/src/kernel/types/auth/managed_auth.py b/src/kernel/types/auth/managed_auth.py
index d0551c9c..23e45d6b 100644
--- a/src/kernel/types/auth/managed_auth.py
+++ b/src/kernel/types/auth/managed_auth.py
@@ -501,6 +501,13 @@ class ManagedAuth(BaseModel):
hosted_url: Optional[str] = None
"""URL to redirect user to for hosted login (present when flow in progress)"""
+ interaction_id: Optional[str] = None
+ """Opaque identifier for the current canonical interaction.
+
+ Required when submitting fields or choices and changes for each new actionable
+ pause.
+ """
+
last_auth_at: Optional[datetime] = None
"""Deprecated alias for `last_auth_check_at`.
diff --git a/tests/api_resources/auth/test_connections.py b/tests/api_resources/auth/test_connections.py
index f026a2cd..f3d4a05c 100644
--- a/tests/api_resources/auth/test_connections.py
+++ b/tests/api_resources/auth/test_connections.py
@@ -556,6 +556,7 @@ def test_method_submit_with_all_params(self, client: Kernel) -> None:
"email": "user@example.com",
"password": "secret",
},
+ interaction_id="mai_abc123xyz",
mfa_option_id="sms",
selected_choice_id="google",
sign_in_option_id="work-account",
@@ -1190,6 +1191,7 @@ async def test_method_submit_with_all_params(self, async_client: AsyncKernel) ->
"email": "user@example.com",
"password": "secret",
},
+ interaction_id="mai_abc123xyz",
mfa_option_id="sms",
selected_choice_id="google",
sign_in_option_id="work-account",
From 5973ab6c766ee31929a523e3ea21fb000bc6879a Mon Sep 17 00:00:00 2001
From: "kernel-internal[bot]"
<260533166+kernel-internal[bot]@users.noreply.github.com>
Date: Wed, 19 Aug 2026 20:31:11 +0000
Subject: [PATCH 203/366] feat: Add proxy_error to browser telemetry event
schema
Stainless-Generated-From: 8c74ee1f6b517af4d7473832f1448514bfc9ef5b
---
api.md | 1 +
src/kernel/types/browsers/__init__.py | 1 +
.../browsers/browser_proxy_error_event.py | 73 +++++++++++++++++++
.../types/browsers/browser_telemetry_event.py | 2 +
4 files changed, 77 insertions(+)
create mode 100644 src/kernel/types/browsers/browser_proxy_error_event.py
diff --git a/api.md b/api.md
index fda90ce0..6f8c4256 100644
--- a/api.md
+++ b/api.md
@@ -146,6 +146,7 @@ from kernel.types.browsers import (
BrowserPageNavigationEvent,
BrowserPageNavigationSettledEvent,
BrowserPageTabOpenedEvent,
+ BrowserProxyErrorEvent,
BrowserServiceCrashedEvent,
BrowserSystemOomKillEvent,
BrowserTelemetryCategoriesConfig,
diff --git a/src/kernel/types/browsers/__init__.py b/src/kernel/types/browsers/__init__.py
index bf9d861a..8ed451a0 100644
--- a/src/kernel/types/browsers/__init__.py
+++ b/src/kernel/types/browsers/__init__.py
@@ -42,6 +42,7 @@
from .browser_telemetry_config import BrowserTelemetryConfig as BrowserTelemetryConfig
from .browser_cdp_connect_event import BrowserCdpConnectEvent as BrowserCdpConnectEvent
from .browser_console_log_event import BrowserConsoleLogEvent as BrowserConsoleLogEvent
+from .browser_proxy_error_event import BrowserProxyErrorEvent as BrowserProxyErrorEvent
from .computer_press_key_params import ComputerPressKeyParams as ComputerPressKeyParams
from .computer_type_text_params import ComputerTypeTextParams as ComputerTypeTextParams
from .f_create_directory_params import FCreateDirectoryParams as FCreateDirectoryParams
diff --git a/src/kernel/types/browsers/browser_proxy_error_event.py b/src/kernel/types/browsers/browser_proxy_error_event.py
new file mode 100644
index 00000000..6f52d3bb
--- /dev/null
+++ b/src/kernel/types/browsers/browser_proxy_error_event.py
@@ -0,0 +1,73 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from typing import Optional
+from typing_extensions import Literal
+
+from ..._models import BaseModel
+from .browser_event_source import BrowserEventSource
+from .browser_event_context import BrowserEventContext
+
+__all__ = ["BrowserProxyErrorEvent", "Data"]
+
+
+class Data(BrowserEventContext):
+ """Browser event context stamped by the browser monitor onto all CDP-sourced events.
+
+ Identifies the target, frame, and navigation epoch in which the event occurred.
+ """
+
+ code: Literal[
+ "destination_blocked",
+ "provider_blacklisted",
+ "provider_unreachable",
+ "proxy_unavailable",
+ "upstream_timeout",
+ "upstream_dns_failure",
+ "upstream_connect_failed",
+ ]
+ """
+ Proxy-layer error code: the X-Kernel-Proxy-Error response header value from a
+ branded 5xx error page served by the metro egress host-proxy. Values mirror what
+ the proxy emits: destination_blocked, provider_blacklisted,
+ provider_unreachable, proxy_unavailable, upstream_timeout, upstream_dns_failure,
+ upstream_connect_failed. Unknown header values are dropped.
+ """
+
+ request_id: str
+ """CDP request identifier matching the originating request."""
+
+ status: int
+ """HTTP response status of the branded error page (502)."""
+
+ method: Optional[str] = None
+ """HTTP method of the failed request, when known."""
+
+ resource_type: Optional[str] = None
+ """CDP Network.ResourceType for the request, when known."""
+
+
+class BrowserProxyErrorEvent(BaseModel):
+ """A branded proxy-layer failure observed by the browser.
+
+ Emitted when the metro egress host-proxy serves a branded 5xx error page whose response carries the X-Kernel-Proxy-Error header. Low-volume and carries a typed code. Its value is per-session and per-URL attribution for sessions that already capture the network stream: proxy failures are only observable while the CDP network collector is running, so this is an opt-in refinement of the raw network events rather than a default-on alerting signal.
+ """
+
+ category: Literal["network"]
+
+ source: BrowserEventSource
+ """Provenance metadata identifying which producer emitted the event."""
+
+ ts: int
+ """Event timestamp in Unix microseconds."""
+
+ type: Literal["proxy_error"]
+
+ data: Optional[Data] = None
+ """Browser event context stamped by the browser monitor onto all CDP-sourced
+ events.
+
+ Identifies the target, frame, and navigation epoch in which the event occurred.
+ """
+
+ truncated: Optional[bool] = None
+ """True if the data field was truncated due to size limits."""
diff --git a/src/kernel/types/browsers/browser_telemetry_event.py b/src/kernel/types/browsers/browser_telemetry_event.py
index d27537e8..d57b92d2 100644
--- a/src/kernel/types/browsers/browser_telemetry_event.py
+++ b/src/kernel/types/browsers/browser_telemetry_event.py
@@ -11,6 +11,7 @@
from .browser_page_load_event import BrowserPageLoadEvent
from .browser_cdp_connect_event import BrowserCdpConnectEvent
from .browser_console_log_event import BrowserConsoleLogEvent
+from .browser_proxy_error_event import BrowserProxyErrorEvent
from .browser_network_idle_event import BrowserNetworkIdleEvent
from .browser_console_error_event import BrowserConsoleErrorEvent
from .browser_cdp_disconnect_event import BrowserCdpDisconnectEvent
@@ -47,6 +48,7 @@
BrowserNetworkResponseEvent,
BrowserNetworkLoadingFailedEvent,
BrowserNetworkIdleEvent,
+ BrowserProxyErrorEvent,
BrowserPageNavigationEvent,
BrowserPageDomContentLoadedEvent,
BrowserPageLoadEvent,
From c9baddf41ffade850c7e9653107086dd6844c659 Mon Sep 17 00:00:00 2001
From: "kernel-internal[bot]"
<260533166+kernel-internal[bot]@users.noreply.github.com>
Date: Wed, 19 Aug 2026 20:35:29 +0000
Subject: [PATCH 204/366] release: 0.93.0
---
.release-please-manifest.json | 2 +-
CHANGELOG.md | 9 +++++++++
pyproject.toml | 2 +-
src/kernel/_version.py | 2 +-
4 files changed, 12 insertions(+), 3 deletions(-)
diff --git a/.release-please-manifest.json b/.release-please-manifest.json
index 0114ce37..c486f6de 100644
--- a/.release-please-manifest.json
+++ b/.release-please-manifest.json
@@ -1,3 +1,3 @@
{
- ".": "0.92.0"
+ ".": "0.93.0"
}
\ No newline at end of file
diff --git a/CHANGELOG.md b/CHANGELOG.md
index fb983168..1283a424 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,14 @@
# Changelog
+## [0.93.0](https://github.com/kernel/kernel-python-sdk/compare/v0.92.0...v0.93.0) (2026-08-19)
+
+
+### Features
+
+* Add proxy_error to browser telemetry event schema ([5973ab6](https://github.com/kernel/kernel-python-sdk/commit/5973ab6c766ee31929a523e3ea21fb000bc6879a))
+* Bind managed auth submissions to interactions ([63fb060](https://github.com/kernel/kernel-python-sdk/commit/63fb0606910b17a99f7a584e425fbd77e2d562e5))
+* Harden canonical managed auth interactions ([3522fcb](https://github.com/kernel/kernel-python-sdk/commit/3522fcb9e70854ad2900585f5b37222e7f0e1b26))
+
## [0.92.0](https://github.com/kernel/kernel-python-sdk/compare/v0.91.0...v0.92.0) (2026-08-17)
diff --git a/pyproject.toml b/pyproject.toml
index dbb578c4..8f8dab0e 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -1,6 +1,6 @@
[project]
name = "kernel"
-version = "0.92.0"
+version = "0.93.0"
description = "The official Python library for the kernel API"
dynamic = ["readme"]
license = "Apache-2.0"
diff --git a/src/kernel/_version.py b/src/kernel/_version.py
index ce129655..2fb26cce 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.92.0" # x-release-please-version
+__version__ = "0.93.0" # x-release-please-version
From 17c38fa6c7668a7e2bb1f058d57053d83238263a Mon Sep 17 00:00:00 2001
From: "kernel-internal[bot]"
<260533166+kernel-internal[bot]@users.noreply.github.com>
Date: Fri, 21 Aug 2026 15:13:33 +0000
Subject: [PATCH 205/366] feat: Mirror the control/platform telemetry split
into the public API
Stainless-Generated-From: 84de8903f89fe67101549c27d1d3c4b16b3324df
---
api.md | 6 +
src/kernel/resources/browsers/browsers.py | 4 +-
src/kernel/resources/browsers/telemetry.py | 2 +
src/kernel/resources/proxies.py | 10 +-
.../types/auth/connection_create_params.py | 14 +-
.../types/auth/connection_login_params.py | 14 +-
.../types/auth/connection_update_params.py | 14 +-
src/kernel/types/auth/managed_auth.py | 14 +-
.../types/auth/managed_auth_browser_config.py | 14 +-
.../auth/managed_auth_browser_config_param.py | 14 +-
src/kernel/types/browser_create_params.py | 16 +-
.../types/browser_pool_acquire_params.py | 14 +-
.../types/browser_pool_create_params.py | 14 +-
.../types/browser_pool_update_params.py | 14 +-
src/kernel/types/browser_update_params.py | 14 +-
src/kernel/types/browsers/__init__.py | 12 +
.../types/browsers/browser_api_call_event.py | 16 +-
.../browsers/browser_cdp_command_event.py | 1883 +++++++++++++++++
.../browsers/browser_cdp_command_method.py | 46 +
.../browsers/browser_cdp_connect_event.py | 13 +-
.../browsers/browser_cdp_disconnect_event.py | 24 +
.../browsers/browser_page_crashed_event.py | 41 +
.../browser_platform_api_call_event.py | 47 +
.../browser_telemetry_categories_config.py | 20 +-
...owser_telemetry_categories_config_param.py | 20 +-
.../browser_telemetry_category_config.py | 4 +-
...browser_telemetry_category_config_param.py | 4 +-
.../browser_telemetry_cdp_control_config.py | 24 +
...wser_telemetry_cdp_control_config_param.py | 26 +
.../browser_telemetry_control_config.py | 28 +
.../browser_telemetry_control_config_param.py | 29 +
.../types/browsers/browser_telemetry_event.py | 6 +
.../types/browsers/telemetry_events_params.py | 1 +
tests/api_resources/auth/test_connections.py | 72 +-
tests/api_resources/test_browser_pools.py | 36 +-
tests/api_resources/test_browsers.py | 24 +-
36 files changed, 2431 insertions(+), 123 deletions(-)
create mode 100644 src/kernel/types/browsers/browser_cdp_command_event.py
create mode 100644 src/kernel/types/browsers/browser_cdp_command_method.py
create mode 100644 src/kernel/types/browsers/browser_page_crashed_event.py
create mode 100644 src/kernel/types/browsers/browser_platform_api_call_event.py
create mode 100644 src/kernel/types/browsers/browser_telemetry_cdp_control_config.py
create mode 100644 src/kernel/types/browsers/browser_telemetry_cdp_control_config_param.py
create mode 100644 src/kernel/types/browsers/browser_telemetry_control_config.py
create mode 100644 src/kernel/types/browsers/browser_telemetry_control_config_param.py
diff --git a/api.md b/api.md
index 6f8c4256..4d75e371 100644
--- a/api.md
+++ b/api.md
@@ -117,6 +117,8 @@ from kernel.types.browsers import (
BrowserAPICallEvent,
BrowserCallStack,
BrowserCaptchaSolveResultEvent,
+ BrowserCdpCommandEvent,
+ BrowserCdpCommandMethod,
BrowserCdpConnectEvent,
BrowserCdpDisconnectEvent,
BrowserConsoleErrorEvent,
@@ -138,6 +140,7 @@ from kernel.types.browsers import (
BrowserNetworkLoadingFailedEvent,
BrowserNetworkRequestEvent,
BrowserNetworkResponseEvent,
+ BrowserPageCrashedEvent,
BrowserPageDomContentLoadedEvent,
BrowserPageLayoutSettledEvent,
BrowserPageLayoutShiftEvent,
@@ -146,12 +149,15 @@ from kernel.types.browsers import (
BrowserPageNavigationEvent,
BrowserPageNavigationSettledEvent,
BrowserPageTabOpenedEvent,
+ BrowserPlatformAPICallEvent,
BrowserProxyErrorEvent,
BrowserServiceCrashedEvent,
BrowserSystemOomKillEvent,
BrowserTelemetryCategoriesConfig,
BrowserTelemetryCategoryConfig,
+ BrowserTelemetryCdpControlConfig,
BrowserTelemetryConfig,
+ BrowserTelemetryControlConfig,
BrowserTelemetryEvent,
BrowserTelemetryExportConfig,
BrowserTelemetryOtlpExportConfig,
diff --git a/src/kernel/resources/browsers/browsers.py b/src/kernel/resources/browsers/browsers.py
index 8078b952..0a15061a 100644
--- a/src/kernel/resources/browsers/browsers.py
+++ b/src/kernel/resources/browsers/browsers.py
@@ -208,7 +208,7 @@ def create(
extensions: List of browser extensions to load into the session. Provide each by id or name.
gpu: If true, enables GPU acceleration for the browser session. Requires Start-Up or
- Enterprise plan and headless=false.
+ Enterprise plan, headless=false, and region=us-east.
headless: If true, launches the browser using a headless image (no VNC/GUI). Defaults to
false.
@@ -828,7 +828,7 @@ async def create(
extensions: List of browser extensions to load into the session. Provide each by id or name.
gpu: If true, enables GPU acceleration for the browser session. Requires Start-Up or
- Enterprise plan and headless=false.
+ Enterprise plan, headless=false, and region=us-east.
headless: If true, launches the browser using a headless image (no VNC/GUI). Defaults to
false.
diff --git a/src/kernel/resources/browsers/telemetry.py b/src/kernel/resources/browsers/telemetry.py
index 5ed61557..c6fa04ea 100644
--- a/src/kernel/resources/browsers/telemetry.py
+++ b/src/kernel/resources/browsers/telemetry.py
@@ -62,6 +62,7 @@ def events(
"page",
"interaction",
"control",
+ "platform",
"connection",
"system",
"screenshot",
@@ -238,6 +239,7 @@ def events(
"page",
"interaction",
"control",
+ "platform",
"connection",
"system",
"screenshot",
diff --git a/src/kernel/resources/proxies.py b/src/kernel/resources/proxies.py
index 08f97e37..ee9f9084 100644
--- a/src/kernel/resources/proxies.py
+++ b/src/kernel/resources/proxies.py
@@ -252,7 +252,10 @@ def delete(
) -> None:
"""Soft delete a proxy.
- Sessions referencing it are not modified.
+ Session records referencing it are not modified. If egress
+ binding polling is enabled, existing tunnels for active sessions using the proxy
+ are terminated within one polling interval; subsequent connections through the
+ deleted proxy are rejected.
Args:
extra_headers: Send extra headers
@@ -553,7 +556,10 @@ async def delete(
) -> None:
"""Soft delete a proxy.
- Sessions referencing it are not modified.
+ Session records referencing it are not modified. If egress
+ binding polling is enabled, existing tunnels for active sessions using the proxy
+ are terminated within one polling interval; subsequent connections through the
+ deleted proxy are rejected.
Args:
extra_headers: Send extra headers
diff --git a/src/kernel/types/auth/connection_create_params.py b/src/kernel/types/auth/connection_create_params.py
index 2fdc808b..54995d37 100644
--- a/src/kernel/types/auth/connection_create_params.py
+++ b/src/kernel/types/auth/connection_create_params.py
@@ -175,13 +175,13 @@ class BrowserTelemetry(TypedDict, total=False):
The operational categories (control, connection, system, captcha) are captured
whenever telemetry is enabled; set one to enabled=false to opt out. The CDP
- categories (console, network, page, interaction) and screenshot are off by
- default; set enabled=true to opt in. On create, provided categories layer onto
- the default set. On update, provided categories merge onto the session's current
- config; when no telemetry is active this falls back to the default set (matching
- create). If browser is omitted or empty, the default set is used. A browser
- config that disables every category stops capture on update and starts no
- capture on create.
+ categories (console, network, page, interaction), screenshot and platform are
+ off by default; set enabled=true to opt in. On create, provided categories layer
+ onto the default set. On update, provided categories merge onto the session's
+ current config; when no telemetry is active this falls back to the default set
+ (matching create). If browser is omitted or empty, the default set is used. A
+ browser config that disables every category stops capture on update and starts
+ no capture on create.
"""
enabled: bool
diff --git a/src/kernel/types/auth/connection_login_params.py b/src/kernel/types/auth/connection_login_params.py
index c26b020f..cbe2bf3b 100644
--- a/src/kernel/types/auth/connection_login_params.py
+++ b/src/kernel/types/auth/connection_login_params.py
@@ -96,13 +96,13 @@ class BrowserTelemetry(TypedDict, total=False):
The operational categories (control, connection, system, captcha) are captured
whenever telemetry is enabled; set one to enabled=false to opt out. The CDP
- categories (console, network, page, interaction) and screenshot are off by
- default; set enabled=true to opt in. On create, provided categories layer onto
- the default set. On update, provided categories merge onto the session's current
- config; when no telemetry is active this falls back to the default set (matching
- create). If browser is omitted or empty, the default set is used. A browser
- config that disables every category stops capture on update and starts no
- capture on create.
+ categories (console, network, page, interaction), screenshot and platform are
+ off by default; set enabled=true to opt in. On create, provided categories layer
+ onto the default set. On update, provided categories merge onto the session's
+ current config; when no telemetry is active this falls back to the default set
+ (matching create). If browser is omitted or empty, the default set is used. A
+ browser config that disables every category stops capture on update and starts
+ no capture on create.
"""
enabled: bool
diff --git a/src/kernel/types/auth/connection_update_params.py b/src/kernel/types/auth/connection_update_params.py
index ad3fbe44..829cf489 100644
--- a/src/kernel/types/auth/connection_update_params.py
+++ b/src/kernel/types/auth/connection_update_params.py
@@ -135,13 +135,13 @@ class BrowserTelemetry(TypedDict, total=False):
The operational categories (control, connection, system, captcha) are captured
whenever telemetry is enabled; set one to enabled=false to opt out. The CDP
- categories (console, network, page, interaction) and screenshot are off by
- default; set enabled=true to opt in. On create, provided categories layer onto
- the default set. On update, provided categories merge onto the session's current
- config; when no telemetry is active this falls back to the default set (matching
- create). If browser is omitted or empty, the default set is used. A browser
- config that disables every category stops capture on update and starts no
- capture on create.
+ categories (console, network, page, interaction), screenshot and platform are
+ off by default; set enabled=true to opt in. On create, provided categories layer
+ onto the default set. On update, provided categories merge onto the session's
+ current config; when no telemetry is active this falls back to the default set
+ (matching create). If browser is omitted or empty, the default set is used. A
+ browser config that disables every category stops capture on update and starts
+ no capture on create.
"""
enabled: bool
diff --git a/src/kernel/types/auth/managed_auth.py b/src/kernel/types/auth/managed_auth.py
index 23e45d6b..7c4ba7d6 100644
--- a/src/kernel/types/auth/managed_auth.py
+++ b/src/kernel/types/auth/managed_auth.py
@@ -79,13 +79,13 @@ class BrowserTelemetry(BaseModel):
The operational categories (control, connection, system, captcha) are captured
whenever telemetry is enabled; set one to enabled=false to opt out. The CDP
- categories (console, network, page, interaction) and screenshot are off by
- default; set enabled=true to opt in. On create, provided categories layer onto
- the default set. On update, provided categories merge onto the session's current
- config; when no telemetry is active this falls back to the default set (matching
- create). If browser is omitted or empty, the default set is used. A browser
- config that disables every category stops capture on update and starts no
- capture on create.
+ categories (console, network, page, interaction), screenshot and platform are
+ off by default; set enabled=true to opt in. On create, provided categories layer
+ onto the default set. On update, provided categories merge onto the session's
+ current config; when no telemetry is active this falls back to the default set
+ (matching create). If browser is omitted or empty, the default set is used. A
+ browser config that disables every category stops capture on update and starts
+ no capture on create.
"""
enabled: Optional[bool] = None
diff --git a/src/kernel/types/auth/managed_auth_browser_config.py b/src/kernel/types/auth/managed_auth_browser_config.py
index a4d63de7..c7e565d1 100644
--- a/src/kernel/types/auth/managed_auth_browser_config.py
+++ b/src/kernel/types/auth/managed_auth_browser_config.py
@@ -67,13 +67,13 @@ class Telemetry(BaseModel):
The operational categories (control, connection, system, captcha) are captured
whenever telemetry is enabled; set one to enabled=false to opt out. The CDP
- categories (console, network, page, interaction) and screenshot are off by
- default; set enabled=true to opt in. On create, provided categories layer onto
- the default set. On update, provided categories merge onto the session's current
- config; when no telemetry is active this falls back to the default set (matching
- create). If browser is omitted or empty, the default set is used. A browser
- config that disables every category stops capture on update and starts no
- capture on create.
+ categories (console, network, page, interaction), screenshot and platform are
+ off by default; set enabled=true to opt in. On create, provided categories layer
+ onto the default set. On update, provided categories merge onto the session's
+ current config; when no telemetry is active this falls back to the default set
+ (matching create). If browser is omitted or empty, the default set is used. A
+ browser config that disables every category stops capture on update and starts
+ no capture on create.
"""
enabled: Optional[bool] = None
diff --git a/src/kernel/types/auth/managed_auth_browser_config_param.py b/src/kernel/types/auth/managed_auth_browser_config_param.py
index 7f8650e0..bfd09f95 100644
--- a/src/kernel/types/auth/managed_auth_browser_config_param.py
+++ b/src/kernel/types/auth/managed_auth_browser_config_param.py
@@ -69,13 +69,13 @@ class Telemetry(TypedDict, total=False):
The operational categories (control, connection, system, captcha) are captured
whenever telemetry is enabled; set one to enabled=false to opt out. The CDP
- categories (console, network, page, interaction) and screenshot are off by
- default; set enabled=true to opt in. On create, provided categories layer onto
- the default set. On update, provided categories merge onto the session's current
- config; when no telemetry is active this falls back to the default set (matching
- create). If browser is omitted or empty, the default set is used. A browser
- config that disables every category stops capture on update and starts no
- capture on create.
+ categories (console, network, page, interaction), screenshot and platform are
+ off by default; set enabled=true to opt in. On create, provided categories layer
+ onto the default set. On update, provided categories merge onto the session's
+ current config; when no telemetry is active this falls back to the default set
+ (matching create). If browser is omitted or empty, the default set is used. A
+ browser config that disables every category stops capture on update and starts
+ no capture on create.
"""
enabled: bool
diff --git a/src/kernel/types/browser_create_params.py b/src/kernel/types/browser_create_params.py
index 2899fb7c..85e0b9bd 100644
--- a/src/kernel/types/browser_create_params.py
+++ b/src/kernel/types/browser_create_params.py
@@ -41,7 +41,7 @@ class BrowserCreateParams(TypedDict, total=False):
gpu: bool
"""If true, enables GPU acceleration for the browser session.
- Requires Start-Up or Enterprise plan and headless=false.
+ Requires Start-Up or Enterprise plan, headless=false, and region=us-east.
"""
headless: bool
@@ -221,13 +221,13 @@ class Telemetry(TypedDict, total=False):
The operational categories (control, connection, system, captcha) are captured
whenever telemetry is enabled; set one to enabled=false to opt out. The CDP
- categories (console, network, page, interaction) and screenshot are off by
- default; set enabled=true to opt in. On create, provided categories layer onto
- the default set. On update, provided categories merge onto the session's current
- config; when no telemetry is active this falls back to the default set (matching
- create). If browser is omitted or empty, the default set is used. A browser
- config that disables every category stops capture on update and starts no
- capture on create.
+ categories (console, network, page, interaction), screenshot and platform are
+ off by default; set enabled=true to opt in. On create, provided categories layer
+ onto the default set. On update, provided categories merge onto the session's
+ current config; when no telemetry is active this falls back to the default set
+ (matching create). If browser is omitted or empty, the default set is used. A
+ browser config that disables every category stops capture on update and starts
+ no capture on create.
"""
enabled: bool
diff --git a/src/kernel/types/browser_pool_acquire_params.py b/src/kernel/types/browser_pool_acquire_params.py
index bd50be1b..b5bd708d 100644
--- a/src/kernel/types/browser_pool_acquire_params.py
+++ b/src/kernel/types/browser_pool_acquire_params.py
@@ -115,13 +115,13 @@ class Telemetry(TypedDict, total=False):
The operational categories (control, connection, system, captcha) are captured
whenever telemetry is enabled; set one to enabled=false to opt out. The CDP
- categories (console, network, page, interaction) and screenshot are off by
- default; set enabled=true to opt in. On create, provided categories layer onto
- the default set. On update, provided categories merge onto the session's current
- config; when no telemetry is active this falls back to the default set (matching
- create). If browser is omitted or empty, the default set is used. A browser
- config that disables every category stops capture on update and starts no
- capture on create.
+ categories (console, network, page, interaction), screenshot and platform are
+ off by default; set enabled=true to opt in. On create, provided categories layer
+ onto the default set. On update, provided categories merge onto the session's
+ current config; when no telemetry is active this falls back to the default set
+ (matching create). If browser is omitted or empty, the default set is used. A
+ browser config that disables every category stops capture on update and starts
+ no capture on create.
"""
enabled: bool
diff --git a/src/kernel/types/browser_pool_create_params.py b/src/kernel/types/browser_pool_create_params.py
index 474e4243..3a736fbc 100644
--- a/src/kernel/types/browser_pool_create_params.py
+++ b/src/kernel/types/browser_pool_create_params.py
@@ -216,13 +216,13 @@ class Telemetry(TypedDict, total=False):
The operational categories (control, connection, system, captcha) are captured
whenever telemetry is enabled; set one to enabled=false to opt out. The CDP
- categories (console, network, page, interaction) and screenshot are off by
- default; set enabled=true to opt in. On create, provided categories layer onto
- the default set. On update, provided categories merge onto the session's current
- config; when no telemetry is active this falls back to the default set (matching
- create). If browser is omitted or empty, the default set is used. A browser
- config that disables every category stops capture on update and starts no
- capture on create.
+ categories (console, network, page, interaction), screenshot and platform are
+ off by default; set enabled=true to opt in. On create, provided categories layer
+ onto the default set. On update, provided categories merge onto the session's
+ current config; when no telemetry is active this falls back to the default set
+ (matching create). If browser is omitted or empty, the default set is used. A
+ browser config that disables every category stops capture on update and starts
+ no capture on create.
"""
enabled: bool
diff --git a/src/kernel/types/browser_pool_update_params.py b/src/kernel/types/browser_pool_update_params.py
index 36693003..9bf898f2 100644
--- a/src/kernel/types/browser_pool_update_params.py
+++ b/src/kernel/types/browser_pool_update_params.py
@@ -226,13 +226,13 @@ class Telemetry(TypedDict, total=False):
The operational categories (control, connection, system, captcha) are captured
whenever telemetry is enabled; set one to enabled=false to opt out. The CDP
- categories (console, network, page, interaction) and screenshot are off by
- default; set enabled=true to opt in. On create, provided categories layer onto
- the default set. On update, provided categories merge onto the session's current
- config; when no telemetry is active this falls back to the default set (matching
- create). If browser is omitted or empty, the default set is used. A browser
- config that disables every category stops capture on update and starts no
- capture on create.
+ categories (console, network, page, interaction), screenshot and platform are
+ off by default; set enabled=true to opt in. On create, provided categories layer
+ onto the default set. On update, provided categories merge onto the session's
+ current config; when no telemetry is active this falls back to the default set
+ (matching create). If browser is omitted or empty, the default set is used. A
+ browser config that disables every category stops capture on update and starts
+ no capture on create.
"""
enabled: bool
diff --git a/src/kernel/types/browser_update_params.py b/src/kernel/types/browser_update_params.py
index a9ea675c..e7769e61 100644
--- a/src/kernel/types/browser_update_params.py
+++ b/src/kernel/types/browser_update_params.py
@@ -136,13 +136,13 @@ class Telemetry(TypedDict, total=False):
The operational categories (control, connection, system, captcha) are captured
whenever telemetry is enabled; set one to enabled=false to opt out. The CDP
- categories (console, network, page, interaction) and screenshot are off by
- default; set enabled=true to opt in. On create, provided categories layer onto
- the default set. On update, provided categories merge onto the session's current
- config; when no telemetry is active this falls back to the default set (matching
- create). If browser is omitted or empty, the default set is used. A browser
- config that disables every category stops capture on update and starts no
- capture on create.
+ categories (console, network, page, interaction), screenshot and platform are
+ off by default; set enabled=true to opt in. On create, provided categories layer
+ onto the default set. On update, provided categories merge onto the session's
+ current config; when no telemetry is active this falls back to the default set
+ (matching create). If browser is omitted or empty, the default set is used. A
+ browser config that disables every category stops capture on update and starts
+ no capture on create.
"""
enabled: bool
diff --git a/src/kernel/types/browsers/__init__.py b/src/kernel/types/browsers/__init__.py
index 8ed451a0..a6d6eb74 100644
--- a/src/kernel/types/browsers/__init__.py
+++ b/src/kernel/types/browsers/__init__.py
@@ -40,6 +40,7 @@
from .telemetry_events_params import TelemetryEventsParams as TelemetryEventsParams
from .telemetry_stream_params import TelemetryStreamParams as TelemetryStreamParams
from .browser_telemetry_config import BrowserTelemetryConfig as BrowserTelemetryConfig
+from .browser_cdp_command_event import BrowserCdpCommandEvent as BrowserCdpCommandEvent
from .browser_cdp_connect_event import BrowserCdpConnectEvent as BrowserCdpConnectEvent
from .browser_console_log_event import BrowserConsoleLogEvent as BrowserConsoleLogEvent
from .browser_proxy_error_event import BrowserProxyErrorEvent as BrowserProxyErrorEvent
@@ -51,7 +52,9 @@
from .playwright_execute_params import PlaywrightExecuteParams as PlaywrightExecuteParams
from .telemetry_events_response import TelemetryEventsResponse as TelemetryEventsResponse
from .telemetry_stream_response import TelemetryStreamResponse as TelemetryStreamResponse
+from .browser_cdp_command_method import BrowserCdpCommandMethod as BrowserCdpCommandMethod
from .browser_network_idle_event import BrowserNetworkIdleEvent as BrowserNetworkIdleEvent
+from .browser_page_crashed_event import BrowserPageCrashedEvent as BrowserPageCrashedEvent
from .computer_drag_mouse_params import ComputerDragMouseParams as ComputerDragMouseParams
from .computer_move_mouse_params import ComputerMoveMouseParams as ComputerMoveMouseParams
from .browser_console_error_event import BrowserConsoleErrorEvent as BrowserConsoleErrorEvent
@@ -70,9 +73,11 @@
from .browser_interaction_click_event import BrowserInteractionClickEvent as BrowserInteractionClickEvent
from .browser_live_view_connect_event import BrowserLiveViewConnectEvent as BrowserLiveViewConnectEvent
from .browser_page_layout_shift_event import BrowserPageLayoutShiftEvent as BrowserPageLayoutShiftEvent
+from .browser_platform_api_call_event import BrowserPlatformAPICallEvent as BrowserPlatformAPICallEvent
from .browser_telemetry_export_config import BrowserTelemetryExportConfig as BrowserTelemetryExportConfig
from .computer_write_clipboard_params import ComputerWriteClipboardParams as ComputerWriteClipboardParams
from .browser_monitor_screenshot_event import BrowserMonitorScreenshotEvent as BrowserMonitorScreenshotEvent
+from .browser_telemetry_control_config import BrowserTelemetryControlConfig as BrowserTelemetryControlConfig
from .computer_read_clipboard_response import ComputerReadClipboardResponse as ComputerReadClipboardResponse
from .browser_monitor_init_failed_event import BrowserMonitorInitFailedEvent as BrowserMonitorInitFailedEvent
from .browser_monitor_reconnected_event import BrowserMonitorReconnectedEvent as BrowserMonitorReconnectedEvent
@@ -84,6 +89,7 @@
from .computer_capture_screenshot_params import ComputerCaptureScreenshotParams as ComputerCaptureScreenshotParams
from .browser_telemetry_categories_config import BrowserTelemetryCategoriesConfig as BrowserTelemetryCategoriesConfig
from .browser_network_loading_failed_event import BrowserNetworkLoadingFailedEvent as BrowserNetworkLoadingFailedEvent
+from .browser_telemetry_cdp_control_config import BrowserTelemetryCdpControlConfig as BrowserTelemetryCdpControlConfig
from .browser_telemetry_otlp_export_config import BrowserTelemetryOtlpExportConfig as BrowserTelemetryOtlpExportConfig
from .computer_get_mouse_position_response import ComputerGetMousePositionResponse as ComputerGetMousePositionResponse
from .browser_page_dom_content_loaded_event import BrowserPageDomContentLoadedEvent as BrowserPageDomContentLoadedEvent
@@ -96,6 +102,9 @@
from .browser_monitor_reconnect_failed_event import (
BrowserMonitorReconnectFailedEvent as BrowserMonitorReconnectFailedEvent,
)
+from .browser_telemetry_control_config_param import (
+ BrowserTelemetryControlConfigParam as BrowserTelemetryControlConfigParam,
+)
from .browser_telemetry_category_config_param import (
BrowserTelemetryCategoryConfigParam as BrowserTelemetryCategoryConfigParam,
)
@@ -108,3 +117,6 @@
from .browser_telemetry_categories_config_param import (
BrowserTelemetryCategoriesConfigParam as BrowserTelemetryCategoriesConfigParam,
)
+from .browser_telemetry_cdp_control_config_param import (
+ BrowserTelemetryCdpControlConfigParam as BrowserTelemetryCdpControlConfigParam,
+)
diff --git a/src/kernel/types/browsers/browser_api_call_event.py b/src/kernel/types/browsers/browser_api_call_event.py
index 702596d8..b4210968 100644
--- a/src/kernel/types/browsers/browser_api_call_event.py
+++ b/src/kernel/types/browsers/browser_api_call_event.py
@@ -14,7 +14,10 @@ class Data(BaseModel):
"""Wall-clock duration of the handler in milliseconds."""
operation_id: str
- """OpenAPI operationId of the matched route (e.g. processExec, takeScreenshot)."""
+ """Matched route's operation, named as the in-VM API names its handler (e.g.
+
+ ProcessExec, TakeScreenshot).
+ """
request_id: str
"""Per-request identifier from the in-VM API request middleware."""
@@ -22,9 +25,18 @@ class Data(BaseModel):
status: int
"""HTTP response status code."""
+ code: Optional[str] = None
+ """
+ Source submitted to the Playwright code-execution endpoint, capped at 8192 bytes
+ like every other captured string. A capped value is cut on a character boundary
+ and ends in `...[truncated]`. Absent for every other operation.
+ """
+
class BrowserAPICallEvent(BaseModel):
- """An agent-driven HTTP call handled by the in-VM API server."""
+ """
+ An agent-driven HTTP call that drives the browser, handled by the in-VM API server. Calls that manage the VM instead emit platform_api_call.
+ """
category: Literal["control"]
diff --git a/src/kernel/types/browsers/browser_cdp_command_event.py b/src/kernel/types/browsers/browser_cdp_command_event.py
new file mode 100644
index 00000000..045be40f
--- /dev/null
+++ b/src/kernel/types/browsers/browser_cdp_command_event.py
@@ -0,0 +1,1883 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from typing import List, Union, Optional
+from typing_extensions import Literal, Annotated, TypeAlias
+
+from ..._utils import PropertyInfo
+from ..._models import BaseModel
+from .browser_event_source import BrowserEventSource
+
+__all__ = [
+ "BrowserCdpCommandEvent",
+ "Data",
+ "DataBrowserCdpInputDispatchMouseEventCommandData",
+ "DataBrowserCdpInputDispatchKeyEventCommandData",
+ "DataBrowserCdpInputInsertTextCommandData",
+ "DataBrowserCdpInputImeSetCompositionCommandData",
+ "DataBrowserCdpInputDispatchTouchEventCommandData",
+ "DataBrowserCdpInputDispatchDragEventCommandData",
+ "DataBrowserCdpInputCancelDraggingCommandData",
+ "DataBrowserCdpInputEmulateTouchFromMouseEventCommandData",
+ "DataBrowserCdpInputSynthesizePinchGestureCommandData",
+ "DataBrowserCdpInputSynthesizeScrollGestureCommandData",
+ "DataBrowserCdpInputSynthesizeTapGestureCommandData",
+ "DataBrowserCdpDomSetFileInputFilesCommandData",
+ "DataBrowserCdpDomFocusCommandData",
+ "DataBrowserCdpDomScrollIntoViewIfNeededCommandData",
+ "DataBrowserCdpPageBringToFrontCommandData",
+ "DataBrowserCdpPageCaptureScreenshotCommandData",
+ "DataBrowserCdpPageCaptureSnapshotCommandData",
+ "DataBrowserCdpPageHandleJavaScriptDialogCommandData",
+ "DataBrowserCdpPageNavigateCommandData",
+ "DataBrowserCdpPageNavigateToHistoryEntryCommandData",
+ "DataBrowserCdpPageReloadCommandData",
+ "DataBrowserCdpPagePrintToPdfCommandData",
+ "DataBrowserCdpPageStartScreencastCommandData",
+ "DataBrowserCdpPageStopScreencastCommandData",
+ "DataBrowserCdpPageStopLoadingCommandData",
+ "DataBrowserCdpPageCloseCommandData",
+ "DataBrowserCdpPageSetWebLifecycleStateCommandData",
+ "DataBrowserCdpTargetActivateTargetCommandData",
+ "DataBrowserCdpTargetCloseTargetCommandData",
+ "DataBrowserCdpTargetCreateTargetCommandData",
+ "DataBrowserCdpTargetCreateBrowserContextCommandData",
+ "DataBrowserCdpTargetDisposeBrowserContextCommandData",
+ "DataBrowserCdpTargetOpenDevToolsCommandData",
+ "DataBrowserCdpBrowserCancelDownloadCommandData",
+ "DataBrowserCdpBrowserCloseCommandData",
+ "DataBrowserCdpBrowserSetWindowBoundsCommandData",
+ "DataBrowserCdpBrowserSetContentsSizeCommandData",
+ "DataBrowserCdpAutofillTriggerCommandData",
+]
+
+
+class DataBrowserCdpInputDispatchMouseEventCommandData(BaseModel):
+ """Sanitized `Input.dispatchMouseEvent` arguments.
+
+ Canonical input: `Input.dispatchMouseEvent` in devtools-protocol@2d019e73, pinned at https://github.com/ChromeDevTools/devtools-protocol/blob/2d019e73eb371d1d6985d26d395d78bd8f8a22ba/json/browser_protocol.json. Every argument of this command has a retained or redacted decision in lib/devtoolsproxy/testdata/cdp_arguments.yaml.
+ """
+
+ event_type: Literal["mousePressed", "mouseReleased", "mouseMoved", "mouseWheel", "other"]
+ """Mouse event phase: `mousePressed`, `mouseReleased`, `mouseMoved` or
+ `mouseWheel`.
+
+ A value the protocol does not define is reported as `other`.
+ """
+
+ method: Literal["Input.dispatchMouseEvent"]
+
+ button: Optional[Literal["none", "left", "middle", "right", "back", "forward", "other"]] = None
+ """
+ Button named by the command (`none`, `left`, `middle`, `right`, `back`,
+ `forward`). A value the protocol does not define is reported as `other`.
+ """
+
+ buttons: Optional[int] = None
+ """Bit field of buttons held down.
+
+ Non-zero on a `mouseMoved` means the move is a drag path.
+ """
+
+ click_count: Optional[int] = None
+ """Number of times the button was clicked (2 is a double click)."""
+
+ command_id: Optional[int] = None
+ """
+ The command's JSON-RPC id, so the command can be joined to the result the
+ browser returned for it. Absent when the client sent none.
+ """
+
+ connection_id: Optional[str] = None
+ """
+ Identifies the CDP proxy connection the command arrived on, matching
+ `cdp_connect` and `cdp_disconnect`. Two clients driving the same browser are
+ told apart by this.
+ """
+
+ delta_x: Optional[float] = None
+ """Horizontal scroll delta, for `mouseWheel`."""
+
+ delta_y: Optional[float] = None
+ """Vertical scroll delta, for `mouseWheel`."""
+
+ force: Optional[float] = None
+ """Normalized pressure, 0 to 1."""
+
+ modifiers: Optional[int] = None
+ """Bit field of held modifier keys (1=Alt, 2=Ctrl, 4=Meta, 8=Shift)."""
+
+ pointer_type: Optional[Literal["mouse", "pen", "other"]] = None
+ """Pointer that generated the event (`mouse` or `pen`).
+
+ A value the protocol does not define is reported as `other`.
+ """
+
+ session_id: Optional[str] = None
+ """CDP session identifier the command was addressed to.
+
+ Absent for browser-level commands. Clipped to 128 characters.
+ """
+
+ tangential_pressure: Optional[float] = None
+ """Normalized tangential pressure, -1 to 1."""
+
+ tilt_x: Optional[float] = None
+ """Pen tilt from the Y-Z plane, in degrees."""
+
+ tilt_y: Optional[float] = None
+ """Pen tilt from the X-Z plane, in degrees."""
+
+ twist: Optional[int] = None
+ """Pen clockwise rotation, in degrees."""
+
+ x: Optional[float] = None
+ """Viewport x coordinate in CSS pixels."""
+
+ y: Optional[float] = None
+ """Viewport y coordinate in CSS pixels."""
+
+
+class DataBrowserCdpInputDispatchKeyEventCommandData(BaseModel):
+ """Sanitized `Input.dispatchKeyEvent` arguments.
+
+ Canonical input: `Input.dispatchKeyEvent` in devtools-protocol@2d019e73, pinned at https://github.com/ChromeDevTools/devtools-protocol/blob/2d019e73eb371d1d6985d26d395d78bd8f8a22ba/json/browser_protocol.json. Every argument of this command has a retained or redacted decision in lib/devtoolsproxy/testdata/cdp_arguments.yaml.
+ """
+
+ event_type: Literal["keyDown", "keyUp", "rawKeyDown", "char", "other"]
+ """Key event phase: `keyDown`, `keyUp`, `rawKeyDown` or `char`.
+
+ A value the protocol does not define is reported as `other`.
+ """
+
+ method: Literal["Input.dispatchKeyEvent"]
+
+ auto_repeat: Optional[bool] = None
+ """Whether the event was generated by key repeat."""
+
+ command_count: Optional[int] = None
+ """Number of editing commands (e.g. `selectAll`) carried by the event."""
+
+ command_id: Optional[int] = None
+ """
+ The command's JSON-RPC id, so the command can be joined to the result the
+ browser returned for it. Absent when the client sent none.
+ """
+
+ connection_id: Optional[str] = None
+ """
+ Identifies the CDP proxy connection the command arrived on, matching
+ `cdp_connect` and `cdp_disconnect`. Two clients driving the same browser are
+ told apart by this.
+ """
+
+ is_keypad: Optional[bool] = None
+ """Whether the key is on the numeric keypad."""
+
+ is_system_key: Optional[bool] = None
+ """Whether the event is a system key event."""
+
+ location: Optional[int] = None
+ """Keyboard location (1=left, 2=right, 3=numpad)."""
+
+ modifiers: Optional[int] = None
+ """Bit field of held modifier keys (1=Alt, 2=Ctrl, 4=Meta, 8=Shift)."""
+
+ named_key: Optional[str] = None
+ """Key that commands the page rather than typing into it (e.g.
+
+ `Enter`, `Tab`, `ArrowDown`, `F5`). Keys that produce a character are never
+ captured; those are counted by `text_length`.
+ """
+
+ session_id: Optional[str] = None
+ """CDP session identifier the command was addressed to.
+
+ Absent for browser-level commands. Clipped to 128 characters.
+ """
+
+ text_length: Optional[int] = None
+ """Number of characters the command submitted. The text itself is never captured."""
+
+
+class DataBrowserCdpInputInsertTextCommandData(BaseModel):
+ """Sanitized `Input.insertText` arguments.
+
+ Canonical input: `Input.insertText` in devtools-protocol@2d019e73, pinned at https://github.com/ChromeDevTools/devtools-protocol/blob/2d019e73eb371d1d6985d26d395d78bd8f8a22ba/json/browser_protocol.json. Every argument of this command has a retained or redacted decision in lib/devtoolsproxy/testdata/cdp_arguments.yaml.
+ """
+
+ method: Literal["Input.insertText"]
+
+ text_length: int
+ """Number of characters inserted. The text itself is never captured."""
+
+ command_id: Optional[int] = None
+ """
+ The command's JSON-RPC id, so the command can be joined to the result the
+ browser returned for it. Absent when the client sent none.
+ """
+
+ connection_id: Optional[str] = None
+ """
+ Identifies the CDP proxy connection the command arrived on, matching
+ `cdp_connect` and `cdp_disconnect`. Two clients driving the same browser are
+ told apart by this.
+ """
+
+ session_id: Optional[str] = None
+ """CDP session identifier the command was addressed to.
+
+ Absent for browser-level commands. Clipped to 128 characters.
+ """
+
+
+class DataBrowserCdpInputImeSetCompositionCommandData(BaseModel):
+ """Sanitized `Input.imeSetComposition` arguments.
+
+ Canonical input: `Input.imeSetComposition` in devtools-protocol@2d019e73, pinned at https://github.com/ChromeDevTools/devtools-protocol/blob/2d019e73eb371d1d6985d26d395d78bd8f8a22ba/json/browser_protocol.json. Every argument of this command has a retained or redacted decision in lib/devtoolsproxy/testdata/cdp_arguments.yaml.
+ """
+
+ method: Literal["Input.imeSetComposition"]
+
+ text_length: int
+ """Number of characters in the composition. The text itself is never captured."""
+
+ command_id: Optional[int] = None
+ """
+ The command's JSON-RPC id, so the command can be joined to the result the
+ browser returned for it. Absent when the client sent none.
+ """
+
+ connection_id: Optional[str] = None
+ """
+ Identifies the CDP proxy connection the command arrived on, matching
+ `cdp_connect` and `cdp_disconnect`. Two clients driving the same browser are
+ told apart by this.
+ """
+
+ replacement_end: Optional[int] = None
+ """Replacement range end offset."""
+
+ replacement_start: Optional[int] = None
+ """Replacement range start offset."""
+
+ selection_end: Optional[int] = None
+ """Selection end offset within the composition."""
+
+ selection_start: Optional[int] = None
+ """Selection start offset within the composition."""
+
+ session_id: Optional[str] = None
+ """CDP session identifier the command was addressed to.
+
+ Absent for browser-level commands. Clipped to 128 characters.
+ """
+
+
+class DataBrowserCdpInputDispatchTouchEventCommandData(BaseModel):
+ """Sanitized `Input.dispatchTouchEvent` arguments.
+
+ Canonical input: `Input.dispatchTouchEvent` in devtools-protocol@2d019e73, pinned at https://github.com/ChromeDevTools/devtools-protocol/blob/2d019e73eb371d1d6985d26d395d78bd8f8a22ba/json/browser_protocol.json. Every argument of this command has a retained or redacted decision in lib/devtoolsproxy/testdata/cdp_arguments.yaml.
+ """
+
+ event_type: Literal["touchStart", "touchEnd", "touchMove", "touchCancel", "other"]
+ """Touch event phase: `touchStart`, `touchEnd`, `touchMove` or `touchCancel`.
+
+ A value the protocol does not define is reported as `other`.
+ """
+
+ method: Literal["Input.dispatchTouchEvent"]
+
+ touch_point_count: int
+ """Number of active touch points the command carried."""
+
+ command_id: Optional[int] = None
+ """
+ The command's JSON-RPC id, so the command can be joined to the result the
+ browser returned for it. Absent when the client sent none.
+ """
+
+ connection_id: Optional[str] = None
+ """
+ Identifies the CDP proxy connection the command arrived on, matching
+ `cdp_connect` and `cdp_disconnect`. Two clients driving the same browser are
+ told apart by this.
+ """
+
+ force: Optional[float] = None
+ """Normalized pressure of the first touch point, 0 to 1."""
+
+ modifiers: Optional[int] = None
+ """Bit field of held modifier keys (1=Alt, 2=Ctrl, 4=Meta, 8=Shift)."""
+
+ radius_x: Optional[float] = None
+ """Horizontal radius of the first touch point."""
+
+ radius_y: Optional[float] = None
+ """Vertical radius of the first touch point."""
+
+ rotation_angle: Optional[float] = None
+ """Rotation of the first touch point, in degrees."""
+
+ session_id: Optional[str] = None
+ """CDP session identifier the command was addressed to.
+
+ Absent for browser-level commands. Clipped to 128 characters.
+ """
+
+ tangential_pressure: Optional[float] = None
+ """Normalized tangential pressure of the first touch point, -1 to 1."""
+
+ tilt_x: Optional[float] = None
+ """Tilt of the first touch point from the Y-Z plane, in degrees."""
+
+ tilt_y: Optional[float] = None
+ """Tilt of the first touch point from the X-Z plane, in degrees."""
+
+ twist: Optional[int] = None
+ """Clockwise rotation of the first touch point, in degrees."""
+
+ x: Optional[float] = None
+ """Viewport x coordinate of the first touch point.
+
+ Touch coordinates live inside `touchPoints`, so this is the primary point rather
+ than a command-level argument.
+ """
+
+ y: Optional[float] = None
+ """Viewport y coordinate of the first touch point."""
+
+
+class DataBrowserCdpInputDispatchDragEventCommandData(BaseModel):
+ """Sanitized `Input.dispatchDragEvent` arguments.
+
+ Canonical input: `Input.dispatchDragEvent` in devtools-protocol@2d019e73, pinned at https://github.com/ChromeDevTools/devtools-protocol/blob/2d019e73eb371d1d6985d26d395d78bd8f8a22ba/json/browser_protocol.json. Every argument of this command has a retained or redacted decision in lib/devtoolsproxy/testdata/cdp_arguments.yaml.
+ """
+
+ event_type: Literal["dragEnter", "dragOver", "drop", "dragCancel", "other"]
+ """Drag event phase: `dragEnter`, `dragOver`, `drop` or `dragCancel`.
+
+ A value the protocol does not define is reported as `other`.
+ """
+
+ method: Literal["Input.dispatchDragEvent"]
+
+ command_id: Optional[int] = None
+ """
+ The command's JSON-RPC id, so the command can be joined to the result the
+ browser returned for it. Absent when the client sent none.
+ """
+
+ connection_id: Optional[str] = None
+ """
+ Identifies the CDP proxy connection the command arrived on, matching
+ `cdp_connect` and `cdp_disconnect`. Two clients driving the same browser are
+ told apart by this.
+ """
+
+ drag_file_count: Optional[int] = None
+ """Number of files in the drag payload. File paths are never captured."""
+
+ drag_item_count: Optional[int] = None
+ """Number of items in the drag payload. Item contents are never captured."""
+
+ drag_mime_categories: Optional[
+ List[
+ Literal["text", "image", "audio", "video", "application", "font", "model", "multipart", "message", "other"]
+ ]
+ ] = None
+ """Distinct top-level MIME categories of the drag items (e.g.
+
+ `text`, `image`, `application`). Subtypes and contents are never captured. A
+ value the protocol does not define is reported as `other`.
+ """
+
+ drag_operations_mask: Optional[int] = None
+ """Bit field of allowed drag operations (1=copy, 2=link, 16=move)."""
+
+ modifiers: Optional[int] = None
+ """Bit field of held modifier keys (1=Alt, 2=Ctrl, 4=Meta, 8=Shift)."""
+
+ session_id: Optional[str] = None
+ """CDP session identifier the command was addressed to.
+
+ Absent for browser-level commands. Clipped to 128 characters.
+ """
+
+ x: Optional[float] = None
+ """Viewport x coordinate in CSS pixels."""
+
+ y: Optional[float] = None
+ """Viewport y coordinate in CSS pixels."""
+
+
+class DataBrowserCdpInputCancelDraggingCommandData(BaseModel):
+ """Sanitized `Input.cancelDragging` arguments.
+
+ Canonical input: `Input.cancelDragging` in devtools-protocol@2d019e73, pinned at https://github.com/ChromeDevTools/devtools-protocol/blob/2d019e73eb371d1d6985d26d395d78bd8f8a22ba/json/browser_protocol.json. Every argument of this command has a retained or redacted decision in lib/devtoolsproxy/testdata/cdp_arguments.yaml.
+ """
+
+ method: Literal["Input.cancelDragging"]
+
+ command_id: Optional[int] = None
+ """
+ The command's JSON-RPC id, so the command can be joined to the result the
+ browser returned for it. Absent when the client sent none.
+ """
+
+ connection_id: Optional[str] = None
+ """
+ Identifies the CDP proxy connection the command arrived on, matching
+ `cdp_connect` and `cdp_disconnect`. Two clients driving the same browser are
+ told apart by this.
+ """
+
+ session_id: Optional[str] = None
+ """CDP session identifier the command was addressed to.
+
+ Absent for browser-level commands. Clipped to 128 characters.
+ """
+
+
+class DataBrowserCdpInputEmulateTouchFromMouseEventCommandData(BaseModel):
+ """Sanitized `Input.emulateTouchFromMouseEvent` arguments.
+
+ Canonical input: `Input.emulateTouchFromMouseEvent` in devtools-protocol@2d019e73, pinned at https://github.com/ChromeDevTools/devtools-protocol/blob/2d019e73eb371d1d6985d26d395d78bd8f8a22ba/json/browser_protocol.json. Every argument of this command has a retained or redacted decision in lib/devtoolsproxy/testdata/cdp_arguments.yaml.
+ """
+
+ event_type: Literal["mousePressed", "mouseReleased", "mouseMoved", "mouseWheel", "other"]
+ """Mouse event phase being emulated as touch.
+
+ A value the protocol does not define is reported as `other`.
+ """
+
+ method: Literal["Input.emulateTouchFromMouseEvent"]
+
+ button: Optional[Literal["none", "left", "middle", "right", "back", "forward", "other"]] = None
+ """Button named by the command.
+
+ A value the protocol does not define is reported as `other`.
+ """
+
+ click_count: Optional[int] = None
+ """Number of times the button was clicked."""
+
+ command_id: Optional[int] = None
+ """
+ The command's JSON-RPC id, so the command can be joined to the result the
+ browser returned for it. Absent when the client sent none.
+ """
+
+ connection_id: Optional[str] = None
+ """
+ Identifies the CDP proxy connection the command arrived on, matching
+ `cdp_connect` and `cdp_disconnect`. Two clients driving the same browser are
+ told apart by this.
+ """
+
+ delta_x: Optional[float] = None
+ """Horizontal scroll delta."""
+
+ delta_y: Optional[float] = None
+ """Vertical scroll delta."""
+
+ modifiers: Optional[int] = None
+ """Bit field of held modifier keys (1=Alt, 2=Ctrl, 4=Meta, 8=Shift)."""
+
+ session_id: Optional[str] = None
+ """CDP session identifier the command was addressed to.
+
+ Absent for browser-level commands. Clipped to 128 characters.
+ """
+
+ x: Optional[float] = None
+ """Viewport x coordinate in CSS pixels."""
+
+ y: Optional[float] = None
+ """Viewport y coordinate in CSS pixels."""
+
+
+class DataBrowserCdpInputSynthesizePinchGestureCommandData(BaseModel):
+ """Sanitized `Input.synthesizePinchGesture` arguments.
+
+ Canonical input: `Input.synthesizePinchGesture` in devtools-protocol@2d019e73, pinned at https://github.com/ChromeDevTools/devtools-protocol/blob/2d019e73eb371d1d6985d26d395d78bd8f8a22ba/json/browser_protocol.json. Every argument of this command has a retained or redacted decision in lib/devtoolsproxy/testdata/cdp_arguments.yaml.
+ """
+
+ method: Literal["Input.synthesizePinchGesture"]
+
+ command_id: Optional[int] = None
+ """
+ The command's JSON-RPC id, so the command can be joined to the result the
+ browser returned for it. Absent when the client sent none.
+ """
+
+ connection_id: Optional[str] = None
+ """
+ Identifies the CDP proxy connection the command arrived on, matching
+ `cdp_connect` and `cdp_disconnect`. Two clients driving the same browser are
+ told apart by this.
+ """
+
+ gesture_source_type: Optional[Literal["default", "touch", "mouse", "other"]] = None
+ """Input source the synthesized gesture emulates.
+
+ A value the protocol does not define is reported as `other`.
+ """
+
+ relative_speed: Optional[int] = None
+ """Relative pointer speed, in pixels per second."""
+
+ scale_factor: Optional[float] = None
+ """Relative scale of the pinch (>1 zooms in)."""
+
+ session_id: Optional[str] = None
+ """CDP session identifier the command was addressed to.
+
+ Absent for browser-level commands. Clipped to 128 characters.
+ """
+
+ x: Optional[float] = None
+ """Viewport x coordinate in CSS pixels."""
+
+ y: Optional[float] = None
+ """Viewport y coordinate in CSS pixels."""
+
+
+class DataBrowserCdpInputSynthesizeScrollGestureCommandData(BaseModel):
+ """Sanitized `Input.synthesizeScrollGesture` arguments.
+
+ Canonical input: `Input.synthesizeScrollGesture` in devtools-protocol@2d019e73, pinned at https://github.com/ChromeDevTools/devtools-protocol/blob/2d019e73eb371d1d6985d26d395d78bd8f8a22ba/json/browser_protocol.json. Every argument of this command has a retained or redacted decision in lib/devtoolsproxy/testdata/cdp_arguments.yaml.
+ """
+
+ method: Literal["Input.synthesizeScrollGesture"]
+
+ command_id: Optional[int] = None
+ """
+ The command's JSON-RPC id, so the command can be joined to the result the
+ browser returned for it. Absent when the client sent none.
+ """
+
+ connection_id: Optional[str] = None
+ """
+ Identifies the CDP proxy connection the command arrived on, matching
+ `cdp_connect` and `cdp_disconnect`. Two clients driving the same browser are
+ told apart by this.
+ """
+
+ gesture_source_type: Optional[Literal["default", "touch", "mouse", "other"]] = None
+ """Input source the synthesized gesture emulates.
+
+ A value the protocol does not define is reported as `other`.
+ """
+
+ prevent_fling: Optional[bool] = None
+ """Whether fling was suppressed."""
+
+ repeat_count: Optional[int] = None
+ """Number of additional repeats of the scroll."""
+
+ repeat_delay_ms: Optional[int] = None
+ """Delay between repeats, in milliseconds."""
+
+ session_id: Optional[str] = None
+ """CDP session identifier the command was addressed to.
+
+ Absent for browser-level commands. Clipped to 128 characters.
+ """
+
+ speed: Optional[int] = None
+ """Swipe speed in pixels per second."""
+
+ x: Optional[float] = None
+ """Viewport x coordinate in CSS pixels."""
+
+ x_distance: Optional[float] = None
+ """Horizontal scroll distance in CSS pixels; positive scrolls left."""
+
+ x_overscroll: Optional[float] = None
+ """Additional horizontal distance scrolled past the end."""
+
+ y: Optional[float] = None
+ """Viewport y coordinate in CSS pixels."""
+
+ y_distance: Optional[float] = None
+ """Vertical scroll distance in CSS pixels; positive scrolls up."""
+
+ y_overscroll: Optional[float] = None
+ """Additional vertical distance scrolled past the end."""
+
+
+class DataBrowserCdpInputSynthesizeTapGestureCommandData(BaseModel):
+ """Sanitized `Input.synthesizeTapGesture` arguments.
+
+ Canonical input: `Input.synthesizeTapGesture` in devtools-protocol@2d019e73, pinned at https://github.com/ChromeDevTools/devtools-protocol/blob/2d019e73eb371d1d6985d26d395d78bd8f8a22ba/json/browser_protocol.json. Every argument of this command has a retained or redacted decision in lib/devtoolsproxy/testdata/cdp_arguments.yaml.
+ """
+
+ method: Literal["Input.synthesizeTapGesture"]
+
+ command_id: Optional[int] = None
+ """
+ The command's JSON-RPC id, so the command can be joined to the result the
+ browser returned for it. Absent when the client sent none.
+ """
+
+ connection_id: Optional[str] = None
+ """
+ Identifies the CDP proxy connection the command arrived on, matching
+ `cdp_connect` and `cdp_disconnect`. Two clients driving the same browser are
+ told apart by this.
+ """
+
+ duration: Optional[int] = None
+ """Duration between touchdown and touchup, in milliseconds."""
+
+ gesture_source_type: Optional[Literal["default", "touch", "mouse", "other"]] = None
+ """Input source the synthesized gesture emulates.
+
+ A value the protocol does not define is reported as `other`.
+ """
+
+ session_id: Optional[str] = None
+ """CDP session identifier the command was addressed to.
+
+ Absent for browser-level commands. Clipped to 128 characters.
+ """
+
+ tap_count: Optional[int] = None
+ """Number of times to tap (2 is a double tap)."""
+
+ x: Optional[float] = None
+ """Viewport x coordinate in CSS pixels."""
+
+ y: Optional[float] = None
+ """Viewport y coordinate in CSS pixels."""
+
+
+class DataBrowserCdpDomSetFileInputFilesCommandData(BaseModel):
+ """Sanitized `DOM.setFileInputFiles` arguments.
+
+ Canonical input: `DOM.setFileInputFiles` in devtools-protocol@2d019e73, pinned at https://github.com/ChromeDevTools/devtools-protocol/blob/2d019e73eb371d1d6985d26d395d78bd8f8a22ba/json/browser_protocol.json. Every argument of this command has a retained or redacted decision in lib/devtoolsproxy/testdata/cdp_arguments.yaml.
+ """
+
+ file_count: int
+ """Number of files handed to the input. File paths are never captured."""
+
+ method: Literal["DOM.setFileInputFiles"]
+
+ backend_node_id: Optional[int] = None
+ """Opaque backend DOM node identifier the command targeted."""
+
+ command_id: Optional[int] = None
+ """
+ The command's JSON-RPC id, so the command can be joined to the result the
+ browser returned for it. Absent when the client sent none.
+ """
+
+ connection_id: Optional[str] = None
+ """
+ Identifies the CDP proxy connection the command arrived on, matching
+ `cdp_connect` and `cdp_disconnect`. Two clients driving the same browser are
+ told apart by this.
+ """
+
+ node_id: Optional[int] = None
+ """Opaque DOM node identifier the command targeted."""
+
+ object_id: Optional[str] = None
+ """Opaque Runtime remote object identifier the command targeted.
+
+ Clipped to 128 characters; a longer value is not a real identifier.
+ """
+
+ session_id: Optional[str] = None
+ """CDP session identifier the command was addressed to.
+
+ Absent for browser-level commands. Clipped to 128 characters.
+ """
+
+
+class DataBrowserCdpDomFocusCommandData(BaseModel):
+ """Sanitized `DOM.focus` arguments.
+
+ Canonical input: `DOM.focus` in devtools-protocol@2d019e73, pinned at https://github.com/ChromeDevTools/devtools-protocol/blob/2d019e73eb371d1d6985d26d395d78bd8f8a22ba/json/browser_protocol.json. Every argument of this command has a retained or redacted decision in lib/devtoolsproxy/testdata/cdp_arguments.yaml.
+ """
+
+ method: Literal["DOM.focus"]
+
+ backend_node_id: Optional[int] = None
+ """Opaque backend DOM node identifier the command targeted."""
+
+ command_id: Optional[int] = None
+ """
+ The command's JSON-RPC id, so the command can be joined to the result the
+ browser returned for it. Absent when the client sent none.
+ """
+
+ connection_id: Optional[str] = None
+ """
+ Identifies the CDP proxy connection the command arrived on, matching
+ `cdp_connect` and `cdp_disconnect`. Two clients driving the same browser are
+ told apart by this.
+ """
+
+ node_id: Optional[int] = None
+ """Opaque DOM node identifier the command targeted."""
+
+ object_id: Optional[str] = None
+ """Opaque Runtime remote object identifier the command targeted.
+
+ Clipped to 128 characters; a longer value is not a real identifier.
+ """
+
+ session_id: Optional[str] = None
+ """CDP session identifier the command was addressed to.
+
+ Absent for browser-level commands. Clipped to 128 characters.
+ """
+
+
+class DataBrowserCdpDomScrollIntoViewIfNeededCommandData(BaseModel):
+ """Sanitized `DOM.scrollIntoViewIfNeeded` arguments.
+
+ Canonical input: `DOM.scrollIntoViewIfNeeded` in devtools-protocol@2d019e73, pinned at https://github.com/ChromeDevTools/devtools-protocol/blob/2d019e73eb371d1d6985d26d395d78bd8f8a22ba/json/browser_protocol.json. Every argument of this command has a retained or redacted decision in lib/devtoolsproxy/testdata/cdp_arguments.yaml.
+ """
+
+ method: Literal["DOM.scrollIntoViewIfNeeded"]
+
+ backend_node_id: Optional[int] = None
+ """Opaque backend DOM node identifier the command targeted."""
+
+ command_id: Optional[int] = None
+ """
+ The command's JSON-RPC id, so the command can be joined to the result the
+ browser returned for it. Absent when the client sent none.
+ """
+
+ connection_id: Optional[str] = None
+ """
+ Identifies the CDP proxy connection the command arrived on, matching
+ `cdp_connect` and `cdp_disconnect`. Two clients driving the same browser are
+ told apart by this.
+ """
+
+ node_id: Optional[int] = None
+ """Opaque DOM node identifier the command targeted."""
+
+ object_id: Optional[str] = None
+ """Opaque Runtime remote object identifier the command targeted.
+
+ Clipped to 128 characters; a longer value is not a real identifier.
+ """
+
+ rect_height: Optional[float] = None
+ """Height of the rect the command scrolled to."""
+
+ rect_width: Optional[float] = None
+ """Width of the rect the command scrolled to."""
+
+ rect_x: Optional[float] = None
+ """X offset of the rect the command scrolled to, relative to the node."""
+
+ rect_y: Optional[float] = None
+ """Y offset of the rect the command scrolled to, relative to the node."""
+
+ session_id: Optional[str] = None
+ """CDP session identifier the command was addressed to.
+
+ Absent for browser-level commands. Clipped to 128 characters.
+ """
+
+
+class DataBrowserCdpPageBringToFrontCommandData(BaseModel):
+ """Sanitized `Page.bringToFront` arguments.
+
+ Canonical input: `Page.bringToFront` in devtools-protocol@2d019e73, pinned at https://github.com/ChromeDevTools/devtools-protocol/blob/2d019e73eb371d1d6985d26d395d78bd8f8a22ba/json/browser_protocol.json. Every argument of this command has a retained or redacted decision in lib/devtoolsproxy/testdata/cdp_arguments.yaml.
+ """
+
+ method: Literal["Page.bringToFront"]
+
+ command_id: Optional[int] = None
+ """
+ The command's JSON-RPC id, so the command can be joined to the result the
+ browser returned for it. Absent when the client sent none.
+ """
+
+ connection_id: Optional[str] = None
+ """
+ Identifies the CDP proxy connection the command arrived on, matching
+ `cdp_connect` and `cdp_disconnect`. Two clients driving the same browser are
+ told apart by this.
+ """
+
+ session_id: Optional[str] = None
+ """CDP session identifier the command was addressed to.
+
+ Absent for browser-level commands. Clipped to 128 characters.
+ """
+
+
+class DataBrowserCdpPageCaptureScreenshotCommandData(BaseModel):
+ """Sanitized `Page.captureScreenshot` arguments.
+
+ Canonical input: `Page.captureScreenshot` in devtools-protocol@2d019e73, pinned at https://github.com/ChromeDevTools/devtools-protocol/blob/2d019e73eb371d1d6985d26d395d78bd8f8a22ba/json/browser_protocol.json. Every argument of this command has a retained or redacted decision in lib/devtoolsproxy/testdata/cdp_arguments.yaml.
+ """
+
+ method: Literal["Page.captureScreenshot"]
+
+ capture_beyond_viewport: Optional[bool] = None
+ """Whether the capture extended past the viewport."""
+
+ clip_height: Optional[float] = None
+ """Clip region height in CSS pixels."""
+
+ clip_scale: Optional[float] = None
+ """Clip region page scale factor."""
+
+ clip_width: Optional[float] = None
+ """Clip region width in CSS pixels."""
+
+ clip_x: Optional[float] = None
+ """Clip region x offset in CSS pixels."""
+
+ clip_y: Optional[float] = None
+ """Clip region y offset in CSS pixels."""
+
+ command_id: Optional[int] = None
+ """
+ The command's JSON-RPC id, so the command can be joined to the result the
+ browser returned for it. Absent when the client sent none.
+ """
+
+ connection_id: Optional[str] = None
+ """
+ Identifies the CDP proxy connection the command arrived on, matching
+ `cdp_connect` and `cdp_disconnect`. Two clients driving the same browser are
+ told apart by this.
+ """
+
+ format: Optional[Literal["jpeg", "png", "webp", "other"]] = None
+ """Image format requested (`jpeg`, `png` or `webp`).
+
+ A value the protocol does not define is reported as `other`.
+ """
+
+ from_surface: Optional[bool] = None
+ """Whether the capture was taken from the surface rather than the view."""
+
+ optimize_for_speed: Optional[bool] = None
+ """Whether encoding favored speed over size."""
+
+ quality: Optional[int] = None
+ """Compression quality, 0 to 100, for lossy formats."""
+
+ session_id: Optional[str] = None
+ """CDP session identifier the command was addressed to.
+
+ Absent for browser-level commands. Clipped to 128 characters.
+ """
+
+
+class DataBrowserCdpPageCaptureSnapshotCommandData(BaseModel):
+ """Sanitized `Page.captureSnapshot` arguments.
+
+ Canonical input: `Page.captureSnapshot` in devtools-protocol@2d019e73, pinned at https://github.com/ChromeDevTools/devtools-protocol/blob/2d019e73eb371d1d6985d26d395d78bd8f8a22ba/json/browser_protocol.json. Every argument of this command has a retained or redacted decision in lib/devtoolsproxy/testdata/cdp_arguments.yaml.
+ """
+
+ method: Literal["Page.captureSnapshot"]
+
+ command_id: Optional[int] = None
+ """
+ The command's JSON-RPC id, so the command can be joined to the result the
+ browser returned for it. Absent when the client sent none.
+ """
+
+ connection_id: Optional[str] = None
+ """
+ Identifies the CDP proxy connection the command arrived on, matching
+ `cdp_connect` and `cdp_disconnect`. Two clients driving the same browser are
+ told apart by this.
+ """
+
+ format: Optional[Literal["mhtml", "other"]] = None
+ """Snapshot format requested (`mhtml`).
+
+ A value the protocol does not define is reported as `other`.
+ """
+
+ session_id: Optional[str] = None
+ """CDP session identifier the command was addressed to.
+
+ Absent for browser-level commands. Clipped to 128 characters.
+ """
+
+
+class DataBrowserCdpPageHandleJavaScriptDialogCommandData(BaseModel):
+ """Sanitized `Page.handleJavaScriptDialog` arguments.
+
+ Canonical input: `Page.handleJavaScriptDialog` in devtools-protocol@2d019e73, pinned at https://github.com/ChromeDevTools/devtools-protocol/blob/2d019e73eb371d1d6985d26d395d78bd8f8a22ba/json/browser_protocol.json. Every argument of this command has a retained or redacted decision in lib/devtoolsproxy/testdata/cdp_arguments.yaml.
+ """
+
+ accept: bool
+ """Whether the dialog was accepted or dismissed."""
+
+ method: Literal["Page.handleJavaScriptDialog"]
+
+ command_id: Optional[int] = None
+ """
+ The command's JSON-RPC id, so the command can be joined to the result the
+ browser returned for it. Absent when the client sent none.
+ """
+
+ connection_id: Optional[str] = None
+ """
+ Identifies the CDP proxy connection the command arrived on, matching
+ `cdp_connect` and `cdp_disconnect`. Two clients driving the same browser are
+ told apart by this.
+ """
+
+ prompt_text_length: Optional[int] = None
+ """Number of characters entered into a prompt dialog.
+
+ The text itself is never captured.
+ """
+
+ session_id: Optional[str] = None
+ """CDP session identifier the command was addressed to.
+
+ Absent for browser-level commands. Clipped to 128 characters.
+ """
+
+
+class DataBrowserCdpPageNavigateCommandData(BaseModel):
+ """Sanitized `Page.navigate` arguments.
+
+ Canonical input: `Page.navigate` in devtools-protocol@2d019e73, pinned at https://github.com/ChromeDevTools/devtools-protocol/blob/2d019e73eb371d1d6985d26d395d78bd8f8a22ba/json/browser_protocol.json. Every argument of this command has a retained or redacted decision in lib/devtoolsproxy/testdata/cdp_arguments.yaml.
+ """
+
+ method: Literal["Page.navigate"]
+
+ command_id: Optional[int] = None
+ """
+ The command's JSON-RPC id, so the command can be joined to the result the
+ browser returned for it. Absent when the client sent none.
+ """
+
+ connection_id: Optional[str] = None
+ """
+ Identifies the CDP proxy connection the command arrived on, matching
+ `cdp_connect` and `cdp_disconnect`. Two clients driving the same browser are
+ told apart by this.
+ """
+
+ frame_id: Optional[str] = None
+ """Opaque frame identifier.
+
+ Clipped to 128 characters; a longer value is not a real identifier.
+ """
+
+ referrer_policy: Optional[
+ Literal[
+ "noReferrer",
+ "noReferrerWhenDowngrade",
+ "origin",
+ "originWhenCrossOrigin",
+ "sameOrigin",
+ "strictOrigin",
+ "strictOriginWhenCrossOrigin",
+ "unsafeUrl",
+ "other",
+ ]
+ ] = None
+ """Referrer policy named by the command.
+
+ A value the protocol does not define is reported as `other`.
+ """
+
+ referrer_present: Optional[bool] = None
+ """Whether the command carried a referrer. The referrer itself is never captured."""
+
+ session_id: Optional[str] = None
+ """CDP session identifier the command was addressed to.
+
+ Absent for browser-level commands. Clipped to 128 characters.
+ """
+
+ transition_type: Optional[
+ Literal[
+ "link",
+ "typed",
+ "address_bar",
+ "auto_bookmark",
+ "auto_subframe",
+ "manual_subframe",
+ "generated",
+ "auto_toplevel",
+ "form_submit",
+ "reload",
+ "keyword",
+ "keyword_generated",
+ "other",
+ ]
+ ] = None
+ """Navigation reason reported by the caller (e.g.
+
+ `link`, `typed`, `reload`). A value the protocol does not define is reported as
+ `other`.
+ """
+
+ url_scheme: Optional[str] = None
+ """Scheme of the destination URL (e.g.
+
+ `https`, `about`, `data`). The rest of the URL is never captured.
+ """
+
+
+class DataBrowserCdpPageNavigateToHistoryEntryCommandData(BaseModel):
+ """Sanitized `Page.navigateToHistoryEntry` arguments.
+
+ Canonical input: `Page.navigateToHistoryEntry` in devtools-protocol@2d019e73, pinned at https://github.com/ChromeDevTools/devtools-protocol/blob/2d019e73eb371d1d6985d26d395d78bd8f8a22ba/json/browser_protocol.json. Every argument of this command has a retained or redacted decision in lib/devtoolsproxy/testdata/cdp_arguments.yaml.
+ """
+
+ entry_id: int
+ """History entry the command navigated to."""
+
+ method: Literal["Page.navigateToHistoryEntry"]
+
+ command_id: Optional[int] = None
+ """
+ The command's JSON-RPC id, so the command can be joined to the result the
+ browser returned for it. Absent when the client sent none.
+ """
+
+ connection_id: Optional[str] = None
+ """
+ Identifies the CDP proxy connection the command arrived on, matching
+ `cdp_connect` and `cdp_disconnect`. Two clients driving the same browser are
+ told apart by this.
+ """
+
+ session_id: Optional[str] = None
+ """CDP session identifier the command was addressed to.
+
+ Absent for browser-level commands. Clipped to 128 characters.
+ """
+
+
+class DataBrowserCdpPageReloadCommandData(BaseModel):
+ """Sanitized `Page.reload` arguments.
+
+ Canonical input: `Page.reload` in devtools-protocol@2d019e73, pinned at https://github.com/ChromeDevTools/devtools-protocol/blob/2d019e73eb371d1d6985d26d395d78bd8f8a22ba/json/browser_protocol.json. Every argument of this command has a retained or redacted decision in lib/devtoolsproxy/testdata/cdp_arguments.yaml.
+ """
+
+ method: Literal["Page.reload"]
+
+ command_id: Optional[int] = None
+ """
+ The command's JSON-RPC id, so the command can be joined to the result the
+ browser returned for it. Absent when the client sent none.
+ """
+
+ connection_id: Optional[str] = None
+ """
+ Identifies the CDP proxy connection the command arrived on, matching
+ `cdp_connect` and `cdp_disconnect`. Two clients driving the same browser are
+ told apart by this.
+ """
+
+ ignore_cache: Optional[bool] = None
+ """Whether the reload bypassed the cache."""
+
+ loader_id: Optional[str] = None
+ """Opaque document loader identifier.
+
+ Clipped to 128 characters; a longer value is not a real identifier.
+ """
+
+ script_length: Optional[int] = None
+ """Number of characters in the injected script."""
+
+ session_id: Optional[str] = None
+ """CDP session identifier the command was addressed to.
+
+ Absent for browser-level commands. Clipped to 128 characters.
+ """
+
+
+class DataBrowserCdpPagePrintToPdfCommandData(BaseModel):
+ """Sanitized `Page.printToPDF` arguments.
+
+ Canonical input: `Page.printToPDF` in devtools-protocol@2d019e73, pinned at https://github.com/ChromeDevTools/devtools-protocol/blob/2d019e73eb371d1d6985d26d395d78bd8f8a22ba/json/browser_protocol.json. Every argument of this command has a retained or redacted decision in lib/devtoolsproxy/testdata/cdp_arguments.yaml.
+ """
+
+ method: Literal["Page.printToPDF"]
+
+ command_id: Optional[int] = None
+ """
+ The command's JSON-RPC id, so the command can be joined to the result the
+ browser returned for it. Absent when the client sent none.
+ """
+
+ connection_id: Optional[str] = None
+ """
+ Identifies the CDP proxy connection the command arrived on, matching
+ `cdp_connect` and `cdp_disconnect`. Two clients driving the same browser are
+ told apart by this.
+ """
+
+ display_header_footer: Optional[bool] = None
+ """Whether a header and footer were rendered."""
+
+ footer_template_present: Optional[bool] = None
+ """Whether a footer template was supplied. The template itself is never captured."""
+
+ generate_document_outline: Optional[bool] = None
+ """Whether a document outline was embedded."""
+
+ generate_tagged_pdf: Optional[bool] = None
+ """Whether a tagged (accessible) PDF was requested."""
+
+ header_template_present: Optional[bool] = None
+ """Whether a header template was supplied. The template itself is never captured."""
+
+ landscape: Optional[bool] = None
+ """Whether the page was laid out in landscape."""
+
+ margin_bottom: Optional[float] = None
+ """Bottom margin in inches."""
+
+ margin_left: Optional[float] = None
+ """Left margin in inches."""
+
+ margin_right: Optional[float] = None
+ """Right margin in inches."""
+
+ margin_top: Optional[float] = None
+ """Top margin in inches."""
+
+ page_ranges_present: Optional[bool] = None
+ """Whether a page range was supplied."""
+
+ paper_height: Optional[float] = None
+ """Paper height in inches."""
+
+ paper_width: Optional[float] = None
+ """Paper width in inches."""
+
+ prefer_css_page_size: Optional[bool] = None
+ """Whether the CSS page size was preferred over the paper size."""
+
+ print_background: Optional[bool] = None
+ """Whether background graphics were printed."""
+
+ scale: Optional[float] = None
+ """Page render scale."""
+
+ session_id: Optional[str] = None
+ """CDP session identifier the command was addressed to.
+
+ Absent for browser-level commands. Clipped to 128 characters.
+ """
+
+ transfer_mode: Optional[Literal["ReturnAsBase64", "ReturnAsStream", "other"]] = None
+ """How the PDF was returned (`ReturnAsBase64` or `ReturnAsStream`).
+
+ A value the protocol does not define is reported as `other`.
+ """
+
+
+class DataBrowserCdpPageStartScreencastCommandData(BaseModel):
+ """Sanitized `Page.startScreencast` arguments.
+
+ Canonical input: `Page.startScreencast` in devtools-protocol@2d019e73, pinned at https://github.com/ChromeDevTools/devtools-protocol/blob/2d019e73eb371d1d6985d26d395d78bd8f8a22ba/json/browser_protocol.json. Every argument of this command has a retained or redacted decision in lib/devtoolsproxy/testdata/cdp_arguments.yaml.
+ """
+
+ method: Literal["Page.startScreencast"]
+
+ command_id: Optional[int] = None
+ """
+ The command's JSON-RPC id, so the command can be joined to the result the
+ browser returned for it. Absent when the client sent none.
+ """
+
+ connection_id: Optional[str] = None
+ """
+ Identifies the CDP proxy connection the command arrived on, matching
+ `cdp_connect` and `cdp_disconnect`. Two clients driving the same browser are
+ told apart by this.
+ """
+
+ every_nth_frame: Optional[int] = None
+ """Frame sampling interval."""
+
+ format: Optional[Literal["jpeg", "png", "other"]] = None
+ """Frame format requested (`jpeg` or `png`).
+
+ A value the protocol does not define is reported as `other`.
+ """
+
+ max_height: Optional[int] = None
+ """Maximum frame height in pixels."""
+
+ max_width: Optional[int] = None
+ """Maximum frame width in pixels."""
+
+ quality: Optional[int] = None
+ """Compression quality, 0 to 100."""
+
+ session_id: Optional[str] = None
+ """CDP session identifier the command was addressed to.
+
+ Absent for browser-level commands. Clipped to 128 characters.
+ """
+
+
+class DataBrowserCdpPageStopScreencastCommandData(BaseModel):
+ """Sanitized `Page.stopScreencast` arguments.
+
+ Canonical input: `Page.stopScreencast` in devtools-protocol@2d019e73, pinned at https://github.com/ChromeDevTools/devtools-protocol/blob/2d019e73eb371d1d6985d26d395d78bd8f8a22ba/json/browser_protocol.json. Every argument of this command has a retained or redacted decision in lib/devtoolsproxy/testdata/cdp_arguments.yaml.
+ """
+
+ method: Literal["Page.stopScreencast"]
+
+ command_id: Optional[int] = None
+ """
+ The command's JSON-RPC id, so the command can be joined to the result the
+ browser returned for it. Absent when the client sent none.
+ """
+
+ connection_id: Optional[str] = None
+ """
+ Identifies the CDP proxy connection the command arrived on, matching
+ `cdp_connect` and `cdp_disconnect`. Two clients driving the same browser are
+ told apart by this.
+ """
+
+ session_id: Optional[str] = None
+ """CDP session identifier the command was addressed to.
+
+ Absent for browser-level commands. Clipped to 128 characters.
+ """
+
+
+class DataBrowserCdpPageStopLoadingCommandData(BaseModel):
+ """Sanitized `Page.stopLoading` arguments.
+
+ Canonical input: `Page.stopLoading` in devtools-protocol@2d019e73, pinned at https://github.com/ChromeDevTools/devtools-protocol/blob/2d019e73eb371d1d6985d26d395d78bd8f8a22ba/json/browser_protocol.json. Every argument of this command has a retained or redacted decision in lib/devtoolsproxy/testdata/cdp_arguments.yaml.
+ """
+
+ method: Literal["Page.stopLoading"]
+
+ command_id: Optional[int] = None
+ """
+ The command's JSON-RPC id, so the command can be joined to the result the
+ browser returned for it. Absent when the client sent none.
+ """
+
+ connection_id: Optional[str] = None
+ """
+ Identifies the CDP proxy connection the command arrived on, matching
+ `cdp_connect` and `cdp_disconnect`. Two clients driving the same browser are
+ told apart by this.
+ """
+
+ session_id: Optional[str] = None
+ """CDP session identifier the command was addressed to.
+
+ Absent for browser-level commands. Clipped to 128 characters.
+ """
+
+
+class DataBrowserCdpPageCloseCommandData(BaseModel):
+ """Sanitized `Page.close` arguments.
+
+ Canonical input: `Page.close` in devtools-protocol@2d019e73, pinned at https://github.com/ChromeDevTools/devtools-protocol/blob/2d019e73eb371d1d6985d26d395d78bd8f8a22ba/json/browser_protocol.json. Every argument of this command has a retained or redacted decision in lib/devtoolsproxy/testdata/cdp_arguments.yaml.
+ """
+
+ method: Literal["Page.close"]
+
+ command_id: Optional[int] = None
+ """
+ The command's JSON-RPC id, so the command can be joined to the result the
+ browser returned for it. Absent when the client sent none.
+ """
+
+ connection_id: Optional[str] = None
+ """
+ Identifies the CDP proxy connection the command arrived on, matching
+ `cdp_connect` and `cdp_disconnect`. Two clients driving the same browser are
+ told apart by this.
+ """
+
+ session_id: Optional[str] = None
+ """CDP session identifier the command was addressed to.
+
+ Absent for browser-level commands. Clipped to 128 characters.
+ """
+
+
+class DataBrowserCdpPageSetWebLifecycleStateCommandData(BaseModel):
+ """Sanitized `Page.setWebLifecycleState` arguments.
+
+ Canonical input: `Page.setWebLifecycleState` in devtools-protocol@2d019e73, pinned at https://github.com/ChromeDevTools/devtools-protocol/blob/2d019e73eb371d1d6985d26d395d78bd8f8a22ba/json/browser_protocol.json. Every argument of this command has a retained or redacted decision in lib/devtoolsproxy/testdata/cdp_arguments.yaml.
+ """
+
+ method: Literal["Page.setWebLifecycleState"]
+
+ state: Literal["frozen", "active", "other"]
+ """Lifecycle state applied (`frozen` or `active`).
+
+ A value the protocol does not define is reported as `other`.
+ """
+
+ command_id: Optional[int] = None
+ """
+ The command's JSON-RPC id, so the command can be joined to the result the
+ browser returned for it. Absent when the client sent none.
+ """
+
+ connection_id: Optional[str] = None
+ """
+ Identifies the CDP proxy connection the command arrived on, matching
+ `cdp_connect` and `cdp_disconnect`. Two clients driving the same browser are
+ told apart by this.
+ """
+
+ session_id: Optional[str] = None
+ """CDP session identifier the command was addressed to.
+
+ Absent for browser-level commands. Clipped to 128 characters.
+ """
+
+
+class DataBrowserCdpTargetActivateTargetCommandData(BaseModel):
+ """Sanitized `Target.activateTarget` arguments.
+
+ Canonical input: `Target.activateTarget` in devtools-protocol@2d019e73, pinned at https://github.com/ChromeDevTools/devtools-protocol/blob/2d019e73eb371d1d6985d26d395d78bd8f8a22ba/json/browser_protocol.json. Every argument of this command has a retained or redacted decision in lib/devtoolsproxy/testdata/cdp_arguments.yaml.
+ """
+
+ method: Literal["Target.activateTarget"]
+
+ target_id: str
+ """Opaque target identifier.
+
+ Clipped to 128 characters; a longer value is not a real identifier.
+ """
+
+ command_id: Optional[int] = None
+ """
+ The command's JSON-RPC id, so the command can be joined to the result the
+ browser returned for it. Absent when the client sent none.
+ """
+
+ connection_id: Optional[str] = None
+ """
+ Identifies the CDP proxy connection the command arrived on, matching
+ `cdp_connect` and `cdp_disconnect`. Two clients driving the same browser are
+ told apart by this.
+ """
+
+ session_id: Optional[str] = None
+ """CDP session identifier the command was addressed to.
+
+ Absent for browser-level commands. Clipped to 128 characters.
+ """
+
+
+class DataBrowserCdpTargetCloseTargetCommandData(BaseModel):
+ """Sanitized `Target.closeTarget` arguments.
+
+ Canonical input: `Target.closeTarget` in devtools-protocol@2d019e73, pinned at https://github.com/ChromeDevTools/devtools-protocol/blob/2d019e73eb371d1d6985d26d395d78bd8f8a22ba/json/browser_protocol.json. Every argument of this command has a retained or redacted decision in lib/devtoolsproxy/testdata/cdp_arguments.yaml.
+ """
+
+ method: Literal["Target.closeTarget"]
+
+ target_id: str
+ """Opaque target identifier.
+
+ Clipped to 128 characters; a longer value is not a real identifier.
+ """
+
+ command_id: Optional[int] = None
+ """
+ The command's JSON-RPC id, so the command can be joined to the result the
+ browser returned for it. Absent when the client sent none.
+ """
+
+ connection_id: Optional[str] = None
+ """
+ Identifies the CDP proxy connection the command arrived on, matching
+ `cdp_connect` and `cdp_disconnect`. Two clients driving the same browser are
+ told apart by this.
+ """
+
+ session_id: Optional[str] = None
+ """CDP session identifier the command was addressed to.
+
+ Absent for browser-level commands. Clipped to 128 characters.
+ """
+
+
+class DataBrowserCdpTargetCreateTargetCommandData(BaseModel):
+ """Sanitized `Target.createTarget` arguments.
+
+ Canonical input: `Target.createTarget` in devtools-protocol@2d019e73, pinned at https://github.com/ChromeDevTools/devtools-protocol/blob/2d019e73eb371d1d6985d26d395d78bd8f8a22ba/json/browser_protocol.json. Every argument of this command has a retained or redacted decision in lib/devtoolsproxy/testdata/cdp_arguments.yaml.
+ """
+
+ method: Literal["Target.createTarget"]
+
+ background: Optional[bool] = None
+ """Whether the target was created in the background."""
+
+ browser_context_id: Optional[str] = None
+ """Opaque browser context identifier.
+
+ Clipped to 128 characters; a longer value is not a real identifier.
+ """
+
+ command_id: Optional[int] = None
+ """
+ The command's JSON-RPC id, so the command can be joined to the result the
+ browser returned for it. Absent when the client sent none.
+ """
+
+ connection_id: Optional[str] = None
+ """
+ Identifies the CDP proxy connection the command arrived on, matching
+ `cdp_connect` and `cdp_disconnect`. Two clients driving the same browser are
+ told apart by this.
+ """
+
+ enable_begin_frame_control: Optional[bool] = None
+ """Whether BeginFrame control was enabled (headless only)."""
+
+ focus: Optional[bool] = None
+ """Whether the new target was focused."""
+
+ for_tab: Optional[bool] = None
+ """Whether a tab target rather than a page target was created."""
+
+ height: Optional[int] = None
+ """Window height in DIP."""
+
+ hidden: Optional[bool] = None
+ """Whether the target was created hidden."""
+
+ left: Optional[int] = None
+ """Window x position in screen coordinates."""
+
+ new_window: Optional[bool] = None
+ """Whether a new window was requested."""
+
+ session_id: Optional[str] = None
+ """CDP session identifier the command was addressed to.
+
+ Absent for browser-level commands. Clipped to 128 characters.
+ """
+
+ top: Optional[int] = None
+ """Window y position in screen coordinates."""
+
+ url_scheme: Optional[str] = None
+ """Scheme of the destination URL (e.g.
+
+ `https`, `about`, `data`). The rest of the URL is never captured.
+ """
+
+ width: Optional[int] = None
+ """Window width in DIP."""
+
+ window_state: Optional[Literal["normal", "minimized", "maximized", "fullscreen", "other"]] = None
+ """Window state requested (`normal`, `minimized`, `maximized`, `fullscreen`).
+
+ A value the protocol does not define is reported as `other`.
+ """
+
+
+class DataBrowserCdpTargetCreateBrowserContextCommandData(BaseModel):
+ """Sanitized `Target.createBrowserContext` arguments.
+
+ Canonical input: `Target.createBrowserContext` in devtools-protocol@2d019e73, pinned at https://github.com/ChromeDevTools/devtools-protocol/blob/2d019e73eb371d1d6985d26d395d78bd8f8a22ba/json/browser_protocol.json. Every argument of this command has a retained or redacted decision in lib/devtoolsproxy/testdata/cdp_arguments.yaml.
+ """
+
+ method: Literal["Target.createBrowserContext"]
+
+ command_id: Optional[int] = None
+ """
+ The command's JSON-RPC id, so the command can be joined to the result the
+ browser returned for it. Absent when the client sent none.
+ """
+
+ connection_id: Optional[str] = None
+ """
+ Identifies the CDP proxy connection the command arrived on, matching
+ `cdp_connect` and `cdp_disconnect`. Two clients driving the same browser are
+ told apart by this.
+ """
+
+ dispose_on_detach: Optional[bool] = None
+ """Whether the context is disposed when the debugging session detaches."""
+
+ proxy_bypass_list_present: Optional[bool] = None
+ """Whether a proxy bypass list was configured."""
+
+ proxy_server_present: Optional[bool] = None
+ """Whether a proxy was configured. The proxy address is never captured."""
+
+ session_id: Optional[str] = None
+ """CDP session identifier the command was addressed to.
+
+ Absent for browser-level commands. Clipped to 128 characters.
+ """
+
+ universal_network_access_origin_count: Optional[int] = None
+ """Number of origins granted universal network access.
+
+ The origins themselves are never captured.
+ """
+
+
+class DataBrowserCdpTargetDisposeBrowserContextCommandData(BaseModel):
+ """Sanitized `Target.disposeBrowserContext` arguments.
+
+ Canonical input: `Target.disposeBrowserContext` in devtools-protocol@2d019e73, pinned at https://github.com/ChromeDevTools/devtools-protocol/blob/2d019e73eb371d1d6985d26d395d78bd8f8a22ba/json/browser_protocol.json. Every argument of this command has a retained or redacted decision in lib/devtoolsproxy/testdata/cdp_arguments.yaml.
+ """
+
+ browser_context_id: str
+ """Opaque browser context identifier.
+
+ Clipped to 128 characters; a longer value is not a real identifier.
+ """
+
+ method: Literal["Target.disposeBrowserContext"]
+
+ command_id: Optional[int] = None
+ """
+ The command's JSON-RPC id, so the command can be joined to the result the
+ browser returned for it. Absent when the client sent none.
+ """
+
+ connection_id: Optional[str] = None
+ """
+ Identifies the CDP proxy connection the command arrived on, matching
+ `cdp_connect` and `cdp_disconnect`. Two clients driving the same browser are
+ told apart by this.
+ """
+
+ session_id: Optional[str] = None
+ """CDP session identifier the command was addressed to.
+
+ Absent for browser-level commands. Clipped to 128 characters.
+ """
+
+
+class DataBrowserCdpTargetOpenDevToolsCommandData(BaseModel):
+ """Sanitized `Target.openDevTools` arguments.
+
+ Canonical input: `Target.openDevTools` in devtools-protocol@2d019e73, pinned at https://github.com/ChromeDevTools/devtools-protocol/blob/2d019e73eb371d1d6985d26d395d78bd8f8a22ba/json/browser_protocol.json. Every argument of this command has a retained or redacted decision in lib/devtoolsproxy/testdata/cdp_arguments.yaml.
+ """
+
+ method: Literal["Target.openDevTools"]
+
+ target_id: str
+ """Opaque target identifier.
+
+ Clipped to 128 characters; a longer value is not a real identifier.
+ """
+
+ command_id: Optional[int] = None
+ """
+ The command's JSON-RPC id, so the command can be joined to the result the
+ browser returned for it. Absent when the client sent none.
+ """
+
+ connection_id: Optional[str] = None
+ """
+ Identifies the CDP proxy connection the command arrived on, matching
+ `cdp_connect` and `cdp_disconnect`. Two clients driving the same browser are
+ told apart by this.
+ """
+
+ panel_id: Optional[str] = None
+ """DevTools panel opened.
+
+ Clipped to 128 characters; a longer value is not a real identifier.
+ """
+
+ session_id: Optional[str] = None
+ """CDP session identifier the command was addressed to.
+
+ Absent for browser-level commands. Clipped to 128 characters.
+ """
+
+
+class DataBrowserCdpBrowserCancelDownloadCommandData(BaseModel):
+ """Sanitized `Browser.cancelDownload` arguments.
+
+ Canonical input: `Browser.cancelDownload` in devtools-protocol@2d019e73, pinned at https://github.com/ChromeDevTools/devtools-protocol/blob/2d019e73eb371d1d6985d26d395d78bd8f8a22ba/json/browser_protocol.json. Every argument of this command has a retained or redacted decision in lib/devtoolsproxy/testdata/cdp_arguments.yaml.
+ """
+
+ download_guid: str
+ """Opaque identifier of the download that was cancelled.
+
+ Clipped to 128 characters; a longer value is not a real identifier.
+ """
+
+ method: Literal["Browser.cancelDownload"]
+
+ browser_context_id: Optional[str] = None
+ """Opaque browser context identifier.
+
+ Clipped to 128 characters; a longer value is not a real identifier.
+ """
+
+ command_id: Optional[int] = None
+ """
+ The command's JSON-RPC id, so the command can be joined to the result the
+ browser returned for it. Absent when the client sent none.
+ """
+
+ connection_id: Optional[str] = None
+ """
+ Identifies the CDP proxy connection the command arrived on, matching
+ `cdp_connect` and `cdp_disconnect`. Two clients driving the same browser are
+ told apart by this.
+ """
+
+ session_id: Optional[str] = None
+ """CDP session identifier the command was addressed to.
+
+ Absent for browser-level commands. Clipped to 128 characters.
+ """
+
+
+class DataBrowserCdpBrowserCloseCommandData(BaseModel):
+ """Sanitized `Browser.close` arguments.
+
+ Canonical input: `Browser.close` in devtools-protocol@2d019e73, pinned at https://github.com/ChromeDevTools/devtools-protocol/blob/2d019e73eb371d1d6985d26d395d78bd8f8a22ba/json/browser_protocol.json. Every argument of this command has a retained or redacted decision in lib/devtoolsproxy/testdata/cdp_arguments.yaml.
+ """
+
+ method: Literal["Browser.close"]
+
+ command_id: Optional[int] = None
+ """
+ The command's JSON-RPC id, so the command can be joined to the result the
+ browser returned for it. Absent when the client sent none.
+ """
+
+ connection_id: Optional[str] = None
+ """
+ Identifies the CDP proxy connection the command arrived on, matching
+ `cdp_connect` and `cdp_disconnect`. Two clients driving the same browser are
+ told apart by this.
+ """
+
+ session_id: Optional[str] = None
+ """CDP session identifier the command was addressed to.
+
+ Absent for browser-level commands. Clipped to 128 characters.
+ """
+
+
+class DataBrowserCdpBrowserSetWindowBoundsCommandData(BaseModel):
+ """Sanitized `Browser.setWindowBounds` arguments.
+
+ Canonical input: `Browser.setWindowBounds` in devtools-protocol@2d019e73, pinned at https://github.com/ChromeDevTools/devtools-protocol/blob/2d019e73eb371d1d6985d26d395d78bd8f8a22ba/json/browser_protocol.json. Every argument of this command has a retained or redacted decision in lib/devtoolsproxy/testdata/cdp_arguments.yaml.
+ """
+
+ method: Literal["Browser.setWindowBounds"]
+
+ window_id: int
+ """Browser window identifier."""
+
+ command_id: Optional[int] = None
+ """
+ The command's JSON-RPC id, so the command can be joined to the result the
+ browser returned for it. Absent when the client sent none.
+ """
+
+ connection_id: Optional[str] = None
+ """
+ Identifies the CDP proxy connection the command arrived on, matching
+ `cdp_connect` and `cdp_disconnect`. Two clients driving the same browser are
+ told apart by this.
+ """
+
+ height: Optional[int] = None
+ """Window height in DIP."""
+
+ left: Optional[int] = None
+ """Window x position in screen coordinates."""
+
+ session_id: Optional[str] = None
+ """CDP session identifier the command was addressed to.
+
+ Absent for browser-level commands. Clipped to 128 characters.
+ """
+
+ top: Optional[int] = None
+ """Window y position in screen coordinates."""
+
+ width: Optional[int] = None
+ """Window width in DIP."""
+
+ window_state: Optional[Literal["normal", "minimized", "maximized", "fullscreen", "other"]] = None
+ """Window state requested (`normal`, `minimized`, `maximized`, `fullscreen`).
+
+ A value the protocol does not define is reported as `other`.
+ """
+
+
+class DataBrowserCdpBrowserSetContentsSizeCommandData(BaseModel):
+ """Sanitized `Browser.setContentsSize` arguments.
+
+ Canonical input: `Browser.setContentsSize` in devtools-protocol@2d019e73, pinned at https://github.com/ChromeDevTools/devtools-protocol/blob/2d019e73eb371d1d6985d26d395d78bd8f8a22ba/json/browser_protocol.json. Every argument of this command has a retained or redacted decision in lib/devtoolsproxy/testdata/cdp_arguments.yaml.
+ """
+
+ method: Literal["Browser.setContentsSize"]
+
+ window_id: int
+ """Browser window identifier."""
+
+ command_id: Optional[int] = None
+ """
+ The command's JSON-RPC id, so the command can be joined to the result the
+ browser returned for it. Absent when the client sent none.
+ """
+
+ connection_id: Optional[str] = None
+ """
+ Identifies the CDP proxy connection the command arrived on, matching
+ `cdp_connect` and `cdp_disconnect`. Two clients driving the same browser are
+ told apart by this.
+ """
+
+ height: Optional[int] = None
+ """Contents height in DIP."""
+
+ session_id: Optional[str] = None
+ """CDP session identifier the command was addressed to.
+
+ Absent for browser-level commands. Clipped to 128 characters.
+ """
+
+ width: Optional[int] = None
+ """Contents width in DIP."""
+
+
+class DataBrowserCdpAutofillTriggerCommandData(BaseModel):
+ """Sanitized `Autofill.trigger` arguments.
+
+ Canonical input: `Autofill.trigger` in devtools-protocol@2d019e73, pinned at https://github.com/ChromeDevTools/devtools-protocol/blob/2d019e73eb371d1d6985d26d395d78bd8f8a22ba/json/browser_protocol.json. Every argument of this command has a retained or redacted decision in lib/devtoolsproxy/testdata/cdp_arguments.yaml.
+ """
+
+ field_id: int
+ """Opaque backend node identifier of the field that was autofilled."""
+
+ method: Literal["Autofill.trigger"]
+
+ address_field_count: Optional[int] = None
+ """Number of address fields the command filled.
+
+ Their names and values are never captured.
+ """
+
+ command_id: Optional[int] = None
+ """
+ The command's JSON-RPC id, so the command can be joined to the result the
+ browser returned for it. Absent when the client sent none.
+ """
+
+ connection_id: Optional[str] = None
+ """
+ Identifies the CDP proxy connection the command arrived on, matching
+ `cdp_connect` and `cdp_disconnect`. Two clients driving the same browser are
+ told apart by this.
+ """
+
+ frame_id: Optional[str] = None
+ """Opaque frame identifier.
+
+ Clipped to 128 characters; a longer value is not a real identifier.
+ """
+
+ mode: Optional[Literal["card", "address"]] = None
+ """What was filled: `card` or `address`. The values themselves are never captured."""
+
+ session_id: Optional[str] = None
+ """CDP session identifier the command was addressed to.
+
+ Absent for browser-level commands. Clipped to 128 characters.
+ """
+
+
+Data: TypeAlias = Annotated[
+ Union[
+ DataBrowserCdpInputDispatchMouseEventCommandData,
+ DataBrowserCdpInputDispatchKeyEventCommandData,
+ DataBrowserCdpInputInsertTextCommandData,
+ DataBrowserCdpInputImeSetCompositionCommandData,
+ DataBrowserCdpInputDispatchTouchEventCommandData,
+ DataBrowserCdpInputDispatchDragEventCommandData,
+ DataBrowserCdpInputCancelDraggingCommandData,
+ DataBrowserCdpInputEmulateTouchFromMouseEventCommandData,
+ DataBrowserCdpInputSynthesizePinchGestureCommandData,
+ DataBrowserCdpInputSynthesizeScrollGestureCommandData,
+ DataBrowserCdpInputSynthesizeTapGestureCommandData,
+ DataBrowserCdpDomSetFileInputFilesCommandData,
+ DataBrowserCdpDomFocusCommandData,
+ DataBrowserCdpDomScrollIntoViewIfNeededCommandData,
+ DataBrowserCdpPageBringToFrontCommandData,
+ DataBrowserCdpPageCaptureScreenshotCommandData,
+ DataBrowserCdpPageCaptureSnapshotCommandData,
+ DataBrowserCdpPageHandleJavaScriptDialogCommandData,
+ DataBrowserCdpPageNavigateCommandData,
+ DataBrowserCdpPageNavigateToHistoryEntryCommandData,
+ DataBrowserCdpPageReloadCommandData,
+ DataBrowserCdpPagePrintToPdfCommandData,
+ DataBrowserCdpPageStartScreencastCommandData,
+ DataBrowserCdpPageStopScreencastCommandData,
+ DataBrowserCdpPageStopLoadingCommandData,
+ DataBrowserCdpPageCloseCommandData,
+ DataBrowserCdpPageSetWebLifecycleStateCommandData,
+ DataBrowserCdpTargetActivateTargetCommandData,
+ DataBrowserCdpTargetCloseTargetCommandData,
+ DataBrowserCdpTargetCreateTargetCommandData,
+ DataBrowserCdpTargetCreateBrowserContextCommandData,
+ DataBrowserCdpTargetDisposeBrowserContextCommandData,
+ DataBrowserCdpTargetOpenDevToolsCommandData,
+ DataBrowserCdpBrowserCancelDownloadCommandData,
+ DataBrowserCdpBrowserCloseCommandData,
+ DataBrowserCdpBrowserSetWindowBoundsCommandData,
+ DataBrowserCdpBrowserSetContentsSizeCommandData,
+ DataBrowserCdpAutofillTriggerCommandData,
+ ],
+ PropertyInfo(discriminator="method"),
+]
+
+
+class BrowserCdpCommandEvent(BaseModel):
+ """
+ A browser-control command a client sent over the CDP WebSocket proxy: input gestures, navigation, dialog handling, file selection and screenshots. Configuration commands and the DOM/Runtime traffic a client library issues on the caller's behalf are not reported.
+ One event per browser-control command that reached the browser. The command stream is not sampled, coalesced or reordered. An event is lost only when the method is excluded by telemetry configuration, when the command's arguments do not decode, or when classification cannot keep up. Exclusions are counted in `cdp_disconnect.telemetry_excluded`; the rest in `cdp_disconnect.telemetry_dropped`.
+ """
+
+ category: Literal["control"]
+
+ data: Data
+ """Per-command payload for `cdp_command` events, discriminated by `method`.
+
+ Each variant carries only the arguments approved for that command: values that
+ could hold a secret — typed and composition text, URLs, referrers, scripts,
+ templates, file paths, drag contents and autofill values — are replaced by a
+ length, a count, a presence flag, an enum or a URL scheme and host.
+ """
+
+ source: BrowserEventSource
+ """Provenance metadata identifying which producer emitted the event."""
+
+ ts: int
+ """Event timestamp in Unix microseconds."""
+
+ type: Literal["cdp_command"]
+
+ truncated: Optional[bool] = None
+ """True if the data field was truncated due to size limits."""
diff --git a/src/kernel/types/browsers/browser_cdp_command_method.py b/src/kernel/types/browsers/browser_cdp_command_method.py
new file mode 100644
index 00000000..18ccbb96
--- /dev/null
+++ b/src/kernel/types/browsers/browser_cdp_command_method.py
@@ -0,0 +1,46 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from typing_extensions import Literal, TypeAlias
+
+__all__ = ["BrowserCdpCommandMethod"]
+
+BrowserCdpCommandMethod: TypeAlias = Literal[
+ "Input.dispatchMouseEvent",
+ "Input.dispatchKeyEvent",
+ "Input.insertText",
+ "Input.imeSetComposition",
+ "Input.dispatchTouchEvent",
+ "Input.dispatchDragEvent",
+ "Input.cancelDragging",
+ "Input.emulateTouchFromMouseEvent",
+ "Input.synthesizePinchGesture",
+ "Input.synthesizeScrollGesture",
+ "Input.synthesizeTapGesture",
+ "DOM.setFileInputFiles",
+ "DOM.focus",
+ "DOM.scrollIntoViewIfNeeded",
+ "Page.bringToFront",
+ "Page.captureScreenshot",
+ "Page.captureSnapshot",
+ "Page.handleJavaScriptDialog",
+ "Page.navigate",
+ "Page.navigateToHistoryEntry",
+ "Page.reload",
+ "Page.printToPDF",
+ "Page.startScreencast",
+ "Page.stopScreencast",
+ "Page.stopLoading",
+ "Page.close",
+ "Page.setWebLifecycleState",
+ "Target.activateTarget",
+ "Target.closeTarget",
+ "Target.createTarget",
+ "Target.createBrowserContext",
+ "Target.disposeBrowserContext",
+ "Target.openDevTools",
+ "Browser.cancelDownload",
+ "Browser.close",
+ "Browser.setWindowBounds",
+ "Browser.setContentsSize",
+ "Autofill.trigger",
+]
diff --git a/src/kernel/types/browsers/browser_cdp_connect_event.py b/src/kernel/types/browsers/browser_cdp_connect_event.py
index ebbc7fa3..042ab131 100644
--- a/src/kernel/types/browsers/browser_cdp_connect_event.py
+++ b/src/kernel/types/browsers/browser_cdp_connect_event.py
@@ -6,7 +6,16 @@
from ..._models import BaseModel
from .browser_event_source import BrowserEventSource
-__all__ = ["BrowserCdpConnectEvent"]
+__all__ = ["BrowserCdpConnectEvent", "Data"]
+
+
+class Data(BaseModel):
+ connection_id: Optional[str] = None
+ """
+ Identifies this CDP proxy connection, matching the connection_id on the
+ cdp_command events that arrived on it. Two clients driving the same browser are
+ told apart by this.
+ """
class BrowserCdpConnectEvent(BaseModel):
@@ -25,5 +34,7 @@ class BrowserCdpConnectEvent(BaseModel):
type: Literal["cdp_connect"]
+ data: Optional[Data] = None
+
truncated: Optional[bool] = None
"""True if the data field was truncated due to size limits."""
diff --git a/src/kernel/types/browsers/browser_cdp_disconnect_event.py b/src/kernel/types/browsers/browser_cdp_disconnect_event.py
index d9260ebc..5ec69172 100644
--- a/src/kernel/types/browsers/browser_cdp_disconnect_event.py
+++ b/src/kernel/types/browsers/browser_cdp_disconnect_event.py
@@ -26,6 +26,30 @@ class Data(BaseModel):
shutdown).
"""
+ connection_id: Optional[str] = None
+ """
+ Identifies this CDP proxy connection, matching the connection_id on the
+ cdp_command events that arrived on it. Two clients driving the same browser are
+ told apart by this.
+ """
+
+ telemetry_dropped: Optional[int] = None
+ """
+ Number of forwarded client frames the classifier never saw, because it could not
+ keep up or because classification failed. An upper bound on lost commands rather
+ than a count: a saturated queue turns away whatever arrives next, which may be
+ library traffic that would have produced no event. Telemetry loss only; every
+ command was still relayed to the browser. Absent on events from a browser image
+ predating the field, which is not the same as zero.
+ """
+
+ telemetry_excluded: Optional[int] = None
+ """
+ Number of forwarded client commands that produced no cdp_command event because
+ their method is listed in control.cdp.excluded_methods. Configuration rather
+ than loss, so it is counted apart from telemetry_dropped.
+ """
+
class BrowserCdpDisconnectEvent(BaseModel):
"""An external client disconnected from the CDP WebSocket proxy on this VM.
diff --git a/src/kernel/types/browsers/browser_page_crashed_event.py b/src/kernel/types/browsers/browser_page_crashed_event.py
new file mode 100644
index 00000000..46e84f32
--- /dev/null
+++ b/src/kernel/types/browsers/browser_page_crashed_event.py
@@ -0,0 +1,41 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from typing import Optional
+from typing_extensions import Literal
+
+from ..._models import BaseModel
+from .browser_event_source import BrowserEventSource
+
+__all__ = ["BrowserPageCrashedEvent", "Data"]
+
+
+class Data(BaseModel):
+ target_id: str
+ """CDP target identifier of the crashed page."""
+
+ target_type: Literal["page", "background_page", "service_worker", "shared_worker", "other"]
+ """CDP target type of the page that produced the event."""
+
+ url: str
+ """URL the page was on when its renderer process crashed."""
+
+
+class BrowserPageCrashedEvent(BaseModel):
+ """
+ A page's renderer process crashed (an "Aw, Snap!" failure) while the browser process itself stayed alive. Reported on the crashed page's session, with the session and target ids on `source.metadata`. Captured only while the `page` category is enabled.
+ """
+
+ category: Literal["page"]
+
+ source: BrowserEventSource
+ """Provenance metadata identifying which producer emitted the event."""
+
+ ts: int
+ """Event timestamp in Unix microseconds."""
+
+ type: Literal["page_crashed"]
+
+ data: Optional[Data] = None
+
+ truncated: Optional[bool] = None
+ """True if the data field was truncated due to size limits."""
diff --git a/src/kernel/types/browsers/browser_platform_api_call_event.py b/src/kernel/types/browsers/browser_platform_api_call_event.py
new file mode 100644
index 00000000..f6e78b3b
--- /dev/null
+++ b/src/kernel/types/browsers/browser_platform_api_call_event.py
@@ -0,0 +1,47 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from typing import Optional
+from typing_extensions import Literal
+
+from ..._models import BaseModel
+from .browser_event_source import BrowserEventSource
+
+__all__ = ["BrowserPlatformAPICallEvent", "Data"]
+
+
+class Data(BaseModel):
+ duration_ms: float
+ """Wall-clock duration of the handler in milliseconds."""
+
+ operation_id: str
+ """Matched route's operation, named as the in-VM API names its handler (e.g.
+
+ ProcessExec, StartRecording).
+ """
+
+ request_id: str
+ """Per-request identifier from the in-VM API request middleware."""
+
+ status: int
+ """HTTP response status code."""
+
+
+class BrowserPlatformAPICallEvent(BaseModel):
+ """
+ An HTTP call that manages the browser VM rather than driving the browser, handled by the in-VM API server — recording lifecycle, filesystem and process management, telemetry and browser configuration. Mostly platform-induced (e.g. profile save, replay capture) rather than agent actions.
+ """
+
+ category: Literal["platform"]
+
+ source: BrowserEventSource
+ """Provenance metadata identifying which producer emitted the event."""
+
+ ts: int
+ """Event timestamp in Unix microseconds."""
+
+ type: Literal["platform_api_call"]
+
+ data: Optional[Data] = None
+
+ truncated: Optional[bool] = None
+ """True if the data field was truncated due to size limits."""
diff --git a/src/kernel/types/browsers/browser_telemetry_categories_config.py b/src/kernel/types/browsers/browser_telemetry_categories_config.py
index b89ff17b..8b1f811a 100644
--- a/src/kernel/types/browsers/browser_telemetry_categories_config.py
+++ b/src/kernel/types/browsers/browser_telemetry_categories_config.py
@@ -3,6 +3,7 @@
from typing import Optional
from ..._models import BaseModel
+from .browser_telemetry_control_config import BrowserTelemetryControlConfig
from .browser_telemetry_category_config import BrowserTelemetryCategoryConfig
__all__ = ["BrowserTelemetryCategoriesConfig"]
@@ -11,7 +12,7 @@
class BrowserTelemetryCategoriesConfig(BaseModel):
"""Per-category telemetry capture settings layered onto the default set.
- The operational signals (control, connection, system, captcha) are on by default and are opt-out: set one to enabled=false to stop capturing it. The CDP categories (console, network, page, interaction) and screenshot are off by default and are opt-in: set enabled=true to capture them.
+ The operational signals (control, connection, system, captcha) are on by default and are opt-out: set one to enabled=false to stop capturing it. The CDP categories (console, network, page, interaction), screenshot and platform are off by default and are opt-in: set enabled=true to capture them.
"""
captcha: Optional[BrowserTelemetryCategoryConfig] = None
@@ -26,11 +27,11 @@ class BrowserTelemetryCategoriesConfig(BaseModel):
CDP category; off by default.
"""
- control: Optional[BrowserTelemetryCategoryConfig] = None
- """Agent-driven actions against the browser, such as inbound calls to the in-VM
- API.
-
- On by default.
+ control: Optional[BrowserTelemetryControlConfig] = None
+ """
+ Agent-driven actions against the browser — computer-control calls, Playwright
+ code execution, screenshots, clipboard access, and browser-control commands sent
+ over the CDP proxy. On by default.
"""
interaction: Optional[BrowserTelemetryCategoryConfig] = None
@@ -54,6 +55,13 @@ class BrowserTelemetryCategoriesConfig(BaseModel):
shifts, and LCP. CDP category; off by default.
"""
+ platform: Optional[BrowserTelemetryCategoryConfig] = None
+ """
+ In-VM API calls that manage the browser VM rather than drive the browser
+ (recording, filesystem, process, telemetry and browser configuration). Mostly
+ platform-induced; off by default and must be opted into.
+ """
+
screenshot: Optional[BrowserTelemetryCategoryConfig] = None
"""Periodic base64-encoded viewport screenshots.
diff --git a/src/kernel/types/browsers/browser_telemetry_categories_config_param.py b/src/kernel/types/browsers/browser_telemetry_categories_config_param.py
index add32385..e707230f 100644
--- a/src/kernel/types/browsers/browser_telemetry_categories_config_param.py
+++ b/src/kernel/types/browsers/browser_telemetry_categories_config_param.py
@@ -4,6 +4,7 @@
from typing_extensions import TypedDict
+from .browser_telemetry_control_config_param import BrowserTelemetryControlConfigParam
from .browser_telemetry_category_config_param import BrowserTelemetryCategoryConfigParam
__all__ = ["BrowserTelemetryCategoriesConfigParam"]
@@ -12,7 +13,7 @@
class BrowserTelemetryCategoriesConfigParam(TypedDict, total=False):
"""Per-category telemetry capture settings layered onto the default set.
- The operational signals (control, connection, system, captcha) are on by default and are opt-out: set one to enabled=false to stop capturing it. The CDP categories (console, network, page, interaction) and screenshot are off by default and are opt-in: set enabled=true to capture them.
+ The operational signals (control, connection, system, captcha) are on by default and are opt-out: set one to enabled=false to stop capturing it. The CDP categories (console, network, page, interaction), screenshot and platform are off by default and are opt-in: set enabled=true to capture them.
"""
captcha: BrowserTelemetryCategoryConfigParam
@@ -27,11 +28,11 @@ class BrowserTelemetryCategoriesConfigParam(TypedDict, total=False):
CDP category; off by default.
"""
- control: BrowserTelemetryCategoryConfigParam
- """Agent-driven actions against the browser, such as inbound calls to the in-VM
- API.
-
- On by default.
+ control: BrowserTelemetryControlConfigParam
+ """
+ Agent-driven actions against the browser — computer-control calls, Playwright
+ code execution, screenshots, clipboard access, and browser-control commands sent
+ over the CDP proxy. On by default.
"""
interaction: BrowserTelemetryCategoryConfigParam
@@ -55,6 +56,13 @@ class BrowserTelemetryCategoriesConfigParam(TypedDict, total=False):
shifts, and LCP. CDP category; off by default.
"""
+ platform: BrowserTelemetryCategoryConfigParam
+ """
+ In-VM API calls that manage the browser VM rather than drive the browser
+ (recording, filesystem, process, telemetry and browser configuration). Mostly
+ platform-induced; off by default and must be opted into.
+ """
+
screenshot: BrowserTelemetryCategoryConfigParam
"""Periodic base64-encoded viewport screenshots.
diff --git a/src/kernel/types/browsers/browser_telemetry_category_config.py b/src/kernel/types/browsers/browser_telemetry_category_config.py
index f63c02f4..1a64f7d6 100644
--- a/src/kernel/types/browsers/browser_telemetry_category_config.py
+++ b/src/kernel/types/browsers/browser_telemetry_category_config.py
@@ -14,6 +14,6 @@ class BrowserTelemetryCategoryConfig(BaseModel):
"""Whether this category is captured.
Operational categories (control, connection, system, captcha) default to true;
- set false to opt out. CDP categories (console, network, page, interaction) and
- screenshot default to false; set true to opt in.
+ set false to opt out. CDP categories (console, network, page, interaction),
+ screenshot and platform default to false; set true to opt in.
"""
diff --git a/src/kernel/types/browsers/browser_telemetry_category_config_param.py b/src/kernel/types/browsers/browser_telemetry_category_config_param.py
index 09dbe418..27f08538 100644
--- a/src/kernel/types/browsers/browser_telemetry_category_config_param.py
+++ b/src/kernel/types/browsers/browser_telemetry_category_config_param.py
@@ -14,6 +14,6 @@ class BrowserTelemetryCategoryConfigParam(TypedDict, total=False):
"""Whether this category is captured.
Operational categories (control, connection, system, captcha) default to true;
- set false to opt out. CDP categories (console, network, page, interaction) and
- screenshot default to false; set true to opt in.
+ set false to opt out. CDP categories (console, network, page, interaction),
+ screenshot and platform default to false; set true to opt in.
"""
diff --git a/src/kernel/types/browsers/browser_telemetry_cdp_control_config.py b/src/kernel/types/browsers/browser_telemetry_cdp_control_config.py
new file mode 100644
index 00000000..fe425713
--- /dev/null
+++ b/src/kernel/types/browsers/browser_telemetry_cdp_control_config.py
@@ -0,0 +1,24 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from typing import List, Optional
+
+from ..._models import BaseModel
+from .browser_cdp_command_method import BrowserCdpCommandMethod
+
+__all__ = ["BrowserTelemetryCdpControlConfig"]
+
+
+class BrowserTelemetryCdpControlConfig(BaseModel):
+ """Settings for the cdp_command events the CDP proxy reports."""
+
+ excluded_methods: Optional[List[BrowserCdpCommandMethod]] = None
+ """Methods to leave out of the cdp_command stream.
+
+ Omit the list to keep the current one; send an empty list to report every
+ supported method again. Exclusion is a telemetry setting only: an excluded
+ command is still relayed to the browser unchanged, it simply produces no event.
+ Use it to drop the highest-volume methods — Input.dispatchMouseEvent during a
+ humanized cursor path, or Page.captureScreenshot under a screencast — without
+ turning the whole category off. Excluded commands are counted in
+ cdp_disconnect.telemetry_excluded.
+ """
diff --git a/src/kernel/types/browsers/browser_telemetry_cdp_control_config_param.py b/src/kernel/types/browsers/browser_telemetry_cdp_control_config_param.py
new file mode 100644
index 00000000..300e9b60
--- /dev/null
+++ b/src/kernel/types/browsers/browser_telemetry_cdp_control_config_param.py
@@ -0,0 +1,26 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from __future__ import annotations
+
+from typing import List
+from typing_extensions import TypedDict
+
+from .browser_cdp_command_method import BrowserCdpCommandMethod
+
+__all__ = ["BrowserTelemetryCdpControlConfigParam"]
+
+
+class BrowserTelemetryCdpControlConfigParam(TypedDict, total=False):
+ """Settings for the cdp_command events the CDP proxy reports."""
+
+ excluded_methods: List[BrowserCdpCommandMethod]
+ """Methods to leave out of the cdp_command stream.
+
+ Omit the list to keep the current one; send an empty list to report every
+ supported method again. Exclusion is a telemetry setting only: an excluded
+ command is still relayed to the browser unchanged, it simply produces no event.
+ Use it to drop the highest-volume methods — Input.dispatchMouseEvent during a
+ humanized cursor path, or Page.captureScreenshot under a screencast — without
+ turning the whole category off. Excluded commands are counted in
+ cdp_disconnect.telemetry_excluded.
+ """
diff --git a/src/kernel/types/browsers/browser_telemetry_control_config.py b/src/kernel/types/browsers/browser_telemetry_control_config.py
new file mode 100644
index 00000000..7a8dfbb3
--- /dev/null
+++ b/src/kernel/types/browsers/browser_telemetry_control_config.py
@@ -0,0 +1,28 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from typing import Optional
+
+from ..._models import BaseModel
+from .browser_telemetry_cdp_control_config import BrowserTelemetryCdpControlConfig
+
+__all__ = ["BrowserTelemetryControlConfig"]
+
+
+class BrowserTelemetryControlConfig(BaseModel):
+ """Configuration for the control category.
+
+ Same enabled semantics as any other category, plus settings for the browser-control commands the CDP proxy reports.
+ """
+
+ cdp: Optional[BrowserTelemetryCdpControlConfig] = None
+ """Settings for the cdp_command events the CDP proxy reports.
+
+ Merged independently of enabled, so a later update that only sets enabled keeps
+ the current exclusion list.
+ """
+
+ enabled: Optional[bool] = None
+ """Whether this category is captured.
+
+ Control is on by default; set false to opt out.
+ """
diff --git a/src/kernel/types/browsers/browser_telemetry_control_config_param.py b/src/kernel/types/browsers/browser_telemetry_control_config_param.py
new file mode 100644
index 00000000..e5afaea5
--- /dev/null
+++ b/src/kernel/types/browsers/browser_telemetry_control_config_param.py
@@ -0,0 +1,29 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from __future__ import annotations
+
+from typing_extensions import TypedDict
+
+from .browser_telemetry_cdp_control_config_param import BrowserTelemetryCdpControlConfigParam
+
+__all__ = ["BrowserTelemetryControlConfigParam"]
+
+
+class BrowserTelemetryControlConfigParam(TypedDict, total=False):
+ """Configuration for the control category.
+
+ Same enabled semantics as any other category, plus settings for the browser-control commands the CDP proxy reports.
+ """
+
+ cdp: BrowserTelemetryCdpControlConfigParam
+ """Settings for the cdp_command events the CDP proxy reports.
+
+ Merged independently of enabled, so a later update that only sets enabled keeps
+ the current exclusion list.
+ """
+
+ enabled: bool
+ """Whether this category is captured.
+
+ Control is on by default; set false to opt out.
+ """
diff --git a/src/kernel/types/browsers/browser_telemetry_event.py b/src/kernel/types/browsers/browser_telemetry_event.py
index d57b92d2..2ce872f4 100644
--- a/src/kernel/types/browsers/browser_telemetry_event.py
+++ b/src/kernel/types/browsers/browser_telemetry_event.py
@@ -9,10 +9,12 @@
from .browser_api_call_event import BrowserAPICallEvent
from .browser_page_lcp_event import BrowserPageLcpEvent
from .browser_page_load_event import BrowserPageLoadEvent
+from .browser_cdp_command_event import BrowserCdpCommandEvent
from .browser_cdp_connect_event import BrowserCdpConnectEvent
from .browser_console_log_event import BrowserConsoleLogEvent
from .browser_proxy_error_event import BrowserProxyErrorEvent
from .browser_network_idle_event import BrowserNetworkIdleEvent
+from .browser_page_crashed_event import BrowserPageCrashedEvent
from .browser_console_error_event import BrowserConsoleErrorEvent
from .browser_cdp_disconnect_event import BrowserCdpDisconnectEvent
from .browser_interaction_key_event import BrowserInteractionKeyEvent
@@ -25,6 +27,7 @@
from .browser_interaction_click_event import BrowserInteractionClickEvent
from .browser_live_view_connect_event import BrowserLiveViewConnectEvent
from .browser_page_layout_shift_event import BrowserPageLayoutShiftEvent
+from .browser_platform_api_call_event import BrowserPlatformAPICallEvent
from .browser_monitor_screenshot_event import BrowserMonitorScreenshotEvent
from .browser_monitor_init_failed_event import BrowserMonitorInitFailedEvent
from .browser_monitor_reconnected_event import BrowserMonitorReconnectedEvent
@@ -53,6 +56,7 @@
BrowserPageDomContentLoadedEvent,
BrowserPageLoadEvent,
BrowserPageTabOpenedEvent,
+ BrowserPageCrashedEvent,
BrowserPageLayoutShiftEvent,
BrowserPageLcpEvent,
BrowserPageLayoutSettledEvent,
@@ -66,6 +70,8 @@
BrowserMonitorReconnectFailedEvent,
BrowserMonitorInitFailedEvent,
BrowserAPICallEvent,
+ BrowserPlatformAPICallEvent,
+ BrowserCdpCommandEvent,
BrowserCdpConnectEvent,
BrowserCdpDisconnectEvent,
BrowserLiveViewConnectEvent,
diff --git a/src/kernel/types/browsers/telemetry_events_params.py b/src/kernel/types/browsers/telemetry_events_params.py
index 4aa0f13c..ec385bb5 100644
--- a/src/kernel/types/browsers/telemetry_events_params.py
+++ b/src/kernel/types/browsers/telemetry_events_params.py
@@ -16,6 +16,7 @@ class TelemetryEventsParams(TypedDict, total=False):
"page",
"interaction",
"control",
+ "platform",
"connection",
"system",
"screenshot",
diff --git a/tests/api_resources/auth/test_connections.py b/tests/api_resources/auth/test_connections.py
index f3d4a05c..4dff13ee 100644
--- a/tests/api_resources/auth/test_connections.py
+++ b/tests/api_resources/auth/test_connections.py
@@ -52,10 +52,14 @@ def test_method_create_with_all_params(self, client: Kernel) -> None:
"captcha": {"enabled": True},
"connection": {"enabled": True},
"console": {"enabled": True},
- "control": {"enabled": True},
+ "control": {
+ "cdp": {"excluded_methods": ["Input.dispatchMouseEvent"]},
+ "enabled": True,
+ },
"interaction": {"enabled": True},
"network": {"enabled": True},
"page": {"enabled": True},
+ "platform": {"enabled": True},
"screenshot": {"enabled": True},
"system": {"enabled": True},
},
@@ -76,10 +80,14 @@ def test_method_create_with_all_params(self, client: Kernel) -> None:
"captcha": {"enabled": True},
"connection": {"enabled": True},
"console": {"enabled": True},
- "control": {"enabled": True},
+ "control": {
+ "cdp": {"excluded_methods": ["Input.dispatchMouseEvent"]},
+ "enabled": True,
+ },
"interaction": {"enabled": True},
"network": {"enabled": True},
"page": {"enabled": True},
+ "platform": {"enabled": True},
"screenshot": {"enabled": True},
"system": {"enabled": True},
},
@@ -209,10 +217,14 @@ def test_method_update_with_all_params(self, client: Kernel) -> None:
"captcha": {"enabled": True},
"connection": {"enabled": True},
"console": {"enabled": True},
- "control": {"enabled": True},
+ "control": {
+ "cdp": {"excluded_methods": ["Input.dispatchMouseEvent"]},
+ "enabled": True,
+ },
"interaction": {"enabled": True},
"network": {"enabled": True},
"page": {"enabled": True},
+ "platform": {"enabled": True},
"screenshot": {"enabled": True},
"system": {"enabled": True},
},
@@ -233,10 +245,14 @@ def test_method_update_with_all_params(self, client: Kernel) -> None:
"captcha": {"enabled": True},
"connection": {"enabled": True},
"console": {"enabled": True},
- "control": {"enabled": True},
+ "control": {
+ "cdp": {"excluded_methods": ["Input.dispatchMouseEvent"]},
+ "enabled": True,
+ },
"interaction": {"enabled": True},
"network": {"enabled": True},
"page": {"enabled": True},
+ "platform": {"enabled": True},
"screenshot": {"enabled": True},
"system": {"enabled": True},
},
@@ -451,10 +467,14 @@ def test_method_login_with_all_params(self, client: Kernel) -> None:
"captcha": {"enabled": True},
"connection": {"enabled": True},
"console": {"enabled": True},
- "control": {"enabled": True},
+ "control": {
+ "cdp": {"excluded_methods": ["Input.dispatchMouseEvent"]},
+ "enabled": True,
+ },
"interaction": {"enabled": True},
"network": {"enabled": True},
"page": {"enabled": True},
+ "platform": {"enabled": True},
"screenshot": {"enabled": True},
"system": {"enabled": True},
},
@@ -475,10 +495,14 @@ def test_method_login_with_all_params(self, client: Kernel) -> None:
"captcha": {"enabled": True},
"connection": {"enabled": True},
"console": {"enabled": True},
- "control": {"enabled": True},
+ "control": {
+ "cdp": {"excluded_methods": ["Input.dispatchMouseEvent"]},
+ "enabled": True,
+ },
"interaction": {"enabled": True},
"network": {"enabled": True},
"page": {"enabled": True},
+ "platform": {"enabled": True},
"screenshot": {"enabled": True},
"system": {"enabled": True},
},
@@ -687,10 +711,14 @@ async def test_method_create_with_all_params(self, async_client: AsyncKernel) ->
"captcha": {"enabled": True},
"connection": {"enabled": True},
"console": {"enabled": True},
- "control": {"enabled": True},
+ "control": {
+ "cdp": {"excluded_methods": ["Input.dispatchMouseEvent"]},
+ "enabled": True,
+ },
"interaction": {"enabled": True},
"network": {"enabled": True},
"page": {"enabled": True},
+ "platform": {"enabled": True},
"screenshot": {"enabled": True},
"system": {"enabled": True},
},
@@ -711,10 +739,14 @@ async def test_method_create_with_all_params(self, async_client: AsyncKernel) ->
"captcha": {"enabled": True},
"connection": {"enabled": True},
"console": {"enabled": True},
- "control": {"enabled": True},
+ "control": {
+ "cdp": {"excluded_methods": ["Input.dispatchMouseEvent"]},
+ "enabled": True,
+ },
"interaction": {"enabled": True},
"network": {"enabled": True},
"page": {"enabled": True},
+ "platform": {"enabled": True},
"screenshot": {"enabled": True},
"system": {"enabled": True},
},
@@ -844,10 +876,14 @@ async def test_method_update_with_all_params(self, async_client: AsyncKernel) ->
"captcha": {"enabled": True},
"connection": {"enabled": True},
"console": {"enabled": True},
- "control": {"enabled": True},
+ "control": {
+ "cdp": {"excluded_methods": ["Input.dispatchMouseEvent"]},
+ "enabled": True,
+ },
"interaction": {"enabled": True},
"network": {"enabled": True},
"page": {"enabled": True},
+ "platform": {"enabled": True},
"screenshot": {"enabled": True},
"system": {"enabled": True},
},
@@ -868,10 +904,14 @@ async def test_method_update_with_all_params(self, async_client: AsyncKernel) ->
"captcha": {"enabled": True},
"connection": {"enabled": True},
"console": {"enabled": True},
- "control": {"enabled": True},
+ "control": {
+ "cdp": {"excluded_methods": ["Input.dispatchMouseEvent"]},
+ "enabled": True,
+ },
"interaction": {"enabled": True},
"network": {"enabled": True},
"page": {"enabled": True},
+ "platform": {"enabled": True},
"screenshot": {"enabled": True},
"system": {"enabled": True},
},
@@ -1086,10 +1126,14 @@ async def test_method_login_with_all_params(self, async_client: AsyncKernel) ->
"captcha": {"enabled": True},
"connection": {"enabled": True},
"console": {"enabled": True},
- "control": {"enabled": True},
+ "control": {
+ "cdp": {"excluded_methods": ["Input.dispatchMouseEvent"]},
+ "enabled": True,
+ },
"interaction": {"enabled": True},
"network": {"enabled": True},
"page": {"enabled": True},
+ "platform": {"enabled": True},
"screenshot": {"enabled": True},
"system": {"enabled": True},
},
@@ -1110,10 +1154,14 @@ async def test_method_login_with_all_params(self, async_client: AsyncKernel) ->
"captcha": {"enabled": True},
"connection": {"enabled": True},
"console": {"enabled": True},
- "control": {"enabled": True},
+ "control": {
+ "cdp": {"excluded_methods": ["Input.dispatchMouseEvent"]},
+ "enabled": True,
+ },
"interaction": {"enabled": True},
"network": {"enabled": True},
"page": {"enabled": True},
+ "platform": {"enabled": True},
"screenshot": {"enabled": True},
"system": {"enabled": True},
},
diff --git a/tests/api_resources/test_browser_pools.py b/tests/api_resources/test_browser_pools.py
index e411df27..1f541959 100644
--- a/tests/api_resources/test_browser_pools.py
+++ b/tests/api_resources/test_browser_pools.py
@@ -60,10 +60,14 @@ def test_method_create_with_all_params(self, client: Kernel) -> None:
"captcha": {"enabled": True},
"connection": {"enabled": True},
"console": {"enabled": True},
- "control": {"enabled": True},
+ "control": {
+ "cdp": {"excluded_methods": ["Input.dispatchMouseEvent"]},
+ "enabled": True,
+ },
"interaction": {"enabled": True},
"network": {"enabled": True},
"page": {"enabled": True},
+ "platform": {"enabled": True},
"screenshot": {"enabled": True},
"system": {"enabled": True},
},
@@ -195,10 +199,14 @@ def test_method_update_with_all_params(self, client: Kernel) -> None:
"captcha": {"enabled": True},
"connection": {"enabled": True},
"console": {"enabled": True},
- "control": {"enabled": True},
+ "control": {
+ "cdp": {"excluded_methods": ["Input.dispatchMouseEvent"]},
+ "enabled": True,
+ },
"interaction": {"enabled": True},
"network": {"enabled": True},
"page": {"enabled": True},
+ "platform": {"enabled": True},
"screenshot": {"enabled": True},
"system": {"enabled": True},
},
@@ -372,10 +380,14 @@ def test_method_acquire_with_all_params(self, client: Kernel) -> None:
"captcha": {"enabled": True},
"connection": {"enabled": True},
"console": {"enabled": True},
- "control": {"enabled": True},
+ "control": {
+ "cdp": {"excluded_methods": ["Input.dispatchMouseEvent"]},
+ "enabled": True,
+ },
"interaction": {"enabled": True},
"network": {"enabled": True},
"page": {"enabled": True},
+ "platform": {"enabled": True},
"screenshot": {"enabled": True},
"system": {"enabled": True},
},
@@ -570,10 +582,14 @@ async def test_method_create_with_all_params(self, async_client: AsyncKernel) ->
"captcha": {"enabled": True},
"connection": {"enabled": True},
"console": {"enabled": True},
- "control": {"enabled": True},
+ "control": {
+ "cdp": {"excluded_methods": ["Input.dispatchMouseEvent"]},
+ "enabled": True,
+ },
"interaction": {"enabled": True},
"network": {"enabled": True},
"page": {"enabled": True},
+ "platform": {"enabled": True},
"screenshot": {"enabled": True},
"system": {"enabled": True},
},
@@ -705,10 +721,14 @@ async def test_method_update_with_all_params(self, async_client: AsyncKernel) ->
"captcha": {"enabled": True},
"connection": {"enabled": True},
"console": {"enabled": True},
- "control": {"enabled": True},
+ "control": {
+ "cdp": {"excluded_methods": ["Input.dispatchMouseEvent"]},
+ "enabled": True,
+ },
"interaction": {"enabled": True},
"network": {"enabled": True},
"page": {"enabled": True},
+ "platform": {"enabled": True},
"screenshot": {"enabled": True},
"system": {"enabled": True},
},
@@ -882,10 +902,14 @@ async def test_method_acquire_with_all_params(self, async_client: AsyncKernel) -
"captcha": {"enabled": True},
"connection": {"enabled": True},
"console": {"enabled": True},
- "control": {"enabled": True},
+ "control": {
+ "cdp": {"excluded_methods": ["Input.dispatchMouseEvent"]},
+ "enabled": True,
+ },
"interaction": {"enabled": True},
"network": {"enabled": True},
"page": {"enabled": True},
+ "platform": {"enabled": True},
"screenshot": {"enabled": True},
"system": {"enabled": True},
},
diff --git a/tests/api_resources/test_browsers.py b/tests/api_resources/test_browsers.py
index 1a352038..55a7e66f 100644
--- a/tests/api_resources/test_browsers.py
+++ b/tests/api_resources/test_browsers.py
@@ -71,10 +71,14 @@ def test_method_create_with_all_params(self, client: Kernel) -> None:
"captcha": {"enabled": True},
"connection": {"enabled": True},
"console": {"enabled": True},
- "control": {"enabled": True},
+ "control": {
+ "cdp": {"excluded_methods": ["Input.dispatchMouseEvent"]},
+ "enabled": True,
+ },
"interaction": {"enabled": True},
"network": {"enabled": True},
"page": {"enabled": True},
+ "platform": {"enabled": True},
"screenshot": {"enabled": True},
"system": {"enabled": True},
},
@@ -206,10 +210,14 @@ def test_method_update_with_all_params(self, client: Kernel) -> None:
"captcha": {"enabled": True},
"connection": {"enabled": True},
"console": {"enabled": True},
- "control": {"enabled": True},
+ "control": {
+ "cdp": {"excluded_methods": ["Input.dispatchMouseEvent"]},
+ "enabled": True,
+ },
"interaction": {"enabled": True},
"network": {"enabled": True},
"page": {"enabled": True},
+ "platform": {"enabled": True},
"screenshot": {"enabled": True},
"system": {"enabled": True},
},
@@ -530,10 +538,14 @@ async def test_method_create_with_all_params(self, async_client: AsyncKernel) ->
"captcha": {"enabled": True},
"connection": {"enabled": True},
"console": {"enabled": True},
- "control": {"enabled": True},
+ "control": {
+ "cdp": {"excluded_methods": ["Input.dispatchMouseEvent"]},
+ "enabled": True,
+ },
"interaction": {"enabled": True},
"network": {"enabled": True},
"page": {"enabled": True},
+ "platform": {"enabled": True},
"screenshot": {"enabled": True},
"system": {"enabled": True},
},
@@ -665,10 +677,14 @@ async def test_method_update_with_all_params(self, async_client: AsyncKernel) ->
"captcha": {"enabled": True},
"connection": {"enabled": True},
"console": {"enabled": True},
- "control": {"enabled": True},
+ "control": {
+ "cdp": {"excluded_methods": ["Input.dispatchMouseEvent"]},
+ "enabled": True,
+ },
"interaction": {"enabled": True},
"network": {"enabled": True},
"page": {"enabled": True},
+ "platform": {"enabled": True},
"screenshot": {"enabled": True},
"system": {"enabled": True},
},
From f525a242bda54cec7301eae0dd3e12dd3c9b4e33 Mon Sep 17 00:00:00 2001
From: tnsardesai <18272584+tnsardesai@users.noreply.github.com>
Date: Fri, 21 Aug 2026 18:04:42 +0000
Subject: [PATCH 206/366] Fall back to the control plane on a stale session JWT
A direct-to-VM 401/403 now evicts the cached route and retries the
original request against the API.
---
src/kernel/_client.py | 15 +++++++++++++
src/kernel/lib/browser_routing/routing.py | 26 +++++++++++++++++++----
tests/test_browser_routing.py | 23 ++++++++++++++++++++
3 files changed, 60 insertions(+), 4 deletions(-)
diff --git a/src/kernel/_client.py b/src/kernel/_client.py
index 497cc5d5..d41fa974 100644
--- a/src/kernel/_client.py
+++ b/src/kernel/_client.py
@@ -42,6 +42,7 @@
rewrite_direct_vm_options,
browser_routing_config_from_env,
maybe_evict_browser_route_from_response,
+ should_retry_stale_direct_vm_auth,
maybe_populate_browser_route_cache_from_response,
)
@@ -353,6 +354,13 @@ def _prepare_options(self, options: Any) -> Any:
def _prepare_request(self, request: httpx.Request) -> None:
strip_direct_vm_auth(request, cache=self.browser_route_cache)
+ @override
+ def _should_retry(self, response: httpx.Response) -> bool:
+ if should_retry_stale_direct_vm_auth(response, cache=self.browser_route_cache):
+ maybe_evict_browser_route_from_response(response, cache=self.browser_route_cache)
+ return True
+ return super()._should_retry(response)
+
@override
def _process_response(
self,
@@ -722,6 +730,13 @@ async def _prepare_options(self, options: Any) -> Any:
async def _prepare_request(self, request: httpx.Request) -> None:
strip_direct_vm_auth(request, cache=self.browser_route_cache)
+ @override
+ def _should_retry(self, response: httpx.Response) -> bool:
+ if should_retry_stale_direct_vm_auth(response, cache=self.browser_route_cache):
+ maybe_evict_browser_route_from_response(response, cache=self.browser_route_cache)
+ return True
+ return super()._should_retry(response)
+
@override
async def _process_response(
self,
diff --git a/src/kernel/lib/browser_routing/routing.py b/src/kernel/lib/browser_routing/routing.py
index 11cd7647..31d8218a 100644
--- a/src/kernel/lib/browser_routing/routing.py
+++ b/src/kernel/lib/browser_routing/routing.py
@@ -110,14 +110,18 @@ def maybe_populate_browser_route_cache_from_response(response: httpx.Response, *
def maybe_evict_browser_route_from_response(response: httpx.Response, *, cache: BrowserRouteCache) -> None:
- if not response.is_success:
+ if response.is_success:
+ session_id = _session_id_to_evict_from_response(response)
+ if session_id:
+ cache.delete(session_id)
return
- session_id = _session_id_to_evict_from_response(response)
- if not session_id:
+ if response.status_code not in {401, 403}:
return
- cache.delete(session_id)
+ session_id = _session_id_from_direct_vm_response(response, cache=cache)
+ if session_id:
+ cache.delete(session_id)
def populate_browser_route_cache_from_value(value: object, *, cache: BrowserRouteCache) -> None:
@@ -161,6 +165,20 @@ def _session_id_to_evict_from_response(response: httpx.Response) -> str | None:
return None
+def _session_id_from_direct_vm_response(response: httpx.Response, *, cache: BrowserRouteCache) -> str | None:
+ raw = str(response.request.url)
+ for route in cache.values():
+ if raw.startswith(route.base_url.rstrip("/") + "/"):
+ return route.session_id
+ return None
+
+
+def should_retry_stale_direct_vm_auth(response: httpx.Response, *, cache: BrowserRouteCache) -> bool:
+ if response.status_code not in {401, 403}:
+ return False
+ return _session_id_from_direct_vm_response(response, cache=cache) is not None
+
+
def _session_id_from_browser_delete_path(path: str) -> str | None:
match = _BROWSER_DELETE_BY_ID_PATH.match(path)
if match is None:
diff --git a/tests/test_browser_routing.py b/tests/test_browser_routing.py
index 0f43f716..d4997d35 100644
--- a/tests/test_browser_routing.py
+++ b/tests/test_browser_routing.py
@@ -523,3 +523,26 @@ def test_process_fs_and_telemetry_events_stay_on_api_origin_by_default(
assert process.called
assert fs_read.called
assert events.called
+
+
+@respx.mock
+def test_stale_direct_vm_jwt_evicts_cache_and_retries_control_plane(
+ monkeypatch: pytest.MonkeyPatch,
+) -> None:
+ monkeypatch.delenv("KERNEL_BROWSER_ROUTING_SUBRESOURCES", raising=False)
+ monkeypatch.setattr("kernel._base_client.SyncAPIClient._sleep_for_retry", lambda *args, **kwargs: None)
+ vm = respx.post("http://browser-session.test/browser/kernel/computer/screenshot").mock(
+ return_value=httpx.Response(401, text="Invalid JWT")
+ )
+ api = respx.post(f"{base_url}/browsers/sess-1/computer/screenshot").mock(
+ return_value=httpx.Response(200, content=b"png", headers={"content-type": "image/png"})
+ )
+ with Kernel(base_url=base_url, api_key=api_key, _strict_response_validation=True) as client:
+ _cache_browser(client)
+ client.browsers.computer.capture_screenshot("sess-1")
+ assert client.browser_route_cache.get("sess-1") is None
+
+ assert vm.called
+ assert api.called
+ api_req = cast(httpx.Request, cast(Any, api.calls[0]).request)
+ assert api_req.headers.get("Authorization") == f"Bearer {api_key}"
From 35806715e783ee1e5379d488011dcc45868097c8 Mon Sep 17 00:00:00 2001
From: tnsardesai <18272584+tnsardesai@users.noreply.github.com>
Date: Fri, 21 Aug 2026 18:15:50 +0000
Subject: [PATCH 207/366] Fix ruff import order and unused lambda args
Lint failed on the stale-JWT retry change.
---
src/kernel/_client.py | 2 +-
tests/test_browser_routing.py | 14 +++++++-------
2 files changed, 8 insertions(+), 8 deletions(-)
diff --git a/src/kernel/_client.py b/src/kernel/_client.py
index d41fa974..7722bf33 100644
--- a/src/kernel/_client.py
+++ b/src/kernel/_client.py
@@ -41,8 +41,8 @@
strip_direct_vm_auth,
rewrite_direct_vm_options,
browser_routing_config_from_env,
- maybe_evict_browser_route_from_response,
should_retry_stale_direct_vm_auth,
+ maybe_evict_browser_route_from_response,
maybe_populate_browser_route_cache_from_response,
)
diff --git a/tests/test_browser_routing.py b/tests/test_browser_routing.py
index d4997d35..1c2ad765 100644
--- a/tests/test_browser_routing.py
+++ b/tests/test_browser_routing.py
@@ -432,9 +432,7 @@ def test_rewrite_direct_vm_options_keeps_telemetry_events_on_control_plane() ->
)
cache = BrowserRouteCache()
- cache.set(
- BrowserRoute(session_id="sess-1", base_url="http://browser-session.test/browser/kernel", jwt="token-abc")
- )
+ cache.set(BrowserRoute(session_id="sess-1", base_url="http://browser-session.test/browser/kernel", jwt="token-abc"))
config = BrowserRoutingConfig(subresources=("curl", "telemetry/stream", "computer", "playwright"))
events = rewrite_direct_vm_options(
@@ -511,9 +509,7 @@ def test_process_fs_and_telemetry_events_stay_on_api_origin_by_default(
fs_read = respx.get(f"{base_url}/browsers/sess-1/fs/read_file").mock(
return_value=httpx.Response(200, content=b"x", headers={"content-type": "application/octet-stream"})
)
- events = respx.get(f"{base_url}/browsers/sess-1/telemetry/events").mock(
- return_value=httpx.Response(200, json=[])
- )
+ events = respx.get(f"{base_url}/browsers/sess-1/telemetry/events").mock(return_value=httpx.Response(200, json=[]))
with Kernel(base_url=base_url, api_key=api_key, _strict_response_validation=True) as client:
_cache_browser(client)
client.browsers.process.exec("sess-1", command="echo")
@@ -530,7 +526,11 @@ def test_stale_direct_vm_jwt_evicts_cache_and_retries_control_plane(
monkeypatch: pytest.MonkeyPatch,
) -> None:
monkeypatch.delenv("KERNEL_BROWSER_ROUTING_SUBRESOURCES", raising=False)
- monkeypatch.setattr("kernel._base_client.SyncAPIClient._sleep_for_retry", lambda *args, **kwargs: None)
+
+ def _skip_retry_sleep(_self: object, **_kwargs: object) -> None:
+ return None
+
+ monkeypatch.setattr("kernel._base_client.SyncAPIClient._sleep_for_retry", _skip_retry_sleep)
vm = respx.post("http://browser-session.test/browser/kernel/computer/screenshot").mock(
return_value=httpx.Response(401, text="Invalid JWT")
)
From 728330d05725f765006fdc43633b4766ee6646e4 Mon Sep 17 00:00:00 2001
From: tnsardesai <18272584+tnsardesai@users.noreply.github.com>
Date: Fri, 21 Aug 2026 19:02:25 +0000
Subject: [PATCH 208/366] Retry stale direct-VM auth without needing the cache
A metro 401/403 with a jwt query param is enough to fall back to the
control plane, so concurrent requests still retry after the first
eviction.
---
src/kernel/_client.py | 4 ++--
src/kernel/lib/browser_routing/routing.py | 10 +++++++---
tests/test_browser_routing.py | 13 +++++++++++++
3 files changed, 22 insertions(+), 5 deletions(-)
diff --git a/src/kernel/_client.py b/src/kernel/_client.py
index 7722bf33..9a040e34 100644
--- a/src/kernel/_client.py
+++ b/src/kernel/_client.py
@@ -356,7 +356,7 @@ def _prepare_request(self, request: httpx.Request) -> None:
@override
def _should_retry(self, response: httpx.Response) -> bool:
- if should_retry_stale_direct_vm_auth(response, cache=self.browser_route_cache):
+ if should_retry_stale_direct_vm_auth(response):
maybe_evict_browser_route_from_response(response, cache=self.browser_route_cache)
return True
return super()._should_retry(response)
@@ -732,7 +732,7 @@ async def _prepare_request(self, request: httpx.Request) -> None:
@override
def _should_retry(self, response: httpx.Response) -> bool:
- if should_retry_stale_direct_vm_auth(response, cache=self.browser_route_cache):
+ if should_retry_stale_direct_vm_auth(response):
maybe_evict_browser_route_from_response(response, cache=self.browser_route_cache)
return True
return super()._should_retry(response)
diff --git a/src/kernel/lib/browser_routing/routing.py b/src/kernel/lib/browser_routing/routing.py
index 31d8218a..1db91027 100644
--- a/src/kernel/lib/browser_routing/routing.py
+++ b/src/kernel/lib/browser_routing/routing.py
@@ -116,7 +116,7 @@ def maybe_evict_browser_route_from_response(response: httpx.Response, *, cache:
cache.delete(session_id)
return
- if response.status_code not in {401, 403}:
+ if not is_stale_direct_vm_auth_response(response):
return
session_id = _session_id_from_direct_vm_response(response, cache=cache)
@@ -173,10 +173,14 @@ def _session_id_from_direct_vm_response(response: httpx.Response, *, cache: Brow
return None
-def should_retry_stale_direct_vm_auth(response: httpx.Response, *, cache: BrowserRouteCache) -> bool:
+def is_stale_direct_vm_auth_response(response: httpx.Response) -> bool:
if response.status_code not in {401, 403}:
return False
- return _session_id_from_direct_vm_response(response, cache=cache) is not None
+ return bool(response.request.url.params.get("jwt"))
+
+
+def should_retry_stale_direct_vm_auth(response: httpx.Response) -> bool:
+ return is_stale_direct_vm_auth_response(response)
def _session_id_from_browser_delete_path(path: str) -> str | None:
diff --git a/tests/test_browser_routing.py b/tests/test_browser_routing.py
index 1c2ad765..c10f7b0b 100644
--- a/tests/test_browser_routing.py
+++ b/tests/test_browser_routing.py
@@ -546,3 +546,16 @@ def _skip_retry_sleep(_self: object, **_kwargs: object) -> None:
assert api.called
api_req = cast(httpx.Request, cast(Any, api.calls[0]).request)
assert api_req.headers.get("Authorization") == f"Bearer {api_key}"
+
+
+def test_stale_direct_vm_auth_retry_does_not_require_cached_route() -> None:
+ from kernel.lib.browser_routing.routing import should_retry_stale_direct_vm_auth
+
+ request = httpx.Request(
+ "POST",
+ "http://browser-session.test/browser/kernel/computer/screenshot?jwt=token-abc",
+ )
+ response = httpx.Response(401, text="Invalid JWT", request=request)
+ empty = BrowserRouteCache()
+ assert should_retry_stale_direct_vm_auth(response) is True
+ assert empty.get("sess-1") is None
From 111aa09126aa01fc4dc88f0ed4bd27bf93d03dca Mon Sep 17 00:00:00 2001
From: tnsardesai <18272584+tnsardesai@users.noreply.github.com>
Date: Fri, 21 Aug 2026 23:34:52 +0000
Subject: [PATCH 209/366] Only evict a stale JWT if it is still the cached one
A later 401 must not delete a route that was refreshed in flight.
---
src/kernel/lib/browser_routing/routing.py | 13 ++++++++--
tests/test_browser_routing.py | 31 +++++++++++++++++++++++
2 files changed, 42 insertions(+), 2 deletions(-)
diff --git a/src/kernel/lib/browser_routing/routing.py b/src/kernel/lib/browser_routing/routing.py
index 1db91027..99bf7830 100644
--- a/src/kernel/lib/browser_routing/routing.py
+++ b/src/kernel/lib/browser_routing/routing.py
@@ -69,6 +69,14 @@ def set(self, route: BrowserRoute) -> None:
def delete(self, session_id: str) -> None:
self._routes.pop(_normalize_session_id(session_id), None)
+ def delete_if_jwt(self, session_id: str, jwt: str) -> bool:
+ key = _normalize_session_id(session_id)
+ route = self._routes.get(key)
+ if route is None or route.jwt != jwt.strip():
+ return False
+ del self._routes[key]
+ return True
+
def values(self) -> list[BrowserRoute]:
return list(self._routes.values())
@@ -119,9 +127,10 @@ def maybe_evict_browser_route_from_response(response: httpx.Response, *, cache:
if not is_stale_direct_vm_auth_response(response):
return
+ jwt = str(response.request.url.params.get("jwt") or "").strip()
session_id = _session_id_from_direct_vm_response(response, cache=cache)
- if session_id:
- cache.delete(session_id)
+ if session_id and jwt:
+ cache.delete_if_jwt(session_id, jwt)
def populate_browser_route_cache_from_value(value: object, *, cache: BrowserRouteCache) -> None:
diff --git a/tests/test_browser_routing.py b/tests/test_browser_routing.py
index c10f7b0b..ee73eafd 100644
--- a/tests/test_browser_routing.py
+++ b/tests/test_browser_routing.py
@@ -548,6 +548,37 @@ def _skip_retry_sleep(_self: object, **_kwargs: object) -> None:
assert api_req.headers.get("Authorization") == f"Bearer {api_key}"
+def test_stale_direct_vm_jwt_does_not_evict_refreshed_route() -> None:
+ from kernel.lib.browser_routing.routing import maybe_evict_browser_route_from_response
+
+ cache = BrowserRouteCache()
+ cache.set(
+ BrowserRoute(
+ session_id="sess-1",
+ base_url="http://browser-session.test/browser/kernel",
+ jwt="token-abc",
+ )
+ )
+ cache.set(
+ BrowserRoute(
+ session_id="sess-1",
+ base_url="http://browser-session.test/browser/kernel",
+ jwt="jwt-FRESH",
+ )
+ )
+ request = httpx.Request(
+ "POST",
+ "http://browser-session.test/browser/kernel/computer/screenshot?jwt=token-abc",
+ )
+ maybe_evict_browser_route_from_response(
+ httpx.Response(401, text="Invalid JWT", request=request),
+ cache=cache,
+ )
+ route = cache.get("sess-1")
+ assert route is not None
+ assert route.jwt == "jwt-FRESH"
+
+
def test_stale_direct_vm_auth_retry_does_not_require_cached_route() -> None:
from kernel.lib.browser_routing.routing import should_retry_stale_direct_vm_auth
From 9f4c4625698b28d9802bd2c74cb199c4ed810f53 Mon Sep 17 00:00:00 2001
From: "kernel-internal[bot]"
<260533166+kernel-internal[bot]@users.noreply.github.com>
Date: Mon, 24 Aug 2026 18:22:15 +0000
Subject: [PATCH 210/366] feat: site configs: report proxy-restricted analyses
Stainless-Generated-From: 656a31cc699c863df57d613e00e021e18cde32ca
---
.stats.yml | 2 +-
api.md | 31 +
src/kernel/_client.py | 44 ++
src/kernel/resources/__init__.py | 14 +
src/kernel/resources/site_configs.py | 596 ++++++++++++++++++
src/kernel/types/__init__.py | 18 +
src/kernel/types/analysis.py | 30 +
src/kernel/types/analysis_summary.py | 13 +
src/kernel/types/browser.py | 32 +
src/kernel/types/evidence.py | 38 ++
src/kernel/types/lookup_response.py | 15 +
src/kernel/types/no_recommendation.py | 20 +
src/kernel/types/proxy.py | 150 +++++
src/kernel/types/recommendation.py | 35 +
src/kernel/types/recommendation_result.py | 12 +
src/kernel/types/recommendation_summary.py | 29 +
src/kernel/types/site_config_list_params.py | 13 +
...site_config_list_recommendations_params.py | 17 +
src/kernel/types/site_config_lookup_params.py | 20 +
.../types/site_config_resolve_params.py | 21 +
src/kernel/types/site_config_response.py | 23 +
src/kernel/types/target.py | 16 +
tests/api_resources/test_site_configs.py | 438 +++++++++++++
23 files changed, 1626 insertions(+), 1 deletion(-)
create mode 100644 src/kernel/resources/site_configs.py
create mode 100644 src/kernel/types/analysis.py
create mode 100644 src/kernel/types/analysis_summary.py
create mode 100644 src/kernel/types/browser.py
create mode 100644 src/kernel/types/evidence.py
create mode 100644 src/kernel/types/lookup_response.py
create mode 100644 src/kernel/types/no_recommendation.py
create mode 100644 src/kernel/types/proxy.py
create mode 100644 src/kernel/types/recommendation.py
create mode 100644 src/kernel/types/recommendation_result.py
create mode 100644 src/kernel/types/recommendation_summary.py
create mode 100644 src/kernel/types/site_config_list_params.py
create mode 100644 src/kernel/types/site_config_list_recommendations_params.py
create mode 100644 src/kernel/types/site_config_lookup_params.py
create mode 100644 src/kernel/types/site_config_resolve_params.py
create mode 100644 src/kernel/types/site_config_response.py
create mode 100644 src/kernel/types/target.py
create mode 100644 tests/api_resources/test_site_configs.py
diff --git a/.stats.yml b/.stats.yml
index f9d1086b..2aae27b8 100644
--- a/.stats.yml
+++ b/.stats.yml
@@ -1 +1 @@
-configured_endpoints: 140
+configured_endpoints: 145
diff --git a/api.md b/api.md
index 4d75e371..60493a41 100644
--- a/api.md
+++ b/api.md
@@ -74,6 +74,37 @@ Methods:
- client.invocations.follow(id, \*\*params) -> InvocationFollowResponse
- client.invocations.list_browsers(id) -> InvocationListBrowsersResponse
+# SiteConfigs
+
+Types:
+
+```python
+from kernel.types import (
+ Analysis,
+ AnalysisSummary,
+ Browser,
+ Evidence,
+ LookupRequest,
+ LookupResponse,
+ NoRecommendation,
+ Proxy,
+ Recommendation,
+ RecommendationResult,
+ RecommendationSummary,
+ ResolveRequest,
+ SiteConfigResponse,
+ Target,
+)
+```
+
+Methods:
+
+- client.site_configs.retrieve(id) -> SiteConfigResponse
+- client.site_configs.list(\*\*params) -> SyncOffsetPagination[AnalysisSummary]
+- client.site_configs.list_recommendations(\*\*params) -> SyncOffsetPagination[RecommendationSummary]
+- client.site_configs.lookup(\*\*params) -> LookupResponse
+- client.site_configs.resolve(\*\*params) -> SiteConfigResponse
+
# Browsers
Types:
diff --git a/src/kernel/_client.py b/src/kernel/_client.py
index 497cc5d5..9ee4b913 100644
--- a/src/kernel/_client.py
+++ b/src/kernel/_client.py
@@ -61,6 +61,7 @@
deployments,
invocations,
organization,
+ site_configs,
browser_pools,
credential_providers,
)
@@ -73,6 +74,7 @@
from .resources.credentials import CredentialsResource, AsyncCredentialsResource
from .resources.deployments import DeploymentsResource, AsyncDeploymentsResource
from .resources.invocations import InvocationsResource, AsyncInvocationsResource
+ from .resources.site_configs import SiteConfigsResource, AsyncSiteConfigsResource
from .resources.browser_pools import BrowserPoolsResource, AsyncBrowserPoolsResource
from .resources.browsers.browsers import BrowsersResource, AsyncBrowsersResource
from .resources.projects.projects import ProjectsResource, AsyncProjectsResource
@@ -222,6 +224,13 @@ def invocations(self) -> InvocationsResource:
return InvocationsResource(self)
+ @cached_property
+ def site_configs(self) -> SiteConfigsResource:
+ """Resolve browser and proxy recommendations for bot-protected sites."""
+ from .resources.site_configs import SiteConfigsResource
+
+ return SiteConfigsResource(self)
+
@cached_property
def browsers(self) -> BrowsersResource:
"""Create and manage browser sessions."""
@@ -591,6 +600,13 @@ def invocations(self) -> AsyncInvocationsResource:
return AsyncInvocationsResource(self)
+ @cached_property
+ def site_configs(self) -> AsyncSiteConfigsResource:
+ """Resolve browser and proxy recommendations for bot-protected sites."""
+ from .resources.site_configs import AsyncSiteConfigsResource
+
+ return AsyncSiteConfigsResource(self)
+
@cached_property
def browsers(self) -> AsyncBrowsersResource:
"""Create and manage browser sessions."""
@@ -864,6 +880,13 @@ def invocations(self) -> invocations.InvocationsResourceWithRawResponse:
return InvocationsResourceWithRawResponse(self._client.invocations)
+ @cached_property
+ def site_configs(self) -> site_configs.SiteConfigsResourceWithRawResponse:
+ """Resolve browser and proxy recommendations for bot-protected sites."""
+ from .resources.site_configs import SiteConfigsResourceWithRawResponse
+
+ return SiteConfigsResourceWithRawResponse(self._client.site_configs)
+
@cached_property
def browsers(self) -> browsers.BrowsersResourceWithRawResponse:
"""Create and manage browser sessions."""
@@ -984,6 +1007,13 @@ def invocations(self) -> invocations.AsyncInvocationsResourceWithRawResponse:
return AsyncInvocationsResourceWithRawResponse(self._client.invocations)
+ @cached_property
+ def site_configs(self) -> site_configs.AsyncSiteConfigsResourceWithRawResponse:
+ """Resolve browser and proxy recommendations for bot-protected sites."""
+ from .resources.site_configs import AsyncSiteConfigsResourceWithRawResponse
+
+ return AsyncSiteConfigsResourceWithRawResponse(self._client.site_configs)
+
@cached_property
def browsers(self) -> browsers.AsyncBrowsersResourceWithRawResponse:
"""Create and manage browser sessions."""
@@ -1104,6 +1134,13 @@ def invocations(self) -> invocations.InvocationsResourceWithStreamingResponse:
return InvocationsResourceWithStreamingResponse(self._client.invocations)
+ @cached_property
+ def site_configs(self) -> site_configs.SiteConfigsResourceWithStreamingResponse:
+ """Resolve browser and proxy recommendations for bot-protected sites."""
+ from .resources.site_configs import SiteConfigsResourceWithStreamingResponse
+
+ return SiteConfigsResourceWithStreamingResponse(self._client.site_configs)
+
@cached_property
def browsers(self) -> browsers.BrowsersResourceWithStreamingResponse:
"""Create and manage browser sessions."""
@@ -1224,6 +1261,13 @@ def invocations(self) -> invocations.AsyncInvocationsResourceWithStreamingRespon
return AsyncInvocationsResourceWithStreamingResponse(self._client.invocations)
+ @cached_property
+ def site_configs(self) -> site_configs.AsyncSiteConfigsResourceWithStreamingResponse:
+ """Resolve browser and proxy recommendations for bot-protected sites."""
+ from .resources.site_configs import AsyncSiteConfigsResourceWithStreamingResponse
+
+ return AsyncSiteConfigsResourceWithStreamingResponse(self._client.site_configs)
+
@cached_property
def browsers(self) -> browsers.AsyncBrowsersResourceWithStreamingResponse:
"""Create and manage browser sessions."""
diff --git a/src/kernel/resources/__init__.py b/src/kernel/resources/__init__.py
index 10f1a88b..2ec29c9c 100644
--- a/src/kernel/resources/__init__.py
+++ b/src/kernel/resources/__init__.py
@@ -112,6 +112,14 @@
OrganizationResourceWithStreamingResponse,
AsyncOrganizationResourceWithStreamingResponse,
)
+from .site_configs import (
+ SiteConfigsResource,
+ AsyncSiteConfigsResource,
+ SiteConfigsResourceWithRawResponse,
+ AsyncSiteConfigsResourceWithRawResponse,
+ SiteConfigsResourceWithStreamingResponse,
+ AsyncSiteConfigsResourceWithStreamingResponse,
+)
from .browser_pools import (
BrowserPoolsResource,
AsyncBrowserPoolsResource,
@@ -148,6 +156,12 @@
"AsyncInvocationsResourceWithRawResponse",
"InvocationsResourceWithStreamingResponse",
"AsyncInvocationsResourceWithStreamingResponse",
+ "SiteConfigsResource",
+ "AsyncSiteConfigsResource",
+ "SiteConfigsResourceWithRawResponse",
+ "AsyncSiteConfigsResourceWithRawResponse",
+ "SiteConfigsResourceWithStreamingResponse",
+ "AsyncSiteConfigsResourceWithStreamingResponse",
"BrowsersResource",
"AsyncBrowsersResource",
"BrowsersResourceWithRawResponse",
diff --git a/src/kernel/resources/site_configs.py b/src/kernel/resources/site_configs.py
new file mode 100644
index 00000000..b98a8d80
--- /dev/null
+++ b/src/kernel/resources/site_configs.py
@@ -0,0 +1,596 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from __future__ import annotations
+
+from typing_extensions import Literal
+
+import httpx
+
+from ..types import (
+ site_config_list_params,
+ site_config_lookup_params,
+ site_config_resolve_params,
+ site_config_list_recommendations_params,
+)
+from .._types import Body, Omit, Query, Headers, NotGiven, SequenceNotStr, omit, not_given
+from .._utils import path_template, maybe_transform, async_maybe_transform
+from .._compat import cached_property
+from .._resource import SyncAPIResource, AsyncAPIResource
+from .._response import (
+ to_raw_response_wrapper,
+ to_streamed_response_wrapper,
+ async_to_raw_response_wrapper,
+ async_to_streamed_response_wrapper,
+)
+from ..pagination import SyncOffsetPagination, AsyncOffsetPagination
+from .._base_client import AsyncPaginator, make_request_options
+from ..types.lookup_response import LookupResponse
+from ..types.analysis_summary import AnalysisSummary
+from ..types.site_config_response import SiteConfigResponse
+from ..types.recommendation_summary import RecommendationSummary
+
+__all__ = ["SiteConfigsResource", "AsyncSiteConfigsResource"]
+
+
+class SiteConfigsResource(SyncAPIResource):
+ """Resolve browser and proxy recommendations for bot-protected sites."""
+
+ @cached_property
+ def with_raw_response(self) -> SiteConfigsResourceWithRawResponse:
+ """
+ This property can be used as a prefix for any HTTP method call to return
+ the raw response object instead of the parsed content.
+
+ For more information, see https://www.github.com/kernel/kernel-python-sdk#accessing-raw-response-data-eg-headers
+ """
+ return SiteConfigsResourceWithRawResponse(self)
+
+ @cached_property
+ def with_streaming_response(self) -> SiteConfigsResourceWithStreamingResponse:
+ """
+ An alternative to `.with_raw_response` that doesn't eagerly read the response body.
+
+ For more information, see https://www.github.com/kernel/kernel-python-sdk#with_streaming_response
+ """
+ return SiteConfigsResourceWithStreamingResponse(self)
+
+ def retrieve(
+ self,
+ id: str,
+ *,
+ # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
+ # The extra values given here take precedence over values defined on the client or passed to this method.
+ extra_headers: Headers | None = None,
+ extra_query: Query | None = None,
+ extra_body: Body | None = None,
+ timeout: float | httpx.Timeout | None | NotGiven = not_given,
+ ) -> SiteConfigResponse:
+ """
+ Returns a project-scoped historical analysis and the recommendation outcome
+ concluded by that run. Later knowledge does not change this response.
+
+ Args:
+ extra_headers: Send extra headers
+
+ extra_query: Add additional query parameters to the request
+
+ extra_body: Add additional JSON properties to the request
+
+ timeout: Override the client-level default timeout for this request, in seconds
+ """
+ if not id:
+ raise ValueError(f"Expected a non-empty value for `id` but received {id!r}")
+ return self._get(
+ path_template("/site-configs/{id}", id=id),
+ options=make_request_options(
+ extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
+ ),
+ cast_to=SiteConfigResponse,
+ )
+
+ def list(
+ self,
+ *,
+ limit: int | Omit = omit,
+ offset: int | Omit = omit,
+ # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
+ # The extra values given here take precedence over values defined on the client or passed to this method.
+ extra_headers: Headers | None = None,
+ extra_query: Query | None = None,
+ extra_body: Body | None = None,
+ timeout: float | httpx.Timeout | None | NotGiven = not_given,
+ ) -> SyncOffsetPagination[AnalysisSummary]:
+ """
+ Lists analyses for the selected project, newest first.
+
+ Args:
+ extra_headers: Send extra headers
+
+ extra_query: Add additional query parameters to the request
+
+ extra_body: Add additional JSON properties to the request
+
+ timeout: Override the client-level default timeout for this request, in seconds
+ """
+ return self._get_api_list(
+ "/site-configs",
+ page=SyncOffsetPagination[AnalysisSummary],
+ options=make_request_options(
+ extra_headers=extra_headers,
+ extra_query=extra_query,
+ extra_body=extra_body,
+ timeout=timeout,
+ query=maybe_transform(
+ {
+ "limit": limit,
+ "offset": offset,
+ },
+ site_config_list_params.SiteConfigListParams,
+ ),
+ ),
+ model=AnalysisSummary,
+ )
+
+ def list_recommendations(
+ self,
+ *,
+ limit: int | Omit = omit,
+ offset: int | Omit = omit,
+ sort_by: Literal["target", "recommended_config", "last_requested_at", "success_rate"] | Omit = omit,
+ sort_order: Literal["asc", "desc"] | Omit = omit,
+ # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
+ # The extra values given here take precedence over values defined on the client or passed to this method.
+ extra_headers: Headers | None = None,
+ extra_query: Query | None = None,
+ extra_body: Body | None = None,
+ timeout: float | httpx.Timeout | None | NotGiven = not_given,
+ ) -> SyncOffsetPagination[RecommendationSummary]:
+ """
+ Lists unique domains previously analyzed by the selected project with their
+ current domain-level recommendations.
+
+ Args:
+ extra_headers: Send extra headers
+
+ extra_query: Add additional query parameters to the request
+
+ extra_body: Add additional JSON properties to the request
+
+ timeout: Override the client-level default timeout for this request, in seconds
+ """
+ return self._get_api_list(
+ "/site-configs/recommendations",
+ page=SyncOffsetPagination[RecommendationSummary],
+ options=make_request_options(
+ extra_headers=extra_headers,
+ extra_query=extra_query,
+ extra_body=extra_body,
+ timeout=timeout,
+ query=maybe_transform(
+ {
+ "limit": limit,
+ "offset": offset,
+ "sort_by": sort_by,
+ "sort_order": sort_order,
+ },
+ site_config_list_recommendations_params.SiteConfigListRecommendationsParams,
+ ),
+ ),
+ model=RecommendationSummary,
+ )
+
+ def lookup(
+ self,
+ *,
+ url: str,
+ allowed_proxy_countries: SequenceNotStr[str] | Omit = omit,
+ # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
+ # The extra values given here take precedence over values defined on the client or passed to this method.
+ extra_headers: Headers | None = None,
+ extra_query: Query | None = None,
+ extra_body: Body | None = None,
+ timeout: float | httpx.Timeout | None | NotGiven = not_given,
+ ) -> LookupResponse:
+ """
+ Returns current global knowledge without resolving DNS, creating an analysis, or
+ updating Site Config data.
+
+ Args:
+ url: Public HTTP(S) URL to look up.
+
+ allowed_proxy_countries: ISO 3166 country codes Kernel may use when returning a proxy configuration. When
+ omitted, Kernel uses its default country selection.
+
+ 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
+ """
+ return self._post(
+ "/site-configs/lookup",
+ body=maybe_transform(
+ {
+ "url": url,
+ "allowed_proxy_countries": allowed_proxy_countries,
+ },
+ site_config_lookup_params.SiteConfigLookupParams,
+ ),
+ options=make_request_options(
+ extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
+ ),
+ cast_to=LookupResponse,
+ )
+
+ def resolve(
+ self,
+ *,
+ url: str,
+ allowed_proxy_countries: SequenceNotStr[str] | Omit = omit,
+ # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
+ # The extra values given here take precedence over values defined on the client or passed to this method.
+ extra_headers: Headers | None = None,
+ extra_query: Query | None = None,
+ extra_body: Body | None = None,
+ timeout: float | httpx.Timeout | None | NotGiven = not_given,
+ ) -> SiteConfigResponse:
+ """
+ Explicitly starts or retries a project-scoped background analysis while
+ preserving current global knowledge when available. Use `/site-configs/lookup`
+ for side-effect-free reads.
+
+ Args:
+ url: Public HTTP(S) URL to refresh.
+
+ allowed_proxy_countries: ISO 3166 country codes Kernel may use when searching for or returning a proxy
+ configuration. Kernel may test a subset of allowed countries. When omitted,
+ Kernel uses its default country selection.
+
+ 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
+ """
+ return self._post(
+ "/site-configs/resolve",
+ body=maybe_transform(
+ {
+ "url": url,
+ "allowed_proxy_countries": allowed_proxy_countries,
+ },
+ site_config_resolve_params.SiteConfigResolveParams,
+ ),
+ options=make_request_options(
+ extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
+ ),
+ cast_to=SiteConfigResponse,
+ )
+
+
+class AsyncSiteConfigsResource(AsyncAPIResource):
+ """Resolve browser and proxy recommendations for bot-protected sites."""
+
+ @cached_property
+ def with_raw_response(self) -> AsyncSiteConfigsResourceWithRawResponse:
+ """
+ This property can be used as a prefix for any HTTP method call to return
+ the raw response object instead of the parsed content.
+
+ For more information, see https://www.github.com/kernel/kernel-python-sdk#accessing-raw-response-data-eg-headers
+ """
+ return AsyncSiteConfigsResourceWithRawResponse(self)
+
+ @cached_property
+ def with_streaming_response(self) -> AsyncSiteConfigsResourceWithStreamingResponse:
+ """
+ An alternative to `.with_raw_response` that doesn't eagerly read the response body.
+
+ For more information, see https://www.github.com/kernel/kernel-python-sdk#with_streaming_response
+ """
+ return AsyncSiteConfigsResourceWithStreamingResponse(self)
+
+ async def retrieve(
+ self,
+ id: str,
+ *,
+ # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
+ # The extra values given here take precedence over values defined on the client or passed to this method.
+ extra_headers: Headers | None = None,
+ extra_query: Query | None = None,
+ extra_body: Body | None = None,
+ timeout: float | httpx.Timeout | None | NotGiven = not_given,
+ ) -> SiteConfigResponse:
+ """
+ Returns a project-scoped historical analysis and the recommendation outcome
+ concluded by that run. Later knowledge does not change this response.
+
+ Args:
+ extra_headers: Send extra headers
+
+ extra_query: Add additional query parameters to the request
+
+ extra_body: Add additional JSON properties to the request
+
+ timeout: Override the client-level default timeout for this request, in seconds
+ """
+ if not id:
+ raise ValueError(f"Expected a non-empty value for `id` but received {id!r}")
+ return await self._get(
+ path_template("/site-configs/{id}", id=id),
+ options=make_request_options(
+ extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
+ ),
+ cast_to=SiteConfigResponse,
+ )
+
+ def list(
+ self,
+ *,
+ limit: int | Omit = omit,
+ offset: int | Omit = omit,
+ # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
+ # The extra values given here take precedence over values defined on the client or passed to this method.
+ extra_headers: Headers | None = None,
+ extra_query: Query | None = None,
+ extra_body: Body | None = None,
+ timeout: float | httpx.Timeout | None | NotGiven = not_given,
+ ) -> AsyncPaginator[AnalysisSummary, AsyncOffsetPagination[AnalysisSummary]]:
+ """
+ Lists analyses for the selected project, newest first.
+
+ Args:
+ extra_headers: Send extra headers
+
+ extra_query: Add additional query parameters to the request
+
+ extra_body: Add additional JSON properties to the request
+
+ timeout: Override the client-level default timeout for this request, in seconds
+ """
+ return self._get_api_list(
+ "/site-configs",
+ page=AsyncOffsetPagination[AnalysisSummary],
+ options=make_request_options(
+ extra_headers=extra_headers,
+ extra_query=extra_query,
+ extra_body=extra_body,
+ timeout=timeout,
+ query=maybe_transform(
+ {
+ "limit": limit,
+ "offset": offset,
+ },
+ site_config_list_params.SiteConfigListParams,
+ ),
+ ),
+ model=AnalysisSummary,
+ )
+
+ def list_recommendations(
+ self,
+ *,
+ limit: int | Omit = omit,
+ offset: int | Omit = omit,
+ sort_by: Literal["target", "recommended_config", "last_requested_at", "success_rate"] | Omit = omit,
+ sort_order: Literal["asc", "desc"] | Omit = omit,
+ # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
+ # The extra values given here take precedence over values defined on the client or passed to this method.
+ extra_headers: Headers | None = None,
+ extra_query: Query | None = None,
+ extra_body: Body | None = None,
+ timeout: float | httpx.Timeout | None | NotGiven = not_given,
+ ) -> AsyncPaginator[RecommendationSummary, AsyncOffsetPagination[RecommendationSummary]]:
+ """
+ Lists unique domains previously analyzed by the selected project with their
+ current domain-level recommendations.
+
+ Args:
+ extra_headers: Send extra headers
+
+ extra_query: Add additional query parameters to the request
+
+ extra_body: Add additional JSON properties to the request
+
+ timeout: Override the client-level default timeout for this request, in seconds
+ """
+ return self._get_api_list(
+ "/site-configs/recommendations",
+ page=AsyncOffsetPagination[RecommendationSummary],
+ options=make_request_options(
+ extra_headers=extra_headers,
+ extra_query=extra_query,
+ extra_body=extra_body,
+ timeout=timeout,
+ query=maybe_transform(
+ {
+ "limit": limit,
+ "offset": offset,
+ "sort_by": sort_by,
+ "sort_order": sort_order,
+ },
+ site_config_list_recommendations_params.SiteConfigListRecommendationsParams,
+ ),
+ ),
+ model=RecommendationSummary,
+ )
+
+ async def lookup(
+ self,
+ *,
+ url: str,
+ allowed_proxy_countries: SequenceNotStr[str] | Omit = omit,
+ # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
+ # The extra values given here take precedence over values defined on the client or passed to this method.
+ extra_headers: Headers | None = None,
+ extra_query: Query | None = None,
+ extra_body: Body | None = None,
+ timeout: float | httpx.Timeout | None | NotGiven = not_given,
+ ) -> LookupResponse:
+ """
+ Returns current global knowledge without resolving DNS, creating an analysis, or
+ updating Site Config data.
+
+ Args:
+ url: Public HTTP(S) URL to look up.
+
+ allowed_proxy_countries: ISO 3166 country codes Kernel may use when returning a proxy configuration. When
+ omitted, Kernel uses its default country selection.
+
+ 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
+ """
+ return await self._post(
+ "/site-configs/lookup",
+ body=await async_maybe_transform(
+ {
+ "url": url,
+ "allowed_proxy_countries": allowed_proxy_countries,
+ },
+ site_config_lookup_params.SiteConfigLookupParams,
+ ),
+ options=make_request_options(
+ extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
+ ),
+ cast_to=LookupResponse,
+ )
+
+ async def resolve(
+ self,
+ *,
+ url: str,
+ allowed_proxy_countries: SequenceNotStr[str] | Omit = omit,
+ # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
+ # The extra values given here take precedence over values defined on the client or passed to this method.
+ extra_headers: Headers | None = None,
+ extra_query: Query | None = None,
+ extra_body: Body | None = None,
+ timeout: float | httpx.Timeout | None | NotGiven = not_given,
+ ) -> SiteConfigResponse:
+ """
+ Explicitly starts or retries a project-scoped background analysis while
+ preserving current global knowledge when available. Use `/site-configs/lookup`
+ for side-effect-free reads.
+
+ Args:
+ url: Public HTTP(S) URL to refresh.
+
+ allowed_proxy_countries: ISO 3166 country codes Kernel may use when searching for or returning a proxy
+ configuration. Kernel may test a subset of allowed countries. When omitted,
+ Kernel uses its default country selection.
+
+ 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
+ """
+ return await self._post(
+ "/site-configs/resolve",
+ body=await async_maybe_transform(
+ {
+ "url": url,
+ "allowed_proxy_countries": allowed_proxy_countries,
+ },
+ site_config_resolve_params.SiteConfigResolveParams,
+ ),
+ options=make_request_options(
+ extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
+ ),
+ cast_to=SiteConfigResponse,
+ )
+
+
+class SiteConfigsResourceWithRawResponse:
+ def __init__(self, site_configs: SiteConfigsResource) -> None:
+ self._site_configs = site_configs
+
+ self.retrieve = to_raw_response_wrapper(
+ site_configs.retrieve,
+ )
+ self.list = to_raw_response_wrapper(
+ site_configs.list,
+ )
+ self.list_recommendations = to_raw_response_wrapper(
+ site_configs.list_recommendations,
+ )
+ self.lookup = to_raw_response_wrapper(
+ site_configs.lookup,
+ )
+ self.resolve = to_raw_response_wrapper(
+ site_configs.resolve,
+ )
+
+
+class AsyncSiteConfigsResourceWithRawResponse:
+ def __init__(self, site_configs: AsyncSiteConfigsResource) -> None:
+ self._site_configs = site_configs
+
+ self.retrieve = async_to_raw_response_wrapper(
+ site_configs.retrieve,
+ )
+ self.list = async_to_raw_response_wrapper(
+ site_configs.list,
+ )
+ self.list_recommendations = async_to_raw_response_wrapper(
+ site_configs.list_recommendations,
+ )
+ self.lookup = async_to_raw_response_wrapper(
+ site_configs.lookup,
+ )
+ self.resolve = async_to_raw_response_wrapper(
+ site_configs.resolve,
+ )
+
+
+class SiteConfigsResourceWithStreamingResponse:
+ def __init__(self, site_configs: SiteConfigsResource) -> None:
+ self._site_configs = site_configs
+
+ self.retrieve = to_streamed_response_wrapper(
+ site_configs.retrieve,
+ )
+ self.list = to_streamed_response_wrapper(
+ site_configs.list,
+ )
+ self.list_recommendations = to_streamed_response_wrapper(
+ site_configs.list_recommendations,
+ )
+ self.lookup = to_streamed_response_wrapper(
+ site_configs.lookup,
+ )
+ self.resolve = to_streamed_response_wrapper(
+ site_configs.resolve,
+ )
+
+
+class AsyncSiteConfigsResourceWithStreamingResponse:
+ def __init__(self, site_configs: AsyncSiteConfigsResource) -> None:
+ self._site_configs = site_configs
+
+ self.retrieve = async_to_streamed_response_wrapper(
+ site_configs.retrieve,
+ )
+ self.list = async_to_streamed_response_wrapper(
+ site_configs.list,
+ )
+ self.list_recommendations = async_to_streamed_response_wrapper(
+ site_configs.list_recommendations,
+ )
+ self.lookup = async_to_streamed_response_wrapper(
+ site_configs.lookup,
+ )
+ self.resolve = async_to_streamed_response_wrapper(
+ site_configs.resolve,
+ )
diff --git a/src/kernel/types/__init__.py b/src/kernel/types/__init__.py
index 6373f24c..9e7af34c 100644
--- a/src/kernel/types/__init__.py
+++ b/src/kernel/types/__init__.py
@@ -5,6 +5,7 @@
from . import browsers
from .. import _compat
from .tags import Tags as Tags
+from .proxy import Proxy as Proxy
from .shared import (
LogEvent as LogEvent,
AppAction as AppAction,
@@ -16,20 +17,28 @@
BrowserViewport as BrowserViewport,
BrowserExtension as BrowserExtension,
)
+from .target import Target as Target
from .api_key import APIKey as APIKey
+from .browser import Browser as Browser
from .profile import Profile as Profile
from .project import Project as Project
+from .analysis import Analysis as Analysis
+from .evidence import Evidence as Evidence
from .credential import Credential as Credential
from .tags_param import TagsParam as TagsParam
from .browser_pool import BrowserPool as BrowserPool
from .browser_proxy import BrowserProxy as BrowserProxy
from .browser_usage import BrowserUsage as BrowserUsage
from .browser_memory import BrowserMemory as BrowserMemory
+from .recommendation import Recommendation as Recommendation
from .app_list_params import AppListParams as AppListParams
from .audit_log_entry import AuditLogEntry as AuditLogEntry
from .created_api_key import CreatedAPIKey as CreatedAPIKey
+from .lookup_response import LookupResponse as LookupResponse
+from .analysis_summary import AnalysisSummary as AnalysisSummary
from .browser_pool_ref import BrowserPoolRef as BrowserPoolRef
from .app_list_response import AppListResponse as AppListResponse
+from .no_recommendation import NoRecommendation as NoRecommendation
from .proxy_list_params import ProxyListParams as ProxyListParams
from .browser_proxy_mode import BrowserProxyMode as BrowserProxyMode
from .proxy_check_params import ProxyCheckParams as ProxyCheckParams
@@ -44,6 +53,7 @@
from .proxy_update_params import ProxyUpdateParams as ProxyUpdateParams
from .browser_proxy_config import BrowserProxyConfig as BrowserProxyConfig
from .proxy_check_response import ProxyCheckResponse as ProxyCheckResponse
+from .site_config_response import SiteConfigResponse as SiteConfigResponse
from .api_key_create_params import APIKeyCreateParams as APIKeyCreateParams
from .api_key_rotate_params import APIKeyRotateParams as APIKeyRotateParams
from .api_key_update_params import APIKeyUpdateParams as APIKeyUpdateParams
@@ -59,6 +69,7 @@
from .project_update_params import ProjectUpdateParams as ProjectUpdateParams
from .proxy_create_response import ProxyCreateResponse as ProxyCreateResponse
from .proxy_update_response import ProxyUpdateResponse as ProxyUpdateResponse
+from .recommendation_result import RecommendationResult as RecommendationResult
from .browser_memory_request import BrowserMemoryRequest as BrowserMemoryRequest
from .browser_network_config import BrowserNetworkConfig as BrowserNetworkConfig
from .credential_list_params import CredentialListParams as CredentialListParams
@@ -67,6 +78,7 @@
from .extension_get_response import ExtensionGetResponse as ExtensionGetResponse
from .invocation_list_params import InvocationListParams as InvocationListParams
from .invocation_state_event import InvocationStateEvent as InvocationStateEvent
+from .recommendation_summary import RecommendationSummary as RecommendationSummary
from .api_key_retrieve_params import APIKeyRetrieveParams as APIKeyRetrieveParams
from .browser_create_response import BrowserCreateResponse as BrowserCreateResponse
from .browser_retrieve_params import BrowserRetrieveParams as BrowserRetrieveParams
@@ -75,6 +87,7 @@
from .extension_upload_params import ExtensionUploadParams as ExtensionUploadParams
from .profile_download_params import ProfileDownloadParams as ProfileDownloadParams
from .proxy_retrieve_response import ProxyRetrieveResponse as ProxyRetrieveResponse
+from .site_config_list_params import SiteConfigListParams as SiteConfigListParams
from .browser_pool_list_params import BrowserPoolListParams as BrowserPoolListParams
from .credential_create_params import CredentialCreateParams as CredentialCreateParams
from .credential_provider_item import CredentialProviderItem as CredentialProviderItem
@@ -88,6 +101,7 @@
from .invocation_update_params import InvocationUpdateParams as InvocationUpdateParams
from .browser_retrieve_response import BrowserRetrieveResponse as BrowserRetrieveResponse
from .extension_upload_response import ExtensionUploadResponse as ExtensionUploadResponse
+from .site_config_lookup_params import SiteConfigLookupParams as SiteConfigLookupParams
from .browser_pool_create_params import BrowserPoolCreateParams as BrowserPoolCreateParams
from .browser_pool_delete_params import BrowserPoolDeleteParams as BrowserPoolDeleteParams
from .browser_pool_update_params import BrowserPoolUpdateParams as BrowserPoolUpdateParams
@@ -97,6 +111,7 @@
from .invocation_create_response import InvocationCreateResponse as InvocationCreateResponse
from .invocation_follow_response import InvocationFollowResponse as InvocationFollowResponse
from .invocation_update_response import InvocationUpdateResponse as InvocationUpdateResponse
+from .site_config_resolve_params import SiteConfigResolveParams as SiteConfigResolveParams
from .browser_pool_acquire_params import BrowserPoolAcquireParams as BrowserPoolAcquireParams
from .browser_pool_release_params import BrowserPoolReleaseParams as BrowserPoolReleaseParams
from .browser_network_config_param import BrowserNetworkConfigParam as BrowserNetworkConfigParam
@@ -114,6 +129,9 @@
from .credential_provider_list_items_response import (
CredentialProviderListItemsResponse as CredentialProviderListItemsResponse,
)
+from .site_config_list_recommendations_params import (
+ SiteConfigListRecommendationsParams as SiteConfigListRecommendationsParams,
+)
from .extension_download_from_chrome_store_params import (
ExtensionDownloadFromChromeStoreParams as ExtensionDownloadFromChromeStoreParams,
)
diff --git a/src/kernel/types/analysis.py b/src/kernel/types/analysis.py
new file mode 100644
index 00000000..c47b956c
--- /dev/null
+++ b/src/kernel/types/analysis.py
@@ -0,0 +1,30 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from typing import Optional
+from datetime import datetime
+from typing_extensions import Literal
+
+from .._models import BaseModel
+from .shared.error_model import ErrorModel
+
+__all__ = ["Analysis"]
+
+
+class Analysis(BaseModel):
+ id: str
+ """Discovery run ID used to poll analysis status."""
+
+ created_at: datetime
+ """Time the analysis was created."""
+
+ failure: Optional[ErrorModel] = None
+ """Present for failed or canceled analyses.
+
+ Messages contain safe retry guidance rather than internal workflow errors.
+ """
+
+ finished_at: Optional[datetime] = None
+ """Time the analysis reached a terminal status. Null while it is running."""
+
+ status: Literal["running", "completed", "failed", "canceled"]
+ """Lifecycle status of the background analysis."""
diff --git a/src/kernel/types/analysis_summary.py b/src/kernel/types/analysis_summary.py
new file mode 100644
index 00000000..fab0d8d8
--- /dev/null
+++ b/src/kernel/types/analysis_summary.py
@@ -0,0 +1,13 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from .target import Target
+from .._models import BaseModel
+from .analysis import Analysis
+
+__all__ = ["AnalysisSummary"]
+
+
+class AnalysisSummary(BaseModel):
+ analysis: Analysis
+
+ target: Target
diff --git a/src/kernel/types/browser.py b/src/kernel/types/browser.py
new file mode 100644
index 00000000..f7905451
--- /dev/null
+++ b/src/kernel/types/browser.py
@@ -0,0 +1,32 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from .._models import BaseModel
+from .shared.browser_viewport import BrowserViewport
+
+__all__ = ["Browser"]
+
+
+class Browser(BaseModel):
+ """Browser settings that can be passed directly to `POST /browsers`."""
+
+ gpu: bool
+
+ headless: bool
+
+ stealth: bool
+
+ viewport: BrowserViewport
+ """
+ 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, 768x1024@60, 390x844@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/evidence.py b/src/kernel/types/evidence.py
new file mode 100644
index 00000000..1a777de9
--- /dev/null
+++ b/src/kernel/types/evidence.py
@@ -0,0 +1,38 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from typing import Optional
+from datetime import datetime
+
+from .._models import BaseModel
+
+__all__ = ["Evidence"]
+
+
+class Evidence(BaseModel):
+ accessed: int
+
+ blocked: int
+
+ inconclusive: int
+
+ last_observed_at: datetime
+ """Most recent contributing observation.
+
+ Recommendations remain eligible regardless of age and can be returned while a
+ new analysis refreshes them.
+ """
+
+ run_count: int
+
+ sample_size: int
+ """Number of judged trials."""
+
+ success_rate: float
+ """Accessed trials divided by judged trials. Inconclusive trials are excluded."""
+
+ last_verified_at: Optional[datetime] = None
+ """Most recent contributing run where this config met the success threshold.
+
+ Omitted for knowledge assembled from runs that did not independently meet the
+ threshold.
+ """
diff --git a/src/kernel/types/lookup_response.py b/src/kernel/types/lookup_response.py
new file mode 100644
index 00000000..cf0f5c16
--- /dev/null
+++ b/src/kernel/types/lookup_response.py
@@ -0,0 +1,15 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from typing import Optional
+
+from .target import Target
+from .._models import BaseModel
+from .recommendation import Recommendation
+
+__all__ = ["LookupResponse"]
+
+
+class LookupResponse(BaseModel):
+ recommendation: Optional[Recommendation] = None
+
+ target: Target
diff --git a/src/kernel/types/no_recommendation.py b/src/kernel/types/no_recommendation.py
new file mode 100644
index 00000000..d1bab62c
--- /dev/null
+++ b/src/kernel/types/no_recommendation.py
@@ -0,0 +1,20 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from typing_extensions import Literal
+
+from .._models import BaseModel
+
+__all__ = ["NoRecommendation"]
+
+
+class NoRecommendation(BaseModel):
+ code: Literal["proxy_restricted", "no_working_configuration", "inconclusive"]
+ """
+ Machine-readable reason Kernel cannot currently provide a Site Config
+ recommendation.
+ """
+
+ message: str
+ """Human-readable explanation suitable for display."""
+
+ type: Literal["no_recommendation"]
diff --git a/src/kernel/types/proxy.py b/src/kernel/types/proxy.py
new file mode 100644
index 00000000..ea31827d
--- /dev/null
+++ b/src/kernel/types/proxy.py
@@ -0,0 +1,150 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from typing import List, Union, Optional
+from typing_extensions import Literal, Annotated, TypeAlias
+
+from .._utils import PropertyInfo
+from .._models import BaseModel
+
+__all__ = [
+ "Proxy",
+ "SiteConfigDirectProxy",
+ "SiteConfigManagedProxy",
+ "SiteConfigManagedProxyCreate",
+ "SiteConfigManagedProxyCreateConfig",
+ "SiteConfigManagedProxyCreateConfigDatacenterProxyConfig",
+ "SiteConfigManagedProxyCreateConfigIspProxyConfig",
+ "SiteConfigManagedProxyCreateConfigResidentialProxyConfig",
+ "SiteConfigManagedProxyCreateConfigMobileProxyConfig",
+ "SiteConfigManagedProxyCreateConfigCreateCustomProxyConfig",
+]
+
+
+class SiteConfigDirectProxy(BaseModel):
+ """Direct egress recipe. Pass `{ "mode": "direct" }` as the browser's `proxy`."""
+
+ mode: Literal["direct"]
+
+
+class SiteConfigManagedProxyCreateConfigDatacenterProxyConfig(BaseModel):
+ """Configuration for a datacenter proxy."""
+
+ country: Optional[str] = None
+ """ISO 3166 country code. Defaults to US if not provided."""
+
+
+class SiteConfigManagedProxyCreateConfigIspProxyConfig(BaseModel):
+ """Configuration for an ISP proxy."""
+
+ country: Optional[str] = None
+ """ISO 3166 country code. Defaults to US if not provided."""
+
+
+class SiteConfigManagedProxyCreateConfigResidentialProxyConfig(BaseModel):
+ """Configuration for residential proxies."""
+
+ asn: Optional[str] = None
+ """Autonomous system number. See https://bgp.potaroo.net/cidr/autnums.html"""
+
+ city: Optional[str] = None
+ """City name (no spaces, e.g.
+
+ `sanfrancisco`). If provided, `country` must also be provided.
+ """
+
+ country: Optional[str] = None
+ """ISO 3166 country code."""
+
+ os: Optional[Literal["windows", "macos", "android"]] = None
+ """Operating system of the residential device."""
+
+ state: Optional[str] = None
+ """Two-letter state code."""
+
+ zip: Optional[str] = None
+ """US ZIP code."""
+
+
+class SiteConfigManagedProxyCreateConfigMobileProxyConfig(BaseModel):
+ """Configuration for mobile proxies."""
+
+ city: Optional[str] = None
+ """Provider city alias. Mobile carrier routing can make observed geo vary."""
+
+ country: Optional[str] = None
+ """ISO 3166 country code"""
+
+ state: Optional[str] = None
+ """US-only state code. Mobile carrier routing can make observed geo vary."""
+
+
+class SiteConfigManagedProxyCreateConfigCreateCustomProxyConfig(BaseModel):
+ """Configuration for a custom proxy (e.g., private proxy server)."""
+
+ host: str
+ """Proxy host address or IP."""
+
+ port: int
+ """Proxy port."""
+
+ ca_bundle: Optional[str] = None
+ """PEM-encoded CA certificate bundle the proxy re-signs upstream TLS with.
+
+ Provide when the proxy terminates TLS (MITM) so the browser trusts its
+ certificates. May contain multiple concatenated certificates.
+ """
+
+ password: Optional[str] = None
+ """Password for proxy authentication."""
+
+ username: Optional[str] = None
+ """Username for proxy authentication."""
+
+
+SiteConfigManagedProxyCreateConfig: TypeAlias = Union[
+ SiteConfigManagedProxyCreateConfigDatacenterProxyConfig,
+ SiteConfigManagedProxyCreateConfigIspProxyConfig,
+ SiteConfigManagedProxyCreateConfigResidentialProxyConfig,
+ SiteConfigManagedProxyCreateConfigMobileProxyConfig,
+ SiteConfigManagedProxyCreateConfigCreateCustomProxyConfig,
+]
+
+
+class SiteConfigManagedProxyCreate(BaseModel):
+ """Configuration for routing traffic through a proxy."""
+
+ type: Literal["datacenter", "isp", "residential", "mobile", "custom"]
+ """Proxy type to use.
+
+ In terms of quality for avoiding bot-detection, from best to worst: `mobile` >
+ `residential` > `isp` > `datacenter`.
+ """
+
+ bypass_hosts: Optional[List[str]] = None
+ """Hostnames that should bypass the parent proxy and connect directly."""
+
+ config: Optional[SiteConfigManagedProxyCreateConfig] = None
+ """Configuration specific to the selected proxy `type`."""
+
+ name: Optional[str] = None
+ """Readable name of the proxy."""
+
+ protocol: Optional[Literal["http", "https"]] = None
+ """Protocol to use for the proxy connection."""
+
+
+class SiteConfigManagedProxy(BaseModel):
+ """Managed proxy recipe.
+
+ `create` is a non-idempotent `POST /proxies` payload:
+ create the resource once, retain its ID, and reuse that ID as the browser's
+ `proxy.id`. Do not submit this recipe before every browser session.
+ """
+
+ create: SiteConfigManagedProxyCreate
+ """Configuration for routing traffic through a proxy."""
+
+ mode: Literal["managed"]
+
+
+Proxy: TypeAlias = Annotated[Union[SiteConfigDirectProxy, SiteConfigManagedProxy], PropertyInfo(discriminator="mode")]
diff --git a/src/kernel/types/recommendation.py b/src/kernel/types/recommendation.py
new file mode 100644
index 00000000..74b1761a
--- /dev/null
+++ b/src/kernel/types/recommendation.py
@@ -0,0 +1,35 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from typing_extensions import Literal
+
+from .proxy import Proxy
+from .browser import Browser
+from .._models import BaseModel
+from .evidence import Evidence
+
+__all__ = ["Recommendation"]
+
+
+class Recommendation(BaseModel):
+ browser: Browser
+ """Browser settings that can be passed directly to `POST /browsers`."""
+
+ evidence: Evidence
+
+ match_scope: Literal["exact", "host", "domain"]
+ """Specificity of knowledge matched for this recommendation."""
+
+ matched_target: str
+ """Target value that supplied the recommendation."""
+
+ proxy: Proxy
+ """Proxy recipe for the recommended browser."""
+
+ type: Literal["recommendation"]
+
+ verification: Literal["verified", "inferred"]
+ """
+ Exact matches meet the evidence threshold; host and domain fallbacks are
+ inferred. Check evidence.last_verified_at for successful verification age and
+ last_observed_at for the latest evidence.
+ """
diff --git a/src/kernel/types/recommendation_result.py b/src/kernel/types/recommendation_result.py
new file mode 100644
index 00000000..82a72d92
--- /dev/null
+++ b/src/kernel/types/recommendation_result.py
@@ -0,0 +1,12 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from typing import Union
+from typing_extensions import Annotated, TypeAlias
+
+from .._utils import PropertyInfo
+from .recommendation import Recommendation
+from .no_recommendation import NoRecommendation
+
+__all__ = ["RecommendationResult"]
+
+RecommendationResult: TypeAlias = Annotated[Union[Recommendation, NoRecommendation], PropertyInfo(discriminator="type")]
diff --git a/src/kernel/types/recommendation_summary.py b/src/kernel/types/recommendation_summary.py
new file mode 100644
index 00000000..abc2ea72
--- /dev/null
+++ b/src/kernel/types/recommendation_summary.py
@@ -0,0 +1,29 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from typing import Optional
+from datetime import datetime
+
+from .._models import BaseModel
+from .recommendation import Recommendation
+
+__all__ = ["RecommendationSummary"]
+
+
+class RecommendationSummary(BaseModel):
+ last_requested_at: datetime
+ """Most recent time the selected project requested an analysis for this domain."""
+
+ recommendation: Optional[Recommendation] = None
+ """Current domain-level recommendation. Null when no eligible knowledge exists."""
+
+ recommended_config_label: Optional[str] = None
+ """Display label for the recommended browser configuration."""
+
+ success_rate: Optional[float] = None
+ """Success rate for the recommended configuration.
+
+ Null when no eligible knowledge exists.
+ """
+
+ target: str
+ """Registrable domain previously analyzed by the selected project."""
diff --git a/src/kernel/types/site_config_list_params.py b/src/kernel/types/site_config_list_params.py
new file mode 100644
index 00000000..ee95bad4
--- /dev/null
+++ b/src/kernel/types/site_config_list_params.py
@@ -0,0 +1,13 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from __future__ import annotations
+
+from typing_extensions import TypedDict
+
+__all__ = ["SiteConfigListParams"]
+
+
+class SiteConfigListParams(TypedDict, total=False):
+ limit: int
+
+ offset: int
diff --git a/src/kernel/types/site_config_list_recommendations_params.py b/src/kernel/types/site_config_list_recommendations_params.py
new file mode 100644
index 00000000..8ff51237
--- /dev/null
+++ b/src/kernel/types/site_config_list_recommendations_params.py
@@ -0,0 +1,17 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from __future__ import annotations
+
+from typing_extensions import Literal, TypedDict
+
+__all__ = ["SiteConfigListRecommendationsParams"]
+
+
+class SiteConfigListRecommendationsParams(TypedDict, total=False):
+ limit: int
+
+ offset: int
+
+ sort_by: Literal["target", "recommended_config", "last_requested_at", "success_rate"]
+
+ sort_order: Literal["asc", "desc"]
diff --git a/src/kernel/types/site_config_lookup_params.py b/src/kernel/types/site_config_lookup_params.py
new file mode 100644
index 00000000..aba315f4
--- /dev/null
+++ b/src/kernel/types/site_config_lookup_params.py
@@ -0,0 +1,20 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from __future__ import annotations
+
+from typing_extensions import Required, TypedDict
+
+from .._types import SequenceNotStr
+
+__all__ = ["SiteConfigLookupParams"]
+
+
+class SiteConfigLookupParams(TypedDict, total=False):
+ url: Required[str]
+ """Public HTTP(S) URL to look up."""
+
+ allowed_proxy_countries: SequenceNotStr[str]
+ """ISO 3166 country codes Kernel may use when returning a proxy configuration.
+
+ When omitted, Kernel uses its default country selection.
+ """
diff --git a/src/kernel/types/site_config_resolve_params.py b/src/kernel/types/site_config_resolve_params.py
new file mode 100644
index 00000000..37a3f44d
--- /dev/null
+++ b/src/kernel/types/site_config_resolve_params.py
@@ -0,0 +1,21 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from __future__ import annotations
+
+from typing_extensions import Required, TypedDict
+
+from .._types import SequenceNotStr
+
+__all__ = ["SiteConfigResolveParams"]
+
+
+class SiteConfigResolveParams(TypedDict, total=False):
+ url: Required[str]
+ """Public HTTP(S) URL to refresh."""
+
+ allowed_proxy_countries: SequenceNotStr[str]
+ """
+ ISO 3166 country codes Kernel may use when searching for or returning a proxy
+ configuration. Kernel may test a subset of allowed countries. When omitted,
+ Kernel uses its default country selection.
+ """
diff --git a/src/kernel/types/site_config_response.py b/src/kernel/types/site_config_response.py
new file mode 100644
index 00000000..b6673f76
--- /dev/null
+++ b/src/kernel/types/site_config_response.py
@@ -0,0 +1,23 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from typing import Optional
+
+from .target import Target
+from .._models import BaseModel
+from .analysis import Analysis
+from .recommendation_result import RecommendationResult
+
+__all__ = ["SiteConfigResponse"]
+
+
+class SiteConfigResponse(BaseModel):
+ analysis: Optional[Analysis] = None
+ """Pollable analysis after workflow submission is acknowledged.
+
+ Null when no refresh was submitted.
+ """
+
+ recommendation: Optional[RecommendationResult] = None
+ """A recommendation or a structured no-recommendation result."""
+
+ target: Target
diff --git a/src/kernel/types/target.py b/src/kernel/types/target.py
new file mode 100644
index 00000000..6950d513
--- /dev/null
+++ b/src/kernel/types/target.py
@@ -0,0 +1,16 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from .._models import BaseModel
+
+__all__ = ["Target"]
+
+
+class Target(BaseModel):
+ domain: str
+ """Registrable domain."""
+
+ host: str
+ """Full hostname, including subdomain."""
+
+ normalized: str
+ """Exact normalized scheme, host, port, and path used for lookup."""
diff --git a/tests/api_resources/test_site_configs.py b/tests/api_resources/test_site_configs.py
new file mode 100644
index 00000000..2099899c
--- /dev/null
+++ b/tests/api_resources/test_site_configs.py
@@ -0,0 +1,438 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from __future__ import annotations
+
+import os
+from typing import Any, cast
+
+import pytest
+
+from kernel import Kernel, AsyncKernel
+from tests.utils import assert_matches_type
+from kernel.types import (
+ LookupResponse,
+ AnalysisSummary,
+ SiteConfigResponse,
+ RecommendationSummary,
+)
+from kernel.pagination import SyncOffsetPagination, AsyncOffsetPagination
+
+base_url = os.environ.get("TEST_API_BASE_URL", "http://127.0.0.1:4010")
+
+
+class TestSiteConfigs:
+ parametrize = pytest.mark.parametrize("client", [False, True], indirect=True, ids=["loose", "strict"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ def test_method_retrieve(self, client: Kernel) -> None:
+ site_config = client.site_configs.retrieve(
+ "id",
+ )
+ assert_matches_type(SiteConfigResponse, site_config, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ def test_raw_response_retrieve(self, client: Kernel) -> None:
+ response = client.site_configs.with_raw_response.retrieve(
+ "id",
+ )
+
+ assert response.is_closed is True
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+ site_config = response.parse()
+ assert_matches_type(SiteConfigResponse, site_config, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ def test_streaming_response_retrieve(self, client: Kernel) -> None:
+ with client.site_configs.with_streaming_response.retrieve(
+ "id",
+ ) as response:
+ assert not response.is_closed
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+
+ site_config = response.parse()
+ assert_matches_type(SiteConfigResponse, site_config, path=["response"])
+
+ assert cast(Any, response.is_closed) is True
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ def test_path_params_retrieve(self, client: Kernel) -> None:
+ with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"):
+ client.site_configs.with_raw_response.retrieve(
+ "",
+ )
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ def test_method_list(self, client: Kernel) -> None:
+ site_config = client.site_configs.list()
+ assert_matches_type(SyncOffsetPagination[AnalysisSummary], site_config, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ def test_method_list_with_all_params(self, client: Kernel) -> None:
+ site_config = client.site_configs.list(
+ limit=1,
+ offset=0,
+ )
+ assert_matches_type(SyncOffsetPagination[AnalysisSummary], site_config, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ def test_raw_response_list(self, client: Kernel) -> None:
+ response = client.site_configs.with_raw_response.list()
+
+ assert response.is_closed is True
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+ site_config = response.parse()
+ assert_matches_type(SyncOffsetPagination[AnalysisSummary], site_config, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ def test_streaming_response_list(self, client: Kernel) -> None:
+ with client.site_configs.with_streaming_response.list() as response:
+ assert not response.is_closed
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+
+ site_config = response.parse()
+ assert_matches_type(SyncOffsetPagination[AnalysisSummary], site_config, path=["response"])
+
+ assert cast(Any, response.is_closed) is True
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ def test_method_list_recommendations(self, client: Kernel) -> None:
+ site_config = client.site_configs.list_recommendations()
+ assert_matches_type(SyncOffsetPagination[RecommendationSummary], site_config, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ def test_method_list_recommendations_with_all_params(self, client: Kernel) -> None:
+ site_config = client.site_configs.list_recommendations(
+ limit=1,
+ offset=0,
+ sort_by="target",
+ sort_order="asc",
+ )
+ assert_matches_type(SyncOffsetPagination[RecommendationSummary], site_config, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ def test_raw_response_list_recommendations(self, client: Kernel) -> None:
+ response = client.site_configs.with_raw_response.list_recommendations()
+
+ assert response.is_closed is True
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+ site_config = response.parse()
+ assert_matches_type(SyncOffsetPagination[RecommendationSummary], site_config, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ def test_streaming_response_list_recommendations(self, client: Kernel) -> None:
+ with client.site_configs.with_streaming_response.list_recommendations() as response:
+ assert not response.is_closed
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+
+ site_config = response.parse()
+ assert_matches_type(SyncOffsetPagination[RecommendationSummary], site_config, path=["response"])
+
+ assert cast(Any, response.is_closed) is True
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ def test_method_lookup(self, client: Kernel) -> None:
+ site_config = client.site_configs.lookup(
+ url="https://example.com",
+ )
+ assert_matches_type(LookupResponse, site_config, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ def test_method_lookup_with_all_params(self, client: Kernel) -> None:
+ site_config = client.site_configs.lookup(
+ url="https://example.com",
+ allowed_proxy_countries=["US"],
+ )
+ assert_matches_type(LookupResponse, site_config, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ def test_raw_response_lookup(self, client: Kernel) -> None:
+ response = client.site_configs.with_raw_response.lookup(
+ url="https://example.com",
+ )
+
+ assert response.is_closed is True
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+ site_config = response.parse()
+ assert_matches_type(LookupResponse, site_config, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ def test_streaming_response_lookup(self, client: Kernel) -> None:
+ with client.site_configs.with_streaming_response.lookup(
+ url="https://example.com",
+ ) as response:
+ assert not response.is_closed
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+
+ site_config = response.parse()
+ assert_matches_type(LookupResponse, site_config, path=["response"])
+
+ assert cast(Any, response.is_closed) is True
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ def test_method_resolve(self, client: Kernel) -> None:
+ site_config = client.site_configs.resolve(
+ url="https://example.com",
+ )
+ assert_matches_type(SiteConfigResponse, site_config, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ def test_method_resolve_with_all_params(self, client: Kernel) -> None:
+ site_config = client.site_configs.resolve(
+ url="https://example.com",
+ allowed_proxy_countries=["US"],
+ )
+ assert_matches_type(SiteConfigResponse, site_config, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ def test_raw_response_resolve(self, client: Kernel) -> None:
+ response = client.site_configs.with_raw_response.resolve(
+ url="https://example.com",
+ )
+
+ assert response.is_closed is True
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+ site_config = response.parse()
+ assert_matches_type(SiteConfigResponse, site_config, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ def test_streaming_response_resolve(self, client: Kernel) -> None:
+ with client.site_configs.with_streaming_response.resolve(
+ url="https://example.com",
+ ) as response:
+ assert not response.is_closed
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+
+ site_config = response.parse()
+ assert_matches_type(SiteConfigResponse, site_config, path=["response"])
+
+ assert cast(Any, response.is_closed) is True
+
+
+class TestAsyncSiteConfigs:
+ parametrize = pytest.mark.parametrize(
+ "async_client", [False, True, {"http_client": "aiohttp"}], indirect=True, ids=["loose", "strict", "aiohttp"]
+ )
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ async def test_method_retrieve(self, async_client: AsyncKernel) -> None:
+ site_config = await async_client.site_configs.retrieve(
+ "id",
+ )
+ assert_matches_type(SiteConfigResponse, site_config, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ async def test_raw_response_retrieve(self, async_client: AsyncKernel) -> None:
+ response = await async_client.site_configs.with_raw_response.retrieve(
+ "id",
+ )
+
+ assert response.is_closed is True
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+ site_config = await response.parse()
+ assert_matches_type(SiteConfigResponse, site_config, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ async def test_streaming_response_retrieve(self, async_client: AsyncKernel) -> None:
+ async with async_client.site_configs.with_streaming_response.retrieve(
+ "id",
+ ) as response:
+ assert not response.is_closed
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+
+ site_config = await response.parse()
+ assert_matches_type(SiteConfigResponse, site_config, 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_retrieve(self, async_client: AsyncKernel) -> None:
+ with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"):
+ await async_client.site_configs.with_raw_response.retrieve(
+ "",
+ )
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ async def test_method_list(self, async_client: AsyncKernel) -> None:
+ site_config = await async_client.site_configs.list()
+ assert_matches_type(AsyncOffsetPagination[AnalysisSummary], site_config, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ async def test_method_list_with_all_params(self, async_client: AsyncKernel) -> None:
+ site_config = await async_client.site_configs.list(
+ limit=1,
+ offset=0,
+ )
+ assert_matches_type(AsyncOffsetPagination[AnalysisSummary], site_config, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ async def test_raw_response_list(self, async_client: AsyncKernel) -> None:
+ response = await async_client.site_configs.with_raw_response.list()
+
+ assert response.is_closed is True
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+ site_config = await response.parse()
+ assert_matches_type(AsyncOffsetPagination[AnalysisSummary], site_config, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ async def test_streaming_response_list(self, async_client: AsyncKernel) -> None:
+ async with async_client.site_configs.with_streaming_response.list() as response:
+ assert not response.is_closed
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+
+ site_config = await response.parse()
+ assert_matches_type(AsyncOffsetPagination[AnalysisSummary], site_config, path=["response"])
+
+ assert cast(Any, response.is_closed) is True
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ async def test_method_list_recommendations(self, async_client: AsyncKernel) -> None:
+ site_config = await async_client.site_configs.list_recommendations()
+ assert_matches_type(AsyncOffsetPagination[RecommendationSummary], site_config, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ async def test_method_list_recommendations_with_all_params(self, async_client: AsyncKernel) -> None:
+ site_config = await async_client.site_configs.list_recommendations(
+ limit=1,
+ offset=0,
+ sort_by="target",
+ sort_order="asc",
+ )
+ assert_matches_type(AsyncOffsetPagination[RecommendationSummary], site_config, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ async def test_raw_response_list_recommendations(self, async_client: AsyncKernel) -> None:
+ response = await async_client.site_configs.with_raw_response.list_recommendations()
+
+ assert response.is_closed is True
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+ site_config = await response.parse()
+ assert_matches_type(AsyncOffsetPagination[RecommendationSummary], site_config, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ async def test_streaming_response_list_recommendations(self, async_client: AsyncKernel) -> None:
+ async with async_client.site_configs.with_streaming_response.list_recommendations() as response:
+ assert not response.is_closed
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+
+ site_config = await response.parse()
+ assert_matches_type(AsyncOffsetPagination[RecommendationSummary], site_config, path=["response"])
+
+ assert cast(Any, response.is_closed) is True
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ async def test_method_lookup(self, async_client: AsyncKernel) -> None:
+ site_config = await async_client.site_configs.lookup(
+ url="https://example.com",
+ )
+ assert_matches_type(LookupResponse, site_config, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ async def test_method_lookup_with_all_params(self, async_client: AsyncKernel) -> None:
+ site_config = await async_client.site_configs.lookup(
+ url="https://example.com",
+ allowed_proxy_countries=["US"],
+ )
+ assert_matches_type(LookupResponse, site_config, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ async def test_raw_response_lookup(self, async_client: AsyncKernel) -> None:
+ response = await async_client.site_configs.with_raw_response.lookup(
+ url="https://example.com",
+ )
+
+ assert response.is_closed is True
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+ site_config = await response.parse()
+ assert_matches_type(LookupResponse, site_config, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ async def test_streaming_response_lookup(self, async_client: AsyncKernel) -> None:
+ async with async_client.site_configs.with_streaming_response.lookup(
+ url="https://example.com",
+ ) as response:
+ assert not response.is_closed
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+
+ site_config = await response.parse()
+ assert_matches_type(LookupResponse, site_config, path=["response"])
+
+ assert cast(Any, response.is_closed) is True
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ async def test_method_resolve(self, async_client: AsyncKernel) -> None:
+ site_config = await async_client.site_configs.resolve(
+ url="https://example.com",
+ )
+ assert_matches_type(SiteConfigResponse, site_config, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ async def test_method_resolve_with_all_params(self, async_client: AsyncKernel) -> None:
+ site_config = await async_client.site_configs.resolve(
+ url="https://example.com",
+ allowed_proxy_countries=["US"],
+ )
+ assert_matches_type(SiteConfigResponse, site_config, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ async def test_raw_response_resolve(self, async_client: AsyncKernel) -> None:
+ response = await async_client.site_configs.with_raw_response.resolve(
+ url="https://example.com",
+ )
+
+ assert response.is_closed is True
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+ site_config = await response.parse()
+ assert_matches_type(SiteConfigResponse, site_config, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ async def test_streaming_response_resolve(self, async_client: AsyncKernel) -> None:
+ async with async_client.site_configs.with_streaming_response.resolve(
+ url="https://example.com",
+ ) as response:
+ assert not response.is_closed
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+
+ site_config = await response.parse()
+ assert_matches_type(SiteConfigResponse, site_config, path=["response"])
+
+ assert cast(Any, response.is_closed) is True
From 27c5a0a75ade568f1feb7e058070771ae1f6db60 Mon Sep 17 00:00:00 2001
From: "kernel-internal[bot]"
<260533166+kernel-internal[bot]@users.noreply.github.com>
Date: Fri, 21 Aug 2026 15:13:33 +0000
Subject: [PATCH 211/366] feat: Mirror the control/platform telemetry split
into the public API
Stainless-Generated-From: 84de8903f89fe67101549c27d1d3c4b16b3324df
---
api.md | 6 +
src/kernel/resources/browsers/browsers.py | 4 +-
src/kernel/resources/browsers/telemetry.py | 2 +
src/kernel/resources/proxies.py | 10 +-
.../types/auth/connection_create_params.py | 14 +-
.../types/auth/connection_login_params.py | 14 +-
.../types/auth/connection_update_params.py | 14 +-
src/kernel/types/auth/managed_auth.py | 14 +-
.../types/auth/managed_auth_browser_config.py | 14 +-
.../auth/managed_auth_browser_config_param.py | 14 +-
src/kernel/types/browser_create_params.py | 16 +-
.../types/browser_pool_acquire_params.py | 14 +-
.../types/browser_pool_create_params.py | 14 +-
.../types/browser_pool_update_params.py | 14 +-
src/kernel/types/browser_update_params.py | 14 +-
src/kernel/types/browsers/__init__.py | 12 +
.../types/browsers/browser_api_call_event.py | 16 +-
.../browsers/browser_cdp_command_event.py | 1883 +++++++++++++++++
.../browsers/browser_cdp_command_method.py | 46 +
.../browsers/browser_cdp_connect_event.py | 13 +-
.../browsers/browser_cdp_disconnect_event.py | 24 +
.../browsers/browser_page_crashed_event.py | 41 +
.../browser_platform_api_call_event.py | 47 +
.../browser_telemetry_categories_config.py | 20 +-
...owser_telemetry_categories_config_param.py | 20 +-
.../browser_telemetry_category_config.py | 4 +-
...browser_telemetry_category_config_param.py | 4 +-
.../browser_telemetry_cdp_control_config.py | 24 +
...wser_telemetry_cdp_control_config_param.py | 26 +
.../browser_telemetry_control_config.py | 28 +
.../browser_telemetry_control_config_param.py | 29 +
.../types/browsers/browser_telemetry_event.py | 6 +
.../types/browsers/telemetry_events_params.py | 1 +
tests/api_resources/auth/test_connections.py | 72 +-
tests/api_resources/test_browser_pools.py | 36 +-
tests/api_resources/test_browsers.py | 24 +-
36 files changed, 2431 insertions(+), 123 deletions(-)
create mode 100644 src/kernel/types/browsers/browser_cdp_command_event.py
create mode 100644 src/kernel/types/browsers/browser_cdp_command_method.py
create mode 100644 src/kernel/types/browsers/browser_page_crashed_event.py
create mode 100644 src/kernel/types/browsers/browser_platform_api_call_event.py
create mode 100644 src/kernel/types/browsers/browser_telemetry_cdp_control_config.py
create mode 100644 src/kernel/types/browsers/browser_telemetry_cdp_control_config_param.py
create mode 100644 src/kernel/types/browsers/browser_telemetry_control_config.py
create mode 100644 src/kernel/types/browsers/browser_telemetry_control_config_param.py
diff --git a/api.md b/api.md
index 6f8c4256..4d75e371 100644
--- a/api.md
+++ b/api.md
@@ -117,6 +117,8 @@ from kernel.types.browsers import (
BrowserAPICallEvent,
BrowserCallStack,
BrowserCaptchaSolveResultEvent,
+ BrowserCdpCommandEvent,
+ BrowserCdpCommandMethod,
BrowserCdpConnectEvent,
BrowserCdpDisconnectEvent,
BrowserConsoleErrorEvent,
@@ -138,6 +140,7 @@ from kernel.types.browsers import (
BrowserNetworkLoadingFailedEvent,
BrowserNetworkRequestEvent,
BrowserNetworkResponseEvent,
+ BrowserPageCrashedEvent,
BrowserPageDomContentLoadedEvent,
BrowserPageLayoutSettledEvent,
BrowserPageLayoutShiftEvent,
@@ -146,12 +149,15 @@ from kernel.types.browsers import (
BrowserPageNavigationEvent,
BrowserPageNavigationSettledEvent,
BrowserPageTabOpenedEvent,
+ BrowserPlatformAPICallEvent,
BrowserProxyErrorEvent,
BrowserServiceCrashedEvent,
BrowserSystemOomKillEvent,
BrowserTelemetryCategoriesConfig,
BrowserTelemetryCategoryConfig,
+ BrowserTelemetryCdpControlConfig,
BrowserTelemetryConfig,
+ BrowserTelemetryControlConfig,
BrowserTelemetryEvent,
BrowserTelemetryExportConfig,
BrowserTelemetryOtlpExportConfig,
diff --git a/src/kernel/resources/browsers/browsers.py b/src/kernel/resources/browsers/browsers.py
index 8078b952..0a15061a 100644
--- a/src/kernel/resources/browsers/browsers.py
+++ b/src/kernel/resources/browsers/browsers.py
@@ -208,7 +208,7 @@ def create(
extensions: List of browser extensions to load into the session. Provide each by id or name.
gpu: If true, enables GPU acceleration for the browser session. Requires Start-Up or
- Enterprise plan and headless=false.
+ Enterprise plan, headless=false, and region=us-east.
headless: If true, launches the browser using a headless image (no VNC/GUI). Defaults to
false.
@@ -828,7 +828,7 @@ async def create(
extensions: List of browser extensions to load into the session. Provide each by id or name.
gpu: If true, enables GPU acceleration for the browser session. Requires Start-Up or
- Enterprise plan and headless=false.
+ Enterprise plan, headless=false, and region=us-east.
headless: If true, launches the browser using a headless image (no VNC/GUI). Defaults to
false.
diff --git a/src/kernel/resources/browsers/telemetry.py b/src/kernel/resources/browsers/telemetry.py
index 5ed61557..c6fa04ea 100644
--- a/src/kernel/resources/browsers/telemetry.py
+++ b/src/kernel/resources/browsers/telemetry.py
@@ -62,6 +62,7 @@ def events(
"page",
"interaction",
"control",
+ "platform",
"connection",
"system",
"screenshot",
@@ -238,6 +239,7 @@ def events(
"page",
"interaction",
"control",
+ "platform",
"connection",
"system",
"screenshot",
diff --git a/src/kernel/resources/proxies.py b/src/kernel/resources/proxies.py
index 08f97e37..ee9f9084 100644
--- a/src/kernel/resources/proxies.py
+++ b/src/kernel/resources/proxies.py
@@ -252,7 +252,10 @@ def delete(
) -> None:
"""Soft delete a proxy.
- Sessions referencing it are not modified.
+ Session records referencing it are not modified. If egress
+ binding polling is enabled, existing tunnels for active sessions using the proxy
+ are terminated within one polling interval; subsequent connections through the
+ deleted proxy are rejected.
Args:
extra_headers: Send extra headers
@@ -553,7 +556,10 @@ async def delete(
) -> None:
"""Soft delete a proxy.
- Sessions referencing it are not modified.
+ Session records referencing it are not modified. If egress
+ binding polling is enabled, existing tunnels for active sessions using the proxy
+ are terminated within one polling interval; subsequent connections through the
+ deleted proxy are rejected.
Args:
extra_headers: Send extra headers
diff --git a/src/kernel/types/auth/connection_create_params.py b/src/kernel/types/auth/connection_create_params.py
index 2fdc808b..54995d37 100644
--- a/src/kernel/types/auth/connection_create_params.py
+++ b/src/kernel/types/auth/connection_create_params.py
@@ -175,13 +175,13 @@ class BrowserTelemetry(TypedDict, total=False):
The operational categories (control, connection, system, captcha) are captured
whenever telemetry is enabled; set one to enabled=false to opt out. The CDP
- categories (console, network, page, interaction) and screenshot are off by
- default; set enabled=true to opt in. On create, provided categories layer onto
- the default set. On update, provided categories merge onto the session's current
- config; when no telemetry is active this falls back to the default set (matching
- create). If browser is omitted or empty, the default set is used. A browser
- config that disables every category stops capture on update and starts no
- capture on create.
+ categories (console, network, page, interaction), screenshot and platform are
+ off by default; set enabled=true to opt in. On create, provided categories layer
+ onto the default set. On update, provided categories merge onto the session's
+ current config; when no telemetry is active this falls back to the default set
+ (matching create). If browser is omitted or empty, the default set is used. A
+ browser config that disables every category stops capture on update and starts
+ no capture on create.
"""
enabled: bool
diff --git a/src/kernel/types/auth/connection_login_params.py b/src/kernel/types/auth/connection_login_params.py
index c26b020f..cbe2bf3b 100644
--- a/src/kernel/types/auth/connection_login_params.py
+++ b/src/kernel/types/auth/connection_login_params.py
@@ -96,13 +96,13 @@ class BrowserTelemetry(TypedDict, total=False):
The operational categories (control, connection, system, captcha) are captured
whenever telemetry is enabled; set one to enabled=false to opt out. The CDP
- categories (console, network, page, interaction) and screenshot are off by
- default; set enabled=true to opt in. On create, provided categories layer onto
- the default set. On update, provided categories merge onto the session's current
- config; when no telemetry is active this falls back to the default set (matching
- create). If browser is omitted or empty, the default set is used. A browser
- config that disables every category stops capture on update and starts no
- capture on create.
+ categories (console, network, page, interaction), screenshot and platform are
+ off by default; set enabled=true to opt in. On create, provided categories layer
+ onto the default set. On update, provided categories merge onto the session's
+ current config; when no telemetry is active this falls back to the default set
+ (matching create). If browser is omitted or empty, the default set is used. A
+ browser config that disables every category stops capture on update and starts
+ no capture on create.
"""
enabled: bool
diff --git a/src/kernel/types/auth/connection_update_params.py b/src/kernel/types/auth/connection_update_params.py
index ad3fbe44..829cf489 100644
--- a/src/kernel/types/auth/connection_update_params.py
+++ b/src/kernel/types/auth/connection_update_params.py
@@ -135,13 +135,13 @@ class BrowserTelemetry(TypedDict, total=False):
The operational categories (control, connection, system, captcha) are captured
whenever telemetry is enabled; set one to enabled=false to opt out. The CDP
- categories (console, network, page, interaction) and screenshot are off by
- default; set enabled=true to opt in. On create, provided categories layer onto
- the default set. On update, provided categories merge onto the session's current
- config; when no telemetry is active this falls back to the default set (matching
- create). If browser is omitted or empty, the default set is used. A browser
- config that disables every category stops capture on update and starts no
- capture on create.
+ categories (console, network, page, interaction), screenshot and platform are
+ off by default; set enabled=true to opt in. On create, provided categories layer
+ onto the default set. On update, provided categories merge onto the session's
+ current config; when no telemetry is active this falls back to the default set
+ (matching create). If browser is omitted or empty, the default set is used. A
+ browser config that disables every category stops capture on update and starts
+ no capture on create.
"""
enabled: bool
diff --git a/src/kernel/types/auth/managed_auth.py b/src/kernel/types/auth/managed_auth.py
index 23e45d6b..7c4ba7d6 100644
--- a/src/kernel/types/auth/managed_auth.py
+++ b/src/kernel/types/auth/managed_auth.py
@@ -79,13 +79,13 @@ class BrowserTelemetry(BaseModel):
The operational categories (control, connection, system, captcha) are captured
whenever telemetry is enabled; set one to enabled=false to opt out. The CDP
- categories (console, network, page, interaction) and screenshot are off by
- default; set enabled=true to opt in. On create, provided categories layer onto
- the default set. On update, provided categories merge onto the session's current
- config; when no telemetry is active this falls back to the default set (matching
- create). If browser is omitted or empty, the default set is used. A browser
- config that disables every category stops capture on update and starts no
- capture on create.
+ categories (console, network, page, interaction), screenshot and platform are
+ off by default; set enabled=true to opt in. On create, provided categories layer
+ onto the default set. On update, provided categories merge onto the session's
+ current config; when no telemetry is active this falls back to the default set
+ (matching create). If browser is omitted or empty, the default set is used. A
+ browser config that disables every category stops capture on update and starts
+ no capture on create.
"""
enabled: Optional[bool] = None
diff --git a/src/kernel/types/auth/managed_auth_browser_config.py b/src/kernel/types/auth/managed_auth_browser_config.py
index a4d63de7..c7e565d1 100644
--- a/src/kernel/types/auth/managed_auth_browser_config.py
+++ b/src/kernel/types/auth/managed_auth_browser_config.py
@@ -67,13 +67,13 @@ class Telemetry(BaseModel):
The operational categories (control, connection, system, captcha) are captured
whenever telemetry is enabled; set one to enabled=false to opt out. The CDP
- categories (console, network, page, interaction) and screenshot are off by
- default; set enabled=true to opt in. On create, provided categories layer onto
- the default set. On update, provided categories merge onto the session's current
- config; when no telemetry is active this falls back to the default set (matching
- create). If browser is omitted or empty, the default set is used. A browser
- config that disables every category stops capture on update and starts no
- capture on create.
+ categories (console, network, page, interaction), screenshot and platform are
+ off by default; set enabled=true to opt in. On create, provided categories layer
+ onto the default set. On update, provided categories merge onto the session's
+ current config; when no telemetry is active this falls back to the default set
+ (matching create). If browser is omitted or empty, the default set is used. A
+ browser config that disables every category stops capture on update and starts
+ no capture on create.
"""
enabled: Optional[bool] = None
diff --git a/src/kernel/types/auth/managed_auth_browser_config_param.py b/src/kernel/types/auth/managed_auth_browser_config_param.py
index 7f8650e0..bfd09f95 100644
--- a/src/kernel/types/auth/managed_auth_browser_config_param.py
+++ b/src/kernel/types/auth/managed_auth_browser_config_param.py
@@ -69,13 +69,13 @@ class Telemetry(TypedDict, total=False):
The operational categories (control, connection, system, captcha) are captured
whenever telemetry is enabled; set one to enabled=false to opt out. The CDP
- categories (console, network, page, interaction) and screenshot are off by
- default; set enabled=true to opt in. On create, provided categories layer onto
- the default set. On update, provided categories merge onto the session's current
- config; when no telemetry is active this falls back to the default set (matching
- create). If browser is omitted or empty, the default set is used. A browser
- config that disables every category stops capture on update and starts no
- capture on create.
+ categories (console, network, page, interaction), screenshot and platform are
+ off by default; set enabled=true to opt in. On create, provided categories layer
+ onto the default set. On update, provided categories merge onto the session's
+ current config; when no telemetry is active this falls back to the default set
+ (matching create). If browser is omitted or empty, the default set is used. A
+ browser config that disables every category stops capture on update and starts
+ no capture on create.
"""
enabled: bool
diff --git a/src/kernel/types/browser_create_params.py b/src/kernel/types/browser_create_params.py
index 2899fb7c..85e0b9bd 100644
--- a/src/kernel/types/browser_create_params.py
+++ b/src/kernel/types/browser_create_params.py
@@ -41,7 +41,7 @@ class BrowserCreateParams(TypedDict, total=False):
gpu: bool
"""If true, enables GPU acceleration for the browser session.
- Requires Start-Up or Enterprise plan and headless=false.
+ Requires Start-Up or Enterprise plan, headless=false, and region=us-east.
"""
headless: bool
@@ -221,13 +221,13 @@ class Telemetry(TypedDict, total=False):
The operational categories (control, connection, system, captcha) are captured
whenever telemetry is enabled; set one to enabled=false to opt out. The CDP
- categories (console, network, page, interaction) and screenshot are off by
- default; set enabled=true to opt in. On create, provided categories layer onto
- the default set. On update, provided categories merge onto the session's current
- config; when no telemetry is active this falls back to the default set (matching
- create). If browser is omitted or empty, the default set is used. A browser
- config that disables every category stops capture on update and starts no
- capture on create.
+ categories (console, network, page, interaction), screenshot and platform are
+ off by default; set enabled=true to opt in. On create, provided categories layer
+ onto the default set. On update, provided categories merge onto the session's
+ current config; when no telemetry is active this falls back to the default set
+ (matching create). If browser is omitted or empty, the default set is used. A
+ browser config that disables every category stops capture on update and starts
+ no capture on create.
"""
enabled: bool
diff --git a/src/kernel/types/browser_pool_acquire_params.py b/src/kernel/types/browser_pool_acquire_params.py
index bd50be1b..b5bd708d 100644
--- a/src/kernel/types/browser_pool_acquire_params.py
+++ b/src/kernel/types/browser_pool_acquire_params.py
@@ -115,13 +115,13 @@ class Telemetry(TypedDict, total=False):
The operational categories (control, connection, system, captcha) are captured
whenever telemetry is enabled; set one to enabled=false to opt out. The CDP
- categories (console, network, page, interaction) and screenshot are off by
- default; set enabled=true to opt in. On create, provided categories layer onto
- the default set. On update, provided categories merge onto the session's current
- config; when no telemetry is active this falls back to the default set (matching
- create). If browser is omitted or empty, the default set is used. A browser
- config that disables every category stops capture on update and starts no
- capture on create.
+ categories (console, network, page, interaction), screenshot and platform are
+ off by default; set enabled=true to opt in. On create, provided categories layer
+ onto the default set. On update, provided categories merge onto the session's
+ current config; when no telemetry is active this falls back to the default set
+ (matching create). If browser is omitted or empty, the default set is used. A
+ browser config that disables every category stops capture on update and starts
+ no capture on create.
"""
enabled: bool
diff --git a/src/kernel/types/browser_pool_create_params.py b/src/kernel/types/browser_pool_create_params.py
index 474e4243..3a736fbc 100644
--- a/src/kernel/types/browser_pool_create_params.py
+++ b/src/kernel/types/browser_pool_create_params.py
@@ -216,13 +216,13 @@ class Telemetry(TypedDict, total=False):
The operational categories (control, connection, system, captcha) are captured
whenever telemetry is enabled; set one to enabled=false to opt out. The CDP
- categories (console, network, page, interaction) and screenshot are off by
- default; set enabled=true to opt in. On create, provided categories layer onto
- the default set. On update, provided categories merge onto the session's current
- config; when no telemetry is active this falls back to the default set (matching
- create). If browser is omitted or empty, the default set is used. A browser
- config that disables every category stops capture on update and starts no
- capture on create.
+ categories (console, network, page, interaction), screenshot and platform are
+ off by default; set enabled=true to opt in. On create, provided categories layer
+ onto the default set. On update, provided categories merge onto the session's
+ current config; when no telemetry is active this falls back to the default set
+ (matching create). If browser is omitted or empty, the default set is used. A
+ browser config that disables every category stops capture on update and starts
+ no capture on create.
"""
enabled: bool
diff --git a/src/kernel/types/browser_pool_update_params.py b/src/kernel/types/browser_pool_update_params.py
index 36693003..9bf898f2 100644
--- a/src/kernel/types/browser_pool_update_params.py
+++ b/src/kernel/types/browser_pool_update_params.py
@@ -226,13 +226,13 @@ class Telemetry(TypedDict, total=False):
The operational categories (control, connection, system, captcha) are captured
whenever telemetry is enabled; set one to enabled=false to opt out. The CDP
- categories (console, network, page, interaction) and screenshot are off by
- default; set enabled=true to opt in. On create, provided categories layer onto
- the default set. On update, provided categories merge onto the session's current
- config; when no telemetry is active this falls back to the default set (matching
- create). If browser is omitted or empty, the default set is used. A browser
- config that disables every category stops capture on update and starts no
- capture on create.
+ categories (console, network, page, interaction), screenshot and platform are
+ off by default; set enabled=true to opt in. On create, provided categories layer
+ onto the default set. On update, provided categories merge onto the session's
+ current config; when no telemetry is active this falls back to the default set
+ (matching create). If browser is omitted or empty, the default set is used. A
+ browser config that disables every category stops capture on update and starts
+ no capture on create.
"""
enabled: bool
diff --git a/src/kernel/types/browser_update_params.py b/src/kernel/types/browser_update_params.py
index a9ea675c..e7769e61 100644
--- a/src/kernel/types/browser_update_params.py
+++ b/src/kernel/types/browser_update_params.py
@@ -136,13 +136,13 @@ class Telemetry(TypedDict, total=False):
The operational categories (control, connection, system, captcha) are captured
whenever telemetry is enabled; set one to enabled=false to opt out. The CDP
- categories (console, network, page, interaction) and screenshot are off by
- default; set enabled=true to opt in. On create, provided categories layer onto
- the default set. On update, provided categories merge onto the session's current
- config; when no telemetry is active this falls back to the default set (matching
- create). If browser is omitted or empty, the default set is used. A browser
- config that disables every category stops capture on update and starts no
- capture on create.
+ categories (console, network, page, interaction), screenshot and platform are
+ off by default; set enabled=true to opt in. On create, provided categories layer
+ onto the default set. On update, provided categories merge onto the session's
+ current config; when no telemetry is active this falls back to the default set
+ (matching create). If browser is omitted or empty, the default set is used. A
+ browser config that disables every category stops capture on update and starts
+ no capture on create.
"""
enabled: bool
diff --git a/src/kernel/types/browsers/__init__.py b/src/kernel/types/browsers/__init__.py
index 8ed451a0..a6d6eb74 100644
--- a/src/kernel/types/browsers/__init__.py
+++ b/src/kernel/types/browsers/__init__.py
@@ -40,6 +40,7 @@
from .telemetry_events_params import TelemetryEventsParams as TelemetryEventsParams
from .telemetry_stream_params import TelemetryStreamParams as TelemetryStreamParams
from .browser_telemetry_config import BrowserTelemetryConfig as BrowserTelemetryConfig
+from .browser_cdp_command_event import BrowserCdpCommandEvent as BrowserCdpCommandEvent
from .browser_cdp_connect_event import BrowserCdpConnectEvent as BrowserCdpConnectEvent
from .browser_console_log_event import BrowserConsoleLogEvent as BrowserConsoleLogEvent
from .browser_proxy_error_event import BrowserProxyErrorEvent as BrowserProxyErrorEvent
@@ -51,7 +52,9 @@
from .playwright_execute_params import PlaywrightExecuteParams as PlaywrightExecuteParams
from .telemetry_events_response import TelemetryEventsResponse as TelemetryEventsResponse
from .telemetry_stream_response import TelemetryStreamResponse as TelemetryStreamResponse
+from .browser_cdp_command_method import BrowserCdpCommandMethod as BrowserCdpCommandMethod
from .browser_network_idle_event import BrowserNetworkIdleEvent as BrowserNetworkIdleEvent
+from .browser_page_crashed_event import BrowserPageCrashedEvent as BrowserPageCrashedEvent
from .computer_drag_mouse_params import ComputerDragMouseParams as ComputerDragMouseParams
from .computer_move_mouse_params import ComputerMoveMouseParams as ComputerMoveMouseParams
from .browser_console_error_event import BrowserConsoleErrorEvent as BrowserConsoleErrorEvent
@@ -70,9 +73,11 @@
from .browser_interaction_click_event import BrowserInteractionClickEvent as BrowserInteractionClickEvent
from .browser_live_view_connect_event import BrowserLiveViewConnectEvent as BrowserLiveViewConnectEvent
from .browser_page_layout_shift_event import BrowserPageLayoutShiftEvent as BrowserPageLayoutShiftEvent
+from .browser_platform_api_call_event import BrowserPlatformAPICallEvent as BrowserPlatformAPICallEvent
from .browser_telemetry_export_config import BrowserTelemetryExportConfig as BrowserTelemetryExportConfig
from .computer_write_clipboard_params import ComputerWriteClipboardParams as ComputerWriteClipboardParams
from .browser_monitor_screenshot_event import BrowserMonitorScreenshotEvent as BrowserMonitorScreenshotEvent
+from .browser_telemetry_control_config import BrowserTelemetryControlConfig as BrowserTelemetryControlConfig
from .computer_read_clipboard_response import ComputerReadClipboardResponse as ComputerReadClipboardResponse
from .browser_monitor_init_failed_event import BrowserMonitorInitFailedEvent as BrowserMonitorInitFailedEvent
from .browser_monitor_reconnected_event import BrowserMonitorReconnectedEvent as BrowserMonitorReconnectedEvent
@@ -84,6 +89,7 @@
from .computer_capture_screenshot_params import ComputerCaptureScreenshotParams as ComputerCaptureScreenshotParams
from .browser_telemetry_categories_config import BrowserTelemetryCategoriesConfig as BrowserTelemetryCategoriesConfig
from .browser_network_loading_failed_event import BrowserNetworkLoadingFailedEvent as BrowserNetworkLoadingFailedEvent
+from .browser_telemetry_cdp_control_config import BrowserTelemetryCdpControlConfig as BrowserTelemetryCdpControlConfig
from .browser_telemetry_otlp_export_config import BrowserTelemetryOtlpExportConfig as BrowserTelemetryOtlpExportConfig
from .computer_get_mouse_position_response import ComputerGetMousePositionResponse as ComputerGetMousePositionResponse
from .browser_page_dom_content_loaded_event import BrowserPageDomContentLoadedEvent as BrowserPageDomContentLoadedEvent
@@ -96,6 +102,9 @@
from .browser_monitor_reconnect_failed_event import (
BrowserMonitorReconnectFailedEvent as BrowserMonitorReconnectFailedEvent,
)
+from .browser_telemetry_control_config_param import (
+ BrowserTelemetryControlConfigParam as BrowserTelemetryControlConfigParam,
+)
from .browser_telemetry_category_config_param import (
BrowserTelemetryCategoryConfigParam as BrowserTelemetryCategoryConfigParam,
)
@@ -108,3 +117,6 @@
from .browser_telemetry_categories_config_param import (
BrowserTelemetryCategoriesConfigParam as BrowserTelemetryCategoriesConfigParam,
)
+from .browser_telemetry_cdp_control_config_param import (
+ BrowserTelemetryCdpControlConfigParam as BrowserTelemetryCdpControlConfigParam,
+)
diff --git a/src/kernel/types/browsers/browser_api_call_event.py b/src/kernel/types/browsers/browser_api_call_event.py
index 702596d8..b4210968 100644
--- a/src/kernel/types/browsers/browser_api_call_event.py
+++ b/src/kernel/types/browsers/browser_api_call_event.py
@@ -14,7 +14,10 @@ class Data(BaseModel):
"""Wall-clock duration of the handler in milliseconds."""
operation_id: str
- """OpenAPI operationId of the matched route (e.g. processExec, takeScreenshot)."""
+ """Matched route's operation, named as the in-VM API names its handler (e.g.
+
+ ProcessExec, TakeScreenshot).
+ """
request_id: str
"""Per-request identifier from the in-VM API request middleware."""
@@ -22,9 +25,18 @@ class Data(BaseModel):
status: int
"""HTTP response status code."""
+ code: Optional[str] = None
+ """
+ Source submitted to the Playwright code-execution endpoint, capped at 8192 bytes
+ like every other captured string. A capped value is cut on a character boundary
+ and ends in `...[truncated]`. Absent for every other operation.
+ """
+
class BrowserAPICallEvent(BaseModel):
- """An agent-driven HTTP call handled by the in-VM API server."""
+ """
+ An agent-driven HTTP call that drives the browser, handled by the in-VM API server. Calls that manage the VM instead emit platform_api_call.
+ """
category: Literal["control"]
diff --git a/src/kernel/types/browsers/browser_cdp_command_event.py b/src/kernel/types/browsers/browser_cdp_command_event.py
new file mode 100644
index 00000000..045be40f
--- /dev/null
+++ b/src/kernel/types/browsers/browser_cdp_command_event.py
@@ -0,0 +1,1883 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from typing import List, Union, Optional
+from typing_extensions import Literal, Annotated, TypeAlias
+
+from ..._utils import PropertyInfo
+from ..._models import BaseModel
+from .browser_event_source import BrowserEventSource
+
+__all__ = [
+ "BrowserCdpCommandEvent",
+ "Data",
+ "DataBrowserCdpInputDispatchMouseEventCommandData",
+ "DataBrowserCdpInputDispatchKeyEventCommandData",
+ "DataBrowserCdpInputInsertTextCommandData",
+ "DataBrowserCdpInputImeSetCompositionCommandData",
+ "DataBrowserCdpInputDispatchTouchEventCommandData",
+ "DataBrowserCdpInputDispatchDragEventCommandData",
+ "DataBrowserCdpInputCancelDraggingCommandData",
+ "DataBrowserCdpInputEmulateTouchFromMouseEventCommandData",
+ "DataBrowserCdpInputSynthesizePinchGestureCommandData",
+ "DataBrowserCdpInputSynthesizeScrollGestureCommandData",
+ "DataBrowserCdpInputSynthesizeTapGestureCommandData",
+ "DataBrowserCdpDomSetFileInputFilesCommandData",
+ "DataBrowserCdpDomFocusCommandData",
+ "DataBrowserCdpDomScrollIntoViewIfNeededCommandData",
+ "DataBrowserCdpPageBringToFrontCommandData",
+ "DataBrowserCdpPageCaptureScreenshotCommandData",
+ "DataBrowserCdpPageCaptureSnapshotCommandData",
+ "DataBrowserCdpPageHandleJavaScriptDialogCommandData",
+ "DataBrowserCdpPageNavigateCommandData",
+ "DataBrowserCdpPageNavigateToHistoryEntryCommandData",
+ "DataBrowserCdpPageReloadCommandData",
+ "DataBrowserCdpPagePrintToPdfCommandData",
+ "DataBrowserCdpPageStartScreencastCommandData",
+ "DataBrowserCdpPageStopScreencastCommandData",
+ "DataBrowserCdpPageStopLoadingCommandData",
+ "DataBrowserCdpPageCloseCommandData",
+ "DataBrowserCdpPageSetWebLifecycleStateCommandData",
+ "DataBrowserCdpTargetActivateTargetCommandData",
+ "DataBrowserCdpTargetCloseTargetCommandData",
+ "DataBrowserCdpTargetCreateTargetCommandData",
+ "DataBrowserCdpTargetCreateBrowserContextCommandData",
+ "DataBrowserCdpTargetDisposeBrowserContextCommandData",
+ "DataBrowserCdpTargetOpenDevToolsCommandData",
+ "DataBrowserCdpBrowserCancelDownloadCommandData",
+ "DataBrowserCdpBrowserCloseCommandData",
+ "DataBrowserCdpBrowserSetWindowBoundsCommandData",
+ "DataBrowserCdpBrowserSetContentsSizeCommandData",
+ "DataBrowserCdpAutofillTriggerCommandData",
+]
+
+
+class DataBrowserCdpInputDispatchMouseEventCommandData(BaseModel):
+ """Sanitized `Input.dispatchMouseEvent` arguments.
+
+ Canonical input: `Input.dispatchMouseEvent` in devtools-protocol@2d019e73, pinned at https://github.com/ChromeDevTools/devtools-protocol/blob/2d019e73eb371d1d6985d26d395d78bd8f8a22ba/json/browser_protocol.json. Every argument of this command has a retained or redacted decision in lib/devtoolsproxy/testdata/cdp_arguments.yaml.
+ """
+
+ event_type: Literal["mousePressed", "mouseReleased", "mouseMoved", "mouseWheel", "other"]
+ """Mouse event phase: `mousePressed`, `mouseReleased`, `mouseMoved` or
+ `mouseWheel`.
+
+ A value the protocol does not define is reported as `other`.
+ """
+
+ method: Literal["Input.dispatchMouseEvent"]
+
+ button: Optional[Literal["none", "left", "middle", "right", "back", "forward", "other"]] = None
+ """
+ Button named by the command (`none`, `left`, `middle`, `right`, `back`,
+ `forward`). A value the protocol does not define is reported as `other`.
+ """
+
+ buttons: Optional[int] = None
+ """Bit field of buttons held down.
+
+ Non-zero on a `mouseMoved` means the move is a drag path.
+ """
+
+ click_count: Optional[int] = None
+ """Number of times the button was clicked (2 is a double click)."""
+
+ command_id: Optional[int] = None
+ """
+ The command's JSON-RPC id, so the command can be joined to the result the
+ browser returned for it. Absent when the client sent none.
+ """
+
+ connection_id: Optional[str] = None
+ """
+ Identifies the CDP proxy connection the command arrived on, matching
+ `cdp_connect` and `cdp_disconnect`. Two clients driving the same browser are
+ told apart by this.
+ """
+
+ delta_x: Optional[float] = None
+ """Horizontal scroll delta, for `mouseWheel`."""
+
+ delta_y: Optional[float] = None
+ """Vertical scroll delta, for `mouseWheel`."""
+
+ force: Optional[float] = None
+ """Normalized pressure, 0 to 1."""
+
+ modifiers: Optional[int] = None
+ """Bit field of held modifier keys (1=Alt, 2=Ctrl, 4=Meta, 8=Shift)."""
+
+ pointer_type: Optional[Literal["mouse", "pen", "other"]] = None
+ """Pointer that generated the event (`mouse` or `pen`).
+
+ A value the protocol does not define is reported as `other`.
+ """
+
+ session_id: Optional[str] = None
+ """CDP session identifier the command was addressed to.
+
+ Absent for browser-level commands. Clipped to 128 characters.
+ """
+
+ tangential_pressure: Optional[float] = None
+ """Normalized tangential pressure, -1 to 1."""
+
+ tilt_x: Optional[float] = None
+ """Pen tilt from the Y-Z plane, in degrees."""
+
+ tilt_y: Optional[float] = None
+ """Pen tilt from the X-Z plane, in degrees."""
+
+ twist: Optional[int] = None
+ """Pen clockwise rotation, in degrees."""
+
+ x: Optional[float] = None
+ """Viewport x coordinate in CSS pixels."""
+
+ y: Optional[float] = None
+ """Viewport y coordinate in CSS pixels."""
+
+
+class DataBrowserCdpInputDispatchKeyEventCommandData(BaseModel):
+ """Sanitized `Input.dispatchKeyEvent` arguments.
+
+ Canonical input: `Input.dispatchKeyEvent` in devtools-protocol@2d019e73, pinned at https://github.com/ChromeDevTools/devtools-protocol/blob/2d019e73eb371d1d6985d26d395d78bd8f8a22ba/json/browser_protocol.json. Every argument of this command has a retained or redacted decision in lib/devtoolsproxy/testdata/cdp_arguments.yaml.
+ """
+
+ event_type: Literal["keyDown", "keyUp", "rawKeyDown", "char", "other"]
+ """Key event phase: `keyDown`, `keyUp`, `rawKeyDown` or `char`.
+
+ A value the protocol does not define is reported as `other`.
+ """
+
+ method: Literal["Input.dispatchKeyEvent"]
+
+ auto_repeat: Optional[bool] = None
+ """Whether the event was generated by key repeat."""
+
+ command_count: Optional[int] = None
+ """Number of editing commands (e.g. `selectAll`) carried by the event."""
+
+ command_id: Optional[int] = None
+ """
+ The command's JSON-RPC id, so the command can be joined to the result the
+ browser returned for it. Absent when the client sent none.
+ """
+
+ connection_id: Optional[str] = None
+ """
+ Identifies the CDP proxy connection the command arrived on, matching
+ `cdp_connect` and `cdp_disconnect`. Two clients driving the same browser are
+ told apart by this.
+ """
+
+ is_keypad: Optional[bool] = None
+ """Whether the key is on the numeric keypad."""
+
+ is_system_key: Optional[bool] = None
+ """Whether the event is a system key event."""
+
+ location: Optional[int] = None
+ """Keyboard location (1=left, 2=right, 3=numpad)."""
+
+ modifiers: Optional[int] = None
+ """Bit field of held modifier keys (1=Alt, 2=Ctrl, 4=Meta, 8=Shift)."""
+
+ named_key: Optional[str] = None
+ """Key that commands the page rather than typing into it (e.g.
+
+ `Enter`, `Tab`, `ArrowDown`, `F5`). Keys that produce a character are never
+ captured; those are counted by `text_length`.
+ """
+
+ session_id: Optional[str] = None
+ """CDP session identifier the command was addressed to.
+
+ Absent for browser-level commands. Clipped to 128 characters.
+ """
+
+ text_length: Optional[int] = None
+ """Number of characters the command submitted. The text itself is never captured."""
+
+
+class DataBrowserCdpInputInsertTextCommandData(BaseModel):
+ """Sanitized `Input.insertText` arguments.
+
+ Canonical input: `Input.insertText` in devtools-protocol@2d019e73, pinned at https://github.com/ChromeDevTools/devtools-protocol/blob/2d019e73eb371d1d6985d26d395d78bd8f8a22ba/json/browser_protocol.json. Every argument of this command has a retained or redacted decision in lib/devtoolsproxy/testdata/cdp_arguments.yaml.
+ """
+
+ method: Literal["Input.insertText"]
+
+ text_length: int
+ """Number of characters inserted. The text itself is never captured."""
+
+ command_id: Optional[int] = None
+ """
+ The command's JSON-RPC id, so the command can be joined to the result the
+ browser returned for it. Absent when the client sent none.
+ """
+
+ connection_id: Optional[str] = None
+ """
+ Identifies the CDP proxy connection the command arrived on, matching
+ `cdp_connect` and `cdp_disconnect`. Two clients driving the same browser are
+ told apart by this.
+ """
+
+ session_id: Optional[str] = None
+ """CDP session identifier the command was addressed to.
+
+ Absent for browser-level commands. Clipped to 128 characters.
+ """
+
+
+class DataBrowserCdpInputImeSetCompositionCommandData(BaseModel):
+ """Sanitized `Input.imeSetComposition` arguments.
+
+ Canonical input: `Input.imeSetComposition` in devtools-protocol@2d019e73, pinned at https://github.com/ChromeDevTools/devtools-protocol/blob/2d019e73eb371d1d6985d26d395d78bd8f8a22ba/json/browser_protocol.json. Every argument of this command has a retained or redacted decision in lib/devtoolsproxy/testdata/cdp_arguments.yaml.
+ """
+
+ method: Literal["Input.imeSetComposition"]
+
+ text_length: int
+ """Number of characters in the composition. The text itself is never captured."""
+
+ command_id: Optional[int] = None
+ """
+ The command's JSON-RPC id, so the command can be joined to the result the
+ browser returned for it. Absent when the client sent none.
+ """
+
+ connection_id: Optional[str] = None
+ """
+ Identifies the CDP proxy connection the command arrived on, matching
+ `cdp_connect` and `cdp_disconnect`. Two clients driving the same browser are
+ told apart by this.
+ """
+
+ replacement_end: Optional[int] = None
+ """Replacement range end offset."""
+
+ replacement_start: Optional[int] = None
+ """Replacement range start offset."""
+
+ selection_end: Optional[int] = None
+ """Selection end offset within the composition."""
+
+ selection_start: Optional[int] = None
+ """Selection start offset within the composition."""
+
+ session_id: Optional[str] = None
+ """CDP session identifier the command was addressed to.
+
+ Absent for browser-level commands. Clipped to 128 characters.
+ """
+
+
+class DataBrowserCdpInputDispatchTouchEventCommandData(BaseModel):
+ """Sanitized `Input.dispatchTouchEvent` arguments.
+
+ Canonical input: `Input.dispatchTouchEvent` in devtools-protocol@2d019e73, pinned at https://github.com/ChromeDevTools/devtools-protocol/blob/2d019e73eb371d1d6985d26d395d78bd8f8a22ba/json/browser_protocol.json. Every argument of this command has a retained or redacted decision in lib/devtoolsproxy/testdata/cdp_arguments.yaml.
+ """
+
+ event_type: Literal["touchStart", "touchEnd", "touchMove", "touchCancel", "other"]
+ """Touch event phase: `touchStart`, `touchEnd`, `touchMove` or `touchCancel`.
+
+ A value the protocol does not define is reported as `other`.
+ """
+
+ method: Literal["Input.dispatchTouchEvent"]
+
+ touch_point_count: int
+ """Number of active touch points the command carried."""
+
+ command_id: Optional[int] = None
+ """
+ The command's JSON-RPC id, so the command can be joined to the result the
+ browser returned for it. Absent when the client sent none.
+ """
+
+ connection_id: Optional[str] = None
+ """
+ Identifies the CDP proxy connection the command arrived on, matching
+ `cdp_connect` and `cdp_disconnect`. Two clients driving the same browser are
+ told apart by this.
+ """
+
+ force: Optional[float] = None
+ """Normalized pressure of the first touch point, 0 to 1."""
+
+ modifiers: Optional[int] = None
+ """Bit field of held modifier keys (1=Alt, 2=Ctrl, 4=Meta, 8=Shift)."""
+
+ radius_x: Optional[float] = None
+ """Horizontal radius of the first touch point."""
+
+ radius_y: Optional[float] = None
+ """Vertical radius of the first touch point."""
+
+ rotation_angle: Optional[float] = None
+ """Rotation of the first touch point, in degrees."""
+
+ session_id: Optional[str] = None
+ """CDP session identifier the command was addressed to.
+
+ Absent for browser-level commands. Clipped to 128 characters.
+ """
+
+ tangential_pressure: Optional[float] = None
+ """Normalized tangential pressure of the first touch point, -1 to 1."""
+
+ tilt_x: Optional[float] = None
+ """Tilt of the first touch point from the Y-Z plane, in degrees."""
+
+ tilt_y: Optional[float] = None
+ """Tilt of the first touch point from the X-Z plane, in degrees."""
+
+ twist: Optional[int] = None
+ """Clockwise rotation of the first touch point, in degrees."""
+
+ x: Optional[float] = None
+ """Viewport x coordinate of the first touch point.
+
+ Touch coordinates live inside `touchPoints`, so this is the primary point rather
+ than a command-level argument.
+ """
+
+ y: Optional[float] = None
+ """Viewport y coordinate of the first touch point."""
+
+
+class DataBrowserCdpInputDispatchDragEventCommandData(BaseModel):
+ """Sanitized `Input.dispatchDragEvent` arguments.
+
+ Canonical input: `Input.dispatchDragEvent` in devtools-protocol@2d019e73, pinned at https://github.com/ChromeDevTools/devtools-protocol/blob/2d019e73eb371d1d6985d26d395d78bd8f8a22ba/json/browser_protocol.json. Every argument of this command has a retained or redacted decision in lib/devtoolsproxy/testdata/cdp_arguments.yaml.
+ """
+
+ event_type: Literal["dragEnter", "dragOver", "drop", "dragCancel", "other"]
+ """Drag event phase: `dragEnter`, `dragOver`, `drop` or `dragCancel`.
+
+ A value the protocol does not define is reported as `other`.
+ """
+
+ method: Literal["Input.dispatchDragEvent"]
+
+ command_id: Optional[int] = None
+ """
+ The command's JSON-RPC id, so the command can be joined to the result the
+ browser returned for it. Absent when the client sent none.
+ """
+
+ connection_id: Optional[str] = None
+ """
+ Identifies the CDP proxy connection the command arrived on, matching
+ `cdp_connect` and `cdp_disconnect`. Two clients driving the same browser are
+ told apart by this.
+ """
+
+ drag_file_count: Optional[int] = None
+ """Number of files in the drag payload. File paths are never captured."""
+
+ drag_item_count: Optional[int] = None
+ """Number of items in the drag payload. Item contents are never captured."""
+
+ drag_mime_categories: Optional[
+ List[
+ Literal["text", "image", "audio", "video", "application", "font", "model", "multipart", "message", "other"]
+ ]
+ ] = None
+ """Distinct top-level MIME categories of the drag items (e.g.
+
+ `text`, `image`, `application`). Subtypes and contents are never captured. A
+ value the protocol does not define is reported as `other`.
+ """
+
+ drag_operations_mask: Optional[int] = None
+ """Bit field of allowed drag operations (1=copy, 2=link, 16=move)."""
+
+ modifiers: Optional[int] = None
+ """Bit field of held modifier keys (1=Alt, 2=Ctrl, 4=Meta, 8=Shift)."""
+
+ session_id: Optional[str] = None
+ """CDP session identifier the command was addressed to.
+
+ Absent for browser-level commands. Clipped to 128 characters.
+ """
+
+ x: Optional[float] = None
+ """Viewport x coordinate in CSS pixels."""
+
+ y: Optional[float] = None
+ """Viewport y coordinate in CSS pixels."""
+
+
+class DataBrowserCdpInputCancelDraggingCommandData(BaseModel):
+ """Sanitized `Input.cancelDragging` arguments.
+
+ Canonical input: `Input.cancelDragging` in devtools-protocol@2d019e73, pinned at https://github.com/ChromeDevTools/devtools-protocol/blob/2d019e73eb371d1d6985d26d395d78bd8f8a22ba/json/browser_protocol.json. Every argument of this command has a retained or redacted decision in lib/devtoolsproxy/testdata/cdp_arguments.yaml.
+ """
+
+ method: Literal["Input.cancelDragging"]
+
+ command_id: Optional[int] = None
+ """
+ The command's JSON-RPC id, so the command can be joined to the result the
+ browser returned for it. Absent when the client sent none.
+ """
+
+ connection_id: Optional[str] = None
+ """
+ Identifies the CDP proxy connection the command arrived on, matching
+ `cdp_connect` and `cdp_disconnect`. Two clients driving the same browser are
+ told apart by this.
+ """
+
+ session_id: Optional[str] = None
+ """CDP session identifier the command was addressed to.
+
+ Absent for browser-level commands. Clipped to 128 characters.
+ """
+
+
+class DataBrowserCdpInputEmulateTouchFromMouseEventCommandData(BaseModel):
+ """Sanitized `Input.emulateTouchFromMouseEvent` arguments.
+
+ Canonical input: `Input.emulateTouchFromMouseEvent` in devtools-protocol@2d019e73, pinned at https://github.com/ChromeDevTools/devtools-protocol/blob/2d019e73eb371d1d6985d26d395d78bd8f8a22ba/json/browser_protocol.json. Every argument of this command has a retained or redacted decision in lib/devtoolsproxy/testdata/cdp_arguments.yaml.
+ """
+
+ event_type: Literal["mousePressed", "mouseReleased", "mouseMoved", "mouseWheel", "other"]
+ """Mouse event phase being emulated as touch.
+
+ A value the protocol does not define is reported as `other`.
+ """
+
+ method: Literal["Input.emulateTouchFromMouseEvent"]
+
+ button: Optional[Literal["none", "left", "middle", "right", "back", "forward", "other"]] = None
+ """Button named by the command.
+
+ A value the protocol does not define is reported as `other`.
+ """
+
+ click_count: Optional[int] = None
+ """Number of times the button was clicked."""
+
+ command_id: Optional[int] = None
+ """
+ The command's JSON-RPC id, so the command can be joined to the result the
+ browser returned for it. Absent when the client sent none.
+ """
+
+ connection_id: Optional[str] = None
+ """
+ Identifies the CDP proxy connection the command arrived on, matching
+ `cdp_connect` and `cdp_disconnect`. Two clients driving the same browser are
+ told apart by this.
+ """
+
+ delta_x: Optional[float] = None
+ """Horizontal scroll delta."""
+
+ delta_y: Optional[float] = None
+ """Vertical scroll delta."""
+
+ modifiers: Optional[int] = None
+ """Bit field of held modifier keys (1=Alt, 2=Ctrl, 4=Meta, 8=Shift)."""
+
+ session_id: Optional[str] = None
+ """CDP session identifier the command was addressed to.
+
+ Absent for browser-level commands. Clipped to 128 characters.
+ """
+
+ x: Optional[float] = None
+ """Viewport x coordinate in CSS pixels."""
+
+ y: Optional[float] = None
+ """Viewport y coordinate in CSS pixels."""
+
+
+class DataBrowserCdpInputSynthesizePinchGestureCommandData(BaseModel):
+ """Sanitized `Input.synthesizePinchGesture` arguments.
+
+ Canonical input: `Input.synthesizePinchGesture` in devtools-protocol@2d019e73, pinned at https://github.com/ChromeDevTools/devtools-protocol/blob/2d019e73eb371d1d6985d26d395d78bd8f8a22ba/json/browser_protocol.json. Every argument of this command has a retained or redacted decision in lib/devtoolsproxy/testdata/cdp_arguments.yaml.
+ """
+
+ method: Literal["Input.synthesizePinchGesture"]
+
+ command_id: Optional[int] = None
+ """
+ The command's JSON-RPC id, so the command can be joined to the result the
+ browser returned for it. Absent when the client sent none.
+ """
+
+ connection_id: Optional[str] = None
+ """
+ Identifies the CDP proxy connection the command arrived on, matching
+ `cdp_connect` and `cdp_disconnect`. Two clients driving the same browser are
+ told apart by this.
+ """
+
+ gesture_source_type: Optional[Literal["default", "touch", "mouse", "other"]] = None
+ """Input source the synthesized gesture emulates.
+
+ A value the protocol does not define is reported as `other`.
+ """
+
+ relative_speed: Optional[int] = None
+ """Relative pointer speed, in pixels per second."""
+
+ scale_factor: Optional[float] = None
+ """Relative scale of the pinch (>1 zooms in)."""
+
+ session_id: Optional[str] = None
+ """CDP session identifier the command was addressed to.
+
+ Absent for browser-level commands. Clipped to 128 characters.
+ """
+
+ x: Optional[float] = None
+ """Viewport x coordinate in CSS pixels."""
+
+ y: Optional[float] = None
+ """Viewport y coordinate in CSS pixels."""
+
+
+class DataBrowserCdpInputSynthesizeScrollGestureCommandData(BaseModel):
+ """Sanitized `Input.synthesizeScrollGesture` arguments.
+
+ Canonical input: `Input.synthesizeScrollGesture` in devtools-protocol@2d019e73, pinned at https://github.com/ChromeDevTools/devtools-protocol/blob/2d019e73eb371d1d6985d26d395d78bd8f8a22ba/json/browser_protocol.json. Every argument of this command has a retained or redacted decision in lib/devtoolsproxy/testdata/cdp_arguments.yaml.
+ """
+
+ method: Literal["Input.synthesizeScrollGesture"]
+
+ command_id: Optional[int] = None
+ """
+ The command's JSON-RPC id, so the command can be joined to the result the
+ browser returned for it. Absent when the client sent none.
+ """
+
+ connection_id: Optional[str] = None
+ """
+ Identifies the CDP proxy connection the command arrived on, matching
+ `cdp_connect` and `cdp_disconnect`. Two clients driving the same browser are
+ told apart by this.
+ """
+
+ gesture_source_type: Optional[Literal["default", "touch", "mouse", "other"]] = None
+ """Input source the synthesized gesture emulates.
+
+ A value the protocol does not define is reported as `other`.
+ """
+
+ prevent_fling: Optional[bool] = None
+ """Whether fling was suppressed."""
+
+ repeat_count: Optional[int] = None
+ """Number of additional repeats of the scroll."""
+
+ repeat_delay_ms: Optional[int] = None
+ """Delay between repeats, in milliseconds."""
+
+ session_id: Optional[str] = None
+ """CDP session identifier the command was addressed to.
+
+ Absent for browser-level commands. Clipped to 128 characters.
+ """
+
+ speed: Optional[int] = None
+ """Swipe speed in pixels per second."""
+
+ x: Optional[float] = None
+ """Viewport x coordinate in CSS pixels."""
+
+ x_distance: Optional[float] = None
+ """Horizontal scroll distance in CSS pixels; positive scrolls left."""
+
+ x_overscroll: Optional[float] = None
+ """Additional horizontal distance scrolled past the end."""
+
+ y: Optional[float] = None
+ """Viewport y coordinate in CSS pixels."""
+
+ y_distance: Optional[float] = None
+ """Vertical scroll distance in CSS pixels; positive scrolls up."""
+
+ y_overscroll: Optional[float] = None
+ """Additional vertical distance scrolled past the end."""
+
+
+class DataBrowserCdpInputSynthesizeTapGestureCommandData(BaseModel):
+ """Sanitized `Input.synthesizeTapGesture` arguments.
+
+ Canonical input: `Input.synthesizeTapGesture` in devtools-protocol@2d019e73, pinned at https://github.com/ChromeDevTools/devtools-protocol/blob/2d019e73eb371d1d6985d26d395d78bd8f8a22ba/json/browser_protocol.json. Every argument of this command has a retained or redacted decision in lib/devtoolsproxy/testdata/cdp_arguments.yaml.
+ """
+
+ method: Literal["Input.synthesizeTapGesture"]
+
+ command_id: Optional[int] = None
+ """
+ The command's JSON-RPC id, so the command can be joined to the result the
+ browser returned for it. Absent when the client sent none.
+ """
+
+ connection_id: Optional[str] = None
+ """
+ Identifies the CDP proxy connection the command arrived on, matching
+ `cdp_connect` and `cdp_disconnect`. Two clients driving the same browser are
+ told apart by this.
+ """
+
+ duration: Optional[int] = None
+ """Duration between touchdown and touchup, in milliseconds."""
+
+ gesture_source_type: Optional[Literal["default", "touch", "mouse", "other"]] = None
+ """Input source the synthesized gesture emulates.
+
+ A value the protocol does not define is reported as `other`.
+ """
+
+ session_id: Optional[str] = None
+ """CDP session identifier the command was addressed to.
+
+ Absent for browser-level commands. Clipped to 128 characters.
+ """
+
+ tap_count: Optional[int] = None
+ """Number of times to tap (2 is a double tap)."""
+
+ x: Optional[float] = None
+ """Viewport x coordinate in CSS pixels."""
+
+ y: Optional[float] = None
+ """Viewport y coordinate in CSS pixels."""
+
+
+class DataBrowserCdpDomSetFileInputFilesCommandData(BaseModel):
+ """Sanitized `DOM.setFileInputFiles` arguments.
+
+ Canonical input: `DOM.setFileInputFiles` in devtools-protocol@2d019e73, pinned at https://github.com/ChromeDevTools/devtools-protocol/blob/2d019e73eb371d1d6985d26d395d78bd8f8a22ba/json/browser_protocol.json. Every argument of this command has a retained or redacted decision in lib/devtoolsproxy/testdata/cdp_arguments.yaml.
+ """
+
+ file_count: int
+ """Number of files handed to the input. File paths are never captured."""
+
+ method: Literal["DOM.setFileInputFiles"]
+
+ backend_node_id: Optional[int] = None
+ """Opaque backend DOM node identifier the command targeted."""
+
+ command_id: Optional[int] = None
+ """
+ The command's JSON-RPC id, so the command can be joined to the result the
+ browser returned for it. Absent when the client sent none.
+ """
+
+ connection_id: Optional[str] = None
+ """
+ Identifies the CDP proxy connection the command arrived on, matching
+ `cdp_connect` and `cdp_disconnect`. Two clients driving the same browser are
+ told apart by this.
+ """
+
+ node_id: Optional[int] = None
+ """Opaque DOM node identifier the command targeted."""
+
+ object_id: Optional[str] = None
+ """Opaque Runtime remote object identifier the command targeted.
+
+ Clipped to 128 characters; a longer value is not a real identifier.
+ """
+
+ session_id: Optional[str] = None
+ """CDP session identifier the command was addressed to.
+
+ Absent for browser-level commands. Clipped to 128 characters.
+ """
+
+
+class DataBrowserCdpDomFocusCommandData(BaseModel):
+ """Sanitized `DOM.focus` arguments.
+
+ Canonical input: `DOM.focus` in devtools-protocol@2d019e73, pinned at https://github.com/ChromeDevTools/devtools-protocol/blob/2d019e73eb371d1d6985d26d395d78bd8f8a22ba/json/browser_protocol.json. Every argument of this command has a retained or redacted decision in lib/devtoolsproxy/testdata/cdp_arguments.yaml.
+ """
+
+ method: Literal["DOM.focus"]
+
+ backend_node_id: Optional[int] = None
+ """Opaque backend DOM node identifier the command targeted."""
+
+ command_id: Optional[int] = None
+ """
+ The command's JSON-RPC id, so the command can be joined to the result the
+ browser returned for it. Absent when the client sent none.
+ """
+
+ connection_id: Optional[str] = None
+ """
+ Identifies the CDP proxy connection the command arrived on, matching
+ `cdp_connect` and `cdp_disconnect`. Two clients driving the same browser are
+ told apart by this.
+ """
+
+ node_id: Optional[int] = None
+ """Opaque DOM node identifier the command targeted."""
+
+ object_id: Optional[str] = None
+ """Opaque Runtime remote object identifier the command targeted.
+
+ Clipped to 128 characters; a longer value is not a real identifier.
+ """
+
+ session_id: Optional[str] = None
+ """CDP session identifier the command was addressed to.
+
+ Absent for browser-level commands. Clipped to 128 characters.
+ """
+
+
+class DataBrowserCdpDomScrollIntoViewIfNeededCommandData(BaseModel):
+ """Sanitized `DOM.scrollIntoViewIfNeeded` arguments.
+
+ Canonical input: `DOM.scrollIntoViewIfNeeded` in devtools-protocol@2d019e73, pinned at https://github.com/ChromeDevTools/devtools-protocol/blob/2d019e73eb371d1d6985d26d395d78bd8f8a22ba/json/browser_protocol.json. Every argument of this command has a retained or redacted decision in lib/devtoolsproxy/testdata/cdp_arguments.yaml.
+ """
+
+ method: Literal["DOM.scrollIntoViewIfNeeded"]
+
+ backend_node_id: Optional[int] = None
+ """Opaque backend DOM node identifier the command targeted."""
+
+ command_id: Optional[int] = None
+ """
+ The command's JSON-RPC id, so the command can be joined to the result the
+ browser returned for it. Absent when the client sent none.
+ """
+
+ connection_id: Optional[str] = None
+ """
+ Identifies the CDP proxy connection the command arrived on, matching
+ `cdp_connect` and `cdp_disconnect`. Two clients driving the same browser are
+ told apart by this.
+ """
+
+ node_id: Optional[int] = None
+ """Opaque DOM node identifier the command targeted."""
+
+ object_id: Optional[str] = None
+ """Opaque Runtime remote object identifier the command targeted.
+
+ Clipped to 128 characters; a longer value is not a real identifier.
+ """
+
+ rect_height: Optional[float] = None
+ """Height of the rect the command scrolled to."""
+
+ rect_width: Optional[float] = None
+ """Width of the rect the command scrolled to."""
+
+ rect_x: Optional[float] = None
+ """X offset of the rect the command scrolled to, relative to the node."""
+
+ rect_y: Optional[float] = None
+ """Y offset of the rect the command scrolled to, relative to the node."""
+
+ session_id: Optional[str] = None
+ """CDP session identifier the command was addressed to.
+
+ Absent for browser-level commands. Clipped to 128 characters.
+ """
+
+
+class DataBrowserCdpPageBringToFrontCommandData(BaseModel):
+ """Sanitized `Page.bringToFront` arguments.
+
+ Canonical input: `Page.bringToFront` in devtools-protocol@2d019e73, pinned at https://github.com/ChromeDevTools/devtools-protocol/blob/2d019e73eb371d1d6985d26d395d78bd8f8a22ba/json/browser_protocol.json. Every argument of this command has a retained or redacted decision in lib/devtoolsproxy/testdata/cdp_arguments.yaml.
+ """
+
+ method: Literal["Page.bringToFront"]
+
+ command_id: Optional[int] = None
+ """
+ The command's JSON-RPC id, so the command can be joined to the result the
+ browser returned for it. Absent when the client sent none.
+ """
+
+ connection_id: Optional[str] = None
+ """
+ Identifies the CDP proxy connection the command arrived on, matching
+ `cdp_connect` and `cdp_disconnect`. Two clients driving the same browser are
+ told apart by this.
+ """
+
+ session_id: Optional[str] = None
+ """CDP session identifier the command was addressed to.
+
+ Absent for browser-level commands. Clipped to 128 characters.
+ """
+
+
+class DataBrowserCdpPageCaptureScreenshotCommandData(BaseModel):
+ """Sanitized `Page.captureScreenshot` arguments.
+
+ Canonical input: `Page.captureScreenshot` in devtools-protocol@2d019e73, pinned at https://github.com/ChromeDevTools/devtools-protocol/blob/2d019e73eb371d1d6985d26d395d78bd8f8a22ba/json/browser_protocol.json. Every argument of this command has a retained or redacted decision in lib/devtoolsproxy/testdata/cdp_arguments.yaml.
+ """
+
+ method: Literal["Page.captureScreenshot"]
+
+ capture_beyond_viewport: Optional[bool] = None
+ """Whether the capture extended past the viewport."""
+
+ clip_height: Optional[float] = None
+ """Clip region height in CSS pixels."""
+
+ clip_scale: Optional[float] = None
+ """Clip region page scale factor."""
+
+ clip_width: Optional[float] = None
+ """Clip region width in CSS pixels."""
+
+ clip_x: Optional[float] = None
+ """Clip region x offset in CSS pixels."""
+
+ clip_y: Optional[float] = None
+ """Clip region y offset in CSS pixels."""
+
+ command_id: Optional[int] = None
+ """
+ The command's JSON-RPC id, so the command can be joined to the result the
+ browser returned for it. Absent when the client sent none.
+ """
+
+ connection_id: Optional[str] = None
+ """
+ Identifies the CDP proxy connection the command arrived on, matching
+ `cdp_connect` and `cdp_disconnect`. Two clients driving the same browser are
+ told apart by this.
+ """
+
+ format: Optional[Literal["jpeg", "png", "webp", "other"]] = None
+ """Image format requested (`jpeg`, `png` or `webp`).
+
+ A value the protocol does not define is reported as `other`.
+ """
+
+ from_surface: Optional[bool] = None
+ """Whether the capture was taken from the surface rather than the view."""
+
+ optimize_for_speed: Optional[bool] = None
+ """Whether encoding favored speed over size."""
+
+ quality: Optional[int] = None
+ """Compression quality, 0 to 100, for lossy formats."""
+
+ session_id: Optional[str] = None
+ """CDP session identifier the command was addressed to.
+
+ Absent for browser-level commands. Clipped to 128 characters.
+ """
+
+
+class DataBrowserCdpPageCaptureSnapshotCommandData(BaseModel):
+ """Sanitized `Page.captureSnapshot` arguments.
+
+ Canonical input: `Page.captureSnapshot` in devtools-protocol@2d019e73, pinned at https://github.com/ChromeDevTools/devtools-protocol/blob/2d019e73eb371d1d6985d26d395d78bd8f8a22ba/json/browser_protocol.json. Every argument of this command has a retained or redacted decision in lib/devtoolsproxy/testdata/cdp_arguments.yaml.
+ """
+
+ method: Literal["Page.captureSnapshot"]
+
+ command_id: Optional[int] = None
+ """
+ The command's JSON-RPC id, so the command can be joined to the result the
+ browser returned for it. Absent when the client sent none.
+ """
+
+ connection_id: Optional[str] = None
+ """
+ Identifies the CDP proxy connection the command arrived on, matching
+ `cdp_connect` and `cdp_disconnect`. Two clients driving the same browser are
+ told apart by this.
+ """
+
+ format: Optional[Literal["mhtml", "other"]] = None
+ """Snapshot format requested (`mhtml`).
+
+ A value the protocol does not define is reported as `other`.
+ """
+
+ session_id: Optional[str] = None
+ """CDP session identifier the command was addressed to.
+
+ Absent for browser-level commands. Clipped to 128 characters.
+ """
+
+
+class DataBrowserCdpPageHandleJavaScriptDialogCommandData(BaseModel):
+ """Sanitized `Page.handleJavaScriptDialog` arguments.
+
+ Canonical input: `Page.handleJavaScriptDialog` in devtools-protocol@2d019e73, pinned at https://github.com/ChromeDevTools/devtools-protocol/blob/2d019e73eb371d1d6985d26d395d78bd8f8a22ba/json/browser_protocol.json. Every argument of this command has a retained or redacted decision in lib/devtoolsproxy/testdata/cdp_arguments.yaml.
+ """
+
+ accept: bool
+ """Whether the dialog was accepted or dismissed."""
+
+ method: Literal["Page.handleJavaScriptDialog"]
+
+ command_id: Optional[int] = None
+ """
+ The command's JSON-RPC id, so the command can be joined to the result the
+ browser returned for it. Absent when the client sent none.
+ """
+
+ connection_id: Optional[str] = None
+ """
+ Identifies the CDP proxy connection the command arrived on, matching
+ `cdp_connect` and `cdp_disconnect`. Two clients driving the same browser are
+ told apart by this.
+ """
+
+ prompt_text_length: Optional[int] = None
+ """Number of characters entered into a prompt dialog.
+
+ The text itself is never captured.
+ """
+
+ session_id: Optional[str] = None
+ """CDP session identifier the command was addressed to.
+
+ Absent for browser-level commands. Clipped to 128 characters.
+ """
+
+
+class DataBrowserCdpPageNavigateCommandData(BaseModel):
+ """Sanitized `Page.navigate` arguments.
+
+ Canonical input: `Page.navigate` in devtools-protocol@2d019e73, pinned at https://github.com/ChromeDevTools/devtools-protocol/blob/2d019e73eb371d1d6985d26d395d78bd8f8a22ba/json/browser_protocol.json. Every argument of this command has a retained or redacted decision in lib/devtoolsproxy/testdata/cdp_arguments.yaml.
+ """
+
+ method: Literal["Page.navigate"]
+
+ command_id: Optional[int] = None
+ """
+ The command's JSON-RPC id, so the command can be joined to the result the
+ browser returned for it. Absent when the client sent none.
+ """
+
+ connection_id: Optional[str] = None
+ """
+ Identifies the CDP proxy connection the command arrived on, matching
+ `cdp_connect` and `cdp_disconnect`. Two clients driving the same browser are
+ told apart by this.
+ """
+
+ frame_id: Optional[str] = None
+ """Opaque frame identifier.
+
+ Clipped to 128 characters; a longer value is not a real identifier.
+ """
+
+ referrer_policy: Optional[
+ Literal[
+ "noReferrer",
+ "noReferrerWhenDowngrade",
+ "origin",
+ "originWhenCrossOrigin",
+ "sameOrigin",
+ "strictOrigin",
+ "strictOriginWhenCrossOrigin",
+ "unsafeUrl",
+ "other",
+ ]
+ ] = None
+ """Referrer policy named by the command.
+
+ A value the protocol does not define is reported as `other`.
+ """
+
+ referrer_present: Optional[bool] = None
+ """Whether the command carried a referrer. The referrer itself is never captured."""
+
+ session_id: Optional[str] = None
+ """CDP session identifier the command was addressed to.
+
+ Absent for browser-level commands. Clipped to 128 characters.
+ """
+
+ transition_type: Optional[
+ Literal[
+ "link",
+ "typed",
+ "address_bar",
+ "auto_bookmark",
+ "auto_subframe",
+ "manual_subframe",
+ "generated",
+ "auto_toplevel",
+ "form_submit",
+ "reload",
+ "keyword",
+ "keyword_generated",
+ "other",
+ ]
+ ] = None
+ """Navigation reason reported by the caller (e.g.
+
+ `link`, `typed`, `reload`). A value the protocol does not define is reported as
+ `other`.
+ """
+
+ url_scheme: Optional[str] = None
+ """Scheme of the destination URL (e.g.
+
+ `https`, `about`, `data`). The rest of the URL is never captured.
+ """
+
+
+class DataBrowserCdpPageNavigateToHistoryEntryCommandData(BaseModel):
+ """Sanitized `Page.navigateToHistoryEntry` arguments.
+
+ Canonical input: `Page.navigateToHistoryEntry` in devtools-protocol@2d019e73, pinned at https://github.com/ChromeDevTools/devtools-protocol/blob/2d019e73eb371d1d6985d26d395d78bd8f8a22ba/json/browser_protocol.json. Every argument of this command has a retained or redacted decision in lib/devtoolsproxy/testdata/cdp_arguments.yaml.
+ """
+
+ entry_id: int
+ """History entry the command navigated to."""
+
+ method: Literal["Page.navigateToHistoryEntry"]
+
+ command_id: Optional[int] = None
+ """
+ The command's JSON-RPC id, so the command can be joined to the result the
+ browser returned for it. Absent when the client sent none.
+ """
+
+ connection_id: Optional[str] = None
+ """
+ Identifies the CDP proxy connection the command arrived on, matching
+ `cdp_connect` and `cdp_disconnect`. Two clients driving the same browser are
+ told apart by this.
+ """
+
+ session_id: Optional[str] = None
+ """CDP session identifier the command was addressed to.
+
+ Absent for browser-level commands. Clipped to 128 characters.
+ """
+
+
+class DataBrowserCdpPageReloadCommandData(BaseModel):
+ """Sanitized `Page.reload` arguments.
+
+ Canonical input: `Page.reload` in devtools-protocol@2d019e73, pinned at https://github.com/ChromeDevTools/devtools-protocol/blob/2d019e73eb371d1d6985d26d395d78bd8f8a22ba/json/browser_protocol.json. Every argument of this command has a retained or redacted decision in lib/devtoolsproxy/testdata/cdp_arguments.yaml.
+ """
+
+ method: Literal["Page.reload"]
+
+ command_id: Optional[int] = None
+ """
+ The command's JSON-RPC id, so the command can be joined to the result the
+ browser returned for it. Absent when the client sent none.
+ """
+
+ connection_id: Optional[str] = None
+ """
+ Identifies the CDP proxy connection the command arrived on, matching
+ `cdp_connect` and `cdp_disconnect`. Two clients driving the same browser are
+ told apart by this.
+ """
+
+ ignore_cache: Optional[bool] = None
+ """Whether the reload bypassed the cache."""
+
+ loader_id: Optional[str] = None
+ """Opaque document loader identifier.
+
+ Clipped to 128 characters; a longer value is not a real identifier.
+ """
+
+ script_length: Optional[int] = None
+ """Number of characters in the injected script."""
+
+ session_id: Optional[str] = None
+ """CDP session identifier the command was addressed to.
+
+ Absent for browser-level commands. Clipped to 128 characters.
+ """
+
+
+class DataBrowserCdpPagePrintToPdfCommandData(BaseModel):
+ """Sanitized `Page.printToPDF` arguments.
+
+ Canonical input: `Page.printToPDF` in devtools-protocol@2d019e73, pinned at https://github.com/ChromeDevTools/devtools-protocol/blob/2d019e73eb371d1d6985d26d395d78bd8f8a22ba/json/browser_protocol.json. Every argument of this command has a retained or redacted decision in lib/devtoolsproxy/testdata/cdp_arguments.yaml.
+ """
+
+ method: Literal["Page.printToPDF"]
+
+ command_id: Optional[int] = None
+ """
+ The command's JSON-RPC id, so the command can be joined to the result the
+ browser returned for it. Absent when the client sent none.
+ """
+
+ connection_id: Optional[str] = None
+ """
+ Identifies the CDP proxy connection the command arrived on, matching
+ `cdp_connect` and `cdp_disconnect`. Two clients driving the same browser are
+ told apart by this.
+ """
+
+ display_header_footer: Optional[bool] = None
+ """Whether a header and footer were rendered."""
+
+ footer_template_present: Optional[bool] = None
+ """Whether a footer template was supplied. The template itself is never captured."""
+
+ generate_document_outline: Optional[bool] = None
+ """Whether a document outline was embedded."""
+
+ generate_tagged_pdf: Optional[bool] = None
+ """Whether a tagged (accessible) PDF was requested."""
+
+ header_template_present: Optional[bool] = None
+ """Whether a header template was supplied. The template itself is never captured."""
+
+ landscape: Optional[bool] = None
+ """Whether the page was laid out in landscape."""
+
+ margin_bottom: Optional[float] = None
+ """Bottom margin in inches."""
+
+ margin_left: Optional[float] = None
+ """Left margin in inches."""
+
+ margin_right: Optional[float] = None
+ """Right margin in inches."""
+
+ margin_top: Optional[float] = None
+ """Top margin in inches."""
+
+ page_ranges_present: Optional[bool] = None
+ """Whether a page range was supplied."""
+
+ paper_height: Optional[float] = None
+ """Paper height in inches."""
+
+ paper_width: Optional[float] = None
+ """Paper width in inches."""
+
+ prefer_css_page_size: Optional[bool] = None
+ """Whether the CSS page size was preferred over the paper size."""
+
+ print_background: Optional[bool] = None
+ """Whether background graphics were printed."""
+
+ scale: Optional[float] = None
+ """Page render scale."""
+
+ session_id: Optional[str] = None
+ """CDP session identifier the command was addressed to.
+
+ Absent for browser-level commands. Clipped to 128 characters.
+ """
+
+ transfer_mode: Optional[Literal["ReturnAsBase64", "ReturnAsStream", "other"]] = None
+ """How the PDF was returned (`ReturnAsBase64` or `ReturnAsStream`).
+
+ A value the protocol does not define is reported as `other`.
+ """
+
+
+class DataBrowserCdpPageStartScreencastCommandData(BaseModel):
+ """Sanitized `Page.startScreencast` arguments.
+
+ Canonical input: `Page.startScreencast` in devtools-protocol@2d019e73, pinned at https://github.com/ChromeDevTools/devtools-protocol/blob/2d019e73eb371d1d6985d26d395d78bd8f8a22ba/json/browser_protocol.json. Every argument of this command has a retained or redacted decision in lib/devtoolsproxy/testdata/cdp_arguments.yaml.
+ """
+
+ method: Literal["Page.startScreencast"]
+
+ command_id: Optional[int] = None
+ """
+ The command's JSON-RPC id, so the command can be joined to the result the
+ browser returned for it. Absent when the client sent none.
+ """
+
+ connection_id: Optional[str] = None
+ """
+ Identifies the CDP proxy connection the command arrived on, matching
+ `cdp_connect` and `cdp_disconnect`. Two clients driving the same browser are
+ told apart by this.
+ """
+
+ every_nth_frame: Optional[int] = None
+ """Frame sampling interval."""
+
+ format: Optional[Literal["jpeg", "png", "other"]] = None
+ """Frame format requested (`jpeg` or `png`).
+
+ A value the protocol does not define is reported as `other`.
+ """
+
+ max_height: Optional[int] = None
+ """Maximum frame height in pixels."""
+
+ max_width: Optional[int] = None
+ """Maximum frame width in pixels."""
+
+ quality: Optional[int] = None
+ """Compression quality, 0 to 100."""
+
+ session_id: Optional[str] = None
+ """CDP session identifier the command was addressed to.
+
+ Absent for browser-level commands. Clipped to 128 characters.
+ """
+
+
+class DataBrowserCdpPageStopScreencastCommandData(BaseModel):
+ """Sanitized `Page.stopScreencast` arguments.
+
+ Canonical input: `Page.stopScreencast` in devtools-protocol@2d019e73, pinned at https://github.com/ChromeDevTools/devtools-protocol/blob/2d019e73eb371d1d6985d26d395d78bd8f8a22ba/json/browser_protocol.json. Every argument of this command has a retained or redacted decision in lib/devtoolsproxy/testdata/cdp_arguments.yaml.
+ """
+
+ method: Literal["Page.stopScreencast"]
+
+ command_id: Optional[int] = None
+ """
+ The command's JSON-RPC id, so the command can be joined to the result the
+ browser returned for it. Absent when the client sent none.
+ """
+
+ connection_id: Optional[str] = None
+ """
+ Identifies the CDP proxy connection the command arrived on, matching
+ `cdp_connect` and `cdp_disconnect`. Two clients driving the same browser are
+ told apart by this.
+ """
+
+ session_id: Optional[str] = None
+ """CDP session identifier the command was addressed to.
+
+ Absent for browser-level commands. Clipped to 128 characters.
+ """
+
+
+class DataBrowserCdpPageStopLoadingCommandData(BaseModel):
+ """Sanitized `Page.stopLoading` arguments.
+
+ Canonical input: `Page.stopLoading` in devtools-protocol@2d019e73, pinned at https://github.com/ChromeDevTools/devtools-protocol/blob/2d019e73eb371d1d6985d26d395d78bd8f8a22ba/json/browser_protocol.json. Every argument of this command has a retained or redacted decision in lib/devtoolsproxy/testdata/cdp_arguments.yaml.
+ """
+
+ method: Literal["Page.stopLoading"]
+
+ command_id: Optional[int] = None
+ """
+ The command's JSON-RPC id, so the command can be joined to the result the
+ browser returned for it. Absent when the client sent none.
+ """
+
+ connection_id: Optional[str] = None
+ """
+ Identifies the CDP proxy connection the command arrived on, matching
+ `cdp_connect` and `cdp_disconnect`. Two clients driving the same browser are
+ told apart by this.
+ """
+
+ session_id: Optional[str] = None
+ """CDP session identifier the command was addressed to.
+
+ Absent for browser-level commands. Clipped to 128 characters.
+ """
+
+
+class DataBrowserCdpPageCloseCommandData(BaseModel):
+ """Sanitized `Page.close` arguments.
+
+ Canonical input: `Page.close` in devtools-protocol@2d019e73, pinned at https://github.com/ChromeDevTools/devtools-protocol/blob/2d019e73eb371d1d6985d26d395d78bd8f8a22ba/json/browser_protocol.json. Every argument of this command has a retained or redacted decision in lib/devtoolsproxy/testdata/cdp_arguments.yaml.
+ """
+
+ method: Literal["Page.close"]
+
+ command_id: Optional[int] = None
+ """
+ The command's JSON-RPC id, so the command can be joined to the result the
+ browser returned for it. Absent when the client sent none.
+ """
+
+ connection_id: Optional[str] = None
+ """
+ Identifies the CDP proxy connection the command arrived on, matching
+ `cdp_connect` and `cdp_disconnect`. Two clients driving the same browser are
+ told apart by this.
+ """
+
+ session_id: Optional[str] = None
+ """CDP session identifier the command was addressed to.
+
+ Absent for browser-level commands. Clipped to 128 characters.
+ """
+
+
+class DataBrowserCdpPageSetWebLifecycleStateCommandData(BaseModel):
+ """Sanitized `Page.setWebLifecycleState` arguments.
+
+ Canonical input: `Page.setWebLifecycleState` in devtools-protocol@2d019e73, pinned at https://github.com/ChromeDevTools/devtools-protocol/blob/2d019e73eb371d1d6985d26d395d78bd8f8a22ba/json/browser_protocol.json. Every argument of this command has a retained or redacted decision in lib/devtoolsproxy/testdata/cdp_arguments.yaml.
+ """
+
+ method: Literal["Page.setWebLifecycleState"]
+
+ state: Literal["frozen", "active", "other"]
+ """Lifecycle state applied (`frozen` or `active`).
+
+ A value the protocol does not define is reported as `other`.
+ """
+
+ command_id: Optional[int] = None
+ """
+ The command's JSON-RPC id, so the command can be joined to the result the
+ browser returned for it. Absent when the client sent none.
+ """
+
+ connection_id: Optional[str] = None
+ """
+ Identifies the CDP proxy connection the command arrived on, matching
+ `cdp_connect` and `cdp_disconnect`. Two clients driving the same browser are
+ told apart by this.
+ """
+
+ session_id: Optional[str] = None
+ """CDP session identifier the command was addressed to.
+
+ Absent for browser-level commands. Clipped to 128 characters.
+ """
+
+
+class DataBrowserCdpTargetActivateTargetCommandData(BaseModel):
+ """Sanitized `Target.activateTarget` arguments.
+
+ Canonical input: `Target.activateTarget` in devtools-protocol@2d019e73, pinned at https://github.com/ChromeDevTools/devtools-protocol/blob/2d019e73eb371d1d6985d26d395d78bd8f8a22ba/json/browser_protocol.json. Every argument of this command has a retained or redacted decision in lib/devtoolsproxy/testdata/cdp_arguments.yaml.
+ """
+
+ method: Literal["Target.activateTarget"]
+
+ target_id: str
+ """Opaque target identifier.
+
+ Clipped to 128 characters; a longer value is not a real identifier.
+ """
+
+ command_id: Optional[int] = None
+ """
+ The command's JSON-RPC id, so the command can be joined to the result the
+ browser returned for it. Absent when the client sent none.
+ """
+
+ connection_id: Optional[str] = None
+ """
+ Identifies the CDP proxy connection the command arrived on, matching
+ `cdp_connect` and `cdp_disconnect`. Two clients driving the same browser are
+ told apart by this.
+ """
+
+ session_id: Optional[str] = None
+ """CDP session identifier the command was addressed to.
+
+ Absent for browser-level commands. Clipped to 128 characters.
+ """
+
+
+class DataBrowserCdpTargetCloseTargetCommandData(BaseModel):
+ """Sanitized `Target.closeTarget` arguments.
+
+ Canonical input: `Target.closeTarget` in devtools-protocol@2d019e73, pinned at https://github.com/ChromeDevTools/devtools-protocol/blob/2d019e73eb371d1d6985d26d395d78bd8f8a22ba/json/browser_protocol.json. Every argument of this command has a retained or redacted decision in lib/devtoolsproxy/testdata/cdp_arguments.yaml.
+ """
+
+ method: Literal["Target.closeTarget"]
+
+ target_id: str
+ """Opaque target identifier.
+
+ Clipped to 128 characters; a longer value is not a real identifier.
+ """
+
+ command_id: Optional[int] = None
+ """
+ The command's JSON-RPC id, so the command can be joined to the result the
+ browser returned for it. Absent when the client sent none.
+ """
+
+ connection_id: Optional[str] = None
+ """
+ Identifies the CDP proxy connection the command arrived on, matching
+ `cdp_connect` and `cdp_disconnect`. Two clients driving the same browser are
+ told apart by this.
+ """
+
+ session_id: Optional[str] = None
+ """CDP session identifier the command was addressed to.
+
+ Absent for browser-level commands. Clipped to 128 characters.
+ """
+
+
+class DataBrowserCdpTargetCreateTargetCommandData(BaseModel):
+ """Sanitized `Target.createTarget` arguments.
+
+ Canonical input: `Target.createTarget` in devtools-protocol@2d019e73, pinned at https://github.com/ChromeDevTools/devtools-protocol/blob/2d019e73eb371d1d6985d26d395d78bd8f8a22ba/json/browser_protocol.json. Every argument of this command has a retained or redacted decision in lib/devtoolsproxy/testdata/cdp_arguments.yaml.
+ """
+
+ method: Literal["Target.createTarget"]
+
+ background: Optional[bool] = None
+ """Whether the target was created in the background."""
+
+ browser_context_id: Optional[str] = None
+ """Opaque browser context identifier.
+
+ Clipped to 128 characters; a longer value is not a real identifier.
+ """
+
+ command_id: Optional[int] = None
+ """
+ The command's JSON-RPC id, so the command can be joined to the result the
+ browser returned for it. Absent when the client sent none.
+ """
+
+ connection_id: Optional[str] = None
+ """
+ Identifies the CDP proxy connection the command arrived on, matching
+ `cdp_connect` and `cdp_disconnect`. Two clients driving the same browser are
+ told apart by this.
+ """
+
+ enable_begin_frame_control: Optional[bool] = None
+ """Whether BeginFrame control was enabled (headless only)."""
+
+ focus: Optional[bool] = None
+ """Whether the new target was focused."""
+
+ for_tab: Optional[bool] = None
+ """Whether a tab target rather than a page target was created."""
+
+ height: Optional[int] = None
+ """Window height in DIP."""
+
+ hidden: Optional[bool] = None
+ """Whether the target was created hidden."""
+
+ left: Optional[int] = None
+ """Window x position in screen coordinates."""
+
+ new_window: Optional[bool] = None
+ """Whether a new window was requested."""
+
+ session_id: Optional[str] = None
+ """CDP session identifier the command was addressed to.
+
+ Absent for browser-level commands. Clipped to 128 characters.
+ """
+
+ top: Optional[int] = None
+ """Window y position in screen coordinates."""
+
+ url_scheme: Optional[str] = None
+ """Scheme of the destination URL (e.g.
+
+ `https`, `about`, `data`). The rest of the URL is never captured.
+ """
+
+ width: Optional[int] = None
+ """Window width in DIP."""
+
+ window_state: Optional[Literal["normal", "minimized", "maximized", "fullscreen", "other"]] = None
+ """Window state requested (`normal`, `minimized`, `maximized`, `fullscreen`).
+
+ A value the protocol does not define is reported as `other`.
+ """
+
+
+class DataBrowserCdpTargetCreateBrowserContextCommandData(BaseModel):
+ """Sanitized `Target.createBrowserContext` arguments.
+
+ Canonical input: `Target.createBrowserContext` in devtools-protocol@2d019e73, pinned at https://github.com/ChromeDevTools/devtools-protocol/blob/2d019e73eb371d1d6985d26d395d78bd8f8a22ba/json/browser_protocol.json. Every argument of this command has a retained or redacted decision in lib/devtoolsproxy/testdata/cdp_arguments.yaml.
+ """
+
+ method: Literal["Target.createBrowserContext"]
+
+ command_id: Optional[int] = None
+ """
+ The command's JSON-RPC id, so the command can be joined to the result the
+ browser returned for it. Absent when the client sent none.
+ """
+
+ connection_id: Optional[str] = None
+ """
+ Identifies the CDP proxy connection the command arrived on, matching
+ `cdp_connect` and `cdp_disconnect`. Two clients driving the same browser are
+ told apart by this.
+ """
+
+ dispose_on_detach: Optional[bool] = None
+ """Whether the context is disposed when the debugging session detaches."""
+
+ proxy_bypass_list_present: Optional[bool] = None
+ """Whether a proxy bypass list was configured."""
+
+ proxy_server_present: Optional[bool] = None
+ """Whether a proxy was configured. The proxy address is never captured."""
+
+ session_id: Optional[str] = None
+ """CDP session identifier the command was addressed to.
+
+ Absent for browser-level commands. Clipped to 128 characters.
+ """
+
+ universal_network_access_origin_count: Optional[int] = None
+ """Number of origins granted universal network access.
+
+ The origins themselves are never captured.
+ """
+
+
+class DataBrowserCdpTargetDisposeBrowserContextCommandData(BaseModel):
+ """Sanitized `Target.disposeBrowserContext` arguments.
+
+ Canonical input: `Target.disposeBrowserContext` in devtools-protocol@2d019e73, pinned at https://github.com/ChromeDevTools/devtools-protocol/blob/2d019e73eb371d1d6985d26d395d78bd8f8a22ba/json/browser_protocol.json. Every argument of this command has a retained or redacted decision in lib/devtoolsproxy/testdata/cdp_arguments.yaml.
+ """
+
+ browser_context_id: str
+ """Opaque browser context identifier.
+
+ Clipped to 128 characters; a longer value is not a real identifier.
+ """
+
+ method: Literal["Target.disposeBrowserContext"]
+
+ command_id: Optional[int] = None
+ """
+ The command's JSON-RPC id, so the command can be joined to the result the
+ browser returned for it. Absent when the client sent none.
+ """
+
+ connection_id: Optional[str] = None
+ """
+ Identifies the CDP proxy connection the command arrived on, matching
+ `cdp_connect` and `cdp_disconnect`. Two clients driving the same browser are
+ told apart by this.
+ """
+
+ session_id: Optional[str] = None
+ """CDP session identifier the command was addressed to.
+
+ Absent for browser-level commands. Clipped to 128 characters.
+ """
+
+
+class DataBrowserCdpTargetOpenDevToolsCommandData(BaseModel):
+ """Sanitized `Target.openDevTools` arguments.
+
+ Canonical input: `Target.openDevTools` in devtools-protocol@2d019e73, pinned at https://github.com/ChromeDevTools/devtools-protocol/blob/2d019e73eb371d1d6985d26d395d78bd8f8a22ba/json/browser_protocol.json. Every argument of this command has a retained or redacted decision in lib/devtoolsproxy/testdata/cdp_arguments.yaml.
+ """
+
+ method: Literal["Target.openDevTools"]
+
+ target_id: str
+ """Opaque target identifier.
+
+ Clipped to 128 characters; a longer value is not a real identifier.
+ """
+
+ command_id: Optional[int] = None
+ """
+ The command's JSON-RPC id, so the command can be joined to the result the
+ browser returned for it. Absent when the client sent none.
+ """
+
+ connection_id: Optional[str] = None
+ """
+ Identifies the CDP proxy connection the command arrived on, matching
+ `cdp_connect` and `cdp_disconnect`. Two clients driving the same browser are
+ told apart by this.
+ """
+
+ panel_id: Optional[str] = None
+ """DevTools panel opened.
+
+ Clipped to 128 characters; a longer value is not a real identifier.
+ """
+
+ session_id: Optional[str] = None
+ """CDP session identifier the command was addressed to.
+
+ Absent for browser-level commands. Clipped to 128 characters.
+ """
+
+
+class DataBrowserCdpBrowserCancelDownloadCommandData(BaseModel):
+ """Sanitized `Browser.cancelDownload` arguments.
+
+ Canonical input: `Browser.cancelDownload` in devtools-protocol@2d019e73, pinned at https://github.com/ChromeDevTools/devtools-protocol/blob/2d019e73eb371d1d6985d26d395d78bd8f8a22ba/json/browser_protocol.json. Every argument of this command has a retained or redacted decision in lib/devtoolsproxy/testdata/cdp_arguments.yaml.
+ """
+
+ download_guid: str
+ """Opaque identifier of the download that was cancelled.
+
+ Clipped to 128 characters; a longer value is not a real identifier.
+ """
+
+ method: Literal["Browser.cancelDownload"]
+
+ browser_context_id: Optional[str] = None
+ """Opaque browser context identifier.
+
+ Clipped to 128 characters; a longer value is not a real identifier.
+ """
+
+ command_id: Optional[int] = None
+ """
+ The command's JSON-RPC id, so the command can be joined to the result the
+ browser returned for it. Absent when the client sent none.
+ """
+
+ connection_id: Optional[str] = None
+ """
+ Identifies the CDP proxy connection the command arrived on, matching
+ `cdp_connect` and `cdp_disconnect`. Two clients driving the same browser are
+ told apart by this.
+ """
+
+ session_id: Optional[str] = None
+ """CDP session identifier the command was addressed to.
+
+ Absent for browser-level commands. Clipped to 128 characters.
+ """
+
+
+class DataBrowserCdpBrowserCloseCommandData(BaseModel):
+ """Sanitized `Browser.close` arguments.
+
+ Canonical input: `Browser.close` in devtools-protocol@2d019e73, pinned at https://github.com/ChromeDevTools/devtools-protocol/blob/2d019e73eb371d1d6985d26d395d78bd8f8a22ba/json/browser_protocol.json. Every argument of this command has a retained or redacted decision in lib/devtoolsproxy/testdata/cdp_arguments.yaml.
+ """
+
+ method: Literal["Browser.close"]
+
+ command_id: Optional[int] = None
+ """
+ The command's JSON-RPC id, so the command can be joined to the result the
+ browser returned for it. Absent when the client sent none.
+ """
+
+ connection_id: Optional[str] = None
+ """
+ Identifies the CDP proxy connection the command arrived on, matching
+ `cdp_connect` and `cdp_disconnect`. Two clients driving the same browser are
+ told apart by this.
+ """
+
+ session_id: Optional[str] = None
+ """CDP session identifier the command was addressed to.
+
+ Absent for browser-level commands. Clipped to 128 characters.
+ """
+
+
+class DataBrowserCdpBrowserSetWindowBoundsCommandData(BaseModel):
+ """Sanitized `Browser.setWindowBounds` arguments.
+
+ Canonical input: `Browser.setWindowBounds` in devtools-protocol@2d019e73, pinned at https://github.com/ChromeDevTools/devtools-protocol/blob/2d019e73eb371d1d6985d26d395d78bd8f8a22ba/json/browser_protocol.json. Every argument of this command has a retained or redacted decision in lib/devtoolsproxy/testdata/cdp_arguments.yaml.
+ """
+
+ method: Literal["Browser.setWindowBounds"]
+
+ window_id: int
+ """Browser window identifier."""
+
+ command_id: Optional[int] = None
+ """
+ The command's JSON-RPC id, so the command can be joined to the result the
+ browser returned for it. Absent when the client sent none.
+ """
+
+ connection_id: Optional[str] = None
+ """
+ Identifies the CDP proxy connection the command arrived on, matching
+ `cdp_connect` and `cdp_disconnect`. Two clients driving the same browser are
+ told apart by this.
+ """
+
+ height: Optional[int] = None
+ """Window height in DIP."""
+
+ left: Optional[int] = None
+ """Window x position in screen coordinates."""
+
+ session_id: Optional[str] = None
+ """CDP session identifier the command was addressed to.
+
+ Absent for browser-level commands. Clipped to 128 characters.
+ """
+
+ top: Optional[int] = None
+ """Window y position in screen coordinates."""
+
+ width: Optional[int] = None
+ """Window width in DIP."""
+
+ window_state: Optional[Literal["normal", "minimized", "maximized", "fullscreen", "other"]] = None
+ """Window state requested (`normal`, `minimized`, `maximized`, `fullscreen`).
+
+ A value the protocol does not define is reported as `other`.
+ """
+
+
+class DataBrowserCdpBrowserSetContentsSizeCommandData(BaseModel):
+ """Sanitized `Browser.setContentsSize` arguments.
+
+ Canonical input: `Browser.setContentsSize` in devtools-protocol@2d019e73, pinned at https://github.com/ChromeDevTools/devtools-protocol/blob/2d019e73eb371d1d6985d26d395d78bd8f8a22ba/json/browser_protocol.json. Every argument of this command has a retained or redacted decision in lib/devtoolsproxy/testdata/cdp_arguments.yaml.
+ """
+
+ method: Literal["Browser.setContentsSize"]
+
+ window_id: int
+ """Browser window identifier."""
+
+ command_id: Optional[int] = None
+ """
+ The command's JSON-RPC id, so the command can be joined to the result the
+ browser returned for it. Absent when the client sent none.
+ """
+
+ connection_id: Optional[str] = None
+ """
+ Identifies the CDP proxy connection the command arrived on, matching
+ `cdp_connect` and `cdp_disconnect`. Two clients driving the same browser are
+ told apart by this.
+ """
+
+ height: Optional[int] = None
+ """Contents height in DIP."""
+
+ session_id: Optional[str] = None
+ """CDP session identifier the command was addressed to.
+
+ Absent for browser-level commands. Clipped to 128 characters.
+ """
+
+ width: Optional[int] = None
+ """Contents width in DIP."""
+
+
+class DataBrowserCdpAutofillTriggerCommandData(BaseModel):
+ """Sanitized `Autofill.trigger` arguments.
+
+ Canonical input: `Autofill.trigger` in devtools-protocol@2d019e73, pinned at https://github.com/ChromeDevTools/devtools-protocol/blob/2d019e73eb371d1d6985d26d395d78bd8f8a22ba/json/browser_protocol.json. Every argument of this command has a retained or redacted decision in lib/devtoolsproxy/testdata/cdp_arguments.yaml.
+ """
+
+ field_id: int
+ """Opaque backend node identifier of the field that was autofilled."""
+
+ method: Literal["Autofill.trigger"]
+
+ address_field_count: Optional[int] = None
+ """Number of address fields the command filled.
+
+ Their names and values are never captured.
+ """
+
+ command_id: Optional[int] = None
+ """
+ The command's JSON-RPC id, so the command can be joined to the result the
+ browser returned for it. Absent when the client sent none.
+ """
+
+ connection_id: Optional[str] = None
+ """
+ Identifies the CDP proxy connection the command arrived on, matching
+ `cdp_connect` and `cdp_disconnect`. Two clients driving the same browser are
+ told apart by this.
+ """
+
+ frame_id: Optional[str] = None
+ """Opaque frame identifier.
+
+ Clipped to 128 characters; a longer value is not a real identifier.
+ """
+
+ mode: Optional[Literal["card", "address"]] = None
+ """What was filled: `card` or `address`. The values themselves are never captured."""
+
+ session_id: Optional[str] = None
+ """CDP session identifier the command was addressed to.
+
+ Absent for browser-level commands. Clipped to 128 characters.
+ """
+
+
+Data: TypeAlias = Annotated[
+ Union[
+ DataBrowserCdpInputDispatchMouseEventCommandData,
+ DataBrowserCdpInputDispatchKeyEventCommandData,
+ DataBrowserCdpInputInsertTextCommandData,
+ DataBrowserCdpInputImeSetCompositionCommandData,
+ DataBrowserCdpInputDispatchTouchEventCommandData,
+ DataBrowserCdpInputDispatchDragEventCommandData,
+ DataBrowserCdpInputCancelDraggingCommandData,
+ DataBrowserCdpInputEmulateTouchFromMouseEventCommandData,
+ DataBrowserCdpInputSynthesizePinchGestureCommandData,
+ DataBrowserCdpInputSynthesizeScrollGestureCommandData,
+ DataBrowserCdpInputSynthesizeTapGestureCommandData,
+ DataBrowserCdpDomSetFileInputFilesCommandData,
+ DataBrowserCdpDomFocusCommandData,
+ DataBrowserCdpDomScrollIntoViewIfNeededCommandData,
+ DataBrowserCdpPageBringToFrontCommandData,
+ DataBrowserCdpPageCaptureScreenshotCommandData,
+ DataBrowserCdpPageCaptureSnapshotCommandData,
+ DataBrowserCdpPageHandleJavaScriptDialogCommandData,
+ DataBrowserCdpPageNavigateCommandData,
+ DataBrowserCdpPageNavigateToHistoryEntryCommandData,
+ DataBrowserCdpPageReloadCommandData,
+ DataBrowserCdpPagePrintToPdfCommandData,
+ DataBrowserCdpPageStartScreencastCommandData,
+ DataBrowserCdpPageStopScreencastCommandData,
+ DataBrowserCdpPageStopLoadingCommandData,
+ DataBrowserCdpPageCloseCommandData,
+ DataBrowserCdpPageSetWebLifecycleStateCommandData,
+ DataBrowserCdpTargetActivateTargetCommandData,
+ DataBrowserCdpTargetCloseTargetCommandData,
+ DataBrowserCdpTargetCreateTargetCommandData,
+ DataBrowserCdpTargetCreateBrowserContextCommandData,
+ DataBrowserCdpTargetDisposeBrowserContextCommandData,
+ DataBrowserCdpTargetOpenDevToolsCommandData,
+ DataBrowserCdpBrowserCancelDownloadCommandData,
+ DataBrowserCdpBrowserCloseCommandData,
+ DataBrowserCdpBrowserSetWindowBoundsCommandData,
+ DataBrowserCdpBrowserSetContentsSizeCommandData,
+ DataBrowserCdpAutofillTriggerCommandData,
+ ],
+ PropertyInfo(discriminator="method"),
+]
+
+
+class BrowserCdpCommandEvent(BaseModel):
+ """
+ A browser-control command a client sent over the CDP WebSocket proxy: input gestures, navigation, dialog handling, file selection and screenshots. Configuration commands and the DOM/Runtime traffic a client library issues on the caller's behalf are not reported.
+ One event per browser-control command that reached the browser. The command stream is not sampled, coalesced or reordered. An event is lost only when the method is excluded by telemetry configuration, when the command's arguments do not decode, or when classification cannot keep up. Exclusions are counted in `cdp_disconnect.telemetry_excluded`; the rest in `cdp_disconnect.telemetry_dropped`.
+ """
+
+ category: Literal["control"]
+
+ data: Data
+ """Per-command payload for `cdp_command` events, discriminated by `method`.
+
+ Each variant carries only the arguments approved for that command: values that
+ could hold a secret — typed and composition text, URLs, referrers, scripts,
+ templates, file paths, drag contents and autofill values — are replaced by a
+ length, a count, a presence flag, an enum or a URL scheme and host.
+ """
+
+ source: BrowserEventSource
+ """Provenance metadata identifying which producer emitted the event."""
+
+ ts: int
+ """Event timestamp in Unix microseconds."""
+
+ type: Literal["cdp_command"]
+
+ truncated: Optional[bool] = None
+ """True if the data field was truncated due to size limits."""
diff --git a/src/kernel/types/browsers/browser_cdp_command_method.py b/src/kernel/types/browsers/browser_cdp_command_method.py
new file mode 100644
index 00000000..18ccbb96
--- /dev/null
+++ b/src/kernel/types/browsers/browser_cdp_command_method.py
@@ -0,0 +1,46 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from typing_extensions import Literal, TypeAlias
+
+__all__ = ["BrowserCdpCommandMethod"]
+
+BrowserCdpCommandMethod: TypeAlias = Literal[
+ "Input.dispatchMouseEvent",
+ "Input.dispatchKeyEvent",
+ "Input.insertText",
+ "Input.imeSetComposition",
+ "Input.dispatchTouchEvent",
+ "Input.dispatchDragEvent",
+ "Input.cancelDragging",
+ "Input.emulateTouchFromMouseEvent",
+ "Input.synthesizePinchGesture",
+ "Input.synthesizeScrollGesture",
+ "Input.synthesizeTapGesture",
+ "DOM.setFileInputFiles",
+ "DOM.focus",
+ "DOM.scrollIntoViewIfNeeded",
+ "Page.bringToFront",
+ "Page.captureScreenshot",
+ "Page.captureSnapshot",
+ "Page.handleJavaScriptDialog",
+ "Page.navigate",
+ "Page.navigateToHistoryEntry",
+ "Page.reload",
+ "Page.printToPDF",
+ "Page.startScreencast",
+ "Page.stopScreencast",
+ "Page.stopLoading",
+ "Page.close",
+ "Page.setWebLifecycleState",
+ "Target.activateTarget",
+ "Target.closeTarget",
+ "Target.createTarget",
+ "Target.createBrowserContext",
+ "Target.disposeBrowserContext",
+ "Target.openDevTools",
+ "Browser.cancelDownload",
+ "Browser.close",
+ "Browser.setWindowBounds",
+ "Browser.setContentsSize",
+ "Autofill.trigger",
+]
diff --git a/src/kernel/types/browsers/browser_cdp_connect_event.py b/src/kernel/types/browsers/browser_cdp_connect_event.py
index ebbc7fa3..042ab131 100644
--- a/src/kernel/types/browsers/browser_cdp_connect_event.py
+++ b/src/kernel/types/browsers/browser_cdp_connect_event.py
@@ -6,7 +6,16 @@
from ..._models import BaseModel
from .browser_event_source import BrowserEventSource
-__all__ = ["BrowserCdpConnectEvent"]
+__all__ = ["BrowserCdpConnectEvent", "Data"]
+
+
+class Data(BaseModel):
+ connection_id: Optional[str] = None
+ """
+ Identifies this CDP proxy connection, matching the connection_id on the
+ cdp_command events that arrived on it. Two clients driving the same browser are
+ told apart by this.
+ """
class BrowserCdpConnectEvent(BaseModel):
@@ -25,5 +34,7 @@ class BrowserCdpConnectEvent(BaseModel):
type: Literal["cdp_connect"]
+ data: Optional[Data] = None
+
truncated: Optional[bool] = None
"""True if the data field was truncated due to size limits."""
diff --git a/src/kernel/types/browsers/browser_cdp_disconnect_event.py b/src/kernel/types/browsers/browser_cdp_disconnect_event.py
index d9260ebc..5ec69172 100644
--- a/src/kernel/types/browsers/browser_cdp_disconnect_event.py
+++ b/src/kernel/types/browsers/browser_cdp_disconnect_event.py
@@ -26,6 +26,30 @@ class Data(BaseModel):
shutdown).
"""
+ connection_id: Optional[str] = None
+ """
+ Identifies this CDP proxy connection, matching the connection_id on the
+ cdp_command events that arrived on it. Two clients driving the same browser are
+ told apart by this.
+ """
+
+ telemetry_dropped: Optional[int] = None
+ """
+ Number of forwarded client frames the classifier never saw, because it could not
+ keep up or because classification failed. An upper bound on lost commands rather
+ than a count: a saturated queue turns away whatever arrives next, which may be
+ library traffic that would have produced no event. Telemetry loss only; every
+ command was still relayed to the browser. Absent on events from a browser image
+ predating the field, which is not the same as zero.
+ """
+
+ telemetry_excluded: Optional[int] = None
+ """
+ Number of forwarded client commands that produced no cdp_command event because
+ their method is listed in control.cdp.excluded_methods. Configuration rather
+ than loss, so it is counted apart from telemetry_dropped.
+ """
+
class BrowserCdpDisconnectEvent(BaseModel):
"""An external client disconnected from the CDP WebSocket proxy on this VM.
diff --git a/src/kernel/types/browsers/browser_page_crashed_event.py b/src/kernel/types/browsers/browser_page_crashed_event.py
new file mode 100644
index 00000000..46e84f32
--- /dev/null
+++ b/src/kernel/types/browsers/browser_page_crashed_event.py
@@ -0,0 +1,41 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from typing import Optional
+from typing_extensions import Literal
+
+from ..._models import BaseModel
+from .browser_event_source import BrowserEventSource
+
+__all__ = ["BrowserPageCrashedEvent", "Data"]
+
+
+class Data(BaseModel):
+ target_id: str
+ """CDP target identifier of the crashed page."""
+
+ target_type: Literal["page", "background_page", "service_worker", "shared_worker", "other"]
+ """CDP target type of the page that produced the event."""
+
+ url: str
+ """URL the page was on when its renderer process crashed."""
+
+
+class BrowserPageCrashedEvent(BaseModel):
+ """
+ A page's renderer process crashed (an "Aw, Snap!" failure) while the browser process itself stayed alive. Reported on the crashed page's session, with the session and target ids on `source.metadata`. Captured only while the `page` category is enabled.
+ """
+
+ category: Literal["page"]
+
+ source: BrowserEventSource
+ """Provenance metadata identifying which producer emitted the event."""
+
+ ts: int
+ """Event timestamp in Unix microseconds."""
+
+ type: Literal["page_crashed"]
+
+ data: Optional[Data] = None
+
+ truncated: Optional[bool] = None
+ """True if the data field was truncated due to size limits."""
diff --git a/src/kernel/types/browsers/browser_platform_api_call_event.py b/src/kernel/types/browsers/browser_platform_api_call_event.py
new file mode 100644
index 00000000..f6e78b3b
--- /dev/null
+++ b/src/kernel/types/browsers/browser_platform_api_call_event.py
@@ -0,0 +1,47 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from typing import Optional
+from typing_extensions import Literal
+
+from ..._models import BaseModel
+from .browser_event_source import BrowserEventSource
+
+__all__ = ["BrowserPlatformAPICallEvent", "Data"]
+
+
+class Data(BaseModel):
+ duration_ms: float
+ """Wall-clock duration of the handler in milliseconds."""
+
+ operation_id: str
+ """Matched route's operation, named as the in-VM API names its handler (e.g.
+
+ ProcessExec, StartRecording).
+ """
+
+ request_id: str
+ """Per-request identifier from the in-VM API request middleware."""
+
+ status: int
+ """HTTP response status code."""
+
+
+class BrowserPlatformAPICallEvent(BaseModel):
+ """
+ An HTTP call that manages the browser VM rather than driving the browser, handled by the in-VM API server — recording lifecycle, filesystem and process management, telemetry and browser configuration. Mostly platform-induced (e.g. profile save, replay capture) rather than agent actions.
+ """
+
+ category: Literal["platform"]
+
+ source: BrowserEventSource
+ """Provenance metadata identifying which producer emitted the event."""
+
+ ts: int
+ """Event timestamp in Unix microseconds."""
+
+ type: Literal["platform_api_call"]
+
+ data: Optional[Data] = None
+
+ truncated: Optional[bool] = None
+ """True if the data field was truncated due to size limits."""
diff --git a/src/kernel/types/browsers/browser_telemetry_categories_config.py b/src/kernel/types/browsers/browser_telemetry_categories_config.py
index b89ff17b..8b1f811a 100644
--- a/src/kernel/types/browsers/browser_telemetry_categories_config.py
+++ b/src/kernel/types/browsers/browser_telemetry_categories_config.py
@@ -3,6 +3,7 @@
from typing import Optional
from ..._models import BaseModel
+from .browser_telemetry_control_config import BrowserTelemetryControlConfig
from .browser_telemetry_category_config import BrowserTelemetryCategoryConfig
__all__ = ["BrowserTelemetryCategoriesConfig"]
@@ -11,7 +12,7 @@
class BrowserTelemetryCategoriesConfig(BaseModel):
"""Per-category telemetry capture settings layered onto the default set.
- The operational signals (control, connection, system, captcha) are on by default and are opt-out: set one to enabled=false to stop capturing it. The CDP categories (console, network, page, interaction) and screenshot are off by default and are opt-in: set enabled=true to capture them.
+ The operational signals (control, connection, system, captcha) are on by default and are opt-out: set one to enabled=false to stop capturing it. The CDP categories (console, network, page, interaction), screenshot and platform are off by default and are opt-in: set enabled=true to capture them.
"""
captcha: Optional[BrowserTelemetryCategoryConfig] = None
@@ -26,11 +27,11 @@ class BrowserTelemetryCategoriesConfig(BaseModel):
CDP category; off by default.
"""
- control: Optional[BrowserTelemetryCategoryConfig] = None
- """Agent-driven actions against the browser, such as inbound calls to the in-VM
- API.
-
- On by default.
+ control: Optional[BrowserTelemetryControlConfig] = None
+ """
+ Agent-driven actions against the browser — computer-control calls, Playwright
+ code execution, screenshots, clipboard access, and browser-control commands sent
+ over the CDP proxy. On by default.
"""
interaction: Optional[BrowserTelemetryCategoryConfig] = None
@@ -54,6 +55,13 @@ class BrowserTelemetryCategoriesConfig(BaseModel):
shifts, and LCP. CDP category; off by default.
"""
+ platform: Optional[BrowserTelemetryCategoryConfig] = None
+ """
+ In-VM API calls that manage the browser VM rather than drive the browser
+ (recording, filesystem, process, telemetry and browser configuration). Mostly
+ platform-induced; off by default and must be opted into.
+ """
+
screenshot: Optional[BrowserTelemetryCategoryConfig] = None
"""Periodic base64-encoded viewport screenshots.
diff --git a/src/kernel/types/browsers/browser_telemetry_categories_config_param.py b/src/kernel/types/browsers/browser_telemetry_categories_config_param.py
index add32385..e707230f 100644
--- a/src/kernel/types/browsers/browser_telemetry_categories_config_param.py
+++ b/src/kernel/types/browsers/browser_telemetry_categories_config_param.py
@@ -4,6 +4,7 @@
from typing_extensions import TypedDict
+from .browser_telemetry_control_config_param import BrowserTelemetryControlConfigParam
from .browser_telemetry_category_config_param import BrowserTelemetryCategoryConfigParam
__all__ = ["BrowserTelemetryCategoriesConfigParam"]
@@ -12,7 +13,7 @@
class BrowserTelemetryCategoriesConfigParam(TypedDict, total=False):
"""Per-category telemetry capture settings layered onto the default set.
- The operational signals (control, connection, system, captcha) are on by default and are opt-out: set one to enabled=false to stop capturing it. The CDP categories (console, network, page, interaction) and screenshot are off by default and are opt-in: set enabled=true to capture them.
+ The operational signals (control, connection, system, captcha) are on by default and are opt-out: set one to enabled=false to stop capturing it. The CDP categories (console, network, page, interaction), screenshot and platform are off by default and are opt-in: set enabled=true to capture them.
"""
captcha: BrowserTelemetryCategoryConfigParam
@@ -27,11 +28,11 @@ class BrowserTelemetryCategoriesConfigParam(TypedDict, total=False):
CDP category; off by default.
"""
- control: BrowserTelemetryCategoryConfigParam
- """Agent-driven actions against the browser, such as inbound calls to the in-VM
- API.
-
- On by default.
+ control: BrowserTelemetryControlConfigParam
+ """
+ Agent-driven actions against the browser — computer-control calls, Playwright
+ code execution, screenshots, clipboard access, and browser-control commands sent
+ over the CDP proxy. On by default.
"""
interaction: BrowserTelemetryCategoryConfigParam
@@ -55,6 +56,13 @@ class BrowserTelemetryCategoriesConfigParam(TypedDict, total=False):
shifts, and LCP. CDP category; off by default.
"""
+ platform: BrowserTelemetryCategoryConfigParam
+ """
+ In-VM API calls that manage the browser VM rather than drive the browser
+ (recording, filesystem, process, telemetry and browser configuration). Mostly
+ platform-induced; off by default and must be opted into.
+ """
+
screenshot: BrowserTelemetryCategoryConfigParam
"""Periodic base64-encoded viewport screenshots.
diff --git a/src/kernel/types/browsers/browser_telemetry_category_config.py b/src/kernel/types/browsers/browser_telemetry_category_config.py
index f63c02f4..1a64f7d6 100644
--- a/src/kernel/types/browsers/browser_telemetry_category_config.py
+++ b/src/kernel/types/browsers/browser_telemetry_category_config.py
@@ -14,6 +14,6 @@ class BrowserTelemetryCategoryConfig(BaseModel):
"""Whether this category is captured.
Operational categories (control, connection, system, captcha) default to true;
- set false to opt out. CDP categories (console, network, page, interaction) and
- screenshot default to false; set true to opt in.
+ set false to opt out. CDP categories (console, network, page, interaction),
+ screenshot and platform default to false; set true to opt in.
"""
diff --git a/src/kernel/types/browsers/browser_telemetry_category_config_param.py b/src/kernel/types/browsers/browser_telemetry_category_config_param.py
index 09dbe418..27f08538 100644
--- a/src/kernel/types/browsers/browser_telemetry_category_config_param.py
+++ b/src/kernel/types/browsers/browser_telemetry_category_config_param.py
@@ -14,6 +14,6 @@ class BrowserTelemetryCategoryConfigParam(TypedDict, total=False):
"""Whether this category is captured.
Operational categories (control, connection, system, captcha) default to true;
- set false to opt out. CDP categories (console, network, page, interaction) and
- screenshot default to false; set true to opt in.
+ set false to opt out. CDP categories (console, network, page, interaction),
+ screenshot and platform default to false; set true to opt in.
"""
diff --git a/src/kernel/types/browsers/browser_telemetry_cdp_control_config.py b/src/kernel/types/browsers/browser_telemetry_cdp_control_config.py
new file mode 100644
index 00000000..fe425713
--- /dev/null
+++ b/src/kernel/types/browsers/browser_telemetry_cdp_control_config.py
@@ -0,0 +1,24 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from typing import List, Optional
+
+from ..._models import BaseModel
+from .browser_cdp_command_method import BrowserCdpCommandMethod
+
+__all__ = ["BrowserTelemetryCdpControlConfig"]
+
+
+class BrowserTelemetryCdpControlConfig(BaseModel):
+ """Settings for the cdp_command events the CDP proxy reports."""
+
+ excluded_methods: Optional[List[BrowserCdpCommandMethod]] = None
+ """Methods to leave out of the cdp_command stream.
+
+ Omit the list to keep the current one; send an empty list to report every
+ supported method again. Exclusion is a telemetry setting only: an excluded
+ command is still relayed to the browser unchanged, it simply produces no event.
+ Use it to drop the highest-volume methods — Input.dispatchMouseEvent during a
+ humanized cursor path, or Page.captureScreenshot under a screencast — without
+ turning the whole category off. Excluded commands are counted in
+ cdp_disconnect.telemetry_excluded.
+ """
diff --git a/src/kernel/types/browsers/browser_telemetry_cdp_control_config_param.py b/src/kernel/types/browsers/browser_telemetry_cdp_control_config_param.py
new file mode 100644
index 00000000..300e9b60
--- /dev/null
+++ b/src/kernel/types/browsers/browser_telemetry_cdp_control_config_param.py
@@ -0,0 +1,26 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from __future__ import annotations
+
+from typing import List
+from typing_extensions import TypedDict
+
+from .browser_cdp_command_method import BrowserCdpCommandMethod
+
+__all__ = ["BrowserTelemetryCdpControlConfigParam"]
+
+
+class BrowserTelemetryCdpControlConfigParam(TypedDict, total=False):
+ """Settings for the cdp_command events the CDP proxy reports."""
+
+ excluded_methods: List[BrowserCdpCommandMethod]
+ """Methods to leave out of the cdp_command stream.
+
+ Omit the list to keep the current one; send an empty list to report every
+ supported method again. Exclusion is a telemetry setting only: an excluded
+ command is still relayed to the browser unchanged, it simply produces no event.
+ Use it to drop the highest-volume methods — Input.dispatchMouseEvent during a
+ humanized cursor path, or Page.captureScreenshot under a screencast — without
+ turning the whole category off. Excluded commands are counted in
+ cdp_disconnect.telemetry_excluded.
+ """
diff --git a/src/kernel/types/browsers/browser_telemetry_control_config.py b/src/kernel/types/browsers/browser_telemetry_control_config.py
new file mode 100644
index 00000000..7a8dfbb3
--- /dev/null
+++ b/src/kernel/types/browsers/browser_telemetry_control_config.py
@@ -0,0 +1,28 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from typing import Optional
+
+from ..._models import BaseModel
+from .browser_telemetry_cdp_control_config import BrowserTelemetryCdpControlConfig
+
+__all__ = ["BrowserTelemetryControlConfig"]
+
+
+class BrowserTelemetryControlConfig(BaseModel):
+ """Configuration for the control category.
+
+ Same enabled semantics as any other category, plus settings for the browser-control commands the CDP proxy reports.
+ """
+
+ cdp: Optional[BrowserTelemetryCdpControlConfig] = None
+ """Settings for the cdp_command events the CDP proxy reports.
+
+ Merged independently of enabled, so a later update that only sets enabled keeps
+ the current exclusion list.
+ """
+
+ enabled: Optional[bool] = None
+ """Whether this category is captured.
+
+ Control is on by default; set false to opt out.
+ """
diff --git a/src/kernel/types/browsers/browser_telemetry_control_config_param.py b/src/kernel/types/browsers/browser_telemetry_control_config_param.py
new file mode 100644
index 00000000..e5afaea5
--- /dev/null
+++ b/src/kernel/types/browsers/browser_telemetry_control_config_param.py
@@ -0,0 +1,29 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from __future__ import annotations
+
+from typing_extensions import TypedDict
+
+from .browser_telemetry_cdp_control_config_param import BrowserTelemetryCdpControlConfigParam
+
+__all__ = ["BrowserTelemetryControlConfigParam"]
+
+
+class BrowserTelemetryControlConfigParam(TypedDict, total=False):
+ """Configuration for the control category.
+
+ Same enabled semantics as any other category, plus settings for the browser-control commands the CDP proxy reports.
+ """
+
+ cdp: BrowserTelemetryCdpControlConfigParam
+ """Settings for the cdp_command events the CDP proxy reports.
+
+ Merged independently of enabled, so a later update that only sets enabled keeps
+ the current exclusion list.
+ """
+
+ enabled: bool
+ """Whether this category is captured.
+
+ Control is on by default; set false to opt out.
+ """
diff --git a/src/kernel/types/browsers/browser_telemetry_event.py b/src/kernel/types/browsers/browser_telemetry_event.py
index d57b92d2..2ce872f4 100644
--- a/src/kernel/types/browsers/browser_telemetry_event.py
+++ b/src/kernel/types/browsers/browser_telemetry_event.py
@@ -9,10 +9,12 @@
from .browser_api_call_event import BrowserAPICallEvent
from .browser_page_lcp_event import BrowserPageLcpEvent
from .browser_page_load_event import BrowserPageLoadEvent
+from .browser_cdp_command_event import BrowserCdpCommandEvent
from .browser_cdp_connect_event import BrowserCdpConnectEvent
from .browser_console_log_event import BrowserConsoleLogEvent
from .browser_proxy_error_event import BrowserProxyErrorEvent
from .browser_network_idle_event import BrowserNetworkIdleEvent
+from .browser_page_crashed_event import BrowserPageCrashedEvent
from .browser_console_error_event import BrowserConsoleErrorEvent
from .browser_cdp_disconnect_event import BrowserCdpDisconnectEvent
from .browser_interaction_key_event import BrowserInteractionKeyEvent
@@ -25,6 +27,7 @@
from .browser_interaction_click_event import BrowserInteractionClickEvent
from .browser_live_view_connect_event import BrowserLiveViewConnectEvent
from .browser_page_layout_shift_event import BrowserPageLayoutShiftEvent
+from .browser_platform_api_call_event import BrowserPlatformAPICallEvent
from .browser_monitor_screenshot_event import BrowserMonitorScreenshotEvent
from .browser_monitor_init_failed_event import BrowserMonitorInitFailedEvent
from .browser_monitor_reconnected_event import BrowserMonitorReconnectedEvent
@@ -53,6 +56,7 @@
BrowserPageDomContentLoadedEvent,
BrowserPageLoadEvent,
BrowserPageTabOpenedEvent,
+ BrowserPageCrashedEvent,
BrowserPageLayoutShiftEvent,
BrowserPageLcpEvent,
BrowserPageLayoutSettledEvent,
@@ -66,6 +70,8 @@
BrowserMonitorReconnectFailedEvent,
BrowserMonitorInitFailedEvent,
BrowserAPICallEvent,
+ BrowserPlatformAPICallEvent,
+ BrowserCdpCommandEvent,
BrowserCdpConnectEvent,
BrowserCdpDisconnectEvent,
BrowserLiveViewConnectEvent,
diff --git a/src/kernel/types/browsers/telemetry_events_params.py b/src/kernel/types/browsers/telemetry_events_params.py
index 4aa0f13c..ec385bb5 100644
--- a/src/kernel/types/browsers/telemetry_events_params.py
+++ b/src/kernel/types/browsers/telemetry_events_params.py
@@ -16,6 +16,7 @@ class TelemetryEventsParams(TypedDict, total=False):
"page",
"interaction",
"control",
+ "platform",
"connection",
"system",
"screenshot",
diff --git a/tests/api_resources/auth/test_connections.py b/tests/api_resources/auth/test_connections.py
index f3d4a05c..4dff13ee 100644
--- a/tests/api_resources/auth/test_connections.py
+++ b/tests/api_resources/auth/test_connections.py
@@ -52,10 +52,14 @@ def test_method_create_with_all_params(self, client: Kernel) -> None:
"captcha": {"enabled": True},
"connection": {"enabled": True},
"console": {"enabled": True},
- "control": {"enabled": True},
+ "control": {
+ "cdp": {"excluded_methods": ["Input.dispatchMouseEvent"]},
+ "enabled": True,
+ },
"interaction": {"enabled": True},
"network": {"enabled": True},
"page": {"enabled": True},
+ "platform": {"enabled": True},
"screenshot": {"enabled": True},
"system": {"enabled": True},
},
@@ -76,10 +80,14 @@ def test_method_create_with_all_params(self, client: Kernel) -> None:
"captcha": {"enabled": True},
"connection": {"enabled": True},
"console": {"enabled": True},
- "control": {"enabled": True},
+ "control": {
+ "cdp": {"excluded_methods": ["Input.dispatchMouseEvent"]},
+ "enabled": True,
+ },
"interaction": {"enabled": True},
"network": {"enabled": True},
"page": {"enabled": True},
+ "platform": {"enabled": True},
"screenshot": {"enabled": True},
"system": {"enabled": True},
},
@@ -209,10 +217,14 @@ def test_method_update_with_all_params(self, client: Kernel) -> None:
"captcha": {"enabled": True},
"connection": {"enabled": True},
"console": {"enabled": True},
- "control": {"enabled": True},
+ "control": {
+ "cdp": {"excluded_methods": ["Input.dispatchMouseEvent"]},
+ "enabled": True,
+ },
"interaction": {"enabled": True},
"network": {"enabled": True},
"page": {"enabled": True},
+ "platform": {"enabled": True},
"screenshot": {"enabled": True},
"system": {"enabled": True},
},
@@ -233,10 +245,14 @@ def test_method_update_with_all_params(self, client: Kernel) -> None:
"captcha": {"enabled": True},
"connection": {"enabled": True},
"console": {"enabled": True},
- "control": {"enabled": True},
+ "control": {
+ "cdp": {"excluded_methods": ["Input.dispatchMouseEvent"]},
+ "enabled": True,
+ },
"interaction": {"enabled": True},
"network": {"enabled": True},
"page": {"enabled": True},
+ "platform": {"enabled": True},
"screenshot": {"enabled": True},
"system": {"enabled": True},
},
@@ -451,10 +467,14 @@ def test_method_login_with_all_params(self, client: Kernel) -> None:
"captcha": {"enabled": True},
"connection": {"enabled": True},
"console": {"enabled": True},
- "control": {"enabled": True},
+ "control": {
+ "cdp": {"excluded_methods": ["Input.dispatchMouseEvent"]},
+ "enabled": True,
+ },
"interaction": {"enabled": True},
"network": {"enabled": True},
"page": {"enabled": True},
+ "platform": {"enabled": True},
"screenshot": {"enabled": True},
"system": {"enabled": True},
},
@@ -475,10 +495,14 @@ def test_method_login_with_all_params(self, client: Kernel) -> None:
"captcha": {"enabled": True},
"connection": {"enabled": True},
"console": {"enabled": True},
- "control": {"enabled": True},
+ "control": {
+ "cdp": {"excluded_methods": ["Input.dispatchMouseEvent"]},
+ "enabled": True,
+ },
"interaction": {"enabled": True},
"network": {"enabled": True},
"page": {"enabled": True},
+ "platform": {"enabled": True},
"screenshot": {"enabled": True},
"system": {"enabled": True},
},
@@ -687,10 +711,14 @@ async def test_method_create_with_all_params(self, async_client: AsyncKernel) ->
"captcha": {"enabled": True},
"connection": {"enabled": True},
"console": {"enabled": True},
- "control": {"enabled": True},
+ "control": {
+ "cdp": {"excluded_methods": ["Input.dispatchMouseEvent"]},
+ "enabled": True,
+ },
"interaction": {"enabled": True},
"network": {"enabled": True},
"page": {"enabled": True},
+ "platform": {"enabled": True},
"screenshot": {"enabled": True},
"system": {"enabled": True},
},
@@ -711,10 +739,14 @@ async def test_method_create_with_all_params(self, async_client: AsyncKernel) ->
"captcha": {"enabled": True},
"connection": {"enabled": True},
"console": {"enabled": True},
- "control": {"enabled": True},
+ "control": {
+ "cdp": {"excluded_methods": ["Input.dispatchMouseEvent"]},
+ "enabled": True,
+ },
"interaction": {"enabled": True},
"network": {"enabled": True},
"page": {"enabled": True},
+ "platform": {"enabled": True},
"screenshot": {"enabled": True},
"system": {"enabled": True},
},
@@ -844,10 +876,14 @@ async def test_method_update_with_all_params(self, async_client: AsyncKernel) ->
"captcha": {"enabled": True},
"connection": {"enabled": True},
"console": {"enabled": True},
- "control": {"enabled": True},
+ "control": {
+ "cdp": {"excluded_methods": ["Input.dispatchMouseEvent"]},
+ "enabled": True,
+ },
"interaction": {"enabled": True},
"network": {"enabled": True},
"page": {"enabled": True},
+ "platform": {"enabled": True},
"screenshot": {"enabled": True},
"system": {"enabled": True},
},
@@ -868,10 +904,14 @@ async def test_method_update_with_all_params(self, async_client: AsyncKernel) ->
"captcha": {"enabled": True},
"connection": {"enabled": True},
"console": {"enabled": True},
- "control": {"enabled": True},
+ "control": {
+ "cdp": {"excluded_methods": ["Input.dispatchMouseEvent"]},
+ "enabled": True,
+ },
"interaction": {"enabled": True},
"network": {"enabled": True},
"page": {"enabled": True},
+ "platform": {"enabled": True},
"screenshot": {"enabled": True},
"system": {"enabled": True},
},
@@ -1086,10 +1126,14 @@ async def test_method_login_with_all_params(self, async_client: AsyncKernel) ->
"captcha": {"enabled": True},
"connection": {"enabled": True},
"console": {"enabled": True},
- "control": {"enabled": True},
+ "control": {
+ "cdp": {"excluded_methods": ["Input.dispatchMouseEvent"]},
+ "enabled": True,
+ },
"interaction": {"enabled": True},
"network": {"enabled": True},
"page": {"enabled": True},
+ "platform": {"enabled": True},
"screenshot": {"enabled": True},
"system": {"enabled": True},
},
@@ -1110,10 +1154,14 @@ async def test_method_login_with_all_params(self, async_client: AsyncKernel) ->
"captcha": {"enabled": True},
"connection": {"enabled": True},
"console": {"enabled": True},
- "control": {"enabled": True},
+ "control": {
+ "cdp": {"excluded_methods": ["Input.dispatchMouseEvent"]},
+ "enabled": True,
+ },
"interaction": {"enabled": True},
"network": {"enabled": True},
"page": {"enabled": True},
+ "platform": {"enabled": True},
"screenshot": {"enabled": True},
"system": {"enabled": True},
},
diff --git a/tests/api_resources/test_browser_pools.py b/tests/api_resources/test_browser_pools.py
index e411df27..1f541959 100644
--- a/tests/api_resources/test_browser_pools.py
+++ b/tests/api_resources/test_browser_pools.py
@@ -60,10 +60,14 @@ def test_method_create_with_all_params(self, client: Kernel) -> None:
"captcha": {"enabled": True},
"connection": {"enabled": True},
"console": {"enabled": True},
- "control": {"enabled": True},
+ "control": {
+ "cdp": {"excluded_methods": ["Input.dispatchMouseEvent"]},
+ "enabled": True,
+ },
"interaction": {"enabled": True},
"network": {"enabled": True},
"page": {"enabled": True},
+ "platform": {"enabled": True},
"screenshot": {"enabled": True},
"system": {"enabled": True},
},
@@ -195,10 +199,14 @@ def test_method_update_with_all_params(self, client: Kernel) -> None:
"captcha": {"enabled": True},
"connection": {"enabled": True},
"console": {"enabled": True},
- "control": {"enabled": True},
+ "control": {
+ "cdp": {"excluded_methods": ["Input.dispatchMouseEvent"]},
+ "enabled": True,
+ },
"interaction": {"enabled": True},
"network": {"enabled": True},
"page": {"enabled": True},
+ "platform": {"enabled": True},
"screenshot": {"enabled": True},
"system": {"enabled": True},
},
@@ -372,10 +380,14 @@ def test_method_acquire_with_all_params(self, client: Kernel) -> None:
"captcha": {"enabled": True},
"connection": {"enabled": True},
"console": {"enabled": True},
- "control": {"enabled": True},
+ "control": {
+ "cdp": {"excluded_methods": ["Input.dispatchMouseEvent"]},
+ "enabled": True,
+ },
"interaction": {"enabled": True},
"network": {"enabled": True},
"page": {"enabled": True},
+ "platform": {"enabled": True},
"screenshot": {"enabled": True},
"system": {"enabled": True},
},
@@ -570,10 +582,14 @@ async def test_method_create_with_all_params(self, async_client: AsyncKernel) ->
"captcha": {"enabled": True},
"connection": {"enabled": True},
"console": {"enabled": True},
- "control": {"enabled": True},
+ "control": {
+ "cdp": {"excluded_methods": ["Input.dispatchMouseEvent"]},
+ "enabled": True,
+ },
"interaction": {"enabled": True},
"network": {"enabled": True},
"page": {"enabled": True},
+ "platform": {"enabled": True},
"screenshot": {"enabled": True},
"system": {"enabled": True},
},
@@ -705,10 +721,14 @@ async def test_method_update_with_all_params(self, async_client: AsyncKernel) ->
"captcha": {"enabled": True},
"connection": {"enabled": True},
"console": {"enabled": True},
- "control": {"enabled": True},
+ "control": {
+ "cdp": {"excluded_methods": ["Input.dispatchMouseEvent"]},
+ "enabled": True,
+ },
"interaction": {"enabled": True},
"network": {"enabled": True},
"page": {"enabled": True},
+ "platform": {"enabled": True},
"screenshot": {"enabled": True},
"system": {"enabled": True},
},
@@ -882,10 +902,14 @@ async def test_method_acquire_with_all_params(self, async_client: AsyncKernel) -
"captcha": {"enabled": True},
"connection": {"enabled": True},
"console": {"enabled": True},
- "control": {"enabled": True},
+ "control": {
+ "cdp": {"excluded_methods": ["Input.dispatchMouseEvent"]},
+ "enabled": True,
+ },
"interaction": {"enabled": True},
"network": {"enabled": True},
"page": {"enabled": True},
+ "platform": {"enabled": True},
"screenshot": {"enabled": True},
"system": {"enabled": True},
},
diff --git a/tests/api_resources/test_browsers.py b/tests/api_resources/test_browsers.py
index 1a352038..55a7e66f 100644
--- a/tests/api_resources/test_browsers.py
+++ b/tests/api_resources/test_browsers.py
@@ -71,10 +71,14 @@ def test_method_create_with_all_params(self, client: Kernel) -> None:
"captcha": {"enabled": True},
"connection": {"enabled": True},
"console": {"enabled": True},
- "control": {"enabled": True},
+ "control": {
+ "cdp": {"excluded_methods": ["Input.dispatchMouseEvent"]},
+ "enabled": True,
+ },
"interaction": {"enabled": True},
"network": {"enabled": True},
"page": {"enabled": True},
+ "platform": {"enabled": True},
"screenshot": {"enabled": True},
"system": {"enabled": True},
},
@@ -206,10 +210,14 @@ def test_method_update_with_all_params(self, client: Kernel) -> None:
"captcha": {"enabled": True},
"connection": {"enabled": True},
"console": {"enabled": True},
- "control": {"enabled": True},
+ "control": {
+ "cdp": {"excluded_methods": ["Input.dispatchMouseEvent"]},
+ "enabled": True,
+ },
"interaction": {"enabled": True},
"network": {"enabled": True},
"page": {"enabled": True},
+ "platform": {"enabled": True},
"screenshot": {"enabled": True},
"system": {"enabled": True},
},
@@ -530,10 +538,14 @@ async def test_method_create_with_all_params(self, async_client: AsyncKernel) ->
"captcha": {"enabled": True},
"connection": {"enabled": True},
"console": {"enabled": True},
- "control": {"enabled": True},
+ "control": {
+ "cdp": {"excluded_methods": ["Input.dispatchMouseEvent"]},
+ "enabled": True,
+ },
"interaction": {"enabled": True},
"network": {"enabled": True},
"page": {"enabled": True},
+ "platform": {"enabled": True},
"screenshot": {"enabled": True},
"system": {"enabled": True},
},
@@ -665,10 +677,14 @@ async def test_method_update_with_all_params(self, async_client: AsyncKernel) ->
"captcha": {"enabled": True},
"connection": {"enabled": True},
"console": {"enabled": True},
- "control": {"enabled": True},
+ "control": {
+ "cdp": {"excluded_methods": ["Input.dispatchMouseEvent"]},
+ "enabled": True,
+ },
"interaction": {"enabled": True},
"network": {"enabled": True},
"page": {"enabled": True},
+ "platform": {"enabled": True},
"screenshot": {"enabled": True},
"system": {"enabled": True},
},
From 7afcfc499d7008407c872d75bcc4d8a1604f7954 Mon Sep 17 00:00:00 2001
From: "kernel-internal[bot]"
<260533166+kernel-internal[bot]@users.noreply.github.com>
Date: Mon, 24 Aug 2026 18:22:15 +0000
Subject: [PATCH 212/366] feat: site configs: report proxy-restricted analyses
Stainless-Generated-From: 656a31cc699c863df57d613e00e021e18cde32ca
---
.stats.yml | 2 +-
api.md | 31 +
src/kernel/_client.py | 44 ++
src/kernel/resources/__init__.py | 14 +
src/kernel/resources/site_configs.py | 596 ++++++++++++++++++
src/kernel/types/__init__.py | 18 +
src/kernel/types/analysis.py | 30 +
src/kernel/types/analysis_summary.py | 13 +
src/kernel/types/browser.py | 32 +
src/kernel/types/evidence.py | 38 ++
src/kernel/types/lookup_response.py | 15 +
src/kernel/types/no_recommendation.py | 20 +
src/kernel/types/proxy.py | 150 +++++
src/kernel/types/recommendation.py | 35 +
src/kernel/types/recommendation_result.py | 12 +
src/kernel/types/recommendation_summary.py | 29 +
src/kernel/types/site_config_list_params.py | 13 +
...site_config_list_recommendations_params.py | 17 +
src/kernel/types/site_config_lookup_params.py | 20 +
.../types/site_config_resolve_params.py | 21 +
src/kernel/types/site_config_response.py | 23 +
src/kernel/types/target.py | 16 +
tests/api_resources/test_site_configs.py | 438 +++++++++++++
23 files changed, 1626 insertions(+), 1 deletion(-)
create mode 100644 src/kernel/resources/site_configs.py
create mode 100644 src/kernel/types/analysis.py
create mode 100644 src/kernel/types/analysis_summary.py
create mode 100644 src/kernel/types/browser.py
create mode 100644 src/kernel/types/evidence.py
create mode 100644 src/kernel/types/lookup_response.py
create mode 100644 src/kernel/types/no_recommendation.py
create mode 100644 src/kernel/types/proxy.py
create mode 100644 src/kernel/types/recommendation.py
create mode 100644 src/kernel/types/recommendation_result.py
create mode 100644 src/kernel/types/recommendation_summary.py
create mode 100644 src/kernel/types/site_config_list_params.py
create mode 100644 src/kernel/types/site_config_list_recommendations_params.py
create mode 100644 src/kernel/types/site_config_lookup_params.py
create mode 100644 src/kernel/types/site_config_resolve_params.py
create mode 100644 src/kernel/types/site_config_response.py
create mode 100644 src/kernel/types/target.py
create mode 100644 tests/api_resources/test_site_configs.py
diff --git a/.stats.yml b/.stats.yml
index f9d1086b..2aae27b8 100644
--- a/.stats.yml
+++ b/.stats.yml
@@ -1 +1 @@
-configured_endpoints: 140
+configured_endpoints: 145
diff --git a/api.md b/api.md
index 4d75e371..60493a41 100644
--- a/api.md
+++ b/api.md
@@ -74,6 +74,37 @@ Methods:
- client.invocations.follow(id, \*\*params) -> InvocationFollowResponse
- client.invocations.list_browsers(id) -> InvocationListBrowsersResponse
+# SiteConfigs
+
+Types:
+
+```python
+from kernel.types import (
+ Analysis,
+ AnalysisSummary,
+ Browser,
+ Evidence,
+ LookupRequest,
+ LookupResponse,
+ NoRecommendation,
+ Proxy,
+ Recommendation,
+ RecommendationResult,
+ RecommendationSummary,
+ ResolveRequest,
+ SiteConfigResponse,
+ Target,
+)
+```
+
+Methods:
+
+- client.site_configs.retrieve(id) -> SiteConfigResponse
+- client.site_configs.list(\*\*params) -> SyncOffsetPagination[AnalysisSummary]
+- client.site_configs.list_recommendations(\*\*params) -> SyncOffsetPagination[RecommendationSummary]
+- client.site_configs.lookup(\*\*params) -> LookupResponse
+- client.site_configs.resolve(\*\*params) -> SiteConfigResponse
+
# Browsers
Types:
diff --git a/src/kernel/_client.py b/src/kernel/_client.py
index 9a040e34..d96b5804 100644
--- a/src/kernel/_client.py
+++ b/src/kernel/_client.py
@@ -62,6 +62,7 @@
deployments,
invocations,
organization,
+ site_configs,
browser_pools,
credential_providers,
)
@@ -74,6 +75,7 @@
from .resources.credentials import CredentialsResource, AsyncCredentialsResource
from .resources.deployments import DeploymentsResource, AsyncDeploymentsResource
from .resources.invocations import InvocationsResource, AsyncInvocationsResource
+ from .resources.site_configs import SiteConfigsResource, AsyncSiteConfigsResource
from .resources.browser_pools import BrowserPoolsResource, AsyncBrowserPoolsResource
from .resources.browsers.browsers import BrowsersResource, AsyncBrowsersResource
from .resources.projects.projects import ProjectsResource, AsyncProjectsResource
@@ -223,6 +225,13 @@ def invocations(self) -> InvocationsResource:
return InvocationsResource(self)
+ @cached_property
+ def site_configs(self) -> SiteConfigsResource:
+ """Resolve browser and proxy recommendations for bot-protected sites."""
+ from .resources.site_configs import SiteConfigsResource
+
+ return SiteConfigsResource(self)
+
@cached_property
def browsers(self) -> BrowsersResource:
"""Create and manage browser sessions."""
@@ -599,6 +608,13 @@ def invocations(self) -> AsyncInvocationsResource:
return AsyncInvocationsResource(self)
+ @cached_property
+ def site_configs(self) -> AsyncSiteConfigsResource:
+ """Resolve browser and proxy recommendations for bot-protected sites."""
+ from .resources.site_configs import AsyncSiteConfigsResource
+
+ return AsyncSiteConfigsResource(self)
+
@cached_property
def browsers(self) -> AsyncBrowsersResource:
"""Create and manage browser sessions."""
@@ -879,6 +895,13 @@ def invocations(self) -> invocations.InvocationsResourceWithRawResponse:
return InvocationsResourceWithRawResponse(self._client.invocations)
+ @cached_property
+ def site_configs(self) -> site_configs.SiteConfigsResourceWithRawResponse:
+ """Resolve browser and proxy recommendations for bot-protected sites."""
+ from .resources.site_configs import SiteConfigsResourceWithRawResponse
+
+ return SiteConfigsResourceWithRawResponse(self._client.site_configs)
+
@cached_property
def browsers(self) -> browsers.BrowsersResourceWithRawResponse:
"""Create and manage browser sessions."""
@@ -999,6 +1022,13 @@ def invocations(self) -> invocations.AsyncInvocationsResourceWithRawResponse:
return AsyncInvocationsResourceWithRawResponse(self._client.invocations)
+ @cached_property
+ def site_configs(self) -> site_configs.AsyncSiteConfigsResourceWithRawResponse:
+ """Resolve browser and proxy recommendations for bot-protected sites."""
+ from .resources.site_configs import AsyncSiteConfigsResourceWithRawResponse
+
+ return AsyncSiteConfigsResourceWithRawResponse(self._client.site_configs)
+
@cached_property
def browsers(self) -> browsers.AsyncBrowsersResourceWithRawResponse:
"""Create and manage browser sessions."""
@@ -1119,6 +1149,13 @@ def invocations(self) -> invocations.InvocationsResourceWithStreamingResponse:
return InvocationsResourceWithStreamingResponse(self._client.invocations)
+ @cached_property
+ def site_configs(self) -> site_configs.SiteConfigsResourceWithStreamingResponse:
+ """Resolve browser and proxy recommendations for bot-protected sites."""
+ from .resources.site_configs import SiteConfigsResourceWithStreamingResponse
+
+ return SiteConfigsResourceWithStreamingResponse(self._client.site_configs)
+
@cached_property
def browsers(self) -> browsers.BrowsersResourceWithStreamingResponse:
"""Create and manage browser sessions."""
@@ -1239,6 +1276,13 @@ def invocations(self) -> invocations.AsyncInvocationsResourceWithStreamingRespon
return AsyncInvocationsResourceWithStreamingResponse(self._client.invocations)
+ @cached_property
+ def site_configs(self) -> site_configs.AsyncSiteConfigsResourceWithStreamingResponse:
+ """Resolve browser and proxy recommendations for bot-protected sites."""
+ from .resources.site_configs import AsyncSiteConfigsResourceWithStreamingResponse
+
+ return AsyncSiteConfigsResourceWithStreamingResponse(self._client.site_configs)
+
@cached_property
def browsers(self) -> browsers.AsyncBrowsersResourceWithStreamingResponse:
"""Create and manage browser sessions."""
diff --git a/src/kernel/resources/__init__.py b/src/kernel/resources/__init__.py
index 10f1a88b..2ec29c9c 100644
--- a/src/kernel/resources/__init__.py
+++ b/src/kernel/resources/__init__.py
@@ -112,6 +112,14 @@
OrganizationResourceWithStreamingResponse,
AsyncOrganizationResourceWithStreamingResponse,
)
+from .site_configs import (
+ SiteConfigsResource,
+ AsyncSiteConfigsResource,
+ SiteConfigsResourceWithRawResponse,
+ AsyncSiteConfigsResourceWithRawResponse,
+ SiteConfigsResourceWithStreamingResponse,
+ AsyncSiteConfigsResourceWithStreamingResponse,
+)
from .browser_pools import (
BrowserPoolsResource,
AsyncBrowserPoolsResource,
@@ -148,6 +156,12 @@
"AsyncInvocationsResourceWithRawResponse",
"InvocationsResourceWithStreamingResponse",
"AsyncInvocationsResourceWithStreamingResponse",
+ "SiteConfigsResource",
+ "AsyncSiteConfigsResource",
+ "SiteConfigsResourceWithRawResponse",
+ "AsyncSiteConfigsResourceWithRawResponse",
+ "SiteConfigsResourceWithStreamingResponse",
+ "AsyncSiteConfigsResourceWithStreamingResponse",
"BrowsersResource",
"AsyncBrowsersResource",
"BrowsersResourceWithRawResponse",
diff --git a/src/kernel/resources/site_configs.py b/src/kernel/resources/site_configs.py
new file mode 100644
index 00000000..b98a8d80
--- /dev/null
+++ b/src/kernel/resources/site_configs.py
@@ -0,0 +1,596 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from __future__ import annotations
+
+from typing_extensions import Literal
+
+import httpx
+
+from ..types import (
+ site_config_list_params,
+ site_config_lookup_params,
+ site_config_resolve_params,
+ site_config_list_recommendations_params,
+)
+from .._types import Body, Omit, Query, Headers, NotGiven, SequenceNotStr, omit, not_given
+from .._utils import path_template, maybe_transform, async_maybe_transform
+from .._compat import cached_property
+from .._resource import SyncAPIResource, AsyncAPIResource
+from .._response import (
+ to_raw_response_wrapper,
+ to_streamed_response_wrapper,
+ async_to_raw_response_wrapper,
+ async_to_streamed_response_wrapper,
+)
+from ..pagination import SyncOffsetPagination, AsyncOffsetPagination
+from .._base_client import AsyncPaginator, make_request_options
+from ..types.lookup_response import LookupResponse
+from ..types.analysis_summary import AnalysisSummary
+from ..types.site_config_response import SiteConfigResponse
+from ..types.recommendation_summary import RecommendationSummary
+
+__all__ = ["SiteConfigsResource", "AsyncSiteConfigsResource"]
+
+
+class SiteConfigsResource(SyncAPIResource):
+ """Resolve browser and proxy recommendations for bot-protected sites."""
+
+ @cached_property
+ def with_raw_response(self) -> SiteConfigsResourceWithRawResponse:
+ """
+ This property can be used as a prefix for any HTTP method call to return
+ the raw response object instead of the parsed content.
+
+ For more information, see https://www.github.com/kernel/kernel-python-sdk#accessing-raw-response-data-eg-headers
+ """
+ return SiteConfigsResourceWithRawResponse(self)
+
+ @cached_property
+ def with_streaming_response(self) -> SiteConfigsResourceWithStreamingResponse:
+ """
+ An alternative to `.with_raw_response` that doesn't eagerly read the response body.
+
+ For more information, see https://www.github.com/kernel/kernel-python-sdk#with_streaming_response
+ """
+ return SiteConfigsResourceWithStreamingResponse(self)
+
+ def retrieve(
+ self,
+ id: str,
+ *,
+ # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
+ # The extra values given here take precedence over values defined on the client or passed to this method.
+ extra_headers: Headers | None = None,
+ extra_query: Query | None = None,
+ extra_body: Body | None = None,
+ timeout: float | httpx.Timeout | None | NotGiven = not_given,
+ ) -> SiteConfigResponse:
+ """
+ Returns a project-scoped historical analysis and the recommendation outcome
+ concluded by that run. Later knowledge does not change this response.
+
+ Args:
+ extra_headers: Send extra headers
+
+ extra_query: Add additional query parameters to the request
+
+ extra_body: Add additional JSON properties to the request
+
+ timeout: Override the client-level default timeout for this request, in seconds
+ """
+ if not id:
+ raise ValueError(f"Expected a non-empty value for `id` but received {id!r}")
+ return self._get(
+ path_template("/site-configs/{id}", id=id),
+ options=make_request_options(
+ extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
+ ),
+ cast_to=SiteConfigResponse,
+ )
+
+ def list(
+ self,
+ *,
+ limit: int | Omit = omit,
+ offset: int | Omit = omit,
+ # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
+ # The extra values given here take precedence over values defined on the client or passed to this method.
+ extra_headers: Headers | None = None,
+ extra_query: Query | None = None,
+ extra_body: Body | None = None,
+ timeout: float | httpx.Timeout | None | NotGiven = not_given,
+ ) -> SyncOffsetPagination[AnalysisSummary]:
+ """
+ Lists analyses for the selected project, newest first.
+
+ Args:
+ extra_headers: Send extra headers
+
+ extra_query: Add additional query parameters to the request
+
+ extra_body: Add additional JSON properties to the request
+
+ timeout: Override the client-level default timeout for this request, in seconds
+ """
+ return self._get_api_list(
+ "/site-configs",
+ page=SyncOffsetPagination[AnalysisSummary],
+ options=make_request_options(
+ extra_headers=extra_headers,
+ extra_query=extra_query,
+ extra_body=extra_body,
+ timeout=timeout,
+ query=maybe_transform(
+ {
+ "limit": limit,
+ "offset": offset,
+ },
+ site_config_list_params.SiteConfigListParams,
+ ),
+ ),
+ model=AnalysisSummary,
+ )
+
+ def list_recommendations(
+ self,
+ *,
+ limit: int | Omit = omit,
+ offset: int | Omit = omit,
+ sort_by: Literal["target", "recommended_config", "last_requested_at", "success_rate"] | Omit = omit,
+ sort_order: Literal["asc", "desc"] | Omit = omit,
+ # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
+ # The extra values given here take precedence over values defined on the client or passed to this method.
+ extra_headers: Headers | None = None,
+ extra_query: Query | None = None,
+ extra_body: Body | None = None,
+ timeout: float | httpx.Timeout | None | NotGiven = not_given,
+ ) -> SyncOffsetPagination[RecommendationSummary]:
+ """
+ Lists unique domains previously analyzed by the selected project with their
+ current domain-level recommendations.
+
+ Args:
+ extra_headers: Send extra headers
+
+ extra_query: Add additional query parameters to the request
+
+ extra_body: Add additional JSON properties to the request
+
+ timeout: Override the client-level default timeout for this request, in seconds
+ """
+ return self._get_api_list(
+ "/site-configs/recommendations",
+ page=SyncOffsetPagination[RecommendationSummary],
+ options=make_request_options(
+ extra_headers=extra_headers,
+ extra_query=extra_query,
+ extra_body=extra_body,
+ timeout=timeout,
+ query=maybe_transform(
+ {
+ "limit": limit,
+ "offset": offset,
+ "sort_by": sort_by,
+ "sort_order": sort_order,
+ },
+ site_config_list_recommendations_params.SiteConfigListRecommendationsParams,
+ ),
+ ),
+ model=RecommendationSummary,
+ )
+
+ def lookup(
+ self,
+ *,
+ url: str,
+ allowed_proxy_countries: SequenceNotStr[str] | Omit = omit,
+ # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
+ # The extra values given here take precedence over values defined on the client or passed to this method.
+ extra_headers: Headers | None = None,
+ extra_query: Query | None = None,
+ extra_body: Body | None = None,
+ timeout: float | httpx.Timeout | None | NotGiven = not_given,
+ ) -> LookupResponse:
+ """
+ Returns current global knowledge without resolving DNS, creating an analysis, or
+ updating Site Config data.
+
+ Args:
+ url: Public HTTP(S) URL to look up.
+
+ allowed_proxy_countries: ISO 3166 country codes Kernel may use when returning a proxy configuration. When
+ omitted, Kernel uses its default country selection.
+
+ 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
+ """
+ return self._post(
+ "/site-configs/lookup",
+ body=maybe_transform(
+ {
+ "url": url,
+ "allowed_proxy_countries": allowed_proxy_countries,
+ },
+ site_config_lookup_params.SiteConfigLookupParams,
+ ),
+ options=make_request_options(
+ extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
+ ),
+ cast_to=LookupResponse,
+ )
+
+ def resolve(
+ self,
+ *,
+ url: str,
+ allowed_proxy_countries: SequenceNotStr[str] | Omit = omit,
+ # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
+ # The extra values given here take precedence over values defined on the client or passed to this method.
+ extra_headers: Headers | None = None,
+ extra_query: Query | None = None,
+ extra_body: Body | None = None,
+ timeout: float | httpx.Timeout | None | NotGiven = not_given,
+ ) -> SiteConfigResponse:
+ """
+ Explicitly starts or retries a project-scoped background analysis while
+ preserving current global knowledge when available. Use `/site-configs/lookup`
+ for side-effect-free reads.
+
+ Args:
+ url: Public HTTP(S) URL to refresh.
+
+ allowed_proxy_countries: ISO 3166 country codes Kernel may use when searching for or returning a proxy
+ configuration. Kernel may test a subset of allowed countries. When omitted,
+ Kernel uses its default country selection.
+
+ 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
+ """
+ return self._post(
+ "/site-configs/resolve",
+ body=maybe_transform(
+ {
+ "url": url,
+ "allowed_proxy_countries": allowed_proxy_countries,
+ },
+ site_config_resolve_params.SiteConfigResolveParams,
+ ),
+ options=make_request_options(
+ extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
+ ),
+ cast_to=SiteConfigResponse,
+ )
+
+
+class AsyncSiteConfigsResource(AsyncAPIResource):
+ """Resolve browser and proxy recommendations for bot-protected sites."""
+
+ @cached_property
+ def with_raw_response(self) -> AsyncSiteConfigsResourceWithRawResponse:
+ """
+ This property can be used as a prefix for any HTTP method call to return
+ the raw response object instead of the parsed content.
+
+ For more information, see https://www.github.com/kernel/kernel-python-sdk#accessing-raw-response-data-eg-headers
+ """
+ return AsyncSiteConfigsResourceWithRawResponse(self)
+
+ @cached_property
+ def with_streaming_response(self) -> AsyncSiteConfigsResourceWithStreamingResponse:
+ """
+ An alternative to `.with_raw_response` that doesn't eagerly read the response body.
+
+ For more information, see https://www.github.com/kernel/kernel-python-sdk#with_streaming_response
+ """
+ return AsyncSiteConfigsResourceWithStreamingResponse(self)
+
+ async def retrieve(
+ self,
+ id: str,
+ *,
+ # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
+ # The extra values given here take precedence over values defined on the client or passed to this method.
+ extra_headers: Headers | None = None,
+ extra_query: Query | None = None,
+ extra_body: Body | None = None,
+ timeout: float | httpx.Timeout | None | NotGiven = not_given,
+ ) -> SiteConfigResponse:
+ """
+ Returns a project-scoped historical analysis and the recommendation outcome
+ concluded by that run. Later knowledge does not change this response.
+
+ Args:
+ extra_headers: Send extra headers
+
+ extra_query: Add additional query parameters to the request
+
+ extra_body: Add additional JSON properties to the request
+
+ timeout: Override the client-level default timeout for this request, in seconds
+ """
+ if not id:
+ raise ValueError(f"Expected a non-empty value for `id` but received {id!r}")
+ return await self._get(
+ path_template("/site-configs/{id}", id=id),
+ options=make_request_options(
+ extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
+ ),
+ cast_to=SiteConfigResponse,
+ )
+
+ def list(
+ self,
+ *,
+ limit: int | Omit = omit,
+ offset: int | Omit = omit,
+ # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
+ # The extra values given here take precedence over values defined on the client or passed to this method.
+ extra_headers: Headers | None = None,
+ extra_query: Query | None = None,
+ extra_body: Body | None = None,
+ timeout: float | httpx.Timeout | None | NotGiven = not_given,
+ ) -> AsyncPaginator[AnalysisSummary, AsyncOffsetPagination[AnalysisSummary]]:
+ """
+ Lists analyses for the selected project, newest first.
+
+ Args:
+ extra_headers: Send extra headers
+
+ extra_query: Add additional query parameters to the request
+
+ extra_body: Add additional JSON properties to the request
+
+ timeout: Override the client-level default timeout for this request, in seconds
+ """
+ return self._get_api_list(
+ "/site-configs",
+ page=AsyncOffsetPagination[AnalysisSummary],
+ options=make_request_options(
+ extra_headers=extra_headers,
+ extra_query=extra_query,
+ extra_body=extra_body,
+ timeout=timeout,
+ query=maybe_transform(
+ {
+ "limit": limit,
+ "offset": offset,
+ },
+ site_config_list_params.SiteConfigListParams,
+ ),
+ ),
+ model=AnalysisSummary,
+ )
+
+ def list_recommendations(
+ self,
+ *,
+ limit: int | Omit = omit,
+ offset: int | Omit = omit,
+ sort_by: Literal["target", "recommended_config", "last_requested_at", "success_rate"] | Omit = omit,
+ sort_order: Literal["asc", "desc"] | Omit = omit,
+ # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
+ # The extra values given here take precedence over values defined on the client or passed to this method.
+ extra_headers: Headers | None = None,
+ extra_query: Query | None = None,
+ extra_body: Body | None = None,
+ timeout: float | httpx.Timeout | None | NotGiven = not_given,
+ ) -> AsyncPaginator[RecommendationSummary, AsyncOffsetPagination[RecommendationSummary]]:
+ """
+ Lists unique domains previously analyzed by the selected project with their
+ current domain-level recommendations.
+
+ Args:
+ extra_headers: Send extra headers
+
+ extra_query: Add additional query parameters to the request
+
+ extra_body: Add additional JSON properties to the request
+
+ timeout: Override the client-level default timeout for this request, in seconds
+ """
+ return self._get_api_list(
+ "/site-configs/recommendations",
+ page=AsyncOffsetPagination[RecommendationSummary],
+ options=make_request_options(
+ extra_headers=extra_headers,
+ extra_query=extra_query,
+ extra_body=extra_body,
+ timeout=timeout,
+ query=maybe_transform(
+ {
+ "limit": limit,
+ "offset": offset,
+ "sort_by": sort_by,
+ "sort_order": sort_order,
+ },
+ site_config_list_recommendations_params.SiteConfigListRecommendationsParams,
+ ),
+ ),
+ model=RecommendationSummary,
+ )
+
+ async def lookup(
+ self,
+ *,
+ url: str,
+ allowed_proxy_countries: SequenceNotStr[str] | Omit = omit,
+ # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
+ # The extra values given here take precedence over values defined on the client or passed to this method.
+ extra_headers: Headers | None = None,
+ extra_query: Query | None = None,
+ extra_body: Body | None = None,
+ timeout: float | httpx.Timeout | None | NotGiven = not_given,
+ ) -> LookupResponse:
+ """
+ Returns current global knowledge without resolving DNS, creating an analysis, or
+ updating Site Config data.
+
+ Args:
+ url: Public HTTP(S) URL to look up.
+
+ allowed_proxy_countries: ISO 3166 country codes Kernel may use when returning a proxy configuration. When
+ omitted, Kernel uses its default country selection.
+
+ 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
+ """
+ return await self._post(
+ "/site-configs/lookup",
+ body=await async_maybe_transform(
+ {
+ "url": url,
+ "allowed_proxy_countries": allowed_proxy_countries,
+ },
+ site_config_lookup_params.SiteConfigLookupParams,
+ ),
+ options=make_request_options(
+ extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
+ ),
+ cast_to=LookupResponse,
+ )
+
+ async def resolve(
+ self,
+ *,
+ url: str,
+ allowed_proxy_countries: SequenceNotStr[str] | Omit = omit,
+ # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
+ # The extra values given here take precedence over values defined on the client or passed to this method.
+ extra_headers: Headers | None = None,
+ extra_query: Query | None = None,
+ extra_body: Body | None = None,
+ timeout: float | httpx.Timeout | None | NotGiven = not_given,
+ ) -> SiteConfigResponse:
+ """
+ Explicitly starts or retries a project-scoped background analysis while
+ preserving current global knowledge when available. Use `/site-configs/lookup`
+ for side-effect-free reads.
+
+ Args:
+ url: Public HTTP(S) URL to refresh.
+
+ allowed_proxy_countries: ISO 3166 country codes Kernel may use when searching for or returning a proxy
+ configuration. Kernel may test a subset of allowed countries. When omitted,
+ Kernel uses its default country selection.
+
+ 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
+ """
+ return await self._post(
+ "/site-configs/resolve",
+ body=await async_maybe_transform(
+ {
+ "url": url,
+ "allowed_proxy_countries": allowed_proxy_countries,
+ },
+ site_config_resolve_params.SiteConfigResolveParams,
+ ),
+ options=make_request_options(
+ extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
+ ),
+ cast_to=SiteConfigResponse,
+ )
+
+
+class SiteConfigsResourceWithRawResponse:
+ def __init__(self, site_configs: SiteConfigsResource) -> None:
+ self._site_configs = site_configs
+
+ self.retrieve = to_raw_response_wrapper(
+ site_configs.retrieve,
+ )
+ self.list = to_raw_response_wrapper(
+ site_configs.list,
+ )
+ self.list_recommendations = to_raw_response_wrapper(
+ site_configs.list_recommendations,
+ )
+ self.lookup = to_raw_response_wrapper(
+ site_configs.lookup,
+ )
+ self.resolve = to_raw_response_wrapper(
+ site_configs.resolve,
+ )
+
+
+class AsyncSiteConfigsResourceWithRawResponse:
+ def __init__(self, site_configs: AsyncSiteConfigsResource) -> None:
+ self._site_configs = site_configs
+
+ self.retrieve = async_to_raw_response_wrapper(
+ site_configs.retrieve,
+ )
+ self.list = async_to_raw_response_wrapper(
+ site_configs.list,
+ )
+ self.list_recommendations = async_to_raw_response_wrapper(
+ site_configs.list_recommendations,
+ )
+ self.lookup = async_to_raw_response_wrapper(
+ site_configs.lookup,
+ )
+ self.resolve = async_to_raw_response_wrapper(
+ site_configs.resolve,
+ )
+
+
+class SiteConfigsResourceWithStreamingResponse:
+ def __init__(self, site_configs: SiteConfigsResource) -> None:
+ self._site_configs = site_configs
+
+ self.retrieve = to_streamed_response_wrapper(
+ site_configs.retrieve,
+ )
+ self.list = to_streamed_response_wrapper(
+ site_configs.list,
+ )
+ self.list_recommendations = to_streamed_response_wrapper(
+ site_configs.list_recommendations,
+ )
+ self.lookup = to_streamed_response_wrapper(
+ site_configs.lookup,
+ )
+ self.resolve = to_streamed_response_wrapper(
+ site_configs.resolve,
+ )
+
+
+class AsyncSiteConfigsResourceWithStreamingResponse:
+ def __init__(self, site_configs: AsyncSiteConfigsResource) -> None:
+ self._site_configs = site_configs
+
+ self.retrieve = async_to_streamed_response_wrapper(
+ site_configs.retrieve,
+ )
+ self.list = async_to_streamed_response_wrapper(
+ site_configs.list,
+ )
+ self.list_recommendations = async_to_streamed_response_wrapper(
+ site_configs.list_recommendations,
+ )
+ self.lookup = async_to_streamed_response_wrapper(
+ site_configs.lookup,
+ )
+ self.resolve = async_to_streamed_response_wrapper(
+ site_configs.resolve,
+ )
diff --git a/src/kernel/types/__init__.py b/src/kernel/types/__init__.py
index 6373f24c..9e7af34c 100644
--- a/src/kernel/types/__init__.py
+++ b/src/kernel/types/__init__.py
@@ -5,6 +5,7 @@
from . import browsers
from .. import _compat
from .tags import Tags as Tags
+from .proxy import Proxy as Proxy
from .shared import (
LogEvent as LogEvent,
AppAction as AppAction,
@@ -16,20 +17,28 @@
BrowserViewport as BrowserViewport,
BrowserExtension as BrowserExtension,
)
+from .target import Target as Target
from .api_key import APIKey as APIKey
+from .browser import Browser as Browser
from .profile import Profile as Profile
from .project import Project as Project
+from .analysis import Analysis as Analysis
+from .evidence import Evidence as Evidence
from .credential import Credential as Credential
from .tags_param import TagsParam as TagsParam
from .browser_pool import BrowserPool as BrowserPool
from .browser_proxy import BrowserProxy as BrowserProxy
from .browser_usage import BrowserUsage as BrowserUsage
from .browser_memory import BrowserMemory as BrowserMemory
+from .recommendation import Recommendation as Recommendation
from .app_list_params import AppListParams as AppListParams
from .audit_log_entry import AuditLogEntry as AuditLogEntry
from .created_api_key import CreatedAPIKey as CreatedAPIKey
+from .lookup_response import LookupResponse as LookupResponse
+from .analysis_summary import AnalysisSummary as AnalysisSummary
from .browser_pool_ref import BrowserPoolRef as BrowserPoolRef
from .app_list_response import AppListResponse as AppListResponse
+from .no_recommendation import NoRecommendation as NoRecommendation
from .proxy_list_params import ProxyListParams as ProxyListParams
from .browser_proxy_mode import BrowserProxyMode as BrowserProxyMode
from .proxy_check_params import ProxyCheckParams as ProxyCheckParams
@@ -44,6 +53,7 @@
from .proxy_update_params import ProxyUpdateParams as ProxyUpdateParams
from .browser_proxy_config import BrowserProxyConfig as BrowserProxyConfig
from .proxy_check_response import ProxyCheckResponse as ProxyCheckResponse
+from .site_config_response import SiteConfigResponse as SiteConfigResponse
from .api_key_create_params import APIKeyCreateParams as APIKeyCreateParams
from .api_key_rotate_params import APIKeyRotateParams as APIKeyRotateParams
from .api_key_update_params import APIKeyUpdateParams as APIKeyUpdateParams
@@ -59,6 +69,7 @@
from .project_update_params import ProjectUpdateParams as ProjectUpdateParams
from .proxy_create_response import ProxyCreateResponse as ProxyCreateResponse
from .proxy_update_response import ProxyUpdateResponse as ProxyUpdateResponse
+from .recommendation_result import RecommendationResult as RecommendationResult
from .browser_memory_request import BrowserMemoryRequest as BrowserMemoryRequest
from .browser_network_config import BrowserNetworkConfig as BrowserNetworkConfig
from .credential_list_params import CredentialListParams as CredentialListParams
@@ -67,6 +78,7 @@
from .extension_get_response import ExtensionGetResponse as ExtensionGetResponse
from .invocation_list_params import InvocationListParams as InvocationListParams
from .invocation_state_event import InvocationStateEvent as InvocationStateEvent
+from .recommendation_summary import RecommendationSummary as RecommendationSummary
from .api_key_retrieve_params import APIKeyRetrieveParams as APIKeyRetrieveParams
from .browser_create_response import BrowserCreateResponse as BrowserCreateResponse
from .browser_retrieve_params import BrowserRetrieveParams as BrowserRetrieveParams
@@ -75,6 +87,7 @@
from .extension_upload_params import ExtensionUploadParams as ExtensionUploadParams
from .profile_download_params import ProfileDownloadParams as ProfileDownloadParams
from .proxy_retrieve_response import ProxyRetrieveResponse as ProxyRetrieveResponse
+from .site_config_list_params import SiteConfigListParams as SiteConfigListParams
from .browser_pool_list_params import BrowserPoolListParams as BrowserPoolListParams
from .credential_create_params import CredentialCreateParams as CredentialCreateParams
from .credential_provider_item import CredentialProviderItem as CredentialProviderItem
@@ -88,6 +101,7 @@
from .invocation_update_params import InvocationUpdateParams as InvocationUpdateParams
from .browser_retrieve_response import BrowserRetrieveResponse as BrowserRetrieveResponse
from .extension_upload_response import ExtensionUploadResponse as ExtensionUploadResponse
+from .site_config_lookup_params import SiteConfigLookupParams as SiteConfigLookupParams
from .browser_pool_create_params import BrowserPoolCreateParams as BrowserPoolCreateParams
from .browser_pool_delete_params import BrowserPoolDeleteParams as BrowserPoolDeleteParams
from .browser_pool_update_params import BrowserPoolUpdateParams as BrowserPoolUpdateParams
@@ -97,6 +111,7 @@
from .invocation_create_response import InvocationCreateResponse as InvocationCreateResponse
from .invocation_follow_response import InvocationFollowResponse as InvocationFollowResponse
from .invocation_update_response import InvocationUpdateResponse as InvocationUpdateResponse
+from .site_config_resolve_params import SiteConfigResolveParams as SiteConfigResolveParams
from .browser_pool_acquire_params import BrowserPoolAcquireParams as BrowserPoolAcquireParams
from .browser_pool_release_params import BrowserPoolReleaseParams as BrowserPoolReleaseParams
from .browser_network_config_param import BrowserNetworkConfigParam as BrowserNetworkConfigParam
@@ -114,6 +129,9 @@
from .credential_provider_list_items_response import (
CredentialProviderListItemsResponse as CredentialProviderListItemsResponse,
)
+from .site_config_list_recommendations_params import (
+ SiteConfigListRecommendationsParams as SiteConfigListRecommendationsParams,
+)
from .extension_download_from_chrome_store_params import (
ExtensionDownloadFromChromeStoreParams as ExtensionDownloadFromChromeStoreParams,
)
diff --git a/src/kernel/types/analysis.py b/src/kernel/types/analysis.py
new file mode 100644
index 00000000..c47b956c
--- /dev/null
+++ b/src/kernel/types/analysis.py
@@ -0,0 +1,30 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from typing import Optional
+from datetime import datetime
+from typing_extensions import Literal
+
+from .._models import BaseModel
+from .shared.error_model import ErrorModel
+
+__all__ = ["Analysis"]
+
+
+class Analysis(BaseModel):
+ id: str
+ """Discovery run ID used to poll analysis status."""
+
+ created_at: datetime
+ """Time the analysis was created."""
+
+ failure: Optional[ErrorModel] = None
+ """Present for failed or canceled analyses.
+
+ Messages contain safe retry guidance rather than internal workflow errors.
+ """
+
+ finished_at: Optional[datetime] = None
+ """Time the analysis reached a terminal status. Null while it is running."""
+
+ status: Literal["running", "completed", "failed", "canceled"]
+ """Lifecycle status of the background analysis."""
diff --git a/src/kernel/types/analysis_summary.py b/src/kernel/types/analysis_summary.py
new file mode 100644
index 00000000..fab0d8d8
--- /dev/null
+++ b/src/kernel/types/analysis_summary.py
@@ -0,0 +1,13 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from .target import Target
+from .._models import BaseModel
+from .analysis import Analysis
+
+__all__ = ["AnalysisSummary"]
+
+
+class AnalysisSummary(BaseModel):
+ analysis: Analysis
+
+ target: Target
diff --git a/src/kernel/types/browser.py b/src/kernel/types/browser.py
new file mode 100644
index 00000000..f7905451
--- /dev/null
+++ b/src/kernel/types/browser.py
@@ -0,0 +1,32 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from .._models import BaseModel
+from .shared.browser_viewport import BrowserViewport
+
+__all__ = ["Browser"]
+
+
+class Browser(BaseModel):
+ """Browser settings that can be passed directly to `POST /browsers`."""
+
+ gpu: bool
+
+ headless: bool
+
+ stealth: bool
+
+ viewport: BrowserViewport
+ """
+ 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, 768x1024@60, 390x844@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/evidence.py b/src/kernel/types/evidence.py
new file mode 100644
index 00000000..1a777de9
--- /dev/null
+++ b/src/kernel/types/evidence.py
@@ -0,0 +1,38 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from typing import Optional
+from datetime import datetime
+
+from .._models import BaseModel
+
+__all__ = ["Evidence"]
+
+
+class Evidence(BaseModel):
+ accessed: int
+
+ blocked: int
+
+ inconclusive: int
+
+ last_observed_at: datetime
+ """Most recent contributing observation.
+
+ Recommendations remain eligible regardless of age and can be returned while a
+ new analysis refreshes them.
+ """
+
+ run_count: int
+
+ sample_size: int
+ """Number of judged trials."""
+
+ success_rate: float
+ """Accessed trials divided by judged trials. Inconclusive trials are excluded."""
+
+ last_verified_at: Optional[datetime] = None
+ """Most recent contributing run where this config met the success threshold.
+
+ Omitted for knowledge assembled from runs that did not independently meet the
+ threshold.
+ """
diff --git a/src/kernel/types/lookup_response.py b/src/kernel/types/lookup_response.py
new file mode 100644
index 00000000..cf0f5c16
--- /dev/null
+++ b/src/kernel/types/lookup_response.py
@@ -0,0 +1,15 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from typing import Optional
+
+from .target import Target
+from .._models import BaseModel
+from .recommendation import Recommendation
+
+__all__ = ["LookupResponse"]
+
+
+class LookupResponse(BaseModel):
+ recommendation: Optional[Recommendation] = None
+
+ target: Target
diff --git a/src/kernel/types/no_recommendation.py b/src/kernel/types/no_recommendation.py
new file mode 100644
index 00000000..d1bab62c
--- /dev/null
+++ b/src/kernel/types/no_recommendation.py
@@ -0,0 +1,20 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from typing_extensions import Literal
+
+from .._models import BaseModel
+
+__all__ = ["NoRecommendation"]
+
+
+class NoRecommendation(BaseModel):
+ code: Literal["proxy_restricted", "no_working_configuration", "inconclusive"]
+ """
+ Machine-readable reason Kernel cannot currently provide a Site Config
+ recommendation.
+ """
+
+ message: str
+ """Human-readable explanation suitable for display."""
+
+ type: Literal["no_recommendation"]
diff --git a/src/kernel/types/proxy.py b/src/kernel/types/proxy.py
new file mode 100644
index 00000000..ea31827d
--- /dev/null
+++ b/src/kernel/types/proxy.py
@@ -0,0 +1,150 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from typing import List, Union, Optional
+from typing_extensions import Literal, Annotated, TypeAlias
+
+from .._utils import PropertyInfo
+from .._models import BaseModel
+
+__all__ = [
+ "Proxy",
+ "SiteConfigDirectProxy",
+ "SiteConfigManagedProxy",
+ "SiteConfigManagedProxyCreate",
+ "SiteConfigManagedProxyCreateConfig",
+ "SiteConfigManagedProxyCreateConfigDatacenterProxyConfig",
+ "SiteConfigManagedProxyCreateConfigIspProxyConfig",
+ "SiteConfigManagedProxyCreateConfigResidentialProxyConfig",
+ "SiteConfigManagedProxyCreateConfigMobileProxyConfig",
+ "SiteConfigManagedProxyCreateConfigCreateCustomProxyConfig",
+]
+
+
+class SiteConfigDirectProxy(BaseModel):
+ """Direct egress recipe. Pass `{ "mode": "direct" }` as the browser's `proxy`."""
+
+ mode: Literal["direct"]
+
+
+class SiteConfigManagedProxyCreateConfigDatacenterProxyConfig(BaseModel):
+ """Configuration for a datacenter proxy."""
+
+ country: Optional[str] = None
+ """ISO 3166 country code. Defaults to US if not provided."""
+
+
+class SiteConfigManagedProxyCreateConfigIspProxyConfig(BaseModel):
+ """Configuration for an ISP proxy."""
+
+ country: Optional[str] = None
+ """ISO 3166 country code. Defaults to US if not provided."""
+
+
+class SiteConfigManagedProxyCreateConfigResidentialProxyConfig(BaseModel):
+ """Configuration for residential proxies."""
+
+ asn: Optional[str] = None
+ """Autonomous system number. See https://bgp.potaroo.net/cidr/autnums.html"""
+
+ city: Optional[str] = None
+ """City name (no spaces, e.g.
+
+ `sanfrancisco`). If provided, `country` must also be provided.
+ """
+
+ country: Optional[str] = None
+ """ISO 3166 country code."""
+
+ os: Optional[Literal["windows", "macos", "android"]] = None
+ """Operating system of the residential device."""
+
+ state: Optional[str] = None
+ """Two-letter state code."""
+
+ zip: Optional[str] = None
+ """US ZIP code."""
+
+
+class SiteConfigManagedProxyCreateConfigMobileProxyConfig(BaseModel):
+ """Configuration for mobile proxies."""
+
+ city: Optional[str] = None
+ """Provider city alias. Mobile carrier routing can make observed geo vary."""
+
+ country: Optional[str] = None
+ """ISO 3166 country code"""
+
+ state: Optional[str] = None
+ """US-only state code. Mobile carrier routing can make observed geo vary."""
+
+
+class SiteConfigManagedProxyCreateConfigCreateCustomProxyConfig(BaseModel):
+ """Configuration for a custom proxy (e.g., private proxy server)."""
+
+ host: str
+ """Proxy host address or IP."""
+
+ port: int
+ """Proxy port."""
+
+ ca_bundle: Optional[str] = None
+ """PEM-encoded CA certificate bundle the proxy re-signs upstream TLS with.
+
+ Provide when the proxy terminates TLS (MITM) so the browser trusts its
+ certificates. May contain multiple concatenated certificates.
+ """
+
+ password: Optional[str] = None
+ """Password for proxy authentication."""
+
+ username: Optional[str] = None
+ """Username for proxy authentication."""
+
+
+SiteConfigManagedProxyCreateConfig: TypeAlias = Union[
+ SiteConfigManagedProxyCreateConfigDatacenterProxyConfig,
+ SiteConfigManagedProxyCreateConfigIspProxyConfig,
+ SiteConfigManagedProxyCreateConfigResidentialProxyConfig,
+ SiteConfigManagedProxyCreateConfigMobileProxyConfig,
+ SiteConfigManagedProxyCreateConfigCreateCustomProxyConfig,
+]
+
+
+class SiteConfigManagedProxyCreate(BaseModel):
+ """Configuration for routing traffic through a proxy."""
+
+ type: Literal["datacenter", "isp", "residential", "mobile", "custom"]
+ """Proxy type to use.
+
+ In terms of quality for avoiding bot-detection, from best to worst: `mobile` >
+ `residential` > `isp` > `datacenter`.
+ """
+
+ bypass_hosts: Optional[List[str]] = None
+ """Hostnames that should bypass the parent proxy and connect directly."""
+
+ config: Optional[SiteConfigManagedProxyCreateConfig] = None
+ """Configuration specific to the selected proxy `type`."""
+
+ name: Optional[str] = None
+ """Readable name of the proxy."""
+
+ protocol: Optional[Literal["http", "https"]] = None
+ """Protocol to use for the proxy connection."""
+
+
+class SiteConfigManagedProxy(BaseModel):
+ """Managed proxy recipe.
+
+ `create` is a non-idempotent `POST /proxies` payload:
+ create the resource once, retain its ID, and reuse that ID as the browser's
+ `proxy.id`. Do not submit this recipe before every browser session.
+ """
+
+ create: SiteConfigManagedProxyCreate
+ """Configuration for routing traffic through a proxy."""
+
+ mode: Literal["managed"]
+
+
+Proxy: TypeAlias = Annotated[Union[SiteConfigDirectProxy, SiteConfigManagedProxy], PropertyInfo(discriminator="mode")]
diff --git a/src/kernel/types/recommendation.py b/src/kernel/types/recommendation.py
new file mode 100644
index 00000000..74b1761a
--- /dev/null
+++ b/src/kernel/types/recommendation.py
@@ -0,0 +1,35 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from typing_extensions import Literal
+
+from .proxy import Proxy
+from .browser import Browser
+from .._models import BaseModel
+from .evidence import Evidence
+
+__all__ = ["Recommendation"]
+
+
+class Recommendation(BaseModel):
+ browser: Browser
+ """Browser settings that can be passed directly to `POST /browsers`."""
+
+ evidence: Evidence
+
+ match_scope: Literal["exact", "host", "domain"]
+ """Specificity of knowledge matched for this recommendation."""
+
+ matched_target: str
+ """Target value that supplied the recommendation."""
+
+ proxy: Proxy
+ """Proxy recipe for the recommended browser."""
+
+ type: Literal["recommendation"]
+
+ verification: Literal["verified", "inferred"]
+ """
+ Exact matches meet the evidence threshold; host and domain fallbacks are
+ inferred. Check evidence.last_verified_at for successful verification age and
+ last_observed_at for the latest evidence.
+ """
diff --git a/src/kernel/types/recommendation_result.py b/src/kernel/types/recommendation_result.py
new file mode 100644
index 00000000..82a72d92
--- /dev/null
+++ b/src/kernel/types/recommendation_result.py
@@ -0,0 +1,12 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from typing import Union
+from typing_extensions import Annotated, TypeAlias
+
+from .._utils import PropertyInfo
+from .recommendation import Recommendation
+from .no_recommendation import NoRecommendation
+
+__all__ = ["RecommendationResult"]
+
+RecommendationResult: TypeAlias = Annotated[Union[Recommendation, NoRecommendation], PropertyInfo(discriminator="type")]
diff --git a/src/kernel/types/recommendation_summary.py b/src/kernel/types/recommendation_summary.py
new file mode 100644
index 00000000..abc2ea72
--- /dev/null
+++ b/src/kernel/types/recommendation_summary.py
@@ -0,0 +1,29 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from typing import Optional
+from datetime import datetime
+
+from .._models import BaseModel
+from .recommendation import Recommendation
+
+__all__ = ["RecommendationSummary"]
+
+
+class RecommendationSummary(BaseModel):
+ last_requested_at: datetime
+ """Most recent time the selected project requested an analysis for this domain."""
+
+ recommendation: Optional[Recommendation] = None
+ """Current domain-level recommendation. Null when no eligible knowledge exists."""
+
+ recommended_config_label: Optional[str] = None
+ """Display label for the recommended browser configuration."""
+
+ success_rate: Optional[float] = None
+ """Success rate for the recommended configuration.
+
+ Null when no eligible knowledge exists.
+ """
+
+ target: str
+ """Registrable domain previously analyzed by the selected project."""
diff --git a/src/kernel/types/site_config_list_params.py b/src/kernel/types/site_config_list_params.py
new file mode 100644
index 00000000..ee95bad4
--- /dev/null
+++ b/src/kernel/types/site_config_list_params.py
@@ -0,0 +1,13 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from __future__ import annotations
+
+from typing_extensions import TypedDict
+
+__all__ = ["SiteConfigListParams"]
+
+
+class SiteConfigListParams(TypedDict, total=False):
+ limit: int
+
+ offset: int
diff --git a/src/kernel/types/site_config_list_recommendations_params.py b/src/kernel/types/site_config_list_recommendations_params.py
new file mode 100644
index 00000000..8ff51237
--- /dev/null
+++ b/src/kernel/types/site_config_list_recommendations_params.py
@@ -0,0 +1,17 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from __future__ import annotations
+
+from typing_extensions import Literal, TypedDict
+
+__all__ = ["SiteConfigListRecommendationsParams"]
+
+
+class SiteConfigListRecommendationsParams(TypedDict, total=False):
+ limit: int
+
+ offset: int
+
+ sort_by: Literal["target", "recommended_config", "last_requested_at", "success_rate"]
+
+ sort_order: Literal["asc", "desc"]
diff --git a/src/kernel/types/site_config_lookup_params.py b/src/kernel/types/site_config_lookup_params.py
new file mode 100644
index 00000000..aba315f4
--- /dev/null
+++ b/src/kernel/types/site_config_lookup_params.py
@@ -0,0 +1,20 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from __future__ import annotations
+
+from typing_extensions import Required, TypedDict
+
+from .._types import SequenceNotStr
+
+__all__ = ["SiteConfigLookupParams"]
+
+
+class SiteConfigLookupParams(TypedDict, total=False):
+ url: Required[str]
+ """Public HTTP(S) URL to look up."""
+
+ allowed_proxy_countries: SequenceNotStr[str]
+ """ISO 3166 country codes Kernel may use when returning a proxy configuration.
+
+ When omitted, Kernel uses its default country selection.
+ """
diff --git a/src/kernel/types/site_config_resolve_params.py b/src/kernel/types/site_config_resolve_params.py
new file mode 100644
index 00000000..37a3f44d
--- /dev/null
+++ b/src/kernel/types/site_config_resolve_params.py
@@ -0,0 +1,21 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from __future__ import annotations
+
+from typing_extensions import Required, TypedDict
+
+from .._types import SequenceNotStr
+
+__all__ = ["SiteConfigResolveParams"]
+
+
+class SiteConfigResolveParams(TypedDict, total=False):
+ url: Required[str]
+ """Public HTTP(S) URL to refresh."""
+
+ allowed_proxy_countries: SequenceNotStr[str]
+ """
+ ISO 3166 country codes Kernel may use when searching for or returning a proxy
+ configuration. Kernel may test a subset of allowed countries. When omitted,
+ Kernel uses its default country selection.
+ """
diff --git a/src/kernel/types/site_config_response.py b/src/kernel/types/site_config_response.py
new file mode 100644
index 00000000..b6673f76
--- /dev/null
+++ b/src/kernel/types/site_config_response.py
@@ -0,0 +1,23 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from typing import Optional
+
+from .target import Target
+from .._models import BaseModel
+from .analysis import Analysis
+from .recommendation_result import RecommendationResult
+
+__all__ = ["SiteConfigResponse"]
+
+
+class SiteConfigResponse(BaseModel):
+ analysis: Optional[Analysis] = None
+ """Pollable analysis after workflow submission is acknowledged.
+
+ Null when no refresh was submitted.
+ """
+
+ recommendation: Optional[RecommendationResult] = None
+ """A recommendation or a structured no-recommendation result."""
+
+ target: Target
diff --git a/src/kernel/types/target.py b/src/kernel/types/target.py
new file mode 100644
index 00000000..6950d513
--- /dev/null
+++ b/src/kernel/types/target.py
@@ -0,0 +1,16 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from .._models import BaseModel
+
+__all__ = ["Target"]
+
+
+class Target(BaseModel):
+ domain: str
+ """Registrable domain."""
+
+ host: str
+ """Full hostname, including subdomain."""
+
+ normalized: str
+ """Exact normalized scheme, host, port, and path used for lookup."""
diff --git a/tests/api_resources/test_site_configs.py b/tests/api_resources/test_site_configs.py
new file mode 100644
index 00000000..2099899c
--- /dev/null
+++ b/tests/api_resources/test_site_configs.py
@@ -0,0 +1,438 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from __future__ import annotations
+
+import os
+from typing import Any, cast
+
+import pytest
+
+from kernel import Kernel, AsyncKernel
+from tests.utils import assert_matches_type
+from kernel.types import (
+ LookupResponse,
+ AnalysisSummary,
+ SiteConfigResponse,
+ RecommendationSummary,
+)
+from kernel.pagination import SyncOffsetPagination, AsyncOffsetPagination
+
+base_url = os.environ.get("TEST_API_BASE_URL", "http://127.0.0.1:4010")
+
+
+class TestSiteConfigs:
+ parametrize = pytest.mark.parametrize("client", [False, True], indirect=True, ids=["loose", "strict"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ def test_method_retrieve(self, client: Kernel) -> None:
+ site_config = client.site_configs.retrieve(
+ "id",
+ )
+ assert_matches_type(SiteConfigResponse, site_config, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ def test_raw_response_retrieve(self, client: Kernel) -> None:
+ response = client.site_configs.with_raw_response.retrieve(
+ "id",
+ )
+
+ assert response.is_closed is True
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+ site_config = response.parse()
+ assert_matches_type(SiteConfigResponse, site_config, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ def test_streaming_response_retrieve(self, client: Kernel) -> None:
+ with client.site_configs.with_streaming_response.retrieve(
+ "id",
+ ) as response:
+ assert not response.is_closed
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+
+ site_config = response.parse()
+ assert_matches_type(SiteConfigResponse, site_config, path=["response"])
+
+ assert cast(Any, response.is_closed) is True
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ def test_path_params_retrieve(self, client: Kernel) -> None:
+ with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"):
+ client.site_configs.with_raw_response.retrieve(
+ "",
+ )
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ def test_method_list(self, client: Kernel) -> None:
+ site_config = client.site_configs.list()
+ assert_matches_type(SyncOffsetPagination[AnalysisSummary], site_config, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ def test_method_list_with_all_params(self, client: Kernel) -> None:
+ site_config = client.site_configs.list(
+ limit=1,
+ offset=0,
+ )
+ assert_matches_type(SyncOffsetPagination[AnalysisSummary], site_config, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ def test_raw_response_list(self, client: Kernel) -> None:
+ response = client.site_configs.with_raw_response.list()
+
+ assert response.is_closed is True
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+ site_config = response.parse()
+ assert_matches_type(SyncOffsetPagination[AnalysisSummary], site_config, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ def test_streaming_response_list(self, client: Kernel) -> None:
+ with client.site_configs.with_streaming_response.list() as response:
+ assert not response.is_closed
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+
+ site_config = response.parse()
+ assert_matches_type(SyncOffsetPagination[AnalysisSummary], site_config, path=["response"])
+
+ assert cast(Any, response.is_closed) is True
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ def test_method_list_recommendations(self, client: Kernel) -> None:
+ site_config = client.site_configs.list_recommendations()
+ assert_matches_type(SyncOffsetPagination[RecommendationSummary], site_config, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ def test_method_list_recommendations_with_all_params(self, client: Kernel) -> None:
+ site_config = client.site_configs.list_recommendations(
+ limit=1,
+ offset=0,
+ sort_by="target",
+ sort_order="asc",
+ )
+ assert_matches_type(SyncOffsetPagination[RecommendationSummary], site_config, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ def test_raw_response_list_recommendations(self, client: Kernel) -> None:
+ response = client.site_configs.with_raw_response.list_recommendations()
+
+ assert response.is_closed is True
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+ site_config = response.parse()
+ assert_matches_type(SyncOffsetPagination[RecommendationSummary], site_config, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ def test_streaming_response_list_recommendations(self, client: Kernel) -> None:
+ with client.site_configs.with_streaming_response.list_recommendations() as response:
+ assert not response.is_closed
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+
+ site_config = response.parse()
+ assert_matches_type(SyncOffsetPagination[RecommendationSummary], site_config, path=["response"])
+
+ assert cast(Any, response.is_closed) is True
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ def test_method_lookup(self, client: Kernel) -> None:
+ site_config = client.site_configs.lookup(
+ url="https://example.com",
+ )
+ assert_matches_type(LookupResponse, site_config, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ def test_method_lookup_with_all_params(self, client: Kernel) -> None:
+ site_config = client.site_configs.lookup(
+ url="https://example.com",
+ allowed_proxy_countries=["US"],
+ )
+ assert_matches_type(LookupResponse, site_config, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ def test_raw_response_lookup(self, client: Kernel) -> None:
+ response = client.site_configs.with_raw_response.lookup(
+ url="https://example.com",
+ )
+
+ assert response.is_closed is True
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+ site_config = response.parse()
+ assert_matches_type(LookupResponse, site_config, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ def test_streaming_response_lookup(self, client: Kernel) -> None:
+ with client.site_configs.with_streaming_response.lookup(
+ url="https://example.com",
+ ) as response:
+ assert not response.is_closed
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+
+ site_config = response.parse()
+ assert_matches_type(LookupResponse, site_config, path=["response"])
+
+ assert cast(Any, response.is_closed) is True
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ def test_method_resolve(self, client: Kernel) -> None:
+ site_config = client.site_configs.resolve(
+ url="https://example.com",
+ )
+ assert_matches_type(SiteConfigResponse, site_config, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ def test_method_resolve_with_all_params(self, client: Kernel) -> None:
+ site_config = client.site_configs.resolve(
+ url="https://example.com",
+ allowed_proxy_countries=["US"],
+ )
+ assert_matches_type(SiteConfigResponse, site_config, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ def test_raw_response_resolve(self, client: Kernel) -> None:
+ response = client.site_configs.with_raw_response.resolve(
+ url="https://example.com",
+ )
+
+ assert response.is_closed is True
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+ site_config = response.parse()
+ assert_matches_type(SiteConfigResponse, site_config, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ def test_streaming_response_resolve(self, client: Kernel) -> None:
+ with client.site_configs.with_streaming_response.resolve(
+ url="https://example.com",
+ ) as response:
+ assert not response.is_closed
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+
+ site_config = response.parse()
+ assert_matches_type(SiteConfigResponse, site_config, path=["response"])
+
+ assert cast(Any, response.is_closed) is True
+
+
+class TestAsyncSiteConfigs:
+ parametrize = pytest.mark.parametrize(
+ "async_client", [False, True, {"http_client": "aiohttp"}], indirect=True, ids=["loose", "strict", "aiohttp"]
+ )
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ async def test_method_retrieve(self, async_client: AsyncKernel) -> None:
+ site_config = await async_client.site_configs.retrieve(
+ "id",
+ )
+ assert_matches_type(SiteConfigResponse, site_config, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ async def test_raw_response_retrieve(self, async_client: AsyncKernel) -> None:
+ response = await async_client.site_configs.with_raw_response.retrieve(
+ "id",
+ )
+
+ assert response.is_closed is True
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+ site_config = await response.parse()
+ assert_matches_type(SiteConfigResponse, site_config, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ async def test_streaming_response_retrieve(self, async_client: AsyncKernel) -> None:
+ async with async_client.site_configs.with_streaming_response.retrieve(
+ "id",
+ ) as response:
+ assert not response.is_closed
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+
+ site_config = await response.parse()
+ assert_matches_type(SiteConfigResponse, site_config, 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_retrieve(self, async_client: AsyncKernel) -> None:
+ with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"):
+ await async_client.site_configs.with_raw_response.retrieve(
+ "",
+ )
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ async def test_method_list(self, async_client: AsyncKernel) -> None:
+ site_config = await async_client.site_configs.list()
+ assert_matches_type(AsyncOffsetPagination[AnalysisSummary], site_config, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ async def test_method_list_with_all_params(self, async_client: AsyncKernel) -> None:
+ site_config = await async_client.site_configs.list(
+ limit=1,
+ offset=0,
+ )
+ assert_matches_type(AsyncOffsetPagination[AnalysisSummary], site_config, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ async def test_raw_response_list(self, async_client: AsyncKernel) -> None:
+ response = await async_client.site_configs.with_raw_response.list()
+
+ assert response.is_closed is True
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+ site_config = await response.parse()
+ assert_matches_type(AsyncOffsetPagination[AnalysisSummary], site_config, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ async def test_streaming_response_list(self, async_client: AsyncKernel) -> None:
+ async with async_client.site_configs.with_streaming_response.list() as response:
+ assert not response.is_closed
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+
+ site_config = await response.parse()
+ assert_matches_type(AsyncOffsetPagination[AnalysisSummary], site_config, path=["response"])
+
+ assert cast(Any, response.is_closed) is True
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ async def test_method_list_recommendations(self, async_client: AsyncKernel) -> None:
+ site_config = await async_client.site_configs.list_recommendations()
+ assert_matches_type(AsyncOffsetPagination[RecommendationSummary], site_config, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ async def test_method_list_recommendations_with_all_params(self, async_client: AsyncKernel) -> None:
+ site_config = await async_client.site_configs.list_recommendations(
+ limit=1,
+ offset=0,
+ sort_by="target",
+ sort_order="asc",
+ )
+ assert_matches_type(AsyncOffsetPagination[RecommendationSummary], site_config, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ async def test_raw_response_list_recommendations(self, async_client: AsyncKernel) -> None:
+ response = await async_client.site_configs.with_raw_response.list_recommendations()
+
+ assert response.is_closed is True
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+ site_config = await response.parse()
+ assert_matches_type(AsyncOffsetPagination[RecommendationSummary], site_config, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ async def test_streaming_response_list_recommendations(self, async_client: AsyncKernel) -> None:
+ async with async_client.site_configs.with_streaming_response.list_recommendations() as response:
+ assert not response.is_closed
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+
+ site_config = await response.parse()
+ assert_matches_type(AsyncOffsetPagination[RecommendationSummary], site_config, path=["response"])
+
+ assert cast(Any, response.is_closed) is True
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ async def test_method_lookup(self, async_client: AsyncKernel) -> None:
+ site_config = await async_client.site_configs.lookup(
+ url="https://example.com",
+ )
+ assert_matches_type(LookupResponse, site_config, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ async def test_method_lookup_with_all_params(self, async_client: AsyncKernel) -> None:
+ site_config = await async_client.site_configs.lookup(
+ url="https://example.com",
+ allowed_proxy_countries=["US"],
+ )
+ assert_matches_type(LookupResponse, site_config, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ async def test_raw_response_lookup(self, async_client: AsyncKernel) -> None:
+ response = await async_client.site_configs.with_raw_response.lookup(
+ url="https://example.com",
+ )
+
+ assert response.is_closed is True
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+ site_config = await response.parse()
+ assert_matches_type(LookupResponse, site_config, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ async def test_streaming_response_lookup(self, async_client: AsyncKernel) -> None:
+ async with async_client.site_configs.with_streaming_response.lookup(
+ url="https://example.com",
+ ) as response:
+ assert not response.is_closed
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+
+ site_config = await response.parse()
+ assert_matches_type(LookupResponse, site_config, path=["response"])
+
+ assert cast(Any, response.is_closed) is True
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ async def test_method_resolve(self, async_client: AsyncKernel) -> None:
+ site_config = await async_client.site_configs.resolve(
+ url="https://example.com",
+ )
+ assert_matches_type(SiteConfigResponse, site_config, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ async def test_method_resolve_with_all_params(self, async_client: AsyncKernel) -> None:
+ site_config = await async_client.site_configs.resolve(
+ url="https://example.com",
+ allowed_proxy_countries=["US"],
+ )
+ assert_matches_type(SiteConfigResponse, site_config, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ async def test_raw_response_resolve(self, async_client: AsyncKernel) -> None:
+ response = await async_client.site_configs.with_raw_response.resolve(
+ url="https://example.com",
+ )
+
+ assert response.is_closed is True
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+ site_config = await response.parse()
+ assert_matches_type(SiteConfigResponse, site_config, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ async def test_streaming_response_resolve(self, async_client: AsyncKernel) -> None:
+ async with async_client.site_configs.with_streaming_response.resolve(
+ url="https://example.com",
+ ) as response:
+ assert not response.is_closed
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+
+ site_config = await response.parse()
+ assert_matches_type(SiteConfigResponse, site_config, path=["response"])
+
+ assert cast(Any, response.is_closed) is True
From 8fe728bc0e2eb61ca55b1af2705ede503b6c97ac Mon Sep 17 00:00:00 2001
From: "kernel-internal[bot]"
<260533166+kernel-internal[bot]@users.noreply.github.com>
Date: Mon, 24 Aug 2026 20:08:54 +0000
Subject: [PATCH 213/366] release: 0.94.0
---
.release-please-manifest.json | 2 +-
CHANGELOG.md | 8 ++++++++
pyproject.toml | 2 +-
src/kernel/_version.py | 2 +-
4 files changed, 11 insertions(+), 3 deletions(-)
diff --git a/.release-please-manifest.json b/.release-please-manifest.json
index c486f6de..91393ed8 100644
--- a/.release-please-manifest.json
+++ b/.release-please-manifest.json
@@ -1,3 +1,3 @@
{
- ".": "0.93.0"
+ ".": "0.94.0"
}
\ No newline at end of file
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 1283a424..ccd152aa 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,13 @@
# Changelog
+## [0.94.0](https://github.com/kernel/kernel-python-sdk/compare/v0.93.0...v0.94.0) (2026-08-24)
+
+
+### Features
+
+* Mirror the control/platform telemetry split into the public API ([27c5a0a](https://github.com/kernel/kernel-python-sdk/commit/27c5a0a75ade568f1feb7e058070771ae1f6db60))
+* site configs: report proxy-restricted analyses ([7afcfc4](https://github.com/kernel/kernel-python-sdk/commit/7afcfc499d7008407c872d75bcc4d8a1604f7954))
+
## [0.93.0](https://github.com/kernel/kernel-python-sdk/compare/v0.92.0...v0.93.0) (2026-08-19)
diff --git a/pyproject.toml b/pyproject.toml
index 8f8dab0e..420e8de4 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -1,6 +1,6 @@
[project]
name = "kernel"
-version = "0.93.0"
+version = "0.94.0"
description = "The official Python library for the kernel API"
dynamic = ["readme"]
license = "Apache-2.0"
diff --git a/src/kernel/_version.py b/src/kernel/_version.py
index 2fb26cce..54fa859a 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.93.0" # x-release-please-version
+__version__ = "0.94.0" # x-release-please-version
From f833c55fee9e7c33c444af6366890a9c876e149c Mon Sep 17 00:00:00 2001
From: tnsardesai <18272584+tnsardesai@users.noreply.github.com>
Date: Tue, 25 Aug 2026 01:10:28 +0000
Subject: [PATCH 214/366] feat: route process calls directly to browser VMs
---
src/kernel/lib/browser_routing/routing.py | 2 +-
tests/test_browser_routing.py | 28 ++++++++++++++---------
2 files changed, 18 insertions(+), 12 deletions(-)
diff --git a/src/kernel/lib/browser_routing/routing.py b/src/kernel/lib/browser_routing/routing.py
index 99bf7830..bad5e4ea 100644
--- a/src/kernel/lib/browser_routing/routing.py
+++ b/src/kernel/lib/browser_routing/routing.py
@@ -44,7 +44,7 @@ def browser_routing_config_from_env() -> BrowserRoutingConfig:
# Path prefixes eligible for direct-to-VM routing. "telemetry/stream" is
# the live SSE endpoint (VM); "telemetry/events" is a historical read
# served by the control plane (S2) and must NOT be here.
- return BrowserRoutingConfig(subresources=("curl", "telemetry/stream", "computer", "playwright"))
+ return BrowserRoutingConfig(subresources=("curl", "telemetry/stream", "computer", "playwright", "process"))
if raw.strip() == "":
return BrowserRoutingConfig()
diff --git a/tests/test_browser_routing.py b/tests/test_browser_routing.py
index ee73eafd..eb47e040 100644
--- a/tests/test_browser_routing.py
+++ b/tests/test_browser_routing.py
@@ -397,6 +397,7 @@ def test_browser_routing_config_from_env_defaults(monkeypatch: pytest.MonkeyPatc
"telemetry/stream",
"computer",
"playwright",
+ "process",
)
@@ -406,7 +407,7 @@ def test_direct_vm_routing_allowlist_segment_boundary() -> None:
# stream-prefixed-but-different path is not matched.
from kernel.lib.browser_routing.routing import _matches_direct_vm_prefix
- prefixes = ("curl", "telemetry/stream", "computer", "playwright")
+ prefixes = ("curl", "telemetry/stream", "computer", "playwright", "process")
assert _matches_direct_vm_prefix("telemetry/stream", prefixes) is True
assert _matches_direct_vm_prefix("telemetry/stream/x", prefixes) is True
assert _matches_direct_vm_prefix("telemetry/events", prefixes) is False
@@ -415,7 +416,8 @@ def test_direct_vm_routing_allowlist_segment_boundary() -> None:
assert _matches_direct_vm_prefix("curl/raw", prefixes) is True
assert _matches_direct_vm_prefix("computer/screenshot", prefixes) is True
assert _matches_direct_vm_prefix("playwright/execute", prefixes) is True
- assert _matches_direct_vm_prefix("process/exec", prefixes) is False
+ assert _matches_direct_vm_prefix("process/exec", prefixes) is True
+ assert _matches_direct_vm_prefix("process/proc-1/stdout/stream", prefixes) is True
assert _matches_direct_vm_prefix("fs/read", prefixes) is False
@@ -433,7 +435,7 @@ def test_rewrite_direct_vm_options_keeps_telemetry_events_on_control_plane() ->
cache = BrowserRouteCache()
cache.set(BrowserRoute(session_id="sess-1", base_url="http://browser-session.test/browser/kernel", jwt="token-abc"))
- config = BrowserRoutingConfig(subresources=("curl", "telemetry/stream", "computer", "playwright"))
+ config = BrowserRoutingConfig(subresources=("curl", "telemetry/stream", "computer", "playwright", "process"))
events = rewrite_direct_vm_options(
FinalRequestOptions(method="get", url="/browsers/sess-1/telemetry/events"), cache=cache, config=config
@@ -458,7 +460,7 @@ def test_rewrite_direct_vm_options_keeps_telemetry_events_on_control_plane() ->
process = rewrite_direct_vm_options(
FinalRequestOptions(method="post", url="/browsers/sess-1/process/exec"), cache=cache, config=config
)
- assert process.url == "/browsers/sess-1/process/exec"
+ assert str(process.url).startswith("http://browser-session.test/browser/kernel/process/exec")
fs_read = rewrite_direct_vm_options(
FinalRequestOptions(method="get", url="/browsers/sess-1/fs/read_file"), cache=cache, config=config
@@ -472,7 +474,7 @@ def test_browser_routing_config_from_env_empty_string_disables_routing(monkeypat
@respx.mock
-def test_computer_screenshot_and_playwright_execute_route_to_vm_by_default(
+def test_default_browser_subresources_route_to_vm(
monkeypatch: pytest.MonkeyPatch,
) -> None:
monkeypatch.delenv("KERNEL_BROWSER_ROUTING_SUBRESOURCES", raising=False)
@@ -482,10 +484,14 @@ def test_computer_screenshot_and_playwright_execute_route_to_vm_by_default(
execute = respx.post("http://browser-session.test/browser/kernel/playwright/execute").mock(
return_value=httpx.Response(200, json={"success": True})
)
+ process = respx.post("http://browser-session.test/browser/kernel/process/exec").mock(
+ return_value=httpx.Response(200, json={"exit_code": 0, "stdout_b64": "", "stderr_b64": ""})
+ )
with Kernel(base_url=base_url, api_key=api_key, _strict_response_validation=True) as client:
_cache_browser(client)
client.browsers.computer.capture_screenshot("sess-1")
out = client.browsers.playwright.execute("sess-1", code="return 1")
+ process_out = client.browsers.process.exec("sess-1", command="echo")
assert screenshot.called
screenshot_req = cast(httpx.Request, cast(Any, screenshot.calls[0]).request)
@@ -495,28 +501,28 @@ def test_computer_screenshot_and_playwright_execute_route_to_vm_by_default(
execute_req = cast(httpx.Request, cast(Any, execute.calls[0]).request)
assert execute_req.url.params.get("jwt") == "token-abc"
assert execute_req.headers.get("Authorization") is None
+ assert process.called
+ process_req = cast(httpx.Request, cast(Any, process.calls[0]).request)
+ assert process_req.url.params.get("jwt") == "token-abc"
+ assert process_req.headers.get("Authorization") is None
assert out.success is True
+ assert process_out.exit_code == 0
@respx.mock
-def test_process_fs_and_telemetry_events_stay_on_api_origin_by_default(
+def test_fs_and_telemetry_events_stay_on_api_origin_by_default(
monkeypatch: pytest.MonkeyPatch,
) -> None:
monkeypatch.delenv("KERNEL_BROWSER_ROUTING_SUBRESOURCES", raising=False)
- process = respx.post(f"{base_url}/browsers/sess-1/process/exec").mock(
- return_value=httpx.Response(200, json={"exit_code": 0, "stdout_b64": "", "stderr_b64": ""})
- )
fs_read = respx.get(f"{base_url}/browsers/sess-1/fs/read_file").mock(
return_value=httpx.Response(200, content=b"x", headers={"content-type": "application/octet-stream"})
)
events = respx.get(f"{base_url}/browsers/sess-1/telemetry/events").mock(return_value=httpx.Response(200, json=[]))
with Kernel(base_url=base_url, api_key=api_key, _strict_response_validation=True) as client:
_cache_browser(client)
- client.browsers.process.exec("sess-1", command="echo")
client.browsers.fs.read_file("sess-1", path="/tmp/x")
client.browsers.telemetry.events("sess-1")
- assert process.called
assert fs_read.called
assert events.called
From 516cbce77afab43b346d9e95df2e4a25757c474b Mon Sep 17 00:00:00 2001
From: "kernel-internal[bot]"
<260533166+kernel-internal[bot]@users.noreply.github.com>
Date: Wed, 26 Aug 2026 18:12:05 +0000
Subject: [PATCH 215/366] feat: Add OTLP destination delivery health storage
Stainless-Generated-From: 3d9fee415fe52e87da8559af4000122383308fb9
---
src/kernel/resources/auth/connections.py | 22 ++++++++++++++-----
src/kernel/resources/browser_pools.py | 8 +++----
src/kernel/resources/browsers/browsers.py | 16 +++++++-------
.../types/auth/connection_create_params.py | 6 +++--
.../types/auth/connection_update_params.py | 7 +++++-
src/kernel/types/auth/managed_auth.py | 8 ++++---
src/kernel/types/browser_create_params.py | 2 +-
src/kernel/types/browser_create_response.py | 2 +-
src/kernel/types/browser_list_params.py | 2 +-
src/kernel/types/browser_list_response.py | 2 +-
src/kernel/types/browser_pool.py | 2 +-
.../types/browser_pool_acquire_response.py | 2 +-
.../types/browser_pool_create_params.py | 2 +-
src/kernel/types/browser_pool_list_params.py | 2 +-
src/kernel/types/browser_retrieve_response.py | 2 +-
src/kernel/types/browser_update_response.py | 2 +-
.../invocation_list_browsers_response.py | 2 +-
17 files changed, 54 insertions(+), 35 deletions(-)
diff --git a/src/kernel/resources/auth/connections.py b/src/kernel/resources/auth/connections.py
index ccc3d890..62858fdd 100644
--- a/src/kernel/resources/auth/connections.py
+++ b/src/kernel/resources/auth/connections.py
@@ -95,8 +95,10 @@ def create(
profile_name: Name of the profile to manage authentication for. If the profile does not exist,
it is created automatically.
- allowed_domains: Additional domains valid for this auth flow (besides the primary domain). Useful
- when login pages redirect to different domains.
+ allowed_domains: Additional hostname roots valid for this auth flow, besides the primary domain.
+ Each value allows credential entry on that exact hostname and its subdomains.
+ Leading `www.` and `*.` labels are normalized away. When omitted or empty,
+ credential entry is unrestricted.
The following SSO/OAuth provider domains are automatically allowed by default
and do not need to be specified:
@@ -254,7 +256,10 @@ def update(
updated.
Args:
- allowed_domains: Additional domains valid for this auth flow (replaces existing list)
+ allowed_domains: Additional hostname roots valid for this auth flow. Each value allows credential
+ entry on that exact hostname and its subdomains; leading `www.` and `*.` labels
+ are normalized away. An empty list leaves credential entry unrestricted.
+ Replaces the existing list.
auto_reauth: Whether automatic re-authentication is permitted for this connection. This is an
opt-in flag only — it does not check whether re-auth is actually feasible. Even
@@ -713,8 +718,10 @@ async def create(
profile_name: Name of the profile to manage authentication for. If the profile does not exist,
it is created automatically.
- allowed_domains: Additional domains valid for this auth flow (besides the primary domain). Useful
- when login pages redirect to different domains.
+ allowed_domains: Additional hostname roots valid for this auth flow, besides the primary domain.
+ Each value allows credential entry on that exact hostname and its subdomains.
+ Leading `www.` and `*.` labels are normalized away. When omitted or empty,
+ credential entry is unrestricted.
The following SSO/OAuth provider domains are automatically allowed by default
and do not need to be specified:
@@ -872,7 +879,10 @@ async def update(
updated.
Args:
- allowed_domains: Additional domains valid for this auth flow (replaces existing list)
+ allowed_domains: Additional hostname roots valid for this auth flow. Each value allows credential
+ entry on that exact hostname and its subdomains; leading `www.` and `*.` labels
+ are normalized away. An empty list leaves credential entry unrestricted.
+ Replaces the existing list.
auto_reauth: Whether automatic re-authentication is permitted for this connection. This is an
opt-in flag only — it does not check whether re-auth is actually feasible. Even
diff --git a/src/kernel/resources/browser_pools.py b/src/kernel/resources/browser_pools.py
index 99bda961..5351d638 100644
--- a/src/kernel/resources/browser_pools.py
+++ b/src/kernel/resources/browser_pools.py
@@ -73,7 +73,7 @@ def create(
profile: browser_pool_create_params.Profile | Omit = omit,
proxy_id: str | Omit = omit,
refresh_on_profile_update: bool | Omit = omit,
- region: Literal["us-east", "eu-west"] | Omit = omit,
+ region: Literal["us-east", "eu-west", "ap-southeast"] | Omit = omit,
start_url: str | Omit = omit,
stealth: bool | Omit = omit,
telemetry: Optional[browser_pool_create_params.Telemetry] | Omit = omit,
@@ -406,7 +406,7 @@ def list(
name: str | Omit = omit,
offset: int | Omit = omit,
query: str | Omit = omit,
- region: Literal["us-east", "eu-west"] | Omit = omit,
+ region: Literal["us-east", "eu-west", "ap-southeast"] | 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,
@@ -703,7 +703,7 @@ async def create(
profile: browser_pool_create_params.Profile | Omit = omit,
proxy_id: str | Omit = omit,
refresh_on_profile_update: bool | Omit = omit,
- region: Literal["us-east", "eu-west"] | Omit = omit,
+ region: Literal["us-east", "eu-west", "ap-southeast"] | Omit = omit,
start_url: str | Omit = omit,
stealth: bool | Omit = omit,
telemetry: Optional[browser_pool_create_params.Telemetry] | Omit = omit,
@@ -1036,7 +1036,7 @@ def list(
name: str | Omit = omit,
offset: int | Omit = omit,
query: str | Omit = omit,
- region: Literal["us-east", "eu-west"] | Omit = omit,
+ region: Literal["us-east", "eu-west", "ap-southeast"] | 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,
diff --git a/src/kernel/resources/browsers/browsers.py b/src/kernel/resources/browsers/browsers.py
index 0a15061a..920a65cf 100644
--- a/src/kernel/resources/browsers/browsers.py
+++ b/src/kernel/resources/browsers/browsers.py
@@ -182,7 +182,7 @@ def create(
profile: BrowserProfile | Omit = omit,
proxy: BrowserProxyConfigParam | Omit = omit,
proxy_id: str | Omit = omit,
- region: Literal["us-east", "eu-west"] | Omit = omit,
+ region: Literal["us-east", "eu-west", "ap-southeast"] | Omit = omit,
start_url: str | Omit = omit,
stealth: bool | Omit = omit,
tags: TagsParam | Omit = omit,
@@ -459,7 +459,7 @@ def list(
limit: int | Omit = omit,
offset: int | Omit = omit,
query: str | Omit = omit,
- region: Literal["us-east", "eu-west"] | Omit = omit,
+ region: Literal["us-east", "eu-west", "ap-southeast"] | Omit = omit,
status: Literal["active", "deleted", "all"] | Omit = omit,
tags: Dict[str, str] | Omit = omit,
# Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
@@ -694,8 +694,8 @@ def load_extensions(
timeout: float | httpx.Timeout | None | NotGiven = not_given,
) -> None:
"""
- Loads one or more unpacked extensions and restarts Chromium on the browser
- instance.
+ Loads one or more unpacked extensions using live CDP activation when eligible.
+ Chromium restarts when enterprise policy requires it or live activation fails.
Args:
extensions: List of extensions to upload and activate
@@ -802,7 +802,7 @@ async def create(
profile: BrowserProfile | Omit = omit,
proxy: BrowserProxyConfigParam | Omit = omit,
proxy_id: str | Omit = omit,
- region: Literal["us-east", "eu-west"] | Omit = omit,
+ region: Literal["us-east", "eu-west", "ap-southeast"] | Omit = omit,
start_url: str | Omit = omit,
stealth: bool | Omit = omit,
tags: TagsParam | Omit = omit,
@@ -1079,7 +1079,7 @@ def list(
limit: int | Omit = omit,
offset: int | Omit = omit,
query: str | Omit = omit,
- region: Literal["us-east", "eu-west"] | Omit = omit,
+ region: Literal["us-east", "eu-west", "ap-southeast"] | Omit = omit,
status: Literal["active", "deleted", "all"] | Omit = omit,
tags: Dict[str, str] | Omit = omit,
# Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
@@ -1314,8 +1314,8 @@ async def load_extensions(
timeout: float | httpx.Timeout | None | NotGiven = not_given,
) -> None:
"""
- Loads one or more unpacked extensions and restarts Chromium on the browser
- instance.
+ Loads one or more unpacked extensions using live CDP activation when eligible.
+ Chromium restarts when enterprise policy requires it or live activation fails.
Args:
extensions: List of extensions to upload and activate
diff --git a/src/kernel/types/auth/connection_create_params.py b/src/kernel/types/auth/connection_create_params.py
index 54995d37..0ea02cbd 100644
--- a/src/kernel/types/auth/connection_create_params.py
+++ b/src/kernel/types/auth/connection_create_params.py
@@ -31,9 +31,11 @@ class ConnectionCreateParams(TypedDict, total=False):
"""
allowed_domains: SequenceNotStr[str]
- """Additional domains valid for this auth flow (besides the primary domain).
+ """Additional hostname roots valid for this auth flow, besides the primary domain.
- Useful when login pages redirect to different domains.
+ Each value allows credential entry on that exact hostname and its subdomains.
+ Leading `www.` and `*.` labels are normalized away. When omitted or empty,
+ credential entry is unrestricted.
The following SSO/OAuth provider domains are automatically allowed by default
and do not need to be specified:
diff --git a/src/kernel/types/auth/connection_update_params.py b/src/kernel/types/auth/connection_update_params.py
index 829cf489..21977396 100644
--- a/src/kernel/types/auth/connection_update_params.py
+++ b/src/kernel/types/auth/connection_update_params.py
@@ -22,7 +22,12 @@
class ConnectionUpdateParams(TypedDict, total=False):
allowed_domains: SequenceNotStr[str]
- """Additional domains valid for this auth flow (replaces existing list)"""
+ """Additional hostname roots valid for this auth flow.
+
+ Each value allows credential entry on that exact hostname and its subdomains;
+ leading `www.` and `*.` labels are normalized away. An empty list leaves
+ credential entry unrestricted. Replaces the existing list.
+ """
auto_reauth: bool
"""Whether automatic re-authentication is permitted for this connection.
diff --git a/src/kernel/types/auth/managed_auth.py b/src/kernel/types/auth/managed_auth.py
index 7c4ba7d6..7c4f9668 100644
--- a/src/kernel/types/auth/managed_auth.py
+++ b/src/kernel/types/auth/managed_auth.py
@@ -309,9 +309,11 @@ class ManagedAuth(BaseModel):
"""Current authentication status of the managed profile"""
allowed_domains: Optional[List[str]] = None
- """
- Additional domains that are valid for this auth flow (besides the primary
- domain). Useful when login pages redirect to different domains.
+ """Additional hostname roots valid for this auth flow, besides the primary domain.
+
+ Each value allows credential entry on that exact hostname and its subdomains.
+ Leading `www.` and `*.` labels are normalized away. When omitted or empty,
+ credential entry is unrestricted.
The following SSO/OAuth provider domains are automatically allowed by default
and do not need to be specified:
diff --git a/src/kernel/types/browser_create_params.py b/src/kernel/types/browser_create_params.py
index 85e0b9bd..da890722 100644
--- a/src/kernel/types/browser_create_params.py
+++ b/src/kernel/types/browser_create_params.py
@@ -101,7 +101,7 @@ class BrowserCreateParams(TypedDict, total=False):
favor of proxy.
"""
- region: Literal["us-east", "eu-west"]
+ region: Literal["us-east", "eu-west", "ap-southeast"]
"""Geographic region for the browser session.
It is fixed once the session is created. Region selection requires a Start-Up or
diff --git a/src/kernel/types/browser_create_response.py b/src/kernel/types/browser_create_response.py
index dcc96f43..e5f63b6c 100644
--- a/src/kernel/types/browser_create_response.py
+++ b/src/kernel/types/browser_create_response.py
@@ -31,7 +31,7 @@ class BrowserCreateResponse(BaseModel):
memory: BrowserMemory
"""Memory allocated to the browser session."""
- region: Literal["us-east", "eu-west"]
+ region: Literal["us-east", "eu-west", "ap-southeast"]
"""Geographic region of the browser session. Fixed once the session is created."""
session_id: str
diff --git a/src/kernel/types/browser_list_params.py b/src/kernel/types/browser_list_params.py
index 8b829a8d..eb5b91dc 100644
--- a/src/kernel/types/browser_list_params.py
+++ b/src/kernel/types/browser_list_params.py
@@ -27,7 +27,7 @@ class BrowserListParams(TypedDict, total=False):
Search browsers by name, session ID, profile name or ID, proxy ID, or pool name.
"""
- region: Literal["us-east", "eu-west"]
+ region: Literal["us-east", "eu-west", "ap-southeast"]
"""Filter sessions by geographic region. Omit to list sessions in all regions."""
status: Literal["active", "deleted", "all"]
diff --git a/src/kernel/types/browser_list_response.py b/src/kernel/types/browser_list_response.py
index 0ef81b9a..da294a61 100644
--- a/src/kernel/types/browser_list_response.py
+++ b/src/kernel/types/browser_list_response.py
@@ -31,7 +31,7 @@ class BrowserListResponse(BaseModel):
memory: BrowserMemory
"""Memory allocated to the browser session."""
- region: Literal["us-east", "eu-west"]
+ region: Literal["us-east", "eu-west", "ap-southeast"]
"""Geographic region of the browser session. Fixed once the session is created."""
session_id: str
diff --git a/src/kernel/types/browser_pool.py b/src/kernel/types/browser_pool.py
index 4504fc5d..68f96450 100644
--- a/src/kernel/types/browser_pool.py
+++ b/src/kernel/types/browser_pool.py
@@ -174,7 +174,7 @@ class BrowserPool(BaseModel):
(echoed as sent on create).
"""
- region: Literal["us-east", "eu-west"]
+ region: Literal["us-east", "eu-west", "ap-southeast"]
"""Geographic region of the browser pool. Fixed once the pool is created."""
name: Optional[str] = None
diff --git a/src/kernel/types/browser_pool_acquire_response.py b/src/kernel/types/browser_pool_acquire_response.py
index d9638105..755f5838 100644
--- a/src/kernel/types/browser_pool_acquire_response.py
+++ b/src/kernel/types/browser_pool_acquire_response.py
@@ -31,7 +31,7 @@ class BrowserPoolAcquireResponse(BaseModel):
memory: BrowserMemory
"""Memory allocated to the browser session."""
- region: Literal["us-east", "eu-west"]
+ region: Literal["us-east", "eu-west", "ap-southeast"]
"""Geographic region of the browser session. Fixed once the session is created."""
session_id: str
diff --git a/src/kernel/types/browser_pool_create_params.py b/src/kernel/types/browser_pool_create_params.py
index 3a736fbc..33cff052 100644
--- a/src/kernel/types/browser_pool_create_params.py
+++ b/src/kernel/types/browser_pool_create_params.py
@@ -87,7 +87,7 @@ class BrowserPoolCreateParams(TypedDict, total=False):
creation, this defaults to true. Requires a profile to be set on the pool.
"""
- region: Literal["us-east", "eu-west"]
+ region: Literal["us-east", "eu-west", "ap-southeast"]
"""Geographic region for the browser pool.
It is fixed once the pool is created. Region selection requires a Start-Up or
diff --git a/src/kernel/types/browser_pool_list_params.py b/src/kernel/types/browser_pool_list_params.py
index 36f2f8ca..7d7a268c 100644
--- a/src/kernel/types/browser_pool_list_params.py
+++ b/src/kernel/types/browser_pool_list_params.py
@@ -28,5 +28,5 @@ class BrowserPoolListParams(TypedDict, total=False):
IDs match by exact value.
"""
- region: Literal["us-east", "eu-west"]
+ region: Literal["us-east", "eu-west", "ap-southeast"]
"""Filter pools by geographic region. Omit to list pools in all regions."""
diff --git a/src/kernel/types/browser_retrieve_response.py b/src/kernel/types/browser_retrieve_response.py
index 8292024b..332585ca 100644
--- a/src/kernel/types/browser_retrieve_response.py
+++ b/src/kernel/types/browser_retrieve_response.py
@@ -31,7 +31,7 @@ class BrowserRetrieveResponse(BaseModel):
memory: BrowserMemory
"""Memory allocated to the browser session."""
- region: Literal["us-east", "eu-west"]
+ region: Literal["us-east", "eu-west", "ap-southeast"]
"""Geographic region of the browser session. Fixed once the session is created."""
session_id: str
diff --git a/src/kernel/types/browser_update_response.py b/src/kernel/types/browser_update_response.py
index 7dd7de88..7af24aee 100644
--- a/src/kernel/types/browser_update_response.py
+++ b/src/kernel/types/browser_update_response.py
@@ -31,7 +31,7 @@ class BrowserUpdateResponse(BaseModel):
memory: BrowserMemory
"""Memory allocated to the browser session."""
- region: Literal["us-east", "eu-west"]
+ region: Literal["us-east", "eu-west", "ap-southeast"]
"""Geographic region of the browser session. Fixed once the session is created."""
session_id: str
diff --git a/src/kernel/types/invocation_list_browsers_response.py b/src/kernel/types/invocation_list_browsers_response.py
index db32954d..e31194b8 100644
--- a/src/kernel/types/invocation_list_browsers_response.py
+++ b/src/kernel/types/invocation_list_browsers_response.py
@@ -31,7 +31,7 @@ class Browser(BaseModel):
memory: BrowserMemory
"""Memory allocated to the browser session."""
- region: Literal["us-east", "eu-west"]
+ region: Literal["us-east", "eu-west", "ap-southeast"]
"""Geographic region of the browser session. Fixed once the session is created."""
session_id: str
From 102823cc1f7897b9129b8d52900de740efc4abc4 Mon Sep 17 00:00:00 2001
From: "kernel-internal[bot]"
<260533166+kernel-internal[bot]@users.noreply.github.com>
Date: Wed, 26 Aug 2026 18:17:03 +0000
Subject: [PATCH 216/366] release: 0.95.0
---
.release-please-manifest.json | 2 +-
CHANGELOG.md | 8 ++++++++
pyproject.toml | 2 +-
src/kernel/_version.py | 2 +-
4 files changed, 11 insertions(+), 3 deletions(-)
diff --git a/.release-please-manifest.json b/.release-please-manifest.json
index 91393ed8..2292ca70 100644
--- a/.release-please-manifest.json
+++ b/.release-please-manifest.json
@@ -1,3 +1,3 @@
{
- ".": "0.94.0"
+ ".": "0.95.0"
}
\ No newline at end of file
diff --git a/CHANGELOG.md b/CHANGELOG.md
index ccd152aa..6416998e 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,13 @@
# Changelog
+## [0.95.0](https://github.com/kernel/kernel-python-sdk/compare/v0.94.0...v0.95.0) (2026-08-26)
+
+
+### Features
+
+* Add OTLP destination delivery health storage ([516cbce](https://github.com/kernel/kernel-python-sdk/commit/516cbce77afab43b346d9e95df2e4a25757c474b))
+* route process calls directly to browser VMs ([f833c55](https://github.com/kernel/kernel-python-sdk/commit/f833c55fee9e7c33c444af6366890a9c876e149c))
+
## [0.94.0](https://github.com/kernel/kernel-python-sdk/compare/v0.93.0...v0.94.0) (2026-08-24)
diff --git a/pyproject.toml b/pyproject.toml
index 420e8de4..d7125601 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -1,6 +1,6 @@
[project]
name = "kernel"
-version = "0.94.0"
+version = "0.95.0"
description = "The official Python library for the kernel API"
dynamic = ["readme"]
license = "Apache-2.0"
diff --git a/src/kernel/_version.py b/src/kernel/_version.py
index 54fa859a..258c1cb0 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.94.0" # x-release-please-version
+__version__ = "0.95.0" # x-release-please-version
From 114c986f0d771049ec09741c65e0be317c98f718 Mon Sep 17 00:00:00 2001
From: "kernel-internal[bot]"
<260533166+kernel-internal[bot]@users.noreply.github.com>
Date: Wed, 26 Aug 2026 23:56:07 +0000
Subject: [PATCH 217/366] feat: Rename the site configs API to config registry
Stainless-Generated-From: c9565b92fe48bc6ffd31ca18b75bc0535fc26324
---
api.md | 19 +-
src/kernel/_client.py | 40 +-
src/kernel/resources/__init__.py | 28 +-
.../resources/config_registry/__init__.py | 33 ++
.../resources/config_registry/analyses.py | 279 +++++++++++
.../config_registry.py} | 372 +++++----------
src/kernel/types/__init__.py | 11 +-
src/kernel/types/analysis.py | 2 +-
src/kernel/types/config_registry/__init__.py | 5 +
.../analysis_list_params.py} | 7 +-
.../types/config_registry_list_params.py | 23 +
...ms.py => config_registry_lookup_params.py} | 4 +-
...s.py => config_registry_resolve_params.py} | 4 +-
...esponse.py => config_registry_response.py} | 4 +-
src/kernel/types/no_recommendation.py | 3 +-
src/kernel/types/proxy.py | 54 +--
src/kernel/types/recommendation_summary.py | 7 +
...site_config_list_recommendations_params.py | 17 -
.../api_resources/config_registry/__init__.py | 1 +
.../config_registry/test_analyses.py | 185 ++++++++
tests/api_resources/test_config_registry.py | 281 +++++++++++
tests/api_resources/test_site_configs.py | 438 ------------------
22 files changed, 1024 insertions(+), 793 deletions(-)
create mode 100644 src/kernel/resources/config_registry/__init__.py
create mode 100644 src/kernel/resources/config_registry/analyses.py
rename src/kernel/resources/{site_configs.py => config_registry/config_registry.py} (53%)
create mode 100644 src/kernel/types/config_registry/__init__.py
rename src/kernel/types/{site_config_list_params.py => config_registry/analysis_list_params.py} (54%)
create mode 100644 src/kernel/types/config_registry_list_params.py
rename src/kernel/types/{site_config_lookup_params.py => config_registry_lookup_params.py} (82%)
rename src/kernel/types/{site_config_resolve_params.py => config_registry_resolve_params.py} (84%)
rename src/kernel/types/{site_config_response.py => config_registry_response.py} (87%)
delete mode 100644 src/kernel/types/site_config_list_recommendations_params.py
create mode 100644 tests/api_resources/config_registry/__init__.py
create mode 100644 tests/api_resources/config_registry/test_analyses.py
create mode 100644 tests/api_resources/test_config_registry.py
delete mode 100644 tests/api_resources/test_site_configs.py
diff --git a/api.md b/api.md
index 60493a41..d5cdf897 100644
--- a/api.md
+++ b/api.md
@@ -74,7 +74,7 @@ Methods:
- client.invocations.follow(id, \*\*params) -> InvocationFollowResponse
- client.invocations.list_browsers(id) -> InvocationListBrowsersResponse
-# SiteConfigs
+# ConfigRegistry
Types:
@@ -83,6 +83,7 @@ from kernel.types import (
Analysis,
AnalysisSummary,
Browser,
+ ConfigRegistryResponse,
Evidence,
LookupRequest,
LookupResponse,
@@ -92,18 +93,22 @@ from kernel.types import (
RecommendationResult,
RecommendationSummary,
ResolveRequest,
- SiteConfigResponse,
Target,
)
```
Methods:
-- client.site_configs.retrieve(id) -> SiteConfigResponse
-- client.site_configs.list(\*\*params) -> SyncOffsetPagination[AnalysisSummary]
-- client.site_configs.list_recommendations(\*\*params) -> SyncOffsetPagination[RecommendationSummary]
-- client.site_configs.lookup(\*\*params) -> LookupResponse
-- client.site_configs.resolve(\*\*params) -> SiteConfigResponse
+- client.config_registry.list(\*\*params) -> SyncOffsetPagination[RecommendationSummary]
+- client.config_registry.lookup(\*\*params) -> LookupResponse
+- client.config_registry.resolve(\*\*params) -> ConfigRegistryResponse
+
+## Analyses
+
+Methods:
+
+- client.config_registry.analyses.retrieve(id) -> ConfigRegistryResponse
+- client.config_registry.analyses.list(\*\*params) -> SyncOffsetPagination[AnalysisSummary]
# Browsers
diff --git a/src/kernel/_client.py b/src/kernel/_client.py
index d96b5804..2c06cb04 100644
--- a/src/kernel/_client.py
+++ b/src/kernel/_client.py
@@ -62,8 +62,8 @@
deployments,
invocations,
organization,
- site_configs,
browser_pools,
+ config_registry,
credential_providers,
)
from .resources.apps import AppsResource, AsyncAppsResource
@@ -75,7 +75,6 @@
from .resources.credentials import CredentialsResource, AsyncCredentialsResource
from .resources.deployments import DeploymentsResource, AsyncDeploymentsResource
from .resources.invocations import InvocationsResource, AsyncInvocationsResource
- from .resources.site_configs import SiteConfigsResource, AsyncSiteConfigsResource
from .resources.browser_pools import BrowserPoolsResource, AsyncBrowserPoolsResource
from .resources.browsers.browsers import BrowsersResource, AsyncBrowsersResource
from .resources.projects.projects import ProjectsResource, AsyncProjectsResource
@@ -83,6 +82,7 @@
from .resources.credential_providers import CredentialProvidersResource, AsyncCredentialProvidersResource
from .resources.audit_logs.audit_logs import AuditLogsResource, AsyncAuditLogsResource
from .resources.organization.organization import OrganizationResource, AsyncOrganizationResource
+ from .resources.config_registry.config_registry import ConfigRegistryResource, AsyncConfigRegistryResource
__all__ = [
"ENVIRONMENTS",
@@ -226,11 +226,11 @@ def invocations(self) -> InvocationsResource:
return InvocationsResource(self)
@cached_property
- def site_configs(self) -> SiteConfigsResource:
+ def config_registry(self) -> ConfigRegistryResource:
"""Resolve browser and proxy recommendations for bot-protected sites."""
- from .resources.site_configs import SiteConfigsResource
+ from .resources.config_registry import ConfigRegistryResource
- return SiteConfigsResource(self)
+ return ConfigRegistryResource(self)
@cached_property
def browsers(self) -> BrowsersResource:
@@ -609,11 +609,11 @@ def invocations(self) -> AsyncInvocationsResource:
return AsyncInvocationsResource(self)
@cached_property
- def site_configs(self) -> AsyncSiteConfigsResource:
+ def config_registry(self) -> AsyncConfigRegistryResource:
"""Resolve browser and proxy recommendations for bot-protected sites."""
- from .resources.site_configs import AsyncSiteConfigsResource
+ from .resources.config_registry import AsyncConfigRegistryResource
- return AsyncSiteConfigsResource(self)
+ return AsyncConfigRegistryResource(self)
@cached_property
def browsers(self) -> AsyncBrowsersResource:
@@ -896,11 +896,11 @@ def invocations(self) -> invocations.InvocationsResourceWithRawResponse:
return InvocationsResourceWithRawResponse(self._client.invocations)
@cached_property
- def site_configs(self) -> site_configs.SiteConfigsResourceWithRawResponse:
+ def config_registry(self) -> config_registry.ConfigRegistryResourceWithRawResponse:
"""Resolve browser and proxy recommendations for bot-protected sites."""
- from .resources.site_configs import SiteConfigsResourceWithRawResponse
+ from .resources.config_registry import ConfigRegistryResourceWithRawResponse
- return SiteConfigsResourceWithRawResponse(self._client.site_configs)
+ return ConfigRegistryResourceWithRawResponse(self._client.config_registry)
@cached_property
def browsers(self) -> browsers.BrowsersResourceWithRawResponse:
@@ -1023,11 +1023,11 @@ def invocations(self) -> invocations.AsyncInvocationsResourceWithRawResponse:
return AsyncInvocationsResourceWithRawResponse(self._client.invocations)
@cached_property
- def site_configs(self) -> site_configs.AsyncSiteConfigsResourceWithRawResponse:
+ def config_registry(self) -> config_registry.AsyncConfigRegistryResourceWithRawResponse:
"""Resolve browser and proxy recommendations for bot-protected sites."""
- from .resources.site_configs import AsyncSiteConfigsResourceWithRawResponse
+ from .resources.config_registry import AsyncConfigRegistryResourceWithRawResponse
- return AsyncSiteConfigsResourceWithRawResponse(self._client.site_configs)
+ return AsyncConfigRegistryResourceWithRawResponse(self._client.config_registry)
@cached_property
def browsers(self) -> browsers.AsyncBrowsersResourceWithRawResponse:
@@ -1150,11 +1150,11 @@ def invocations(self) -> invocations.InvocationsResourceWithStreamingResponse:
return InvocationsResourceWithStreamingResponse(self._client.invocations)
@cached_property
- def site_configs(self) -> site_configs.SiteConfigsResourceWithStreamingResponse:
+ def config_registry(self) -> config_registry.ConfigRegistryResourceWithStreamingResponse:
"""Resolve browser and proxy recommendations for bot-protected sites."""
- from .resources.site_configs import SiteConfigsResourceWithStreamingResponse
+ from .resources.config_registry import ConfigRegistryResourceWithStreamingResponse
- return SiteConfigsResourceWithStreamingResponse(self._client.site_configs)
+ return ConfigRegistryResourceWithStreamingResponse(self._client.config_registry)
@cached_property
def browsers(self) -> browsers.BrowsersResourceWithStreamingResponse:
@@ -1277,11 +1277,11 @@ def invocations(self) -> invocations.AsyncInvocationsResourceWithStreamingRespon
return AsyncInvocationsResourceWithStreamingResponse(self._client.invocations)
@cached_property
- def site_configs(self) -> site_configs.AsyncSiteConfigsResourceWithStreamingResponse:
+ def config_registry(self) -> config_registry.AsyncConfigRegistryResourceWithStreamingResponse:
"""Resolve browser and proxy recommendations for bot-protected sites."""
- from .resources.site_configs import AsyncSiteConfigsResourceWithStreamingResponse
+ from .resources.config_registry import AsyncConfigRegistryResourceWithStreamingResponse
- return AsyncSiteConfigsResourceWithStreamingResponse(self._client.site_configs)
+ return AsyncConfigRegistryResourceWithStreamingResponse(self._client.config_registry)
@cached_property
def browsers(self) -> browsers.AsyncBrowsersResourceWithStreamingResponse:
diff --git a/src/kernel/resources/__init__.py b/src/kernel/resources/__init__.py
index 2ec29c9c..c6496534 100644
--- a/src/kernel/resources/__init__.py
+++ b/src/kernel/resources/__init__.py
@@ -112,14 +112,6 @@
OrganizationResourceWithStreamingResponse,
AsyncOrganizationResourceWithStreamingResponse,
)
-from .site_configs import (
- SiteConfigsResource,
- AsyncSiteConfigsResource,
- SiteConfigsResourceWithRawResponse,
- AsyncSiteConfigsResourceWithRawResponse,
- SiteConfigsResourceWithStreamingResponse,
- AsyncSiteConfigsResourceWithStreamingResponse,
-)
from .browser_pools import (
BrowserPoolsResource,
AsyncBrowserPoolsResource,
@@ -128,6 +120,14 @@
BrowserPoolsResourceWithStreamingResponse,
AsyncBrowserPoolsResourceWithStreamingResponse,
)
+from .config_registry import (
+ ConfigRegistryResource,
+ AsyncConfigRegistryResource,
+ ConfigRegistryResourceWithRawResponse,
+ AsyncConfigRegistryResourceWithRawResponse,
+ ConfigRegistryResourceWithStreamingResponse,
+ AsyncConfigRegistryResourceWithStreamingResponse,
+)
from .credential_providers import (
CredentialProvidersResource,
AsyncCredentialProvidersResource,
@@ -156,12 +156,12 @@
"AsyncInvocationsResourceWithRawResponse",
"InvocationsResourceWithStreamingResponse",
"AsyncInvocationsResourceWithStreamingResponse",
- "SiteConfigsResource",
- "AsyncSiteConfigsResource",
- "SiteConfigsResourceWithRawResponse",
- "AsyncSiteConfigsResourceWithRawResponse",
- "SiteConfigsResourceWithStreamingResponse",
- "AsyncSiteConfigsResourceWithStreamingResponse",
+ "ConfigRegistryResource",
+ "AsyncConfigRegistryResource",
+ "ConfigRegistryResourceWithRawResponse",
+ "AsyncConfigRegistryResourceWithRawResponse",
+ "ConfigRegistryResourceWithStreamingResponse",
+ "AsyncConfigRegistryResourceWithStreamingResponse",
"BrowsersResource",
"AsyncBrowsersResource",
"BrowsersResourceWithRawResponse",
diff --git a/src/kernel/resources/config_registry/__init__.py b/src/kernel/resources/config_registry/__init__.py
new file mode 100644
index 00000000..9e904868
--- /dev/null
+++ b/src/kernel/resources/config_registry/__init__.py
@@ -0,0 +1,33 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from .analyses import (
+ AnalysesResource,
+ AsyncAnalysesResource,
+ AnalysesResourceWithRawResponse,
+ AsyncAnalysesResourceWithRawResponse,
+ AnalysesResourceWithStreamingResponse,
+ AsyncAnalysesResourceWithStreamingResponse,
+)
+from .config_registry import (
+ ConfigRegistryResource,
+ AsyncConfigRegistryResource,
+ ConfigRegistryResourceWithRawResponse,
+ AsyncConfigRegistryResourceWithRawResponse,
+ ConfigRegistryResourceWithStreamingResponse,
+ AsyncConfigRegistryResourceWithStreamingResponse,
+)
+
+__all__ = [
+ "AnalysesResource",
+ "AsyncAnalysesResource",
+ "AnalysesResourceWithRawResponse",
+ "AsyncAnalysesResourceWithRawResponse",
+ "AnalysesResourceWithStreamingResponse",
+ "AsyncAnalysesResourceWithStreamingResponse",
+ "ConfigRegistryResource",
+ "AsyncConfigRegistryResource",
+ "ConfigRegistryResourceWithRawResponse",
+ "AsyncConfigRegistryResourceWithRawResponse",
+ "ConfigRegistryResourceWithStreamingResponse",
+ "AsyncConfigRegistryResourceWithStreamingResponse",
+]
diff --git a/src/kernel/resources/config_registry/analyses.py b/src/kernel/resources/config_registry/analyses.py
new file mode 100644
index 00000000..c0cbd2c7
--- /dev/null
+++ b/src/kernel/resources/config_registry/analyses.py
@@ -0,0 +1,279 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from __future__ import annotations
+
+import httpx
+
+from ..._types import Body, Omit, Query, Headers, NotGiven, omit, not_given
+from ..._utils import path_template, maybe_transform
+from ..._compat import cached_property
+from ..._resource import SyncAPIResource, AsyncAPIResource
+from ..._response import (
+ to_raw_response_wrapper,
+ to_streamed_response_wrapper,
+ async_to_raw_response_wrapper,
+ async_to_streamed_response_wrapper,
+)
+from ...pagination import SyncOffsetPagination, AsyncOffsetPagination
+from ..._base_client import AsyncPaginator, make_request_options
+from ...types.config_registry import analysis_list_params
+from ...types.analysis_summary import AnalysisSummary
+from ...types.config_registry_response import ConfigRegistryResponse
+
+__all__ = ["AnalysesResource", "AsyncAnalysesResource"]
+
+
+class AnalysesResource(SyncAPIResource):
+ """Resolve browser and proxy recommendations for bot-protected sites."""
+
+ @cached_property
+ def with_raw_response(self) -> AnalysesResourceWithRawResponse:
+ """
+ This property can be used as a prefix for any HTTP method call to return
+ the raw response object instead of the parsed content.
+
+ For more information, see https://www.github.com/kernel/kernel-python-sdk#accessing-raw-response-data-eg-headers
+ """
+ return AnalysesResourceWithRawResponse(self)
+
+ @cached_property
+ def with_streaming_response(self) -> AnalysesResourceWithStreamingResponse:
+ """
+ An alternative to `.with_raw_response` that doesn't eagerly read the response body.
+
+ For more information, see https://www.github.com/kernel/kernel-python-sdk#with_streaming_response
+ """
+ return AnalysesResourceWithStreamingResponse(self)
+
+ def retrieve(
+ self,
+ id: str,
+ *,
+ # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
+ # The extra values given here take precedence over values defined on the client or passed to this method.
+ extra_headers: Headers | None = None,
+ extra_query: Query | None = None,
+ extra_body: Body | None = None,
+ timeout: float | httpx.Timeout | None | NotGiven = not_given,
+ ) -> ConfigRegistryResponse:
+ """
+ Returns a project-scoped historical analysis and the recommendation outcome
+ concluded by that run. Later knowledge does not change this response.
+
+ Args:
+ extra_headers: Send extra headers
+
+ extra_query: Add additional query parameters to the request
+
+ extra_body: Add additional JSON properties to the request
+
+ timeout: Override the client-level default timeout for this request, in seconds
+ """
+ if not id:
+ raise ValueError(f"Expected a non-empty value for `id` but received {id!r}")
+ return self._get(
+ path_template("/config-registry/analyses/{id}", id=id),
+ options=make_request_options(
+ extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
+ ),
+ cast_to=ConfigRegistryResponse,
+ )
+
+ def list(
+ self,
+ *,
+ limit: int | Omit = omit,
+ offset: int | Omit = omit,
+ search: str | Omit = omit,
+ # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
+ # The extra values given here take precedence over values defined on the client or passed to this method.
+ extra_headers: Headers | None = None,
+ extra_query: Query | None = None,
+ extra_body: Body | None = None,
+ timeout: float | httpx.Timeout | None | NotGiven = not_given,
+ ) -> SyncOffsetPagination[AnalysisSummary]:
+ """
+ Lists analyses for the selected project, newest first.
+
+ Args:
+ search: Case-insensitive substring search over requested URLs.
+
+ 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
+ """
+ return self._get_api_list(
+ "/config-registry/analyses",
+ page=SyncOffsetPagination[AnalysisSummary],
+ options=make_request_options(
+ extra_headers=extra_headers,
+ extra_query=extra_query,
+ extra_body=extra_body,
+ timeout=timeout,
+ query=maybe_transform(
+ {
+ "limit": limit,
+ "offset": offset,
+ "search": search,
+ },
+ analysis_list_params.AnalysisListParams,
+ ),
+ ),
+ model=AnalysisSummary,
+ )
+
+
+class AsyncAnalysesResource(AsyncAPIResource):
+ """Resolve browser and proxy recommendations for bot-protected sites."""
+
+ @cached_property
+ def with_raw_response(self) -> AsyncAnalysesResourceWithRawResponse:
+ """
+ This property can be used as a prefix for any HTTP method call to return
+ the raw response object instead of the parsed content.
+
+ For more information, see https://www.github.com/kernel/kernel-python-sdk#accessing-raw-response-data-eg-headers
+ """
+ return AsyncAnalysesResourceWithRawResponse(self)
+
+ @cached_property
+ def with_streaming_response(self) -> AsyncAnalysesResourceWithStreamingResponse:
+ """
+ An alternative to `.with_raw_response` that doesn't eagerly read the response body.
+
+ For more information, see https://www.github.com/kernel/kernel-python-sdk#with_streaming_response
+ """
+ return AsyncAnalysesResourceWithStreamingResponse(self)
+
+ async def retrieve(
+ self,
+ id: str,
+ *,
+ # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
+ # The extra values given here take precedence over values defined on the client or passed to this method.
+ extra_headers: Headers | None = None,
+ extra_query: Query | None = None,
+ extra_body: Body | None = None,
+ timeout: float | httpx.Timeout | None | NotGiven = not_given,
+ ) -> ConfigRegistryResponse:
+ """
+ Returns a project-scoped historical analysis and the recommendation outcome
+ concluded by that run. Later knowledge does not change this response.
+
+ Args:
+ extra_headers: Send extra headers
+
+ extra_query: Add additional query parameters to the request
+
+ extra_body: Add additional JSON properties to the request
+
+ timeout: Override the client-level default timeout for this request, in seconds
+ """
+ if not id:
+ raise ValueError(f"Expected a non-empty value for `id` but received {id!r}")
+ return await self._get(
+ path_template("/config-registry/analyses/{id}", id=id),
+ options=make_request_options(
+ extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
+ ),
+ cast_to=ConfigRegistryResponse,
+ )
+
+ def list(
+ self,
+ *,
+ limit: int | Omit = omit,
+ offset: int | Omit = omit,
+ search: str | Omit = omit,
+ # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
+ # The extra values given here take precedence over values defined on the client or passed to this method.
+ extra_headers: Headers | None = None,
+ extra_query: Query | None = None,
+ extra_body: Body | None = None,
+ timeout: float | httpx.Timeout | None | NotGiven = not_given,
+ ) -> AsyncPaginator[AnalysisSummary, AsyncOffsetPagination[AnalysisSummary]]:
+ """
+ Lists analyses for the selected project, newest first.
+
+ Args:
+ search: Case-insensitive substring search over requested URLs.
+
+ 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
+ """
+ return self._get_api_list(
+ "/config-registry/analyses",
+ page=AsyncOffsetPagination[AnalysisSummary],
+ options=make_request_options(
+ extra_headers=extra_headers,
+ extra_query=extra_query,
+ extra_body=extra_body,
+ timeout=timeout,
+ query=maybe_transform(
+ {
+ "limit": limit,
+ "offset": offset,
+ "search": search,
+ },
+ analysis_list_params.AnalysisListParams,
+ ),
+ ),
+ model=AnalysisSummary,
+ )
+
+
+class AnalysesResourceWithRawResponse:
+ def __init__(self, analyses: AnalysesResource) -> None:
+ self._analyses = analyses
+
+ self.retrieve = to_raw_response_wrapper(
+ analyses.retrieve,
+ )
+ self.list = to_raw_response_wrapper(
+ analyses.list,
+ )
+
+
+class AsyncAnalysesResourceWithRawResponse:
+ def __init__(self, analyses: AsyncAnalysesResource) -> None:
+ self._analyses = analyses
+
+ self.retrieve = async_to_raw_response_wrapper(
+ analyses.retrieve,
+ )
+ self.list = async_to_raw_response_wrapper(
+ analyses.list,
+ )
+
+
+class AnalysesResourceWithStreamingResponse:
+ def __init__(self, analyses: AnalysesResource) -> None:
+ self._analyses = analyses
+
+ self.retrieve = to_streamed_response_wrapper(
+ analyses.retrieve,
+ )
+ self.list = to_streamed_response_wrapper(
+ analyses.list,
+ )
+
+
+class AsyncAnalysesResourceWithStreamingResponse:
+ def __init__(self, analyses: AsyncAnalysesResource) -> None:
+ self._analyses = analyses
+
+ self.retrieve = async_to_streamed_response_wrapper(
+ analyses.retrieve,
+ )
+ self.list = async_to_streamed_response_wrapper(
+ analyses.list,
+ )
diff --git a/src/kernel/resources/site_configs.py b/src/kernel/resources/config_registry/config_registry.py
similarity index 53%
rename from src/kernel/resources/site_configs.py
rename to src/kernel/resources/config_registry/config_registry.py
index b98a8d80..e5aa40d6 100644
--- a/src/kernel/resources/site_configs.py
+++ b/src/kernel/resources/config_registry/config_registry.py
@@ -6,137 +6,69 @@
import httpx
-from ..types import (
- site_config_list_params,
- site_config_lookup_params,
- site_config_resolve_params,
- site_config_list_recommendations_params,
+from ...types import config_registry_list_params, config_registry_lookup_params, config_registry_resolve_params
+from ..._types import Body, Omit, Query, Headers, NotGiven, SequenceNotStr, omit, not_given
+from ..._utils import maybe_transform, async_maybe_transform
+from .analyses import (
+ AnalysesResource,
+ AsyncAnalysesResource,
+ AnalysesResourceWithRawResponse,
+ AsyncAnalysesResourceWithRawResponse,
+ AnalysesResourceWithStreamingResponse,
+ AsyncAnalysesResourceWithStreamingResponse,
)
-from .._types import Body, Omit, Query, Headers, NotGiven, SequenceNotStr, omit, not_given
-from .._utils import path_template, maybe_transform, async_maybe_transform
-from .._compat import cached_property
-from .._resource import SyncAPIResource, AsyncAPIResource
-from .._response import (
+from ..._compat import cached_property
+from ..._resource import SyncAPIResource, AsyncAPIResource
+from ..._response import (
to_raw_response_wrapper,
to_streamed_response_wrapper,
async_to_raw_response_wrapper,
async_to_streamed_response_wrapper,
)
-from ..pagination import SyncOffsetPagination, AsyncOffsetPagination
-from .._base_client import AsyncPaginator, make_request_options
-from ..types.lookup_response import LookupResponse
-from ..types.analysis_summary import AnalysisSummary
-from ..types.site_config_response import SiteConfigResponse
-from ..types.recommendation_summary import RecommendationSummary
+from ...pagination import SyncOffsetPagination, AsyncOffsetPagination
+from ..._base_client import AsyncPaginator, make_request_options
+from ...types.lookup_response import LookupResponse
+from ...types.recommendation_summary import RecommendationSummary
+from ...types.config_registry_response import ConfigRegistryResponse
-__all__ = ["SiteConfigsResource", "AsyncSiteConfigsResource"]
+__all__ = ["ConfigRegistryResource", "AsyncConfigRegistryResource"]
-class SiteConfigsResource(SyncAPIResource):
+class ConfigRegistryResource(SyncAPIResource):
"""Resolve browser and proxy recommendations for bot-protected sites."""
@cached_property
- def with_raw_response(self) -> SiteConfigsResourceWithRawResponse:
+ def analyses(self) -> AnalysesResource:
+ """Resolve browser and proxy recommendations for bot-protected sites."""
+ return AnalysesResource(self._client)
+
+ @cached_property
+ def with_raw_response(self) -> ConfigRegistryResourceWithRawResponse:
"""
This property can be used as a prefix for any HTTP method call to return
the raw response object instead of the parsed content.
For more information, see https://www.github.com/kernel/kernel-python-sdk#accessing-raw-response-data-eg-headers
"""
- return SiteConfigsResourceWithRawResponse(self)
+ return ConfigRegistryResourceWithRawResponse(self)
@cached_property
- def with_streaming_response(self) -> SiteConfigsResourceWithStreamingResponse:
+ def with_streaming_response(self) -> ConfigRegistryResourceWithStreamingResponse:
"""
An alternative to `.with_raw_response` that doesn't eagerly read the response body.
For more information, see https://www.github.com/kernel/kernel-python-sdk#with_streaming_response
"""
- return SiteConfigsResourceWithStreamingResponse(self)
-
- def retrieve(
- self,
- id: str,
- *,
- # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
- # The extra values given here take precedence over values defined on the client or passed to this method.
- extra_headers: Headers | None = None,
- extra_query: Query | None = None,
- extra_body: Body | None = None,
- timeout: float | httpx.Timeout | None | NotGiven = not_given,
- ) -> SiteConfigResponse:
- """
- Returns a project-scoped historical analysis and the recommendation outcome
- concluded by that run. Later knowledge does not change this response.
-
- Args:
- extra_headers: Send extra headers
-
- extra_query: Add additional query parameters to the request
-
- extra_body: Add additional JSON properties to the request
-
- timeout: Override the client-level default timeout for this request, in seconds
- """
- if not id:
- raise ValueError(f"Expected a non-empty value for `id` but received {id!r}")
- return self._get(
- path_template("/site-configs/{id}", id=id),
- options=make_request_options(
- extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
- ),
- cast_to=SiteConfigResponse,
- )
+ return ConfigRegistryResourceWithStreamingResponse(self)
def list(
self,
*,
limit: int | Omit = omit,
offset: int | Omit = omit,
- # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
- # The extra values given here take precedence over values defined on the client or passed to this method.
- extra_headers: Headers | None = None,
- extra_query: Query | None = None,
- extra_body: Body | None = None,
- timeout: float | httpx.Timeout | None | NotGiven = not_given,
- ) -> SyncOffsetPagination[AnalysisSummary]:
- """
- Lists analyses for the selected project, newest first.
-
- Args:
- extra_headers: Send extra headers
-
- extra_query: Add additional query parameters to the request
-
- extra_body: Add additional JSON properties to the request
-
- timeout: Override the client-level default timeout for this request, in seconds
- """
- return self._get_api_list(
- "/site-configs",
- page=SyncOffsetPagination[AnalysisSummary],
- options=make_request_options(
- extra_headers=extra_headers,
- extra_query=extra_query,
- extra_body=extra_body,
- timeout=timeout,
- query=maybe_transform(
- {
- "limit": limit,
- "offset": offset,
- },
- site_config_list_params.SiteConfigListParams,
- ),
- ),
- model=AnalysisSummary,
- )
-
- def list_recommendations(
- self,
- *,
- limit: int | Omit = omit,
- offset: int | Omit = omit,
- sort_by: Literal["target", "recommended_config", "last_requested_at", "success_rate"] | Omit = omit,
+ search: str | Omit = omit,
+ sort_by: Literal["target", "analysis_status", "recommended_config", "last_requested_at", "success_rate"]
+ | Omit = omit,
sort_order: Literal["asc", "desc"] | Omit = omit,
# Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
# The extra values given here take precedence over values defined on the client or passed to this method.
@@ -150,6 +82,9 @@ def list_recommendations(
current domain-level recommendations.
Args:
+ search: Case-insensitive domain search. Full URLs are reduced to their registrable
+ domain.
+
extra_headers: Send extra headers
extra_query: Add additional query parameters to the request
@@ -159,7 +94,7 @@ def list_recommendations(
timeout: Override the client-level default timeout for this request, in seconds
"""
return self._get_api_list(
- "/site-configs/recommendations",
+ "/config-registry",
page=SyncOffsetPagination[RecommendationSummary],
options=make_request_options(
extra_headers=extra_headers,
@@ -170,10 +105,11 @@ def list_recommendations(
{
"limit": limit,
"offset": offset,
+ "search": search,
"sort_by": sort_by,
"sort_order": sort_order,
},
- site_config_list_recommendations_params.SiteConfigListRecommendationsParams,
+ config_registry_list_params.ConfigRegistryListParams,
),
),
model=RecommendationSummary,
@@ -193,7 +129,7 @@ def lookup(
) -> LookupResponse:
"""
Returns current global knowledge without resolving DNS, creating an analysis, or
- updating Site Config data.
+ updating config registry data.
Args:
url: Public HTTP(S) URL to look up.
@@ -210,13 +146,13 @@ def lookup(
timeout: Override the client-level default timeout for this request, in seconds
"""
return self._post(
- "/site-configs/lookup",
+ "/config-registry/lookup",
body=maybe_transform(
{
"url": url,
"allowed_proxy_countries": allowed_proxy_countries,
},
- site_config_lookup_params.SiteConfigLookupParams,
+ config_registry_lookup_params.ConfigRegistryLookupParams,
),
options=make_request_options(
extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
@@ -235,11 +171,11 @@ def resolve(
extra_query: Query | None = None,
extra_body: Body | None = None,
timeout: float | httpx.Timeout | None | NotGiven = not_given,
- ) -> SiteConfigResponse:
+ ) -> ConfigRegistryResponse:
"""
Explicitly starts or retries a project-scoped background analysis while
- preserving current global knowledge when available. Use `/site-configs/lookup`
- for side-effect-free reads.
+ preserving current global knowledge when available. Use
+ `/config-registry/lookup` for side-effect-free reads.
Args:
url: Public HTTP(S) URL to refresh.
@@ -257,126 +193,56 @@ def resolve(
timeout: Override the client-level default timeout for this request, in seconds
"""
return self._post(
- "/site-configs/resolve",
+ "/config-registry/resolve",
body=maybe_transform(
{
"url": url,
"allowed_proxy_countries": allowed_proxy_countries,
},
- site_config_resolve_params.SiteConfigResolveParams,
+ config_registry_resolve_params.ConfigRegistryResolveParams,
),
options=make_request_options(
extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
),
- cast_to=SiteConfigResponse,
+ cast_to=ConfigRegistryResponse,
)
-class AsyncSiteConfigsResource(AsyncAPIResource):
+class AsyncConfigRegistryResource(AsyncAPIResource):
"""Resolve browser and proxy recommendations for bot-protected sites."""
@cached_property
- def with_raw_response(self) -> AsyncSiteConfigsResourceWithRawResponse:
+ def analyses(self) -> AsyncAnalysesResource:
+ """Resolve browser and proxy recommendations for bot-protected sites."""
+ return AsyncAnalysesResource(self._client)
+
+ @cached_property
+ def with_raw_response(self) -> AsyncConfigRegistryResourceWithRawResponse:
"""
This property can be used as a prefix for any HTTP method call to return
the raw response object instead of the parsed content.
For more information, see https://www.github.com/kernel/kernel-python-sdk#accessing-raw-response-data-eg-headers
"""
- return AsyncSiteConfigsResourceWithRawResponse(self)
+ return AsyncConfigRegistryResourceWithRawResponse(self)
@cached_property
- def with_streaming_response(self) -> AsyncSiteConfigsResourceWithStreamingResponse:
+ def with_streaming_response(self) -> AsyncConfigRegistryResourceWithStreamingResponse:
"""
An alternative to `.with_raw_response` that doesn't eagerly read the response body.
For more information, see https://www.github.com/kernel/kernel-python-sdk#with_streaming_response
"""
- return AsyncSiteConfigsResourceWithStreamingResponse(self)
-
- async def retrieve(
- self,
- id: str,
- *,
- # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
- # The extra values given here take precedence over values defined on the client or passed to this method.
- extra_headers: Headers | None = None,
- extra_query: Query | None = None,
- extra_body: Body | None = None,
- timeout: float | httpx.Timeout | None | NotGiven = not_given,
- ) -> SiteConfigResponse:
- """
- Returns a project-scoped historical analysis and the recommendation outcome
- concluded by that run. Later knowledge does not change this response.
-
- Args:
- extra_headers: Send extra headers
-
- extra_query: Add additional query parameters to the request
-
- extra_body: Add additional JSON properties to the request
-
- timeout: Override the client-level default timeout for this request, in seconds
- """
- if not id:
- raise ValueError(f"Expected a non-empty value for `id` but received {id!r}")
- return await self._get(
- path_template("/site-configs/{id}", id=id),
- options=make_request_options(
- extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
- ),
- cast_to=SiteConfigResponse,
- )
+ return AsyncConfigRegistryResourceWithStreamingResponse(self)
def list(
self,
*,
limit: int | Omit = omit,
offset: int | Omit = omit,
- # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
- # The extra values given here take precedence over values defined on the client or passed to this method.
- extra_headers: Headers | None = None,
- extra_query: Query | None = None,
- extra_body: Body | None = None,
- timeout: float | httpx.Timeout | None | NotGiven = not_given,
- ) -> AsyncPaginator[AnalysisSummary, AsyncOffsetPagination[AnalysisSummary]]:
- """
- Lists analyses for the selected project, newest first.
-
- Args:
- extra_headers: Send extra headers
-
- extra_query: Add additional query parameters to the request
-
- extra_body: Add additional JSON properties to the request
-
- timeout: Override the client-level default timeout for this request, in seconds
- """
- return self._get_api_list(
- "/site-configs",
- page=AsyncOffsetPagination[AnalysisSummary],
- options=make_request_options(
- extra_headers=extra_headers,
- extra_query=extra_query,
- extra_body=extra_body,
- timeout=timeout,
- query=maybe_transform(
- {
- "limit": limit,
- "offset": offset,
- },
- site_config_list_params.SiteConfigListParams,
- ),
- ),
- model=AnalysisSummary,
- )
-
- def list_recommendations(
- self,
- *,
- limit: int | Omit = omit,
- offset: int | Omit = omit,
- sort_by: Literal["target", "recommended_config", "last_requested_at", "success_rate"] | Omit = omit,
+ search: str | Omit = omit,
+ sort_by: Literal["target", "analysis_status", "recommended_config", "last_requested_at", "success_rate"]
+ | Omit = omit,
sort_order: Literal["asc", "desc"] | Omit = omit,
# Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
# The extra values given here take precedence over values defined on the client or passed to this method.
@@ -390,6 +256,9 @@ def list_recommendations(
current domain-level recommendations.
Args:
+ search: Case-insensitive domain search. Full URLs are reduced to their registrable
+ domain.
+
extra_headers: Send extra headers
extra_query: Add additional query parameters to the request
@@ -399,7 +268,7 @@ def list_recommendations(
timeout: Override the client-level default timeout for this request, in seconds
"""
return self._get_api_list(
- "/site-configs/recommendations",
+ "/config-registry",
page=AsyncOffsetPagination[RecommendationSummary],
options=make_request_options(
extra_headers=extra_headers,
@@ -410,10 +279,11 @@ def list_recommendations(
{
"limit": limit,
"offset": offset,
+ "search": search,
"sort_by": sort_by,
"sort_order": sort_order,
},
- site_config_list_recommendations_params.SiteConfigListRecommendationsParams,
+ config_registry_list_params.ConfigRegistryListParams,
),
),
model=RecommendationSummary,
@@ -433,7 +303,7 @@ async def lookup(
) -> LookupResponse:
"""
Returns current global knowledge without resolving DNS, creating an analysis, or
- updating Site Config data.
+ updating config registry data.
Args:
url: Public HTTP(S) URL to look up.
@@ -450,13 +320,13 @@ async def lookup(
timeout: Override the client-level default timeout for this request, in seconds
"""
return await self._post(
- "/site-configs/lookup",
+ "/config-registry/lookup",
body=await async_maybe_transform(
{
"url": url,
"allowed_proxy_countries": allowed_proxy_countries,
},
- site_config_lookup_params.SiteConfigLookupParams,
+ config_registry_lookup_params.ConfigRegistryLookupParams,
),
options=make_request_options(
extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
@@ -475,11 +345,11 @@ async def resolve(
extra_query: Query | None = None,
extra_body: Body | None = None,
timeout: float | httpx.Timeout | None | NotGiven = not_given,
- ) -> SiteConfigResponse:
+ ) -> ConfigRegistryResponse:
"""
Explicitly starts or retries a project-scoped background analysis while
- preserving current global knowledge when available. Use `/site-configs/lookup`
- for side-effect-free reads.
+ preserving current global knowledge when available. Use
+ `/config-registry/lookup` for side-effect-free reads.
Args:
url: Public HTTP(S) URL to refresh.
@@ -497,100 +367,96 @@ async def resolve(
timeout: Override the client-level default timeout for this request, in seconds
"""
return await self._post(
- "/site-configs/resolve",
+ "/config-registry/resolve",
body=await async_maybe_transform(
{
"url": url,
"allowed_proxy_countries": allowed_proxy_countries,
},
- site_config_resolve_params.SiteConfigResolveParams,
+ config_registry_resolve_params.ConfigRegistryResolveParams,
),
options=make_request_options(
extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
),
- cast_to=SiteConfigResponse,
+ cast_to=ConfigRegistryResponse,
)
-class SiteConfigsResourceWithRawResponse:
- def __init__(self, site_configs: SiteConfigsResource) -> None:
- self._site_configs = site_configs
+class ConfigRegistryResourceWithRawResponse:
+ def __init__(self, config_registry: ConfigRegistryResource) -> None:
+ self._config_registry = config_registry
- self.retrieve = to_raw_response_wrapper(
- site_configs.retrieve,
- )
self.list = to_raw_response_wrapper(
- site_configs.list,
- )
- self.list_recommendations = to_raw_response_wrapper(
- site_configs.list_recommendations,
+ config_registry.list,
)
self.lookup = to_raw_response_wrapper(
- site_configs.lookup,
+ config_registry.lookup,
)
self.resolve = to_raw_response_wrapper(
- site_configs.resolve,
+ config_registry.resolve,
)
+ @cached_property
+ def analyses(self) -> AnalysesResourceWithRawResponse:
+ """Resolve browser and proxy recommendations for bot-protected sites."""
+ return AnalysesResourceWithRawResponse(self._config_registry.analyses)
-class AsyncSiteConfigsResourceWithRawResponse:
- def __init__(self, site_configs: AsyncSiteConfigsResource) -> None:
- self._site_configs = site_configs
- self.retrieve = async_to_raw_response_wrapper(
- site_configs.retrieve,
- )
+class AsyncConfigRegistryResourceWithRawResponse:
+ def __init__(self, config_registry: AsyncConfigRegistryResource) -> None:
+ self._config_registry = config_registry
+
self.list = async_to_raw_response_wrapper(
- site_configs.list,
- )
- self.list_recommendations = async_to_raw_response_wrapper(
- site_configs.list_recommendations,
+ config_registry.list,
)
self.lookup = async_to_raw_response_wrapper(
- site_configs.lookup,
+ config_registry.lookup,
)
self.resolve = async_to_raw_response_wrapper(
- site_configs.resolve,
+ config_registry.resolve,
)
+ @cached_property
+ def analyses(self) -> AsyncAnalysesResourceWithRawResponse:
+ """Resolve browser and proxy recommendations for bot-protected sites."""
+ return AsyncAnalysesResourceWithRawResponse(self._config_registry.analyses)
-class SiteConfigsResourceWithStreamingResponse:
- def __init__(self, site_configs: SiteConfigsResource) -> None:
- self._site_configs = site_configs
- self.retrieve = to_streamed_response_wrapper(
- site_configs.retrieve,
- )
+class ConfigRegistryResourceWithStreamingResponse:
+ def __init__(self, config_registry: ConfigRegistryResource) -> None:
+ self._config_registry = config_registry
+
self.list = to_streamed_response_wrapper(
- site_configs.list,
- )
- self.list_recommendations = to_streamed_response_wrapper(
- site_configs.list_recommendations,
+ config_registry.list,
)
self.lookup = to_streamed_response_wrapper(
- site_configs.lookup,
+ config_registry.lookup,
)
self.resolve = to_streamed_response_wrapper(
- site_configs.resolve,
+ config_registry.resolve,
)
+ @cached_property
+ def analyses(self) -> AnalysesResourceWithStreamingResponse:
+ """Resolve browser and proxy recommendations for bot-protected sites."""
+ return AnalysesResourceWithStreamingResponse(self._config_registry.analyses)
-class AsyncSiteConfigsResourceWithStreamingResponse:
- def __init__(self, site_configs: AsyncSiteConfigsResource) -> None:
- self._site_configs = site_configs
- self.retrieve = async_to_streamed_response_wrapper(
- site_configs.retrieve,
- )
+class AsyncConfigRegistryResourceWithStreamingResponse:
+ def __init__(self, config_registry: AsyncConfigRegistryResource) -> None:
+ self._config_registry = config_registry
+
self.list = async_to_streamed_response_wrapper(
- site_configs.list,
- )
- self.list_recommendations = async_to_streamed_response_wrapper(
- site_configs.list_recommendations,
+ config_registry.list,
)
self.lookup = async_to_streamed_response_wrapper(
- site_configs.lookup,
+ config_registry.lookup,
)
self.resolve = async_to_streamed_response_wrapper(
- site_configs.resolve,
+ config_registry.resolve,
)
+
+ @cached_property
+ def analyses(self) -> AsyncAnalysesResourceWithStreamingResponse:
+ """Resolve browser and proxy recommendations for bot-protected sites."""
+ return AsyncAnalysesResourceWithStreamingResponse(self._config_registry.analyses)
diff --git a/src/kernel/types/__init__.py b/src/kernel/types/__init__.py
index 9e7af34c..fffffd45 100644
--- a/src/kernel/types/__init__.py
+++ b/src/kernel/types/__init__.py
@@ -53,7 +53,6 @@
from .proxy_update_params import ProxyUpdateParams as ProxyUpdateParams
from .browser_proxy_config import BrowserProxyConfig as BrowserProxyConfig
from .proxy_check_response import ProxyCheckResponse as ProxyCheckResponse
-from .site_config_response import SiteConfigResponse as SiteConfigResponse
from .api_key_create_params import APIKeyCreateParams as APIKeyCreateParams
from .api_key_rotate_params import APIKeyRotateParams as APIKeyRotateParams
from .api_key_update_params import APIKeyUpdateParams as APIKeyUpdateParams
@@ -87,8 +86,8 @@
from .extension_upload_params import ExtensionUploadParams as ExtensionUploadParams
from .profile_download_params import ProfileDownloadParams as ProfileDownloadParams
from .proxy_retrieve_response import ProxyRetrieveResponse as ProxyRetrieveResponse
-from .site_config_list_params import SiteConfigListParams as SiteConfigListParams
from .browser_pool_list_params import BrowserPoolListParams as BrowserPoolListParams
+from .config_registry_response import ConfigRegistryResponse as ConfigRegistryResponse
from .credential_create_params import CredentialCreateParams as CredentialCreateParams
from .credential_provider_item import CredentialProviderItem as CredentialProviderItem
from .credential_update_params import CredentialUpdateParams as CredentialUpdateParams
@@ -101,7 +100,6 @@
from .invocation_update_params import InvocationUpdateParams as InvocationUpdateParams
from .browser_retrieve_response import BrowserRetrieveResponse as BrowserRetrieveResponse
from .extension_upload_response import ExtensionUploadResponse as ExtensionUploadResponse
-from .site_config_lookup_params import SiteConfigLookupParams as SiteConfigLookupParams
from .browser_pool_create_params import BrowserPoolCreateParams as BrowserPoolCreateParams
from .browser_pool_delete_params import BrowserPoolDeleteParams as BrowserPoolDeleteParams
from .browser_pool_update_params import BrowserPoolUpdateParams as BrowserPoolUpdateParams
@@ -111,16 +109,18 @@
from .invocation_create_response import InvocationCreateResponse as InvocationCreateResponse
from .invocation_follow_response import InvocationFollowResponse as InvocationFollowResponse
from .invocation_update_response import InvocationUpdateResponse as InvocationUpdateResponse
-from .site_config_resolve_params import SiteConfigResolveParams as SiteConfigResolveParams
from .browser_pool_acquire_params import BrowserPoolAcquireParams as BrowserPoolAcquireParams
from .browser_pool_release_params import BrowserPoolReleaseParams as BrowserPoolReleaseParams
+from .config_registry_list_params import ConfigRegistryListParams as ConfigRegistryListParams
from .browser_network_config_param import BrowserNetworkConfigParam as BrowserNetworkConfigParam
from .deployment_retrieve_response import DeploymentRetrieveResponse as DeploymentRetrieveResponse
from .invocation_retrieve_response import InvocationRetrieveResponse as InvocationRetrieveResponse
from .audit_log_export_chunk_params import AuditLogExportChunkParams as AuditLogExportChunkParams
from .browser_pool_acquire_response import BrowserPoolAcquireResponse as BrowserPoolAcquireResponse
+from .config_registry_lookup_params import ConfigRegistryLookupParams as ConfigRegistryLookupParams
from .credential_totp_code_response import CredentialTotpCodeResponse as CredentialTotpCodeResponse
from .browser_load_extensions_params import BrowserLoadExtensionsParams as BrowserLoadExtensionsParams
+from .config_registry_resolve_params import ConfigRegistryResolveParams as ConfigRegistryResolveParams
from .credential_provider_list_params import CredentialProviderListParams as CredentialProviderListParams
from .credential_provider_test_result import CredentialProviderTestResult as CredentialProviderTestResult
from .credential_provider_create_params import CredentialProviderCreateParams as CredentialProviderCreateParams
@@ -129,9 +129,6 @@
from .credential_provider_list_items_response import (
CredentialProviderListItemsResponse as CredentialProviderListItemsResponse,
)
-from .site_config_list_recommendations_params import (
- SiteConfigListRecommendationsParams as SiteConfigListRecommendationsParams,
-)
from .extension_download_from_chrome_store_params import (
ExtensionDownloadFromChromeStoreParams as ExtensionDownloadFromChromeStoreParams,
)
diff --git a/src/kernel/types/analysis.py b/src/kernel/types/analysis.py
index c47b956c..9bce3019 100644
--- a/src/kernel/types/analysis.py
+++ b/src/kernel/types/analysis.py
@@ -27,4 +27,4 @@ class Analysis(BaseModel):
"""Time the analysis reached a terminal status. Null while it is running."""
status: Literal["running", "completed", "failed", "canceled"]
- """Lifecycle status of the background analysis."""
+ """Lifecycle status of a background analysis."""
diff --git a/src/kernel/types/config_registry/__init__.py b/src/kernel/types/config_registry/__init__.py
new file mode 100644
index 00000000..2950bf63
--- /dev/null
+++ b/src/kernel/types/config_registry/__init__.py
@@ -0,0 +1,5 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from __future__ import annotations
+
+from .analysis_list_params import AnalysisListParams as AnalysisListParams
diff --git a/src/kernel/types/site_config_list_params.py b/src/kernel/types/config_registry/analysis_list_params.py
similarity index 54%
rename from src/kernel/types/site_config_list_params.py
rename to src/kernel/types/config_registry/analysis_list_params.py
index ee95bad4..b94f5dca 100644
--- a/src/kernel/types/site_config_list_params.py
+++ b/src/kernel/types/config_registry/analysis_list_params.py
@@ -4,10 +4,13 @@
from typing_extensions import TypedDict
-__all__ = ["SiteConfigListParams"]
+__all__ = ["AnalysisListParams"]
-class SiteConfigListParams(TypedDict, total=False):
+class AnalysisListParams(TypedDict, total=False):
limit: int
offset: int
+
+ search: str
+ """Case-insensitive substring search over requested URLs."""
diff --git a/src/kernel/types/config_registry_list_params.py b/src/kernel/types/config_registry_list_params.py
new file mode 100644
index 00000000..5259e4b7
--- /dev/null
+++ b/src/kernel/types/config_registry_list_params.py
@@ -0,0 +1,23 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from __future__ import annotations
+
+from typing_extensions import Literal, TypedDict
+
+__all__ = ["ConfigRegistryListParams"]
+
+
+class ConfigRegistryListParams(TypedDict, total=False):
+ limit: int
+
+ offset: int
+
+ search: str
+ """Case-insensitive domain search.
+
+ Full URLs are reduced to their registrable domain.
+ """
+
+ sort_by: Literal["target", "analysis_status", "recommended_config", "last_requested_at", "success_rate"]
+
+ sort_order: Literal["asc", "desc"]
diff --git a/src/kernel/types/site_config_lookup_params.py b/src/kernel/types/config_registry_lookup_params.py
similarity index 82%
rename from src/kernel/types/site_config_lookup_params.py
rename to src/kernel/types/config_registry_lookup_params.py
index aba315f4..13703a5a 100644
--- a/src/kernel/types/site_config_lookup_params.py
+++ b/src/kernel/types/config_registry_lookup_params.py
@@ -6,10 +6,10 @@
from .._types import SequenceNotStr
-__all__ = ["SiteConfigLookupParams"]
+__all__ = ["ConfigRegistryLookupParams"]
-class SiteConfigLookupParams(TypedDict, total=False):
+class ConfigRegistryLookupParams(TypedDict, total=False):
url: Required[str]
"""Public HTTP(S) URL to look up."""
diff --git a/src/kernel/types/site_config_resolve_params.py b/src/kernel/types/config_registry_resolve_params.py
similarity index 84%
rename from src/kernel/types/site_config_resolve_params.py
rename to src/kernel/types/config_registry_resolve_params.py
index 37a3f44d..644c4b7a 100644
--- a/src/kernel/types/site_config_resolve_params.py
+++ b/src/kernel/types/config_registry_resolve_params.py
@@ -6,10 +6,10 @@
from .._types import SequenceNotStr
-__all__ = ["SiteConfigResolveParams"]
+__all__ = ["ConfigRegistryResolveParams"]
-class SiteConfigResolveParams(TypedDict, total=False):
+class ConfigRegistryResolveParams(TypedDict, total=False):
url: Required[str]
"""Public HTTP(S) URL to refresh."""
diff --git a/src/kernel/types/site_config_response.py b/src/kernel/types/config_registry_response.py
similarity index 87%
rename from src/kernel/types/site_config_response.py
rename to src/kernel/types/config_registry_response.py
index b6673f76..9995173f 100644
--- a/src/kernel/types/site_config_response.py
+++ b/src/kernel/types/config_registry_response.py
@@ -7,10 +7,10 @@
from .analysis import Analysis
from .recommendation_result import RecommendationResult
-__all__ = ["SiteConfigResponse"]
+__all__ = ["ConfigRegistryResponse"]
-class SiteConfigResponse(BaseModel):
+class ConfigRegistryResponse(BaseModel):
analysis: Optional[Analysis] = None
"""Pollable analysis after workflow submission is acknowledged.
diff --git a/src/kernel/types/no_recommendation.py b/src/kernel/types/no_recommendation.py
index d1bab62c..05faec86 100644
--- a/src/kernel/types/no_recommendation.py
+++ b/src/kernel/types/no_recommendation.py
@@ -10,8 +10,7 @@
class NoRecommendation(BaseModel):
code: Literal["proxy_restricted", "no_working_configuration", "inconclusive"]
"""
- Machine-readable reason Kernel cannot currently provide a Site Config
- recommendation.
+ Machine-readable reason Kernel cannot currently provide a config recommendation.
"""
message: str
diff --git a/src/kernel/types/proxy.py b/src/kernel/types/proxy.py
index ea31827d..d5238ad7 100644
--- a/src/kernel/types/proxy.py
+++ b/src/kernel/types/proxy.py
@@ -8,39 +8,39 @@
__all__ = [
"Proxy",
- "SiteConfigDirectProxy",
- "SiteConfigManagedProxy",
- "SiteConfigManagedProxyCreate",
- "SiteConfigManagedProxyCreateConfig",
- "SiteConfigManagedProxyCreateConfigDatacenterProxyConfig",
- "SiteConfigManagedProxyCreateConfigIspProxyConfig",
- "SiteConfigManagedProxyCreateConfigResidentialProxyConfig",
- "SiteConfigManagedProxyCreateConfigMobileProxyConfig",
- "SiteConfigManagedProxyCreateConfigCreateCustomProxyConfig",
+ "ConfigRegistryDirectProxy",
+ "ConfigRegistryManagedProxy",
+ "ConfigRegistryManagedProxyCreate",
+ "ConfigRegistryManagedProxyCreateConfig",
+ "ConfigRegistryManagedProxyCreateConfigDatacenterProxyConfig",
+ "ConfigRegistryManagedProxyCreateConfigIspProxyConfig",
+ "ConfigRegistryManagedProxyCreateConfigResidentialProxyConfig",
+ "ConfigRegistryManagedProxyCreateConfigMobileProxyConfig",
+ "ConfigRegistryManagedProxyCreateConfigCreateCustomProxyConfig",
]
-class SiteConfigDirectProxy(BaseModel):
+class ConfigRegistryDirectProxy(BaseModel):
"""Direct egress recipe. Pass `{ "mode": "direct" }` as the browser's `proxy`."""
mode: Literal["direct"]
-class SiteConfigManagedProxyCreateConfigDatacenterProxyConfig(BaseModel):
+class ConfigRegistryManagedProxyCreateConfigDatacenterProxyConfig(BaseModel):
"""Configuration for a datacenter proxy."""
country: Optional[str] = None
"""ISO 3166 country code. Defaults to US if not provided."""
-class SiteConfigManagedProxyCreateConfigIspProxyConfig(BaseModel):
+class ConfigRegistryManagedProxyCreateConfigIspProxyConfig(BaseModel):
"""Configuration for an ISP proxy."""
country: Optional[str] = None
"""ISO 3166 country code. Defaults to US if not provided."""
-class SiteConfigManagedProxyCreateConfigResidentialProxyConfig(BaseModel):
+class ConfigRegistryManagedProxyCreateConfigResidentialProxyConfig(BaseModel):
"""Configuration for residential proxies."""
asn: Optional[str] = None
@@ -65,7 +65,7 @@ class SiteConfigManagedProxyCreateConfigResidentialProxyConfig(BaseModel):
"""US ZIP code."""
-class SiteConfigManagedProxyCreateConfigMobileProxyConfig(BaseModel):
+class ConfigRegistryManagedProxyCreateConfigMobileProxyConfig(BaseModel):
"""Configuration for mobile proxies."""
city: Optional[str] = None
@@ -78,7 +78,7 @@ class SiteConfigManagedProxyCreateConfigMobileProxyConfig(BaseModel):
"""US-only state code. Mobile carrier routing can make observed geo vary."""
-class SiteConfigManagedProxyCreateConfigCreateCustomProxyConfig(BaseModel):
+class ConfigRegistryManagedProxyCreateConfigCreateCustomProxyConfig(BaseModel):
"""Configuration for a custom proxy (e.g., private proxy server)."""
host: str
@@ -101,16 +101,16 @@ class SiteConfigManagedProxyCreateConfigCreateCustomProxyConfig(BaseModel):
"""Username for proxy authentication."""
-SiteConfigManagedProxyCreateConfig: TypeAlias = Union[
- SiteConfigManagedProxyCreateConfigDatacenterProxyConfig,
- SiteConfigManagedProxyCreateConfigIspProxyConfig,
- SiteConfigManagedProxyCreateConfigResidentialProxyConfig,
- SiteConfigManagedProxyCreateConfigMobileProxyConfig,
- SiteConfigManagedProxyCreateConfigCreateCustomProxyConfig,
+ConfigRegistryManagedProxyCreateConfig: TypeAlias = Union[
+ ConfigRegistryManagedProxyCreateConfigDatacenterProxyConfig,
+ ConfigRegistryManagedProxyCreateConfigIspProxyConfig,
+ ConfigRegistryManagedProxyCreateConfigResidentialProxyConfig,
+ ConfigRegistryManagedProxyCreateConfigMobileProxyConfig,
+ ConfigRegistryManagedProxyCreateConfigCreateCustomProxyConfig,
]
-class SiteConfigManagedProxyCreate(BaseModel):
+class ConfigRegistryManagedProxyCreate(BaseModel):
"""Configuration for routing traffic through a proxy."""
type: Literal["datacenter", "isp", "residential", "mobile", "custom"]
@@ -123,7 +123,7 @@ class SiteConfigManagedProxyCreate(BaseModel):
bypass_hosts: Optional[List[str]] = None
"""Hostnames that should bypass the parent proxy and connect directly."""
- config: Optional[SiteConfigManagedProxyCreateConfig] = None
+ config: Optional[ConfigRegistryManagedProxyCreateConfig] = None
"""Configuration specific to the selected proxy `type`."""
name: Optional[str] = None
@@ -133,7 +133,7 @@ class SiteConfigManagedProxyCreate(BaseModel):
"""Protocol to use for the proxy connection."""
-class SiteConfigManagedProxy(BaseModel):
+class ConfigRegistryManagedProxy(BaseModel):
"""Managed proxy recipe.
`create` is a non-idempotent `POST /proxies` payload:
@@ -141,10 +141,12 @@ class SiteConfigManagedProxy(BaseModel):
`proxy.id`. Do not submit this recipe before every browser session.
"""
- create: SiteConfigManagedProxyCreate
+ create: ConfigRegistryManagedProxyCreate
"""Configuration for routing traffic through a proxy."""
mode: Literal["managed"]
-Proxy: TypeAlias = Annotated[Union[SiteConfigDirectProxy, SiteConfigManagedProxy], PropertyInfo(discriminator="mode")]
+Proxy: TypeAlias = Annotated[
+ Union[ConfigRegistryDirectProxy, ConfigRegistryManagedProxy], PropertyInfo(discriminator="mode")
+]
diff --git a/src/kernel/types/recommendation_summary.py b/src/kernel/types/recommendation_summary.py
index abc2ea72..df83ae35 100644
--- a/src/kernel/types/recommendation_summary.py
+++ b/src/kernel/types/recommendation_summary.py
@@ -2,6 +2,7 @@
from typing import Optional
from datetime import datetime
+from typing_extensions import Literal
from .._models import BaseModel
from .recommendation import Recommendation
@@ -10,6 +11,12 @@
class RecommendationSummary(BaseModel):
+ analysis_id: str
+ """ID of the most recently requested analysis for this domain."""
+
+ analysis_status: Literal["running", "completed", "failed", "canceled"]
+ """Lifecycle status of the most recently requested analysis for this domain."""
+
last_requested_at: datetime
"""Most recent time the selected project requested an analysis for this domain."""
diff --git a/src/kernel/types/site_config_list_recommendations_params.py b/src/kernel/types/site_config_list_recommendations_params.py
deleted file mode 100644
index 8ff51237..00000000
--- a/src/kernel/types/site_config_list_recommendations_params.py
+++ /dev/null
@@ -1,17 +0,0 @@
-# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
-
-from __future__ import annotations
-
-from typing_extensions import Literal, TypedDict
-
-__all__ = ["SiteConfigListRecommendationsParams"]
-
-
-class SiteConfigListRecommendationsParams(TypedDict, total=False):
- limit: int
-
- offset: int
-
- sort_by: Literal["target", "recommended_config", "last_requested_at", "success_rate"]
-
- sort_order: Literal["asc", "desc"]
diff --git a/tests/api_resources/config_registry/__init__.py b/tests/api_resources/config_registry/__init__.py
new file mode 100644
index 00000000..fd8019a9
--- /dev/null
+++ b/tests/api_resources/config_registry/__init__.py
@@ -0,0 +1 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
diff --git a/tests/api_resources/config_registry/test_analyses.py b/tests/api_resources/config_registry/test_analyses.py
new file mode 100644
index 00000000..50b608df
--- /dev/null
+++ b/tests/api_resources/config_registry/test_analyses.py
@@ -0,0 +1,185 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from __future__ import annotations
+
+import os
+from typing import Any, cast
+
+import pytest
+
+from kernel import Kernel, AsyncKernel
+from tests.utils import assert_matches_type
+from kernel.types import AnalysisSummary, ConfigRegistryResponse
+from kernel.pagination import SyncOffsetPagination, AsyncOffsetPagination
+
+base_url = os.environ.get("TEST_API_BASE_URL", "http://127.0.0.1:4010")
+
+
+class TestAnalyses:
+ parametrize = pytest.mark.parametrize("client", [False, True], indirect=True, ids=["loose", "strict"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ def test_method_retrieve(self, client: Kernel) -> None:
+ analysis = client.config_registry.analyses.retrieve(
+ "id",
+ )
+ assert_matches_type(ConfigRegistryResponse, analysis, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ def test_raw_response_retrieve(self, client: Kernel) -> None:
+ response = client.config_registry.analyses.with_raw_response.retrieve(
+ "id",
+ )
+
+ assert response.is_closed is True
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+ analysis = response.parse()
+ assert_matches_type(ConfigRegistryResponse, analysis, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ def test_streaming_response_retrieve(self, client: Kernel) -> None:
+ with client.config_registry.analyses.with_streaming_response.retrieve(
+ "id",
+ ) as response:
+ assert not response.is_closed
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+
+ analysis = response.parse()
+ assert_matches_type(ConfigRegistryResponse, analysis, path=["response"])
+
+ assert cast(Any, response.is_closed) is True
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ def test_path_params_retrieve(self, client: Kernel) -> None:
+ with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"):
+ client.config_registry.analyses.with_raw_response.retrieve(
+ "",
+ )
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ def test_method_list(self, client: Kernel) -> None:
+ analysis = client.config_registry.analyses.list()
+ assert_matches_type(SyncOffsetPagination[AnalysisSummary], analysis, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ def test_method_list_with_all_params(self, client: Kernel) -> None:
+ analysis = client.config_registry.analyses.list(
+ limit=1,
+ offset=0,
+ search="search",
+ )
+ assert_matches_type(SyncOffsetPagination[AnalysisSummary], analysis, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ def test_raw_response_list(self, client: Kernel) -> None:
+ response = client.config_registry.analyses.with_raw_response.list()
+
+ assert response.is_closed is True
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+ analysis = response.parse()
+ assert_matches_type(SyncOffsetPagination[AnalysisSummary], analysis, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ def test_streaming_response_list(self, client: Kernel) -> None:
+ with client.config_registry.analyses.with_streaming_response.list() as response:
+ assert not response.is_closed
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+
+ analysis = response.parse()
+ assert_matches_type(SyncOffsetPagination[AnalysisSummary], analysis, path=["response"])
+
+ assert cast(Any, response.is_closed) is True
+
+
+class TestAsyncAnalyses:
+ parametrize = pytest.mark.parametrize(
+ "async_client", [False, True, {"http_client": "aiohttp"}], indirect=True, ids=["loose", "strict", "aiohttp"]
+ )
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ async def test_method_retrieve(self, async_client: AsyncKernel) -> None:
+ analysis = await async_client.config_registry.analyses.retrieve(
+ "id",
+ )
+ assert_matches_type(ConfigRegistryResponse, analysis, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ async def test_raw_response_retrieve(self, async_client: AsyncKernel) -> None:
+ response = await async_client.config_registry.analyses.with_raw_response.retrieve(
+ "id",
+ )
+
+ assert response.is_closed is True
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+ analysis = await response.parse()
+ assert_matches_type(ConfigRegistryResponse, analysis, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ async def test_streaming_response_retrieve(self, async_client: AsyncKernel) -> None:
+ async with async_client.config_registry.analyses.with_streaming_response.retrieve(
+ "id",
+ ) as response:
+ assert not response.is_closed
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+
+ analysis = await response.parse()
+ assert_matches_type(ConfigRegistryResponse, analysis, 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_retrieve(self, async_client: AsyncKernel) -> None:
+ with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"):
+ await async_client.config_registry.analyses.with_raw_response.retrieve(
+ "",
+ )
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ async def test_method_list(self, async_client: AsyncKernel) -> None:
+ analysis = await async_client.config_registry.analyses.list()
+ assert_matches_type(AsyncOffsetPagination[AnalysisSummary], analysis, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ async def test_method_list_with_all_params(self, async_client: AsyncKernel) -> None:
+ analysis = await async_client.config_registry.analyses.list(
+ limit=1,
+ offset=0,
+ search="search",
+ )
+ assert_matches_type(AsyncOffsetPagination[AnalysisSummary], analysis, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ async def test_raw_response_list(self, async_client: AsyncKernel) -> None:
+ response = await async_client.config_registry.analyses.with_raw_response.list()
+
+ assert response.is_closed is True
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+ analysis = await response.parse()
+ assert_matches_type(AsyncOffsetPagination[AnalysisSummary], analysis, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ async def test_streaming_response_list(self, async_client: AsyncKernel) -> None:
+ async with async_client.config_registry.analyses.with_streaming_response.list() as response:
+ assert not response.is_closed
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+
+ analysis = await response.parse()
+ assert_matches_type(AsyncOffsetPagination[AnalysisSummary], analysis, path=["response"])
+
+ assert cast(Any, response.is_closed) is True
diff --git a/tests/api_resources/test_config_registry.py b/tests/api_resources/test_config_registry.py
new file mode 100644
index 00000000..710d3088
--- /dev/null
+++ b/tests/api_resources/test_config_registry.py
@@ -0,0 +1,281 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from __future__ import annotations
+
+import os
+from typing import Any, cast
+
+import pytest
+
+from kernel import Kernel, AsyncKernel
+from tests.utils import assert_matches_type
+from kernel.types import (
+ LookupResponse,
+ RecommendationSummary,
+ ConfigRegistryResponse,
+)
+from kernel.pagination import SyncOffsetPagination, AsyncOffsetPagination
+
+base_url = os.environ.get("TEST_API_BASE_URL", "http://127.0.0.1:4010")
+
+
+class TestConfigRegistry:
+ parametrize = pytest.mark.parametrize("client", [False, True], indirect=True, ids=["loose", "strict"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ def test_method_list(self, client: Kernel) -> None:
+ config_registry = client.config_registry.list()
+ assert_matches_type(SyncOffsetPagination[RecommendationSummary], config_registry, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ def test_method_list_with_all_params(self, client: Kernel) -> None:
+ config_registry = client.config_registry.list(
+ limit=1,
+ offset=0,
+ search="search",
+ sort_by="target",
+ sort_order="asc",
+ )
+ assert_matches_type(SyncOffsetPagination[RecommendationSummary], config_registry, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ def test_raw_response_list(self, client: Kernel) -> None:
+ response = client.config_registry.with_raw_response.list()
+
+ assert response.is_closed is True
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+ config_registry = response.parse()
+ assert_matches_type(SyncOffsetPagination[RecommendationSummary], config_registry, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ def test_streaming_response_list(self, client: Kernel) -> None:
+ with client.config_registry.with_streaming_response.list() as response:
+ assert not response.is_closed
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+
+ config_registry = response.parse()
+ assert_matches_type(SyncOffsetPagination[RecommendationSummary], config_registry, path=["response"])
+
+ assert cast(Any, response.is_closed) is True
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ def test_method_lookup(self, client: Kernel) -> None:
+ config_registry = client.config_registry.lookup(
+ url="https://example.com",
+ )
+ assert_matches_type(LookupResponse, config_registry, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ def test_method_lookup_with_all_params(self, client: Kernel) -> None:
+ config_registry = client.config_registry.lookup(
+ url="https://example.com",
+ allowed_proxy_countries=["US"],
+ )
+ assert_matches_type(LookupResponse, config_registry, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ def test_raw_response_lookup(self, client: Kernel) -> None:
+ response = client.config_registry.with_raw_response.lookup(
+ url="https://example.com",
+ )
+
+ assert response.is_closed is True
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+ config_registry = response.parse()
+ assert_matches_type(LookupResponse, config_registry, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ def test_streaming_response_lookup(self, client: Kernel) -> None:
+ with client.config_registry.with_streaming_response.lookup(
+ url="https://example.com",
+ ) as response:
+ assert not response.is_closed
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+
+ config_registry = response.parse()
+ assert_matches_type(LookupResponse, config_registry, path=["response"])
+
+ assert cast(Any, response.is_closed) is True
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ def test_method_resolve(self, client: Kernel) -> None:
+ config_registry = client.config_registry.resolve(
+ url="https://example.com",
+ )
+ assert_matches_type(ConfigRegistryResponse, config_registry, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ def test_method_resolve_with_all_params(self, client: Kernel) -> None:
+ config_registry = client.config_registry.resolve(
+ url="https://example.com",
+ allowed_proxy_countries=["US"],
+ )
+ assert_matches_type(ConfigRegistryResponse, config_registry, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ def test_raw_response_resolve(self, client: Kernel) -> None:
+ response = client.config_registry.with_raw_response.resolve(
+ url="https://example.com",
+ )
+
+ assert response.is_closed is True
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+ config_registry = response.parse()
+ assert_matches_type(ConfigRegistryResponse, config_registry, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ def test_streaming_response_resolve(self, client: Kernel) -> None:
+ with client.config_registry.with_streaming_response.resolve(
+ url="https://example.com",
+ ) as response:
+ assert not response.is_closed
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+
+ config_registry = response.parse()
+ assert_matches_type(ConfigRegistryResponse, config_registry, path=["response"])
+
+ assert cast(Any, response.is_closed) is True
+
+
+class TestAsyncConfigRegistry:
+ parametrize = pytest.mark.parametrize(
+ "async_client", [False, True, {"http_client": "aiohttp"}], indirect=True, ids=["loose", "strict", "aiohttp"]
+ )
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ async def test_method_list(self, async_client: AsyncKernel) -> None:
+ config_registry = await async_client.config_registry.list()
+ assert_matches_type(AsyncOffsetPagination[RecommendationSummary], config_registry, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ async def test_method_list_with_all_params(self, async_client: AsyncKernel) -> None:
+ config_registry = await async_client.config_registry.list(
+ limit=1,
+ offset=0,
+ search="search",
+ sort_by="target",
+ sort_order="asc",
+ )
+ assert_matches_type(AsyncOffsetPagination[RecommendationSummary], config_registry, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ async def test_raw_response_list(self, async_client: AsyncKernel) -> None:
+ response = await async_client.config_registry.with_raw_response.list()
+
+ assert response.is_closed is True
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+ config_registry = await response.parse()
+ assert_matches_type(AsyncOffsetPagination[RecommendationSummary], config_registry, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ async def test_streaming_response_list(self, async_client: AsyncKernel) -> None:
+ async with async_client.config_registry.with_streaming_response.list() as response:
+ assert not response.is_closed
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+
+ config_registry = await response.parse()
+ assert_matches_type(AsyncOffsetPagination[RecommendationSummary], config_registry, path=["response"])
+
+ assert cast(Any, response.is_closed) is True
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ async def test_method_lookup(self, async_client: AsyncKernel) -> None:
+ config_registry = await async_client.config_registry.lookup(
+ url="https://example.com",
+ )
+ assert_matches_type(LookupResponse, config_registry, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ async def test_method_lookup_with_all_params(self, async_client: AsyncKernel) -> None:
+ config_registry = await async_client.config_registry.lookup(
+ url="https://example.com",
+ allowed_proxy_countries=["US"],
+ )
+ assert_matches_type(LookupResponse, config_registry, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ async def test_raw_response_lookup(self, async_client: AsyncKernel) -> None:
+ response = await async_client.config_registry.with_raw_response.lookup(
+ url="https://example.com",
+ )
+
+ assert response.is_closed is True
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+ config_registry = await response.parse()
+ assert_matches_type(LookupResponse, config_registry, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ async def test_streaming_response_lookup(self, async_client: AsyncKernel) -> None:
+ async with async_client.config_registry.with_streaming_response.lookup(
+ url="https://example.com",
+ ) as response:
+ assert not response.is_closed
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+
+ config_registry = await response.parse()
+ assert_matches_type(LookupResponse, config_registry, path=["response"])
+
+ assert cast(Any, response.is_closed) is True
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ async def test_method_resolve(self, async_client: AsyncKernel) -> None:
+ config_registry = await async_client.config_registry.resolve(
+ url="https://example.com",
+ )
+ assert_matches_type(ConfigRegistryResponse, config_registry, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ async def test_method_resolve_with_all_params(self, async_client: AsyncKernel) -> None:
+ config_registry = await async_client.config_registry.resolve(
+ url="https://example.com",
+ allowed_proxy_countries=["US"],
+ )
+ assert_matches_type(ConfigRegistryResponse, config_registry, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ async def test_raw_response_resolve(self, async_client: AsyncKernel) -> None:
+ response = await async_client.config_registry.with_raw_response.resolve(
+ url="https://example.com",
+ )
+
+ assert response.is_closed is True
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+ config_registry = await response.parse()
+ assert_matches_type(ConfigRegistryResponse, config_registry, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ async def test_streaming_response_resolve(self, async_client: AsyncKernel) -> None:
+ async with async_client.config_registry.with_streaming_response.resolve(
+ url="https://example.com",
+ ) as response:
+ assert not response.is_closed
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+
+ config_registry = await response.parse()
+ assert_matches_type(ConfigRegistryResponse, config_registry, path=["response"])
+
+ assert cast(Any, response.is_closed) is True
diff --git a/tests/api_resources/test_site_configs.py b/tests/api_resources/test_site_configs.py
deleted file mode 100644
index 2099899c..00000000
--- a/tests/api_resources/test_site_configs.py
+++ /dev/null
@@ -1,438 +0,0 @@
-# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
-
-from __future__ import annotations
-
-import os
-from typing import Any, cast
-
-import pytest
-
-from kernel import Kernel, AsyncKernel
-from tests.utils import assert_matches_type
-from kernel.types import (
- LookupResponse,
- AnalysisSummary,
- SiteConfigResponse,
- RecommendationSummary,
-)
-from kernel.pagination import SyncOffsetPagination, AsyncOffsetPagination
-
-base_url = os.environ.get("TEST_API_BASE_URL", "http://127.0.0.1:4010")
-
-
-class TestSiteConfigs:
- parametrize = pytest.mark.parametrize("client", [False, True], indirect=True, ids=["loose", "strict"])
-
- @pytest.mark.skip(reason="Mock server tests are disabled")
- @parametrize
- def test_method_retrieve(self, client: Kernel) -> None:
- site_config = client.site_configs.retrieve(
- "id",
- )
- assert_matches_type(SiteConfigResponse, site_config, path=["response"])
-
- @pytest.mark.skip(reason="Mock server tests are disabled")
- @parametrize
- def test_raw_response_retrieve(self, client: Kernel) -> None:
- response = client.site_configs.with_raw_response.retrieve(
- "id",
- )
-
- assert response.is_closed is True
- assert response.http_request.headers.get("X-Stainless-Lang") == "python"
- site_config = response.parse()
- assert_matches_type(SiteConfigResponse, site_config, path=["response"])
-
- @pytest.mark.skip(reason="Mock server tests are disabled")
- @parametrize
- def test_streaming_response_retrieve(self, client: Kernel) -> None:
- with client.site_configs.with_streaming_response.retrieve(
- "id",
- ) as response:
- assert not response.is_closed
- assert response.http_request.headers.get("X-Stainless-Lang") == "python"
-
- site_config = response.parse()
- assert_matches_type(SiteConfigResponse, site_config, path=["response"])
-
- assert cast(Any, response.is_closed) is True
-
- @pytest.mark.skip(reason="Mock server tests are disabled")
- @parametrize
- def test_path_params_retrieve(self, client: Kernel) -> None:
- with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"):
- client.site_configs.with_raw_response.retrieve(
- "",
- )
-
- @pytest.mark.skip(reason="Mock server tests are disabled")
- @parametrize
- def test_method_list(self, client: Kernel) -> None:
- site_config = client.site_configs.list()
- assert_matches_type(SyncOffsetPagination[AnalysisSummary], site_config, path=["response"])
-
- @pytest.mark.skip(reason="Mock server tests are disabled")
- @parametrize
- def test_method_list_with_all_params(self, client: Kernel) -> None:
- site_config = client.site_configs.list(
- limit=1,
- offset=0,
- )
- assert_matches_type(SyncOffsetPagination[AnalysisSummary], site_config, path=["response"])
-
- @pytest.mark.skip(reason="Mock server tests are disabled")
- @parametrize
- def test_raw_response_list(self, client: Kernel) -> None:
- response = client.site_configs.with_raw_response.list()
-
- assert response.is_closed is True
- assert response.http_request.headers.get("X-Stainless-Lang") == "python"
- site_config = response.parse()
- assert_matches_type(SyncOffsetPagination[AnalysisSummary], site_config, path=["response"])
-
- @pytest.mark.skip(reason="Mock server tests are disabled")
- @parametrize
- def test_streaming_response_list(self, client: Kernel) -> None:
- with client.site_configs.with_streaming_response.list() as response:
- assert not response.is_closed
- assert response.http_request.headers.get("X-Stainless-Lang") == "python"
-
- site_config = response.parse()
- assert_matches_type(SyncOffsetPagination[AnalysisSummary], site_config, path=["response"])
-
- assert cast(Any, response.is_closed) is True
-
- @pytest.mark.skip(reason="Mock server tests are disabled")
- @parametrize
- def test_method_list_recommendations(self, client: Kernel) -> None:
- site_config = client.site_configs.list_recommendations()
- assert_matches_type(SyncOffsetPagination[RecommendationSummary], site_config, path=["response"])
-
- @pytest.mark.skip(reason="Mock server tests are disabled")
- @parametrize
- def test_method_list_recommendations_with_all_params(self, client: Kernel) -> None:
- site_config = client.site_configs.list_recommendations(
- limit=1,
- offset=0,
- sort_by="target",
- sort_order="asc",
- )
- assert_matches_type(SyncOffsetPagination[RecommendationSummary], site_config, path=["response"])
-
- @pytest.mark.skip(reason="Mock server tests are disabled")
- @parametrize
- def test_raw_response_list_recommendations(self, client: Kernel) -> None:
- response = client.site_configs.with_raw_response.list_recommendations()
-
- assert response.is_closed is True
- assert response.http_request.headers.get("X-Stainless-Lang") == "python"
- site_config = response.parse()
- assert_matches_type(SyncOffsetPagination[RecommendationSummary], site_config, path=["response"])
-
- @pytest.mark.skip(reason="Mock server tests are disabled")
- @parametrize
- def test_streaming_response_list_recommendations(self, client: Kernel) -> None:
- with client.site_configs.with_streaming_response.list_recommendations() as response:
- assert not response.is_closed
- assert response.http_request.headers.get("X-Stainless-Lang") == "python"
-
- site_config = response.parse()
- assert_matches_type(SyncOffsetPagination[RecommendationSummary], site_config, path=["response"])
-
- assert cast(Any, response.is_closed) is True
-
- @pytest.mark.skip(reason="Mock server tests are disabled")
- @parametrize
- def test_method_lookup(self, client: Kernel) -> None:
- site_config = client.site_configs.lookup(
- url="https://example.com",
- )
- assert_matches_type(LookupResponse, site_config, path=["response"])
-
- @pytest.mark.skip(reason="Mock server tests are disabled")
- @parametrize
- def test_method_lookup_with_all_params(self, client: Kernel) -> None:
- site_config = client.site_configs.lookup(
- url="https://example.com",
- allowed_proxy_countries=["US"],
- )
- assert_matches_type(LookupResponse, site_config, path=["response"])
-
- @pytest.mark.skip(reason="Mock server tests are disabled")
- @parametrize
- def test_raw_response_lookup(self, client: Kernel) -> None:
- response = client.site_configs.with_raw_response.lookup(
- url="https://example.com",
- )
-
- assert response.is_closed is True
- assert response.http_request.headers.get("X-Stainless-Lang") == "python"
- site_config = response.parse()
- assert_matches_type(LookupResponse, site_config, path=["response"])
-
- @pytest.mark.skip(reason="Mock server tests are disabled")
- @parametrize
- def test_streaming_response_lookup(self, client: Kernel) -> None:
- with client.site_configs.with_streaming_response.lookup(
- url="https://example.com",
- ) as response:
- assert not response.is_closed
- assert response.http_request.headers.get("X-Stainless-Lang") == "python"
-
- site_config = response.parse()
- assert_matches_type(LookupResponse, site_config, path=["response"])
-
- assert cast(Any, response.is_closed) is True
-
- @pytest.mark.skip(reason="Mock server tests are disabled")
- @parametrize
- def test_method_resolve(self, client: Kernel) -> None:
- site_config = client.site_configs.resolve(
- url="https://example.com",
- )
- assert_matches_type(SiteConfigResponse, site_config, path=["response"])
-
- @pytest.mark.skip(reason="Mock server tests are disabled")
- @parametrize
- def test_method_resolve_with_all_params(self, client: Kernel) -> None:
- site_config = client.site_configs.resolve(
- url="https://example.com",
- allowed_proxy_countries=["US"],
- )
- assert_matches_type(SiteConfigResponse, site_config, path=["response"])
-
- @pytest.mark.skip(reason="Mock server tests are disabled")
- @parametrize
- def test_raw_response_resolve(self, client: Kernel) -> None:
- response = client.site_configs.with_raw_response.resolve(
- url="https://example.com",
- )
-
- assert response.is_closed is True
- assert response.http_request.headers.get("X-Stainless-Lang") == "python"
- site_config = response.parse()
- assert_matches_type(SiteConfigResponse, site_config, path=["response"])
-
- @pytest.mark.skip(reason="Mock server tests are disabled")
- @parametrize
- def test_streaming_response_resolve(self, client: Kernel) -> None:
- with client.site_configs.with_streaming_response.resolve(
- url="https://example.com",
- ) as response:
- assert not response.is_closed
- assert response.http_request.headers.get("X-Stainless-Lang") == "python"
-
- site_config = response.parse()
- assert_matches_type(SiteConfigResponse, site_config, path=["response"])
-
- assert cast(Any, response.is_closed) is True
-
-
-class TestAsyncSiteConfigs:
- parametrize = pytest.mark.parametrize(
- "async_client", [False, True, {"http_client": "aiohttp"}], indirect=True, ids=["loose", "strict", "aiohttp"]
- )
-
- @pytest.mark.skip(reason="Mock server tests are disabled")
- @parametrize
- async def test_method_retrieve(self, async_client: AsyncKernel) -> None:
- site_config = await async_client.site_configs.retrieve(
- "id",
- )
- assert_matches_type(SiteConfigResponse, site_config, path=["response"])
-
- @pytest.mark.skip(reason="Mock server tests are disabled")
- @parametrize
- async def test_raw_response_retrieve(self, async_client: AsyncKernel) -> None:
- response = await async_client.site_configs.with_raw_response.retrieve(
- "id",
- )
-
- assert response.is_closed is True
- assert response.http_request.headers.get("X-Stainless-Lang") == "python"
- site_config = await response.parse()
- assert_matches_type(SiteConfigResponse, site_config, path=["response"])
-
- @pytest.mark.skip(reason="Mock server tests are disabled")
- @parametrize
- async def test_streaming_response_retrieve(self, async_client: AsyncKernel) -> None:
- async with async_client.site_configs.with_streaming_response.retrieve(
- "id",
- ) as response:
- assert not response.is_closed
- assert response.http_request.headers.get("X-Stainless-Lang") == "python"
-
- site_config = await response.parse()
- assert_matches_type(SiteConfigResponse, site_config, 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_retrieve(self, async_client: AsyncKernel) -> None:
- with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"):
- await async_client.site_configs.with_raw_response.retrieve(
- "",
- )
-
- @pytest.mark.skip(reason="Mock server tests are disabled")
- @parametrize
- async def test_method_list(self, async_client: AsyncKernel) -> None:
- site_config = await async_client.site_configs.list()
- assert_matches_type(AsyncOffsetPagination[AnalysisSummary], site_config, path=["response"])
-
- @pytest.mark.skip(reason="Mock server tests are disabled")
- @parametrize
- async def test_method_list_with_all_params(self, async_client: AsyncKernel) -> None:
- site_config = await async_client.site_configs.list(
- limit=1,
- offset=0,
- )
- assert_matches_type(AsyncOffsetPagination[AnalysisSummary], site_config, path=["response"])
-
- @pytest.mark.skip(reason="Mock server tests are disabled")
- @parametrize
- async def test_raw_response_list(self, async_client: AsyncKernel) -> None:
- response = await async_client.site_configs.with_raw_response.list()
-
- assert response.is_closed is True
- assert response.http_request.headers.get("X-Stainless-Lang") == "python"
- site_config = await response.parse()
- assert_matches_type(AsyncOffsetPagination[AnalysisSummary], site_config, path=["response"])
-
- @pytest.mark.skip(reason="Mock server tests are disabled")
- @parametrize
- async def test_streaming_response_list(self, async_client: AsyncKernel) -> None:
- async with async_client.site_configs.with_streaming_response.list() as response:
- assert not response.is_closed
- assert response.http_request.headers.get("X-Stainless-Lang") == "python"
-
- site_config = await response.parse()
- assert_matches_type(AsyncOffsetPagination[AnalysisSummary], site_config, path=["response"])
-
- assert cast(Any, response.is_closed) is True
-
- @pytest.mark.skip(reason="Mock server tests are disabled")
- @parametrize
- async def test_method_list_recommendations(self, async_client: AsyncKernel) -> None:
- site_config = await async_client.site_configs.list_recommendations()
- assert_matches_type(AsyncOffsetPagination[RecommendationSummary], site_config, path=["response"])
-
- @pytest.mark.skip(reason="Mock server tests are disabled")
- @parametrize
- async def test_method_list_recommendations_with_all_params(self, async_client: AsyncKernel) -> None:
- site_config = await async_client.site_configs.list_recommendations(
- limit=1,
- offset=0,
- sort_by="target",
- sort_order="asc",
- )
- assert_matches_type(AsyncOffsetPagination[RecommendationSummary], site_config, path=["response"])
-
- @pytest.mark.skip(reason="Mock server tests are disabled")
- @parametrize
- async def test_raw_response_list_recommendations(self, async_client: AsyncKernel) -> None:
- response = await async_client.site_configs.with_raw_response.list_recommendations()
-
- assert response.is_closed is True
- assert response.http_request.headers.get("X-Stainless-Lang") == "python"
- site_config = await response.parse()
- assert_matches_type(AsyncOffsetPagination[RecommendationSummary], site_config, path=["response"])
-
- @pytest.mark.skip(reason="Mock server tests are disabled")
- @parametrize
- async def test_streaming_response_list_recommendations(self, async_client: AsyncKernel) -> None:
- async with async_client.site_configs.with_streaming_response.list_recommendations() as response:
- assert not response.is_closed
- assert response.http_request.headers.get("X-Stainless-Lang") == "python"
-
- site_config = await response.parse()
- assert_matches_type(AsyncOffsetPagination[RecommendationSummary], site_config, path=["response"])
-
- assert cast(Any, response.is_closed) is True
-
- @pytest.mark.skip(reason="Mock server tests are disabled")
- @parametrize
- async def test_method_lookup(self, async_client: AsyncKernel) -> None:
- site_config = await async_client.site_configs.lookup(
- url="https://example.com",
- )
- assert_matches_type(LookupResponse, site_config, path=["response"])
-
- @pytest.mark.skip(reason="Mock server tests are disabled")
- @parametrize
- async def test_method_lookup_with_all_params(self, async_client: AsyncKernel) -> None:
- site_config = await async_client.site_configs.lookup(
- url="https://example.com",
- allowed_proxy_countries=["US"],
- )
- assert_matches_type(LookupResponse, site_config, path=["response"])
-
- @pytest.mark.skip(reason="Mock server tests are disabled")
- @parametrize
- async def test_raw_response_lookup(self, async_client: AsyncKernel) -> None:
- response = await async_client.site_configs.with_raw_response.lookup(
- url="https://example.com",
- )
-
- assert response.is_closed is True
- assert response.http_request.headers.get("X-Stainless-Lang") == "python"
- site_config = await response.parse()
- assert_matches_type(LookupResponse, site_config, path=["response"])
-
- @pytest.mark.skip(reason="Mock server tests are disabled")
- @parametrize
- async def test_streaming_response_lookup(self, async_client: AsyncKernel) -> None:
- async with async_client.site_configs.with_streaming_response.lookup(
- url="https://example.com",
- ) as response:
- assert not response.is_closed
- assert response.http_request.headers.get("X-Stainless-Lang") == "python"
-
- site_config = await response.parse()
- assert_matches_type(LookupResponse, site_config, path=["response"])
-
- assert cast(Any, response.is_closed) is True
-
- @pytest.mark.skip(reason="Mock server tests are disabled")
- @parametrize
- async def test_method_resolve(self, async_client: AsyncKernel) -> None:
- site_config = await async_client.site_configs.resolve(
- url="https://example.com",
- )
- assert_matches_type(SiteConfigResponse, site_config, path=["response"])
-
- @pytest.mark.skip(reason="Mock server tests are disabled")
- @parametrize
- async def test_method_resolve_with_all_params(self, async_client: AsyncKernel) -> None:
- site_config = await async_client.site_configs.resolve(
- url="https://example.com",
- allowed_proxy_countries=["US"],
- )
- assert_matches_type(SiteConfigResponse, site_config, path=["response"])
-
- @pytest.mark.skip(reason="Mock server tests are disabled")
- @parametrize
- async def test_raw_response_resolve(self, async_client: AsyncKernel) -> None:
- response = await async_client.site_configs.with_raw_response.resolve(
- url="https://example.com",
- )
-
- assert response.is_closed is True
- assert response.http_request.headers.get("X-Stainless-Lang") == "python"
- site_config = await response.parse()
- assert_matches_type(SiteConfigResponse, site_config, path=["response"])
-
- @pytest.mark.skip(reason="Mock server tests are disabled")
- @parametrize
- async def test_streaming_response_resolve(self, async_client: AsyncKernel) -> None:
- async with async_client.site_configs.with_streaming_response.resolve(
- url="https://example.com",
- ) as response:
- assert not response.is_closed
- assert response.http_request.headers.get("X-Stainless-Lang") == "python"
-
- site_config = await response.parse()
- assert_matches_type(SiteConfigResponse, site_config, path=["response"])
-
- assert cast(Any, response.is_closed) is True
From 826f5e44d16b12494d27812bdd64ffcd1c618a12 Mon Sep 17 00:00:00 2001
From: "kernel-internal[bot]"
<260533166+kernel-internal[bot]@users.noreply.github.com>
Date: Thu, 27 Aug 2026 00:00:18 +0000
Subject: [PATCH 218/366] release: 0.96.0
---
.release-please-manifest.json | 2 +-
CHANGELOG.md | 7 +++++++
pyproject.toml | 2 +-
src/kernel/_version.py | 2 +-
4 files changed, 10 insertions(+), 3 deletions(-)
diff --git a/.release-please-manifest.json b/.release-please-manifest.json
index 2292ca70..d486cde7 100644
--- a/.release-please-manifest.json
+++ b/.release-please-manifest.json
@@ -1,3 +1,3 @@
{
- ".": "0.95.0"
+ ".": "0.96.0"
}
\ No newline at end of file
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 6416998e..f4fdbc82 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,12 @@
# Changelog
+## [0.96.0](https://github.com/kernel/kernel-python-sdk/compare/v0.95.0...v0.96.0) (2026-08-27)
+
+
+### Features
+
+* Rename the site configs API to config registry ([114c986](https://github.com/kernel/kernel-python-sdk/commit/114c986f0d771049ec09741c65e0be317c98f718))
+
## [0.95.0](https://github.com/kernel/kernel-python-sdk/compare/v0.94.0...v0.95.0) (2026-08-26)
diff --git a/pyproject.toml b/pyproject.toml
index d7125601..32520ba7 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -1,6 +1,6 @@
[project]
name = "kernel"
-version = "0.95.0"
+version = "0.96.0"
description = "The official Python library for the kernel API"
dynamic = ["readme"]
license = "Apache-2.0"
diff --git a/src/kernel/_version.py b/src/kernel/_version.py
index 258c1cb0..428f45a2 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.95.0" # x-release-please-version
+__version__ = "0.96.0" # x-release-please-version
From 42c33c6e08ed6018cd93ddf6704ea70b206ca159 Mon Sep 17 00:00:00 2001
From: "kernel-internal[bot]"
<260533166+kernel-internal[bot]@users.noreply.github.com>
Date: Fri, 28 Aug 2026 12:56:22 +0000
Subject: [PATCH 219/366] feat: Expose captcha task and challenge outcomes
Stainless-Generated-From: 14730d8ad0f9f106005c29e8088dfff1ccfce75a
---
api.md | 2 +
src/kernel/types/browsers/__init__.py | 4 +
.../browser_captcha_challenge_result_event.py | 85 +++++++++++++++++++
.../browser_captcha_solve_result_event.py | 25 ++++--
.../browser_captcha_solve_started_event.py | 70 +++++++++++++++
.../browser_telemetry_categories_config.py | 2 +-
...owser_telemetry_categories_config_param.py | 2 +-
.../types/browsers/browser_telemetry_event.py | 4 +
8 files changed, 186 insertions(+), 8 deletions(-)
create mode 100644 src/kernel/types/browsers/browser_captcha_challenge_result_event.py
create mode 100644 src/kernel/types/browsers/browser_captcha_solve_started_event.py
diff --git a/api.md b/api.md
index d5cdf897..6cd72abc 100644
--- a/api.md
+++ b/api.md
@@ -152,7 +152,9 @@ Types:
from kernel.types.browsers import (
BrowserAPICallEvent,
BrowserCallStack,
+ BrowserCaptchaChallengeResultEvent,
BrowserCaptchaSolveResultEvent,
+ BrowserCaptchaSolveStartedEvent,
BrowserCdpCommandEvent,
BrowserCdpCommandMethod,
BrowserCdpConnectEvent,
diff --git a/src/kernel/types/browsers/__init__.py b/src/kernel/types/browsers/__init__.py
index a6d6eb74..927f8a40 100644
--- a/src/kernel/types/browsers/__init__.py
+++ b/src/kernel/types/browsers/__init__.py
@@ -87,6 +87,7 @@
from .browser_live_view_disconnect_event import BrowserLiveViewDisconnectEvent as BrowserLiveViewDisconnectEvent
from .browser_monitor_disconnected_event import BrowserMonitorDisconnectedEvent as BrowserMonitorDisconnectedEvent
from .computer_capture_screenshot_params import ComputerCaptureScreenshotParams as ComputerCaptureScreenshotParams
+from .browser_captcha_solve_started_event import BrowserCaptchaSolveStartedEvent as BrowserCaptchaSolveStartedEvent
from .browser_telemetry_categories_config import BrowserTelemetryCategoriesConfig as BrowserTelemetryCategoriesConfig
from .browser_network_loading_failed_event import BrowserNetworkLoadingFailedEvent as BrowserNetworkLoadingFailedEvent
from .browser_telemetry_cdp_control_config import BrowserTelemetryCdpControlConfig as BrowserTelemetryCdpControlConfig
@@ -99,6 +100,9 @@
from .computer_set_cursor_visibility_params import (
ComputerSetCursorVisibilityParams as ComputerSetCursorVisibilityParams,
)
+from .browser_captcha_challenge_result_event import (
+ BrowserCaptchaChallengeResultEvent as BrowserCaptchaChallengeResultEvent,
+)
from .browser_monitor_reconnect_failed_event import (
BrowserMonitorReconnectFailedEvent as BrowserMonitorReconnectFailedEvent,
)
diff --git a/src/kernel/types/browsers/browser_captcha_challenge_result_event.py b/src/kernel/types/browsers/browser_captcha_challenge_result_event.py
new file mode 100644
index 00000000..978c49cf
--- /dev/null
+++ b/src/kernel/types/browsers/browser_captcha_challenge_result_event.py
@@ -0,0 +1,85 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from typing import Optional
+from typing_extensions import Literal
+
+from ..._models import BaseModel
+from .browser_event_source import BrowserEventSource
+
+__all__ = ["BrowserCaptchaChallengeResultEvent", "Data"]
+
+
+class Data(BaseModel):
+ """Per-challenge payload.
+
+ This event is emitted once per challenge and determines its overall outcome; captcha_solve_started and captcha_solve_result describe individual tasks and may occur multiple times within the challenge.
+ """
+
+ captcha_type: Literal["hcaptcha", "recaptcha_v2", "recaptcha_v3", "turnstile", "geetest", "press_and_hold", "other"]
+ """Captcha kind.
+
+ Enterprise reCAPTCHA variants are grouped into their version bucket
+ (recaptcha_v2 or recaptcha_v3), press-and-hold challenges use press_and_hold,
+ and unlisted kinds use other.
+ """
+
+ challenge_id: str
+ """Opaque identifier shared by events for one visible challenge.
+
+ An image-grid captcha may create multiple task_id values for one challenge_id.
+ The same value may continue across a page reload when the challenge episode
+ continues. It does not indicate task ordering or challenge completion.
+ """
+
+ duration_ms: float
+ """
+ Wall-clock duration from the challenge appearing to its terminal outcome,
+ covering every solver attempt in between.
+ """
+
+ status: Literal["solved", "failure", "timeout", "abandoned"]
+ """Terminal outcome of the visible challenge.
+
+ solved: the page observed the challenge clear after a solver attempt. failure: a
+ terminal solver failure occurred, or all attempts ended while the challenge
+ remained. timeout: the challenge-level wait budget expired while the challenge
+ remained. abandoned: observation ended without an attributable terminal
+ challenge outcome. This includes a dismissed widget or page unload without a
+ solved signal or terminal solver outcome, and a token appearing while multiple
+ same-provider challenges are open, because the producer cannot attribute that
+ token to this visible challenge. A captcha_solve_result with the same
+ challenge_id may therefore report success while the challenge result reports
+ abandoned. A solved challenge does not prove the site accepted the token or that
+ the guarded action succeeded.
+ """
+
+ website_host: Optional[str] = None
+ """Host of the page where the challenge appeared."""
+
+ website_path: Optional[str] = None
+ """Path of the page where the challenge appeared. Query string excluded."""
+
+
+class BrowserCaptchaChallengeResultEvent(BaseModel):
+ """A visible captcha challenge reached a terminal outcome."""
+
+ category: Literal["captcha"]
+
+ data: Data
+ """Per-challenge payload.
+
+ This event is emitted once per challenge and determines its overall outcome;
+ captcha_solve_started and captcha_solve_result describe individual tasks and may
+ occur multiple times within the challenge.
+ """
+
+ source: BrowserEventSource
+ """Provenance metadata identifying which producer emitted the event."""
+
+ ts: int
+ """Event timestamp in Unix microseconds."""
+
+ type: Literal["captcha_challenge_result"]
+
+ truncated: Optional[bool] = None
+ """True if the data field was truncated due to size limits."""
diff --git a/src/kernel/types/browsers/browser_captcha_solve_result_event.py b/src/kernel/types/browsers/browser_captcha_solve_result_event.py
index fb8476d4..2e023cc5 100644
--- a/src/kernel/types/browsers/browser_captcha_solve_result_event.py
+++ b/src/kernel/types/browsers/browser_captcha_solve_result_event.py
@@ -10,15 +10,20 @@
class Data(BaseModel):
- captcha_type: Literal["hcaptcha", "recaptcha_v2", "recaptcha_v3", "turnstile", "geetest", "other"]
- """Captcha vendor family.
+ captcha_type: Literal["hcaptcha", "recaptcha_v2", "recaptcha_v3", "turnstile", "geetest", "press_and_hold", "other"]
+ """Captcha kind.
- Provider-specific task names are normalized into this set; anything not covered
- is reported as other.
+ Enterprise reCAPTCHA variants are grouped into their version bucket
+ (recaptcha_v2 or recaptcha_v3), press-and-hold challenges use press_and_hold,
+ and unlisted kinds use other.
"""
duration_ms: float
- """Wall-clock duration from solve start to terminal outcome."""
+ """Wall-clock duration from solve start to terminal outcome.
+
+ Authoritative solve timing; do not derive it from the gap to a
+ captcha_solve_started event, whose delivery and ordering are not guaranteed.
+ """
status: Literal["success", "failure", "timeout", "abandoned"]
"""Terminal outcome.
@@ -28,6 +33,14 @@ class Data(BaseModel):
budget. abandoned: caller cancelled or the page navigated away mid-solve.
"""
+ challenge_id: Optional[str] = None
+ """Opaque identifier shared by events for one visible challenge.
+
+ An image-grid captcha may create multiple task_id values for one challenge_id.
+ The same value may continue across a page reload when the challenge episode
+ continues. It does not indicate task ordering or challenge completion.
+ """
+
error_code: Optional[str] = None
"""Solver-specific error code on failure (e.g.
@@ -35,7 +48,7 @@ class Data(BaseModel):
"""
task_id: Optional[str] = None
- """Solver-assigned identifier. Opaque, useful for support cross-references."""
+ """Opaque identifier shared with the matching captcha_solve_started."""
website_host: Optional[str] = None
"""Host of the page where the captcha was solved."""
diff --git a/src/kernel/types/browsers/browser_captcha_solve_started_event.py b/src/kernel/types/browsers/browser_captcha_solve_started_event.py
new file mode 100644
index 00000000..546c1c38
--- /dev/null
+++ b/src/kernel/types/browsers/browser_captcha_solve_started_event.py
@@ -0,0 +1,70 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from typing import Optional
+from typing_extensions import Literal
+
+from ..._models import BaseModel
+from .browser_event_source import BrowserEventSource
+
+__all__ = ["BrowserCaptchaSolveStartedEvent", "Data"]
+
+
+class Data(BaseModel):
+ """Per-task payload.
+
+ A visible challenge may create multiple tasks. When present, task_id correlates this event with a captcha_solve_result, while challenge_id groups tasks from the same challenge. Events may arrive out of order or be absent, so their arrival does not indicate current solve state.
+ """
+
+ captcha_type: Literal["hcaptcha", "recaptcha_v2", "recaptcha_v3", "turnstile", "geetest", "press_and_hold", "other"]
+ """Captcha kind.
+
+ Enterprise reCAPTCHA variants are grouped into their version bucket
+ (recaptcha_v2 or recaptcha_v3), press-and-hold challenges use press_and_hold,
+ and unlisted kinds use other.
+ """
+
+ challenge_id: Optional[str] = None
+ """Opaque identifier shared by events for one visible challenge.
+
+ An image-grid captcha may create multiple task_id values for one challenge_id.
+ The same value may continue across a page reload when the challenge episode
+ continues. It does not indicate task ordering or challenge completion.
+ """
+
+ task_id: Optional[str] = None
+ """Opaque identifier shared with the matching captcha_solve_result."""
+
+ website_host: Optional[str] = None
+ """Host of the page where the captcha is being solved.
+
+ May be empty for solver tasks that carry no page URL.
+ """
+
+ website_path: Optional[str] = None
+ """Path of the page where the captcha is being solved. Query string excluded."""
+
+
+class BrowserCaptchaSolveStartedEvent(BaseModel):
+ """A captcha solver accepted a task."""
+
+ category: Literal["captcha"]
+
+ data: Data
+ """Per-task payload.
+
+ A visible challenge may create multiple tasks. When present, task_id correlates
+ this event with a captcha_solve_result, while challenge_id groups tasks from the
+ same challenge. Events may arrive out of order or be absent, so their arrival
+ does not indicate current solve state.
+ """
+
+ source: BrowserEventSource
+ """Provenance metadata identifying which producer emitted the event."""
+
+ ts: int
+ """Event timestamp in Unix microseconds."""
+
+ type: Literal["captcha_solve_started"]
+
+ truncated: Optional[bool] = None
+ """True if the data field was truncated due to size limits."""
diff --git a/src/kernel/types/browsers/browser_telemetry_categories_config.py b/src/kernel/types/browsers/browser_telemetry_categories_config.py
index 8b1f811a..062b360a 100644
--- a/src/kernel/types/browsers/browser_telemetry_categories_config.py
+++ b/src/kernel/types/browsers/browser_telemetry_categories_config.py
@@ -16,7 +16,7 @@ class BrowserTelemetryCategoriesConfig(BaseModel):
"""
captcha: Optional[BrowserTelemetryCategoryConfig] = None
- """Captcha solve attempt outcomes. On by default."""
+ """Captcha solver tasks and visible challenge outcomes. On by default."""
connection: Optional[BrowserTelemetryCategoryConfig] = None
"""Client attach/detach lifecycle for the CDP proxy and live view. On by default."""
diff --git a/src/kernel/types/browsers/browser_telemetry_categories_config_param.py b/src/kernel/types/browsers/browser_telemetry_categories_config_param.py
index e707230f..d84d79d8 100644
--- a/src/kernel/types/browsers/browser_telemetry_categories_config_param.py
+++ b/src/kernel/types/browsers/browser_telemetry_categories_config_param.py
@@ -17,7 +17,7 @@ class BrowserTelemetryCategoriesConfigParam(TypedDict, total=False):
"""
captcha: BrowserTelemetryCategoryConfigParam
- """Captcha solve attempt outcomes. On by default."""
+ """Captcha solver tasks and visible challenge outcomes. On by default."""
connection: BrowserTelemetryCategoryConfigParam
"""Client attach/detach lifecycle for the CDP proxy and live view. On by default."""
diff --git a/src/kernel/types/browsers/browser_telemetry_event.py b/src/kernel/types/browsers/browser_telemetry_event.py
index 2ce872f4..d6c58a00 100644
--- a/src/kernel/types/browsers/browser_telemetry_event.py
+++ b/src/kernel/types/browsers/browser_telemetry_event.py
@@ -35,9 +35,11 @@
from .browser_captcha_solve_result_event import BrowserCaptchaSolveResultEvent
from .browser_live_view_disconnect_event import BrowserLiveViewDisconnectEvent
from .browser_monitor_disconnected_event import BrowserMonitorDisconnectedEvent
+from .browser_captcha_solve_started_event import BrowserCaptchaSolveStartedEvent
from .browser_network_loading_failed_event import BrowserNetworkLoadingFailedEvent
from .browser_page_dom_content_loaded_event import BrowserPageDomContentLoadedEvent
from .browser_page_navigation_settled_event import BrowserPageNavigationSettledEvent
+from .browser_captcha_challenge_result_event import BrowserCaptchaChallengeResultEvent
from .browser_monitor_reconnect_failed_event import BrowserMonitorReconnectFailedEvent
from .browser_interaction_scroll_settled_event import BrowserInteractionScrollSettledEvent
@@ -76,7 +78,9 @@
BrowserCdpDisconnectEvent,
BrowserLiveViewConnectEvent,
BrowserLiveViewDisconnectEvent,
+ BrowserCaptchaSolveStartedEvent,
BrowserCaptchaSolveResultEvent,
+ BrowserCaptchaChallengeResultEvent,
BrowserSystemOomKillEvent,
BrowserServiceCrashedEvent,
],
From 8e11fb2ab0b6ddddcb28af0d503489b94cfba1f0 Mon Sep 17 00:00:00 2001
From: "kernel-internal[bot]"
<260533166+kernel-internal[bot]@users.noreply.github.com>
Date: Mon, 31 Aug 2026 00:41:07 +0000
Subject: [PATCH 220/366] feat: Unify managed auth reauth eligibility and
blockers
Stainless-Generated-From: f5bdf95e42b3b049f1382fa0c48381a8002bdc5c
---
src/kernel/types/auth/managed_auth.py | 7 +++++--
1 file changed, 5 insertions(+), 2 deletions(-)
diff --git a/src/kernel/types/auth/managed_auth.py b/src/kernel/types/auth/managed_auth.py
index 7c4f9668..a358197e 100644
--- a/src/kernel/types/auth/managed_auth.py
+++ b/src/kernel/types/auth/managed_auth.py
@@ -377,6 +377,7 @@ class ManagedAuth(BaseModel):
"has_credential",
"viable_plans_found",
"no_requirements_recorded",
+ "totp_reauth_allowed",
"requirements_satisfiable",
"no_prior_successful_login",
"no_credential",
@@ -400,8 +401,10 @@ class ManagedAuth(BaseModel):
- `viable_plans_found` — at least one stored login plan can be replayed
- `no_requirements_recorded` — no recorded credential requirements to fail
against
- - `requirements_satisfiable` — recorded requirements can be met by the attached
- credential
+ - `totp_reauth_allowed` — TOTP is the only recorded requirement and is safe to
+ attempt automatically
+ - `requirements_satisfiable` — recorded requirements contain no recognized
+ blocker
Negative values (a human must complete the login flow):
From 862852af33954ede94a04c23da342ba7d17e3489 Mon Sep 17 00:00:00 2001
From: "kernel-internal[bot]"
<260533166+kernel-internal[bot]@users.noreply.github.com>
Date: Mon, 31 Aug 2026 00:45:39 +0000
Subject: [PATCH 221/366] release: 0.97.0
---
.release-please-manifest.json | 2 +-
CHANGELOG.md | 8 ++++++++
pyproject.toml | 2 +-
src/kernel/_version.py | 2 +-
4 files changed, 11 insertions(+), 3 deletions(-)
diff --git a/.release-please-manifest.json b/.release-please-manifest.json
index d486cde7..ca93fdaa 100644
--- a/.release-please-manifest.json
+++ b/.release-please-manifest.json
@@ -1,3 +1,3 @@
{
- ".": "0.96.0"
+ ".": "0.97.0"
}
\ No newline at end of file
diff --git a/CHANGELOG.md b/CHANGELOG.md
index f4fdbc82..01df1612 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,13 @@
# Changelog
+## [0.97.0](https://github.com/kernel/kernel-python-sdk/compare/v0.96.0...v0.97.0) (2026-08-31)
+
+
+### Features
+
+* Expose captcha task and challenge outcomes ([42c33c6](https://github.com/kernel/kernel-python-sdk/commit/42c33c6e08ed6018cd93ddf6704ea70b206ca159))
+* Unify managed auth reauth eligibility and blockers ([8e11fb2](https://github.com/kernel/kernel-python-sdk/commit/8e11fb2ab0b6ddddcb28af0d503489b94cfba1f0))
+
## [0.96.0](https://github.com/kernel/kernel-python-sdk/compare/v0.95.0...v0.96.0) (2026-08-27)
diff --git a/pyproject.toml b/pyproject.toml
index 32520ba7..2400b318 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -1,6 +1,6 @@
[project]
name = "kernel"
-version = "0.96.0"
+version = "0.97.0"
description = "The official Python library for the kernel API"
dynamic = ["readme"]
license = "Apache-2.0"
diff --git a/src/kernel/_version.py b/src/kernel/_version.py
index 428f45a2..f1214100 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.96.0" # x-release-please-version
+__version__ = "0.97.0" # x-release-please-version
From a60fa2bd48b06c8ca61005c3e29c42918ee371f2 Mon Sep 17 00:00:00 2001
From: "kernel-internal[bot]"
<260533166+kernel-internal[bot]@users.noreply.github.com>
Date: Wed, 2 Sep 2026 19:01:35 +0000
Subject: [PATCH 222/366] feat: Accept browser session name on all
/browsers/{id_or_name} sub-resource routes
Stainless-Generated-From: c2d7f09673a4d607dd628011d15d4a531c152d8e
---
.stats.yml | 2 +-
api.md | 110 ++++---
src/kernel/resources/browsers/__init__.py | 14 +
src/kernel/resources/browsers/browsers.py | 70 +++-
src/kernel/resources/browsers/computer.py | 192 +++++------
src/kernel/resources/browsers/fs/fs.py | 192 +++++------
src/kernel/resources/browsers/fs/watch.py | 52 +--
src/kernel/resources/browsers/logs.py | 16 +-
src/kernel/resources/browsers/playwright.py | 30 +-
src/kernel/resources/browsers/process.py | 136 ++++----
src/kernel/resources/browsers/replays.py | 68 ++--
src/kernel/resources/browsers/telemetry.py | 32 +-
src/kernel/resources/browsers/webmcp.py | 288 +++++++++++++++++
.../resources/telemetry/destinations.py | 66 ++--
src/kernel/types/browsers/__init__.py | 7 +
.../types/browsers/invocation_result.py | 22 ++
.../types/browsers/process_kill_params.py | 2 +-
.../types/browsers/process_resize_params.py | 2 +-
.../types/browsers/process_stdin_params.py | 2 +-
src/kernel/types/browsers/tool.py | 31 ++
src/kernel/types/browsers/tool_annotations.py | 20 ++
src/kernel/types/browsers/tool_frame.py | 16 +
src/kernel/types/browsers/tool_source.py | 34 ++
src/kernel/types/browsers/tools_response.py | 12 +
.../browsers/webmcp_invoke_tool_params.py | 17 +
.../telemetry/destination_create_params.py | 2 +-
.../types/telemetry/otlp_destination.py | 6 +-
tests/api_resources/browsers/fs/test_watch.py | 72 ++---
tests/api_resources/browsers/test_computer.py | 292 +++++++++--------
tests/api_resources/browsers/test_fs.py | 300 ++++++++++--------
tests/api_resources/browsers/test_logs.py | 24 +-
.../api_resources/browsers/test_playwright.py | 24 +-
tests/api_resources/browsers/test_process.py | 168 +++++-----
tests/api_resources/browsers/test_replays.py | 112 ++++---
.../api_resources/browsers/test_telemetry.py | 48 +--
tests/api_resources/browsers/test_webmcp.py | 230 ++++++++++++++
tests/api_resources/test_browsers.py | 44 +--
37 files changed, 1809 insertions(+), 946 deletions(-)
create mode 100644 src/kernel/resources/browsers/webmcp.py
create mode 100644 src/kernel/types/browsers/invocation_result.py
create mode 100644 src/kernel/types/browsers/tool.py
create mode 100644 src/kernel/types/browsers/tool_annotations.py
create mode 100644 src/kernel/types/browsers/tool_frame.py
create mode 100644 src/kernel/types/browsers/tool_source.py
create mode 100644 src/kernel/types/browsers/tools_response.py
create mode 100644 src/kernel/types/browsers/webmcp_invoke_tool_params.py
create mode 100644 tests/api_resources/browsers/test_webmcp.py
diff --git a/.stats.yml b/.stats.yml
index 2aae27b8..a867372d 100644
--- a/.stats.yml
+++ b/.stats.yml
@@ -1 +1 @@
-configured_endpoints: 145
+configured_endpoints: 147
diff --git a/api.md b/api.md
index 6cd72abc..4a09a5d9 100644
--- a/api.md
+++ b/api.md
@@ -140,9 +140,9 @@ Methods:
- client.browsers.retrieve(id_or_name, \*\*params) -> BrowserRetrieveResponse
- client.browsers.update(id_or_name, \*\*params) -> BrowserUpdateResponse
- client.browsers.list(\*\*params) -> SyncOffsetPagination[BrowserListResponse]
-- client.browsers.curl(id, \*\*params) -> BrowserCurlResponse
+- client.browsers.curl(id_or_name, \*\*params) -> BrowserCurlResponse
- client.browsers.delete_by_id(id_or_name) -> None
-- client.browsers.load_extensions(id, \*\*params) -> None
+- client.browsers.load_extensions(id_or_name, \*\*params) -> None
## Telemetry
@@ -206,8 +206,8 @@ from kernel.types.browsers import (
Methods:
-- client.browsers.telemetry.events(id, \*\*params) -> SyncOffsetPagination[TelemetryEventsResponse]
-- client.browsers.telemetry.stream(id, \*\*params) -> TelemetryStreamResponse
+- client.browsers.telemetry.events(id_or_name, \*\*params) -> SyncOffsetPagination[TelemetryEventsResponse]
+- client.browsers.telemetry.stream(id_or_name, \*\*params) -> TelemetryStreamResponse
## Replays
@@ -219,10 +219,10 @@ from kernel.types.browsers import ReplayListResponse, ReplayStartResponse
Methods:
-- client.browsers.replays.list(id) -> ReplayListResponse
-- client.browsers.replays.download(replay_id, \*, id) -> BinaryAPIResponse
-- client.browsers.replays.start(id, \*\*params) -> ReplayStartResponse
-- client.browsers.replays.stop(replay_id, \*, id) -> None
+- client.browsers.replays.list(id_or_name) -> ReplayListResponse
+- client.browsers.replays.download(replay_id, \*, id_or_name) -> BinaryAPIResponse
+- client.browsers.replays.start(id_or_name, \*\*params) -> ReplayStartResponse
+- client.browsers.replays.stop(replay_id, \*, id_or_name) -> None
## Fs
@@ -234,18 +234,18 @@ from kernel.types.browsers import FFileInfoResponse, FListFilesResponse
Methods:
-- client.browsers.fs.create_directory(id, \*\*params) -> None
-- client.browsers.fs.delete_directory(id, \*\*params) -> None
-- client.browsers.fs.delete_file(id, \*\*params) -> None
-- client.browsers.fs.download_dir_zip(id, \*\*params) -> BinaryAPIResponse
-- client.browsers.fs.file_info(id, \*\*params) -> FFileInfoResponse
-- client.browsers.fs.list_files(id, \*\*params) -> FListFilesResponse
-- client.browsers.fs.move(id, \*\*params) -> None
-- client.browsers.fs.read_file(id, \*\*params) -> BinaryAPIResponse
-- client.browsers.fs.set_file_permissions(id, \*\*params) -> None
-- client.browsers.fs.upload(id, \*\*params) -> None
-- client.browsers.fs.upload_zip(id, \*\*params) -> None
-- client.browsers.fs.write_file(id, contents, \*\*params) -> None
+- client.browsers.fs.create_directory(id_or_name, \*\*params) -> None
+- client.browsers.fs.delete_directory(id_or_name, \*\*params) -> None
+- client.browsers.fs.delete_file(id_or_name, \*\*params) -> None
+- client.browsers.fs.download_dir_zip(id_or_name, \*\*params) -> BinaryAPIResponse
+- client.browsers.fs.file_info(id_or_name, \*\*params) -> FFileInfoResponse
+- client.browsers.fs.list_files(id_or_name, \*\*params) -> FListFilesResponse
+- client.browsers.fs.move(id_or_name, \*\*params) -> None
+- client.browsers.fs.read_file(id_or_name, \*\*params) -> BinaryAPIResponse
+- client.browsers.fs.set_file_permissions(id_or_name, \*\*params) -> None
+- client.browsers.fs.upload(id_or_name, \*\*params) -> None
+- client.browsers.fs.upload_zip(id_or_name, \*\*params) -> None
+- client.browsers.fs.write_file(id_or_name, contents, \*\*params) -> None
### Watch
@@ -257,9 +257,9 @@ from kernel.types.browsers.fs import WatchEventsResponse, WatchStartResponse
Methods:
-- client.browsers.fs.watch.events(watch_id, \*, id) -> WatchEventsResponse
-- client.browsers.fs.watch.start(id, \*\*params) -> WatchStartResponse
-- client.browsers.fs.watch.stop(watch_id, \*, id) -> None
+- client.browsers.fs.watch.events(watch_id, \*, id_or_name) -> WatchEventsResponse
+- client.browsers.fs.watch.start(id_or_name, \*\*params) -> WatchStartResponse
+- client.browsers.fs.watch.stop(watch_id, \*, id_or_name) -> None
## Process
@@ -279,19 +279,19 @@ from kernel.types.browsers import (
Methods:
-- client.browsers.process.exec(id, \*\*params) -> ProcessExecResponse
-- client.browsers.process.kill(process_id, \*, id, \*\*params) -> ProcessKillResponse
-- client.browsers.process.resize(process_id, \*, id, \*\*params) -> ProcessResizeResponse
-- client.browsers.process.spawn(id, \*\*params) -> ProcessSpawnResponse
-- client.browsers.process.status(process_id, \*, id) -> ProcessStatusResponse
-- client.browsers.process.stdin(process_id, \*, id, \*\*params) -> ProcessStdinResponse
-- client.browsers.process.stdout_stream(process_id, \*, id) -> ProcessStdoutStreamResponse
+- client.browsers.process.exec(id_or_name, \*\*params) -> ProcessExecResponse
+- client.browsers.process.kill(process_id, \*, id_or_name, \*\*params) -> ProcessKillResponse
+- client.browsers.process.resize(process_id, \*, id_or_name, \*\*params) -> ProcessResizeResponse
+- client.browsers.process.spawn(id_or_name, \*\*params) -> ProcessSpawnResponse
+- client.browsers.process.status(process_id, \*, id_or_name) -> ProcessStatusResponse
+- client.browsers.process.stdin(process_id, \*, id_or_name, \*\*params) -> ProcessStdinResponse
+- client.browsers.process.stdout_stream(process_id, \*, id_or_name) -> ProcessStdoutStreamResponse
## Logs
Methods:
-- client.browsers.logs.stream(id, \*\*params) -> LogEvent
+- client.browsers.logs.stream(id_or_name, \*\*params) -> LogEvent
## Computer
@@ -307,18 +307,18 @@ from kernel.types.browsers import (
Methods:
-- client.browsers.computer.batch(id, \*\*params) -> None
-- client.browsers.computer.capture_screenshot(id, \*\*params) -> BinaryAPIResponse
-- client.browsers.computer.click_mouse(id, \*\*params) -> None
-- client.browsers.computer.drag_mouse(id, \*\*params) -> None
-- client.browsers.computer.get_mouse_position(id) -> ComputerGetMousePositionResponse
-- client.browsers.computer.move_mouse(id, \*\*params) -> None
-- client.browsers.computer.press_key(id, \*\*params) -> None
-- client.browsers.computer.read_clipboard(id) -> ComputerReadClipboardResponse
-- client.browsers.computer.scroll(id, \*\*params) -> None
-- client.browsers.computer.set_cursor_visibility(id, \*\*params) -> ComputerSetCursorVisibilityResponse
-- client.browsers.computer.type_text(id, \*\*params) -> None
-- client.browsers.computer.write_clipboard(id, \*\*params) -> None
+- client.browsers.computer.batch(id_or_name, \*\*params) -> None
+- client.browsers.computer.capture_screenshot(id_or_name, \*\*params) -> BinaryAPIResponse
+- client.browsers.computer.click_mouse(id_or_name, \*\*params) -> None
+- client.browsers.computer.drag_mouse(id_or_name, \*\*params) -> None
+- client.browsers.computer.get_mouse_position(id_or_name) -> ComputerGetMousePositionResponse
+- client.browsers.computer.move_mouse(id_or_name, \*\*params) -> None
+- client.browsers.computer.press_key(id_or_name, \*\*params) -> None
+- client.browsers.computer.read_clipboard(id_or_name) -> ComputerReadClipboardResponse
+- client.browsers.computer.scroll(id_or_name, \*\*params) -> None
+- client.browsers.computer.set_cursor_visibility(id_or_name, \*\*params) -> ComputerSetCursorVisibilityResponse
+- client.browsers.computer.type_text(id_or_name, \*\*params) -> None
+- client.browsers.computer.write_clipboard(id_or_name, \*\*params) -> None
## Playwright
@@ -330,7 +330,29 @@ from kernel.types.browsers import PlaywrightExecuteResponse
Methods:
-- client.browsers.playwright.execute(id, \*\*params) -> PlaywrightExecuteResponse
+- client.browsers.playwright.execute(id_or_name, \*\*params) -> PlaywrightExecuteResponse
+
+## Webmcp
+
+Types:
+
+```python
+from kernel.types.browsers import (
+ InvocationFailure,
+ InvocationResult,
+ InvokeRequest,
+ Tool,
+ ToolAnnotations,
+ ToolFrame,
+ ToolSource,
+ ToolsResponse,
+)
+```
+
+Methods:
+
+- client.browsers.webmcp.invoke_tool(id_or_name, \*\*params) -> InvocationResult
+- client.browsers.webmcp.list_tools(id_or_name) -> ToolsResponse
# Profiles
diff --git a/src/kernel/resources/browsers/__init__.py b/src/kernel/resources/browsers/__init__.py
index 928d4553..c6ee6a5f 100644
--- a/src/kernel/resources/browsers/__init__.py
+++ b/src/kernel/resources/browsers/__init__.py
@@ -16,6 +16,14 @@
LogsResourceWithStreamingResponse,
AsyncLogsResourceWithStreamingResponse,
)
+from .webmcp import (
+ WebmcpResource,
+ AsyncWebmcpResource,
+ WebmcpResourceWithRawResponse,
+ AsyncWebmcpResourceWithRawResponse,
+ WebmcpResourceWithStreamingResponse,
+ AsyncWebmcpResourceWithStreamingResponse,
+)
from .process import (
ProcessResource,
AsyncProcessResource,
@@ -108,6 +116,12 @@
"AsyncPlaywrightResourceWithRawResponse",
"PlaywrightResourceWithStreamingResponse",
"AsyncPlaywrightResourceWithStreamingResponse",
+ "WebmcpResource",
+ "AsyncWebmcpResource",
+ "WebmcpResourceWithRawResponse",
+ "AsyncWebmcpResourceWithRawResponse",
+ "WebmcpResourceWithStreamingResponse",
+ "AsyncWebmcpResourceWithStreamingResponse",
"BrowsersResource",
"AsyncBrowsersResource",
"BrowsersResourceWithRawResponse",
diff --git a/src/kernel/resources/browsers/browsers.py b/src/kernel/resources/browsers/browsers.py
index 920a65cf..d703df24 100644
--- a/src/kernel/resources/browsers/browsers.py
+++ b/src/kernel/resources/browsers/browsers.py
@@ -24,6 +24,14 @@
FsResourceWithStreamingResponse,
AsyncFsResourceWithStreamingResponse,
)
+from .webmcp import (
+ WebmcpResource,
+ AsyncWebmcpResource,
+ WebmcpResourceWithRawResponse,
+ AsyncWebmcpResourceWithRawResponse,
+ WebmcpResourceWithStreamingResponse,
+ AsyncWebmcpResourceWithStreamingResponse,
+)
from ...types import (
BrowserMemoryRequest,
browser_curl_params,
@@ -148,6 +156,11 @@ def playwright(self) -> PlaywrightResource:
"""Execute Playwright code against the browser instance."""
return PlaywrightResource(self._client)
+ @cached_property
+ def webmcp(self) -> WebmcpResource:
+ """Discover and invoke native page tools across the browser instance."""
+ return WebmcpResource(self._client)
+
@cached_property
def with_raw_response(self) -> BrowsersResourceWithRawResponse:
"""
@@ -527,7 +540,7 @@ def list(
def curl(
self,
- id: str,
+ id_or_name: str,
*,
url: str,
body: str | Omit = omit,
@@ -568,10 +581,10 @@ def curl(
timeout: Override the client-level default timeout for this request, in seconds
"""
- if not id:
- raise ValueError(f"Expected a non-empty value for `id` but received {id!r}")
+ if not id_or_name:
+ raise ValueError(f"Expected a non-empty value for `id_or_name` but received {id_or_name!r}")
return self._post(
- path_template("/browsers/{id}/curl", id=id),
+ path_template("/browsers/{id_or_name}/curl", id_or_name=id_or_name),
body=maybe_transform(
{
"url": url,
@@ -683,7 +696,7 @@ def delete_by_id(
def load_extensions(
self,
- id: str,
+ id_or_name: str,
*,
extensions: Iterable[browser_load_extensions_params.Extension],
# Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
@@ -708,8 +721,8 @@ def load_extensions(
timeout: Override the client-level default timeout for this request, in seconds
"""
- if not id:
- raise ValueError(f"Expected a non-empty value for `id` but received {id!r}")
+ if not id_or_name:
+ raise ValueError(f"Expected a non-empty value for `id_or_name` but received {id_or_name!r}")
extra_headers = {"Accept": "*/*", **(extra_headers or {})}
body = deepcopy_with_paths({"extensions": extensions}, [["extensions", "", "zip_file"]])
files = extract_files(cast(Mapping[str, object], body), paths=[["extensions", "", "zip_file"]])
@@ -718,7 +731,7 @@ def load_extensions(
# multipart/form-data; boundary=---abc--
extra_headers["Content-Type"] = "multipart/form-data"
return self._post(
- path_template("/browsers/{id}/extensions", id=id),
+ path_template("/browsers/{id_or_name}/extensions", id_or_name=id_or_name),
body=maybe_transform(body, browser_load_extensions_params.BrowserLoadExtensionsParams),
files=files,
options=make_request_options(
@@ -768,6 +781,11 @@ def playwright(self) -> AsyncPlaywrightResource:
"""Execute Playwright code against the browser instance."""
return AsyncPlaywrightResource(self._client)
+ @cached_property
+ def webmcp(self) -> AsyncWebmcpResource:
+ """Discover and invoke native page tools across the browser instance."""
+ return AsyncWebmcpResource(self._client)
+
@cached_property
def with_raw_response(self) -> AsyncBrowsersResourceWithRawResponse:
"""
@@ -1147,7 +1165,7 @@ def list(
async def curl(
self,
- id: str,
+ id_or_name: str,
*,
url: str,
body: str | Omit = omit,
@@ -1188,10 +1206,10 @@ async def curl(
timeout: Override the client-level default timeout for this request, in seconds
"""
- if not id:
- raise ValueError(f"Expected a non-empty value for `id` but received {id!r}")
+ if not 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(
- path_template("/browsers/{id}/curl", id=id),
+ path_template("/browsers/{id_or_name}/curl", id_or_name=id_or_name),
body=await async_maybe_transform(
{
"url": url,
@@ -1303,7 +1321,7 @@ async def delete_by_id(
async def load_extensions(
self,
- id: str,
+ id_or_name: str,
*,
extensions: Iterable[browser_load_extensions_params.Extension],
# Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
@@ -1328,8 +1346,8 @@ async def load_extensions(
timeout: Override the client-level default timeout for this request, in seconds
"""
- if not id:
- raise ValueError(f"Expected a non-empty value for `id` but received {id!r}")
+ if not id_or_name:
+ raise ValueError(f"Expected a non-empty value for `id_or_name` but received {id_or_name!r}")
extra_headers = {"Accept": "*/*", **(extra_headers or {})}
body = deepcopy_with_paths({"extensions": extensions}, [["extensions", "", "zip_file"]])
files = extract_files(cast(Mapping[str, object], body), paths=[["extensions", "", "zip_file"]])
@@ -1338,7 +1356,7 @@ async def load_extensions(
# multipart/form-data; boundary=---abc--
extra_headers["Content-Type"] = "multipart/form-data"
return await self._post(
- path_template("/browsers/{id}/extensions", id=id),
+ path_template("/browsers/{id_or_name}/extensions", id_or_name=id_or_name),
body=await async_maybe_transform(body, browser_load_extensions_params.BrowserLoadExtensionsParams),
files=files,
options=make_request_options(
@@ -1411,6 +1429,11 @@ def playwright(self) -> PlaywrightResourceWithRawResponse:
"""Execute Playwright code against the browser instance."""
return PlaywrightResourceWithRawResponse(self._browsers.playwright)
+ @cached_property
+ def webmcp(self) -> WebmcpResourceWithRawResponse:
+ """Discover and invoke native page tools across the browser instance."""
+ return WebmcpResourceWithRawResponse(self._browsers.webmcp)
+
class AsyncBrowsersResourceWithRawResponse:
def __init__(self, browsers: AsyncBrowsersResource) -> None:
@@ -1475,6 +1498,11 @@ def playwright(self) -> AsyncPlaywrightResourceWithRawResponse:
"""Execute Playwright code against the browser instance."""
return AsyncPlaywrightResourceWithRawResponse(self._browsers.playwright)
+ @cached_property
+ def webmcp(self) -> AsyncWebmcpResourceWithRawResponse:
+ """Discover and invoke native page tools across the browser instance."""
+ return AsyncWebmcpResourceWithRawResponse(self._browsers.webmcp)
+
class BrowsersResourceWithStreamingResponse:
def __init__(self, browsers: BrowsersResource) -> None:
@@ -1539,6 +1567,11 @@ def playwright(self) -> PlaywrightResourceWithStreamingResponse:
"""Execute Playwright code against the browser instance."""
return PlaywrightResourceWithStreamingResponse(self._browsers.playwright)
+ @cached_property
+ def webmcp(self) -> WebmcpResourceWithStreamingResponse:
+ """Discover and invoke native page tools across the browser instance."""
+ return WebmcpResourceWithStreamingResponse(self._browsers.webmcp)
+
class AsyncBrowsersResourceWithStreamingResponse:
def __init__(self, browsers: AsyncBrowsersResource) -> None:
@@ -1602,3 +1635,8 @@ def computer(self) -> AsyncComputerResourceWithStreamingResponse:
def playwright(self) -> AsyncPlaywrightResourceWithStreamingResponse:
"""Execute Playwright code against the browser instance."""
return AsyncPlaywrightResourceWithStreamingResponse(self._browsers.playwright)
+
+ @cached_property
+ def webmcp(self) -> AsyncWebmcpResourceWithStreamingResponse:
+ """Discover and invoke native page tools across the browser instance."""
+ return AsyncWebmcpResourceWithStreamingResponse(self._browsers.webmcp)
diff --git a/src/kernel/resources/browsers/computer.py b/src/kernel/resources/browsers/computer.py
index d2b57304..dcb242c6 100644
--- a/src/kernel/resources/browsers/computer.py
+++ b/src/kernel/resources/browsers/computer.py
@@ -69,7 +69,7 @@ def with_streaming_response(self) -> ComputerResourceWithStreamingResponse:
def batch(
self,
- id: str,
+ id_or_name: str,
*,
actions: Iterable[computer_batch_params.Action],
# Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
@@ -95,11 +95,11 @@ def batch(
timeout: Override the client-level default timeout for this request, in seconds
"""
- if not id:
- raise ValueError(f"Expected a non-empty value for `id` but received {id!r}")
+ if not id_or_name:
+ 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(
- path_template("/browsers/{id}/computer/batch", id=id),
+ path_template("/browsers/{id_or_name}/computer/batch", id_or_name=id_or_name),
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
@@ -109,7 +109,7 @@ def batch(
def capture_screenshot(
self,
- id: str,
+ id_or_name: str,
*,
region: computer_capture_screenshot_params.Region | Omit = omit,
# Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
@@ -131,11 +131,11 @@ def capture_screenshot(
timeout: Override the client-level default timeout for this request, in seconds
"""
- if not id:
- raise ValueError(f"Expected a non-empty value for `id` but received {id!r}")
+ if not id_or_name:
+ raise ValueError(f"Expected a non-empty value for `id_or_name` but received {id_or_name!r}")
extra_headers = {"Accept": "image/png", **(extra_headers or {})}
return self._post(
- path_template("/browsers/{id}/computer/screenshot", id=id),
+ path_template("/browsers/{id_or_name}/computer/screenshot", id_or_name=id_or_name),
body=maybe_transform(
{"region": region}, computer_capture_screenshot_params.ComputerCaptureScreenshotParams
),
@@ -147,7 +147,7 @@ def capture_screenshot(
def click_mouse(
self,
- id: str,
+ id_or_name: str,
*,
x: int,
y: int,
@@ -186,11 +186,11 @@ def click_mouse(
timeout: Override the client-level default timeout for this request, in seconds
"""
- if not id:
- raise ValueError(f"Expected a non-empty value for `id` but received {id!r}")
+ if not id_or_name:
+ 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(
- path_template("/browsers/{id}/computer/click_mouse", id=id),
+ path_template("/browsers/{id_or_name}/computer/click_mouse", id_or_name=id_or_name),
body=maybe_transform(
{
"x": x,
@@ -210,7 +210,7 @@ def click_mouse(
def drag_mouse(
self,
- id: str,
+ id_or_name: str,
*,
path: Iterable[Iterable[int]],
button: Literal["left", "middle", "right"] | Omit = omit,
@@ -259,11 +259,11 @@ def drag_mouse(
timeout: Override the client-level default timeout for this request, in seconds
"""
- if not id:
- raise ValueError(f"Expected a non-empty value for `id` but received {id!r}")
+ if not id_or_name:
+ 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(
- path_template("/browsers/{id}/computer/drag_mouse", id=id),
+ path_template("/browsers/{id_or_name}/computer/drag_mouse", id_or_name=id_or_name),
body=maybe_transform(
{
"path": path,
@@ -285,7 +285,7 @@ def drag_mouse(
def get_mouse_position(
self,
- id: str,
+ id_or_name: str,
*,
# Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
# The extra values given here take precedence over values defined on the client or passed to this method.
@@ -306,10 +306,10 @@ def get_mouse_position(
timeout: Override the client-level default timeout for this request, in seconds
"""
- if not id:
- raise ValueError(f"Expected a non-empty value for `id` but received {id!r}")
+ if not id_or_name:
+ raise ValueError(f"Expected a non-empty value for `id_or_name` but received {id_or_name!r}")
return self._post(
- path_template("/browsers/{id}/computer/get_mouse_position", id=id),
+ path_template("/browsers/{id_or_name}/computer/get_mouse_position", id_or_name=id_or_name),
options=make_request_options(
extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
),
@@ -318,7 +318,7 @@ def get_mouse_position(
def move_mouse(
self,
- id: str,
+ id_or_name: str,
*,
x: int,
y: int,
@@ -355,11 +355,11 @@ def move_mouse(
timeout: Override the client-level default timeout for this request, in seconds
"""
- if not id:
- raise ValueError(f"Expected a non-empty value for `id` but received {id!r}")
+ if not id_or_name:
+ 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(
- path_template("/browsers/{id}/computer/move_mouse", id=id),
+ path_template("/browsers/{id_or_name}/computer/move_mouse", id_or_name=id_or_name),
body=maybe_transform(
{
"x": x,
@@ -378,7 +378,7 @@ def move_mouse(
def press_key(
self,
- id: str,
+ id_or_name: str,
*,
keys: SequenceNotStr[str],
duration: int | Omit = omit,
@@ -412,11 +412,11 @@ def press_key(
timeout: Override the client-level default timeout for this request, in seconds
"""
- if not id:
- raise ValueError(f"Expected a non-empty value for `id` but received {id!r}")
+ if not id_or_name:
+ 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(
- path_template("/browsers/{id}/computer/press_key", id=id),
+ path_template("/browsers/{id_or_name}/computer/press_key", id_or_name=id_or_name),
body=maybe_transform(
{
"keys": keys,
@@ -433,7 +433,7 @@ def press_key(
def read_clipboard(
self,
- id: str,
+ id_or_name: str,
*,
# Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
# The extra values given here take precedence over values defined on the client or passed to this method.
@@ -454,10 +454,10 @@ def read_clipboard(
timeout: Override the client-level default timeout for this request, in seconds
"""
- if not id:
- raise ValueError(f"Expected a non-empty value for `id` but received {id!r}")
+ if not id_or_name:
+ raise ValueError(f"Expected a non-empty value for `id_or_name` but received {id_or_name!r}")
return self._post(
- path_template("/browsers/{id}/computer/clipboard/read", id=id),
+ path_template("/browsers/{id_or_name}/computer/clipboard/read", id_or_name=id_or_name),
options=make_request_options(
extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
),
@@ -466,7 +466,7 @@ def read_clipboard(
def scroll(
self,
- id: str,
+ id_or_name: str,
*,
x: int,
y: int,
@@ -504,11 +504,11 @@ def scroll(
timeout: Override the client-level default timeout for this request, in seconds
"""
- if not id:
- raise ValueError(f"Expected a non-empty value for `id` but received {id!r}")
+ if not id_or_name:
+ 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(
- path_template("/browsers/{id}/computer/scroll", id=id),
+ path_template("/browsers/{id_or_name}/computer/scroll", id_or_name=id_or_name),
body=maybe_transform(
{
"x": x,
@@ -527,7 +527,7 @@ def scroll(
def set_cursor_visibility(
self,
- id: str,
+ id_or_name: str,
*,
hidden: bool,
# Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
@@ -551,10 +551,10 @@ def set_cursor_visibility(
timeout: Override the client-level default timeout for this request, in seconds
"""
- if not id:
- raise ValueError(f"Expected a non-empty value for `id` but received {id!r}")
+ if not id_or_name:
+ raise ValueError(f"Expected a non-empty value for `id_or_name` but received {id_or_name!r}")
return self._post(
- path_template("/browsers/{id}/computer/cursor", id=id),
+ path_template("/browsers/{id_or_name}/computer/cursor", id_or_name=id_or_name),
body=maybe_transform(
{"hidden": hidden}, computer_set_cursor_visibility_params.ComputerSetCursorVisibilityParams
),
@@ -566,7 +566,7 @@ def set_cursor_visibility(
def type_text(
self,
- id: str,
+ id_or_name: str,
*,
text: str,
delay: int | Omit = omit,
@@ -593,11 +593,11 @@ def type_text(
timeout: Override the client-level default timeout for this request, in seconds
"""
- if not id:
- raise ValueError(f"Expected a non-empty value for `id` but received {id!r}")
+ if not id_or_name:
+ 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(
- path_template("/browsers/{id}/computer/type", id=id),
+ path_template("/browsers/{id_or_name}/computer/type", id_or_name=id_or_name),
body=maybe_transform(
{
"text": text,
@@ -613,7 +613,7 @@ def type_text(
def write_clipboard(
self,
- id: str,
+ id_or_name: str,
*,
text: str,
# Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
@@ -637,11 +637,11 @@ def write_clipboard(
timeout: Override the client-level default timeout for this request, in seconds
"""
- if not id:
- raise ValueError(f"Expected a non-empty value for `id` but received {id!r}")
+ if not id_or_name:
+ 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(
- path_template("/browsers/{id}/computer/clipboard/write", id=id),
+ path_template("/browsers/{id_or_name}/computer/clipboard/write", id_or_name=id_or_name),
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
@@ -674,7 +674,7 @@ def with_streaming_response(self) -> AsyncComputerResourceWithStreamingResponse:
async def batch(
self,
- id: str,
+ id_or_name: str,
*,
actions: Iterable[computer_batch_params.Action],
# Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
@@ -700,11 +700,11 @@ async def batch(
timeout: Override the client-level default timeout for this request, in seconds
"""
- if not id:
- raise ValueError(f"Expected a non-empty value for `id` but received {id!r}")
+ if not id_or_name:
+ 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(
- path_template("/browsers/{id}/computer/batch", id=id),
+ path_template("/browsers/{id_or_name}/computer/batch", id_or_name=id_or_name),
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
@@ -714,7 +714,7 @@ async def batch(
async def capture_screenshot(
self,
- id: str,
+ id_or_name: str,
*,
region: computer_capture_screenshot_params.Region | Omit = omit,
# Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
@@ -736,11 +736,11 @@ async def capture_screenshot(
timeout: Override the client-level default timeout for this request, in seconds
"""
- if not id:
- raise ValueError(f"Expected a non-empty value for `id` but received {id!r}")
+ if not id_or_name:
+ raise ValueError(f"Expected a non-empty value for `id_or_name` but received {id_or_name!r}")
extra_headers = {"Accept": "image/png", **(extra_headers or {})}
return await self._post(
- path_template("/browsers/{id}/computer/screenshot", id=id),
+ path_template("/browsers/{id_or_name}/computer/screenshot", id_or_name=id_or_name),
body=await async_maybe_transform(
{"region": region}, computer_capture_screenshot_params.ComputerCaptureScreenshotParams
),
@@ -752,7 +752,7 @@ async def capture_screenshot(
async def click_mouse(
self,
- id: str,
+ id_or_name: str,
*,
x: int,
y: int,
@@ -791,11 +791,11 @@ async def click_mouse(
timeout: Override the client-level default timeout for this request, in seconds
"""
- if not id:
- raise ValueError(f"Expected a non-empty value for `id` but received {id!r}")
+ if not id_or_name:
+ 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(
- path_template("/browsers/{id}/computer/click_mouse", id=id),
+ path_template("/browsers/{id_or_name}/computer/click_mouse", id_or_name=id_or_name),
body=await async_maybe_transform(
{
"x": x,
@@ -815,7 +815,7 @@ async def click_mouse(
async def drag_mouse(
self,
- id: str,
+ id_or_name: str,
*,
path: Iterable[Iterable[int]],
button: Literal["left", "middle", "right"] | Omit = omit,
@@ -864,11 +864,11 @@ async def drag_mouse(
timeout: Override the client-level default timeout for this request, in seconds
"""
- if not id:
- raise ValueError(f"Expected a non-empty value for `id` but received {id!r}")
+ if not id_or_name:
+ 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(
- path_template("/browsers/{id}/computer/drag_mouse", id=id),
+ path_template("/browsers/{id_or_name}/computer/drag_mouse", id_or_name=id_or_name),
body=await async_maybe_transform(
{
"path": path,
@@ -890,7 +890,7 @@ async def drag_mouse(
async def get_mouse_position(
self,
- id: str,
+ id_or_name: str,
*,
# Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
# The extra values given here take precedence over values defined on the client or passed to this method.
@@ -911,10 +911,10 @@ async def get_mouse_position(
timeout: Override the client-level default timeout for this request, in seconds
"""
- if not id:
- raise ValueError(f"Expected a non-empty value for `id` but received {id!r}")
+ if not 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(
- path_template("/browsers/{id}/computer/get_mouse_position", id=id),
+ path_template("/browsers/{id_or_name}/computer/get_mouse_position", id_or_name=id_or_name),
options=make_request_options(
extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
),
@@ -923,7 +923,7 @@ async def get_mouse_position(
async def move_mouse(
self,
- id: str,
+ id_or_name: str,
*,
x: int,
y: int,
@@ -960,11 +960,11 @@ async def move_mouse(
timeout: Override the client-level default timeout for this request, in seconds
"""
- if not id:
- raise ValueError(f"Expected a non-empty value for `id` but received {id!r}")
+ if not id_or_name:
+ 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(
- path_template("/browsers/{id}/computer/move_mouse", id=id),
+ path_template("/browsers/{id_or_name}/computer/move_mouse", id_or_name=id_or_name),
body=await async_maybe_transform(
{
"x": x,
@@ -983,7 +983,7 @@ async def move_mouse(
async def press_key(
self,
- id: str,
+ id_or_name: str,
*,
keys: SequenceNotStr[str],
duration: int | Omit = omit,
@@ -1017,11 +1017,11 @@ async def press_key(
timeout: Override the client-level default timeout for this request, in seconds
"""
- if not id:
- raise ValueError(f"Expected a non-empty value for `id` but received {id!r}")
+ if not id_or_name:
+ 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(
- path_template("/browsers/{id}/computer/press_key", id=id),
+ path_template("/browsers/{id_or_name}/computer/press_key", id_or_name=id_or_name),
body=await async_maybe_transform(
{
"keys": keys,
@@ -1038,7 +1038,7 @@ async def press_key(
async def read_clipboard(
self,
- id: str,
+ id_or_name: str,
*,
# Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
# The extra values given here take precedence over values defined on the client or passed to this method.
@@ -1059,10 +1059,10 @@ async def read_clipboard(
timeout: Override the client-level default timeout for this request, in seconds
"""
- if not id:
- raise ValueError(f"Expected a non-empty value for `id` but received {id!r}")
+ if not 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(
- path_template("/browsers/{id}/computer/clipboard/read", id=id),
+ path_template("/browsers/{id_or_name}/computer/clipboard/read", id_or_name=id_or_name),
options=make_request_options(
extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
),
@@ -1071,7 +1071,7 @@ async def read_clipboard(
async def scroll(
self,
- id: str,
+ id_or_name: str,
*,
x: int,
y: int,
@@ -1109,11 +1109,11 @@ async def scroll(
timeout: Override the client-level default timeout for this request, in seconds
"""
- if not id:
- raise ValueError(f"Expected a non-empty value for `id` but received {id!r}")
+ if not id_or_name:
+ 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(
- path_template("/browsers/{id}/computer/scroll", id=id),
+ path_template("/browsers/{id_or_name}/computer/scroll", id_or_name=id_or_name),
body=await async_maybe_transform(
{
"x": x,
@@ -1132,7 +1132,7 @@ async def scroll(
async def set_cursor_visibility(
self,
- id: str,
+ id_or_name: str,
*,
hidden: bool,
# Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
@@ -1156,10 +1156,10 @@ async def set_cursor_visibility(
timeout: Override the client-level default timeout for this request, in seconds
"""
- if not id:
- raise ValueError(f"Expected a non-empty value for `id` but received {id!r}")
+ if not 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(
- path_template("/browsers/{id}/computer/cursor", id=id),
+ path_template("/browsers/{id_or_name}/computer/cursor", id_or_name=id_or_name),
body=await async_maybe_transform(
{"hidden": hidden}, computer_set_cursor_visibility_params.ComputerSetCursorVisibilityParams
),
@@ -1171,7 +1171,7 @@ async def set_cursor_visibility(
async def type_text(
self,
- id: str,
+ id_or_name: str,
*,
text: str,
delay: int | Omit = omit,
@@ -1198,11 +1198,11 @@ async def type_text(
timeout: Override the client-level default timeout for this request, in seconds
"""
- if not id:
- raise ValueError(f"Expected a non-empty value for `id` but received {id!r}")
+ if not id_or_name:
+ 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(
- path_template("/browsers/{id}/computer/type", id=id),
+ path_template("/browsers/{id_or_name}/computer/type", id_or_name=id_or_name),
body=await async_maybe_transform(
{
"text": text,
@@ -1218,7 +1218,7 @@ async def type_text(
async def write_clipboard(
self,
- id: str,
+ id_or_name: str,
*,
text: str,
# Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
@@ -1242,11 +1242,11 @@ async def write_clipboard(
timeout: Override the client-level default timeout for this request, in seconds
"""
- if not id:
- raise ValueError(f"Expected a non-empty value for `id` but received {id!r}")
+ if not id_or_name:
+ 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(
- path_template("/browsers/{id}/computer/clipboard/write", id=id),
+ path_template("/browsers/{id_or_name}/computer/clipboard/write", id_or_name=id_or_name),
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 17149d51..a89d86f2 100644
--- a/src/kernel/resources/browsers/fs/fs.py
+++ b/src/kernel/resources/browsers/fs/fs.py
@@ -97,7 +97,7 @@ def with_streaming_response(self) -> FsResourceWithStreamingResponse:
def create_directory(
self,
- id: str,
+ id_or_name: str,
*,
path: str,
mode: str | Omit = omit,
@@ -124,11 +124,11 @@ def create_directory(
timeout: Override the client-level default timeout for this request, in seconds
"""
- if not id:
- raise ValueError(f"Expected a non-empty value for `id` but received {id!r}")
+ if not id_or_name:
+ 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._put(
- path_template("/browsers/{id}/fs/create_directory", id=id),
+ path_template("/browsers/{id_or_name}/fs/create_directory", id_or_name=id_or_name),
body=maybe_transform(
{
"path": path,
@@ -144,7 +144,7 @@ def create_directory(
def delete_directory(
self,
- id: str,
+ id_or_name: str,
*,
path: str,
# Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
@@ -168,11 +168,11 @@ def delete_directory(
timeout: Override the client-level default timeout for this request, in seconds
"""
- if not id:
- raise ValueError(f"Expected a non-empty value for `id` but received {id!r}")
+ if not id_or_name:
+ 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._put(
- path_template("/browsers/{id}/fs/delete_directory", id=id),
+ path_template("/browsers/{id_or_name}/fs/delete_directory", id_or_name=id_or_name),
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
@@ -182,7 +182,7 @@ def delete_directory(
def delete_file(
self,
- id: str,
+ id_or_name: str,
*,
path: str,
# Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
@@ -206,11 +206,11 @@ def delete_file(
timeout: Override the client-level default timeout for this request, in seconds
"""
- if not id:
- raise ValueError(f"Expected a non-empty value for `id` but received {id!r}")
+ if not id_or_name:
+ 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._put(
- path_template("/browsers/{id}/fs/delete_file", id=id),
+ path_template("/browsers/{id_or_name}/fs/delete_file", id_or_name=id_or_name),
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
@@ -220,7 +220,7 @@ def delete_file(
def download_dir_zip(
self,
- id: str,
+ id_or_name: str,
*,
path: str,
# Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
@@ -244,11 +244,11 @@ def download_dir_zip(
timeout: Override the client-level default timeout for this request, in seconds
"""
- if not id:
- raise ValueError(f"Expected a non-empty value for `id` but received {id!r}")
+ if not id_or_name:
+ raise ValueError(f"Expected a non-empty value for `id_or_name` but received {id_or_name!r}")
extra_headers = {"Accept": "application/zip", **(extra_headers or {})}
return self._get(
- path_template("/browsers/{id}/fs/download_dir_zip", id=id),
+ path_template("/browsers/{id_or_name}/fs/download_dir_zip", id_or_name=id_or_name),
options=make_request_options(
extra_headers=extra_headers,
extra_query=extra_query,
@@ -261,7 +261,7 @@ def download_dir_zip(
def file_info(
self,
- id: str,
+ id_or_name: str,
*,
path: str,
# Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
@@ -285,10 +285,10 @@ def file_info(
timeout: Override the client-level default timeout for this request, in seconds
"""
- if not id:
- raise ValueError(f"Expected a non-empty value for `id` but received {id!r}")
+ if not id_or_name:
+ raise ValueError(f"Expected a non-empty value for `id_or_name` but received {id_or_name!r}")
return self._get(
- path_template("/browsers/{id}/fs/file_info", id=id),
+ path_template("/browsers/{id_or_name}/fs/file_info", id_or_name=id_or_name),
options=make_request_options(
extra_headers=extra_headers,
extra_query=extra_query,
@@ -301,7 +301,7 @@ def file_info(
def list_files(
self,
- id: str,
+ id_or_name: str,
*,
path: str,
# Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
@@ -325,10 +325,10 @@ def list_files(
timeout: Override the client-level default timeout for this request, in seconds
"""
- if not id:
- raise ValueError(f"Expected a non-empty value for `id` but received {id!r}")
+ if not id_or_name:
+ raise ValueError(f"Expected a non-empty value for `id_or_name` but received {id_or_name!r}")
return self._get(
- path_template("/browsers/{id}/fs/list_files", id=id),
+ path_template("/browsers/{id_or_name}/fs/list_files", id_or_name=id_or_name),
options=make_request_options(
extra_headers=extra_headers,
extra_query=extra_query,
@@ -341,7 +341,7 @@ def list_files(
def move(
self,
- id: str,
+ id_or_name: str,
*,
dest_path: str,
src_path: str,
@@ -368,11 +368,11 @@ def move(
timeout: Override the client-level default timeout for this request, in seconds
"""
- if not id:
- raise ValueError(f"Expected a non-empty value for `id` but received {id!r}")
+ if not id_or_name:
+ 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._put(
- path_template("/browsers/{id}/fs/move", id=id),
+ path_template("/browsers/{id_or_name}/fs/move", id_or_name=id_or_name),
body=maybe_transform(
{
"dest_path": dest_path,
@@ -388,7 +388,7 @@ def move(
def read_file(
self,
- id: str,
+ id_or_name: str,
*,
path: str,
# Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
@@ -412,11 +412,11 @@ def read_file(
timeout: Override the client-level default timeout for this request, in seconds
"""
- if not id:
- raise ValueError(f"Expected a non-empty value for `id` but received {id!r}")
+ if not id_or_name:
+ 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(
- path_template("/browsers/{id}/fs/read_file", id=id),
+ path_template("/browsers/{id_or_name}/fs/read_file", id_or_name=id_or_name),
options=make_request_options(
extra_headers=extra_headers,
extra_query=extra_query,
@@ -429,7 +429,7 @@ def read_file(
def set_file_permissions(
self,
- id: str,
+ id_or_name: str,
*,
mode: str,
path: str,
@@ -462,11 +462,11 @@ def set_file_permissions(
timeout: Override the client-level default timeout for this request, in seconds
"""
- if not id:
- raise ValueError(f"Expected a non-empty value for `id` but received {id!r}")
+ if not id_or_name:
+ 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._put(
- path_template("/browsers/{id}/fs/set_file_permissions", id=id),
+ path_template("/browsers/{id_or_name}/fs/set_file_permissions", id_or_name=id_or_name),
body=maybe_transform(
{
"mode": mode,
@@ -484,7 +484,7 @@ def set_file_permissions(
def upload(
self,
- id: str,
+ id_or_name: str,
*,
files: Iterable[f_upload_params.File],
# Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
@@ -506,8 +506,8 @@ def upload(
timeout: Override the client-level default timeout for this request, in seconds
"""
- if not id:
- raise ValueError(f"Expected a non-empty value for `id` but received {id!r}")
+ if not id_or_name:
+ raise ValueError(f"Expected a non-empty value for `id_or_name` but received {id_or_name!r}")
extra_headers = {"Accept": "*/*", **(extra_headers or {})}
body = deepcopy_with_paths({"files": files}, [["files", "", "file"]])
extracted_files = extract_files(cast(Mapping[str, object], body), paths=[["files", "", "file"]])
@@ -516,7 +516,7 @@ def upload(
# multipart/form-data; boundary=---abc--
extra_headers["Content-Type"] = "multipart/form-data"
return self._post(
- path_template("/browsers/{id}/fs/upload", id=id),
+ path_template("/browsers/{id_or_name}/fs/upload", id_or_name=id_or_name),
body=maybe_transform(body, f_upload_params.FUploadParams),
files=extracted_files,
options=make_request_options(
@@ -527,7 +527,7 @@ def upload(
def upload_zip(
self,
- id: str,
+ id_or_name: str,
*,
dest_path: str,
zip_file: FileTypes,
@@ -552,8 +552,8 @@ def upload_zip(
timeout: Override the client-level default timeout for this request, in seconds
"""
- if not id:
- raise ValueError(f"Expected a non-empty value for `id` but received {id!r}")
+ if not id_or_name:
+ raise ValueError(f"Expected a non-empty value for `id_or_name` but received {id_or_name!r}")
extra_headers = {"Accept": "*/*", **(extra_headers or {})}
body = deepcopy_with_paths(
{
@@ -568,7 +568,7 @@ def upload_zip(
# multipart/form-data; boundary=---abc--
extra_headers["Content-Type"] = "multipart/form-data"
return self._post(
- path_template("/browsers/{id}/fs/upload_zip", id=id),
+ path_template("/browsers/{id_or_name}/fs/upload_zip", id_or_name=id_or_name),
body=maybe_transform(body, f_upload_zip_params.FUploadZipParams),
files=files,
options=make_request_options(
@@ -579,7 +579,7 @@ def upload_zip(
def write_file(
self,
- id: str,
+ id_or_name: str,
contents: FileContent | BinaryTypes,
*,
path: str,
@@ -607,12 +607,12 @@ def write_file(
timeout: Override the client-level default timeout for this request, in seconds
"""
- if not id:
- raise ValueError(f"Expected a non-empty value for `id` but received {id!r}")
+ if not id_or_name:
+ raise ValueError(f"Expected a non-empty value for `id_or_name` but received {id_or_name!r}")
extra_headers = {"Accept": "*/*", **(extra_headers or {})}
extra_headers["Content-Type"] = "application/octet-stream"
return self._put(
- path_template("/browsers/{id}/fs/write_file", id=id),
+ path_template("/browsers/{id_or_name}/fs/write_file", id_or_name=id_or_name),
content=read_file_content(contents) if isinstance(contents, os.PathLike) else contents,
options=make_request_options(
extra_headers=extra_headers,
@@ -660,7 +660,7 @@ def with_streaming_response(self) -> AsyncFsResourceWithStreamingResponse:
async def create_directory(
self,
- id: str,
+ id_or_name: str,
*,
path: str,
mode: str | Omit = omit,
@@ -687,11 +687,11 @@ async def create_directory(
timeout: Override the client-level default timeout for this request, in seconds
"""
- if not id:
- raise ValueError(f"Expected a non-empty value for `id` but received {id!r}")
+ if not id_or_name:
+ 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._put(
- path_template("/browsers/{id}/fs/create_directory", id=id),
+ path_template("/browsers/{id_or_name}/fs/create_directory", id_or_name=id_or_name),
body=await async_maybe_transform(
{
"path": path,
@@ -707,7 +707,7 @@ async def create_directory(
async def delete_directory(
self,
- id: str,
+ id_or_name: str,
*,
path: str,
# Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
@@ -731,11 +731,11 @@ async def delete_directory(
timeout: Override the client-level default timeout for this request, in seconds
"""
- if not id:
- raise ValueError(f"Expected a non-empty value for `id` but received {id!r}")
+ if not id_or_name:
+ 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._put(
- path_template("/browsers/{id}/fs/delete_directory", id=id),
+ path_template("/browsers/{id_or_name}/fs/delete_directory", id_or_name=id_or_name),
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
@@ -745,7 +745,7 @@ async def delete_directory(
async def delete_file(
self,
- id: str,
+ id_or_name: str,
*,
path: str,
# Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
@@ -769,11 +769,11 @@ async def delete_file(
timeout: Override the client-level default timeout for this request, in seconds
"""
- if not id:
- raise ValueError(f"Expected a non-empty value for `id` but received {id!r}")
+ if not id_or_name:
+ 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._put(
- path_template("/browsers/{id}/fs/delete_file", id=id),
+ path_template("/browsers/{id_or_name}/fs/delete_file", id_or_name=id_or_name),
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
@@ -783,7 +783,7 @@ async def delete_file(
async def download_dir_zip(
self,
- id: str,
+ id_or_name: str,
*,
path: str,
# Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
@@ -807,11 +807,11 @@ async def download_dir_zip(
timeout: Override the client-level default timeout for this request, in seconds
"""
- if not id:
- raise ValueError(f"Expected a non-empty value for `id` but received {id!r}")
+ if not id_or_name:
+ raise ValueError(f"Expected a non-empty value for `id_or_name` but received {id_or_name!r}")
extra_headers = {"Accept": "application/zip", **(extra_headers or {})}
return await self._get(
- path_template("/browsers/{id}/fs/download_dir_zip", id=id),
+ path_template("/browsers/{id_or_name}/fs/download_dir_zip", id_or_name=id_or_name),
options=make_request_options(
extra_headers=extra_headers,
extra_query=extra_query,
@@ -824,7 +824,7 @@ async def download_dir_zip(
async def file_info(
self,
- id: str,
+ id_or_name: str,
*,
path: str,
# Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
@@ -848,10 +848,10 @@ async def file_info(
timeout: Override the client-level default timeout for this request, in seconds
"""
- if not id:
- raise ValueError(f"Expected a non-empty value for `id` but received {id!r}")
+ if not id_or_name:
+ raise ValueError(f"Expected a non-empty value for `id_or_name` but received {id_or_name!r}")
return await self._get(
- path_template("/browsers/{id}/fs/file_info", id=id),
+ path_template("/browsers/{id_or_name}/fs/file_info", id_or_name=id_or_name),
options=make_request_options(
extra_headers=extra_headers,
extra_query=extra_query,
@@ -864,7 +864,7 @@ async def file_info(
async def list_files(
self,
- id: str,
+ id_or_name: str,
*,
path: str,
# Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
@@ -888,10 +888,10 @@ async def list_files(
timeout: Override the client-level default timeout for this request, in seconds
"""
- if not id:
- raise ValueError(f"Expected a non-empty value for `id` but received {id!r}")
+ if not id_or_name:
+ raise ValueError(f"Expected a non-empty value for `id_or_name` but received {id_or_name!r}")
return await self._get(
- path_template("/browsers/{id}/fs/list_files", id=id),
+ path_template("/browsers/{id_or_name}/fs/list_files", id_or_name=id_or_name),
options=make_request_options(
extra_headers=extra_headers,
extra_query=extra_query,
@@ -904,7 +904,7 @@ async def list_files(
async def move(
self,
- id: str,
+ id_or_name: str,
*,
dest_path: str,
src_path: str,
@@ -931,11 +931,11 @@ async def move(
timeout: Override the client-level default timeout for this request, in seconds
"""
- if not id:
- raise ValueError(f"Expected a non-empty value for `id` but received {id!r}")
+ if not id_or_name:
+ 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._put(
- path_template("/browsers/{id}/fs/move", id=id),
+ path_template("/browsers/{id_or_name}/fs/move", id_or_name=id_or_name),
body=await async_maybe_transform(
{
"dest_path": dest_path,
@@ -951,7 +951,7 @@ async def move(
async def read_file(
self,
- id: str,
+ id_or_name: str,
*,
path: str,
# Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
@@ -975,11 +975,11 @@ async def read_file(
timeout: Override the client-level default timeout for this request, in seconds
"""
- if not id:
- raise ValueError(f"Expected a non-empty value for `id` but received {id!r}")
+ if not id_or_name:
+ 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(
- path_template("/browsers/{id}/fs/read_file", id=id),
+ path_template("/browsers/{id_or_name}/fs/read_file", id_or_name=id_or_name),
options=make_request_options(
extra_headers=extra_headers,
extra_query=extra_query,
@@ -992,7 +992,7 @@ async def read_file(
async def set_file_permissions(
self,
- id: str,
+ id_or_name: str,
*,
mode: str,
path: str,
@@ -1025,11 +1025,11 @@ async def set_file_permissions(
timeout: Override the client-level default timeout for this request, in seconds
"""
- if not id:
- raise ValueError(f"Expected a non-empty value for `id` but received {id!r}")
+ if not id_or_name:
+ 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._put(
- path_template("/browsers/{id}/fs/set_file_permissions", id=id),
+ path_template("/browsers/{id_or_name}/fs/set_file_permissions", id_or_name=id_or_name),
body=await async_maybe_transform(
{
"mode": mode,
@@ -1047,7 +1047,7 @@ async def set_file_permissions(
async def upload(
self,
- id: str,
+ id_or_name: str,
*,
files: Iterable[f_upload_params.File],
# Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
@@ -1069,8 +1069,8 @@ async def upload(
timeout: Override the client-level default timeout for this request, in seconds
"""
- if not id:
- raise ValueError(f"Expected a non-empty value for `id` but received {id!r}")
+ if not id_or_name:
+ raise ValueError(f"Expected a non-empty value for `id_or_name` but received {id_or_name!r}")
extra_headers = {"Accept": "*/*", **(extra_headers or {})}
body = deepcopy_with_paths({"files": files}, [["files", "", "file"]])
extracted_files = extract_files(cast(Mapping[str, object], body), paths=[["files", "", "file"]])
@@ -1079,7 +1079,7 @@ async def upload(
# multipart/form-data; boundary=---abc--
extra_headers["Content-Type"] = "multipart/form-data"
return await self._post(
- path_template("/browsers/{id}/fs/upload", id=id),
+ path_template("/browsers/{id_or_name}/fs/upload", id_or_name=id_or_name),
body=await async_maybe_transform(body, f_upload_params.FUploadParams),
files=extracted_files,
options=make_request_options(
@@ -1090,7 +1090,7 @@ async def upload(
async def upload_zip(
self,
- id: str,
+ id_or_name: str,
*,
dest_path: str,
zip_file: FileTypes,
@@ -1115,8 +1115,8 @@ async def upload_zip(
timeout: Override the client-level default timeout for this request, in seconds
"""
- if not id:
- raise ValueError(f"Expected a non-empty value for `id` but received {id!r}")
+ if not id_or_name:
+ raise ValueError(f"Expected a non-empty value for `id_or_name` but received {id_or_name!r}")
extra_headers = {"Accept": "*/*", **(extra_headers or {})}
body = deepcopy_with_paths(
{
@@ -1131,7 +1131,7 @@ async def upload_zip(
# multipart/form-data; boundary=---abc--
extra_headers["Content-Type"] = "multipart/form-data"
return await self._post(
- path_template("/browsers/{id}/fs/upload_zip", id=id),
+ path_template("/browsers/{id_or_name}/fs/upload_zip", id_or_name=id_or_name),
body=await async_maybe_transform(body, f_upload_zip_params.FUploadZipParams),
files=files,
options=make_request_options(
@@ -1142,7 +1142,7 @@ async def upload_zip(
async def write_file(
self,
- id: str,
+ id_or_name: str,
contents: FileContent | AsyncBinaryTypes,
*,
path: str,
@@ -1170,12 +1170,12 @@ async def write_file(
timeout: Override the client-level default timeout for this request, in seconds
"""
- if not id:
- raise ValueError(f"Expected a non-empty value for `id` but received {id!r}")
+ if not id_or_name:
+ raise ValueError(f"Expected a non-empty value for `id_or_name` but received {id_or_name!r}")
extra_headers = {"Accept": "*/*", **(extra_headers or {})}
extra_headers["Content-Type"] = "application/octet-stream"
return await self._put(
- path_template("/browsers/{id}/fs/write_file", id=id),
+ path_template("/browsers/{id_or_name}/fs/write_file", id_or_name=id_or_name),
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 bc046053..06ba75ef 100644
--- a/src/kernel/resources/browsers/fs/watch.py
+++ b/src/kernel/resources/browsers/fs/watch.py
@@ -49,7 +49,7 @@ def events(
self,
watch_id: str,
*,
- id: str,
+ id_or_name: str,
# Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
# The extra values given here take precedence over values defined on the client or passed to this method.
extra_headers: Headers | None = None,
@@ -69,13 +69,15 @@ def events(
timeout: Override the client-level default timeout for this request, in seconds
"""
- if not id:
- raise ValueError(f"Expected a non-empty value for `id` but received {id!r}")
+ if not id_or_name:
+ raise ValueError(f"Expected a non-empty value for `id_or_name` but received {id_or_name!r}")
if not watch_id:
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(
- path_template("/browsers/{id}/fs/watch/{watch_id}/events", id=id, watch_id=watch_id),
+ path_template(
+ "/browsers/{id_or_name}/fs/watch/{watch_id}/events", id_or_name=id_or_name, watch_id=watch_id
+ ),
options=make_request_options(
extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
),
@@ -86,7 +88,7 @@ def events(
def start(
self,
- id: str,
+ id_or_name: str,
*,
path: str,
recursive: bool | Omit = omit,
@@ -113,10 +115,10 @@ def start(
timeout: Override the client-level default timeout for this request, in seconds
"""
- if not id:
- raise ValueError(f"Expected a non-empty value for `id` but received {id!r}")
+ if not id_or_name:
+ raise ValueError(f"Expected a non-empty value for `id_or_name` but received {id_or_name!r}")
return self._post(
- path_template("/browsers/{id}/fs/watch", id=id),
+ path_template("/browsers/{id_or_name}/fs/watch", id_or_name=id_or_name),
body=maybe_transform(
{
"path": path,
@@ -134,7 +136,7 @@ def stop(
self,
watch_id: str,
*,
- id: str,
+ id_or_name: str,
# Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
# The extra values given here take precedence over values defined on the client or passed to this method.
extra_headers: Headers | None = None,
@@ -154,13 +156,13 @@ def stop(
timeout: Override the client-level default timeout for this request, in seconds
"""
- if not id:
- raise ValueError(f"Expected a non-empty value for `id` but received {id!r}")
+ if not id_or_name:
+ raise ValueError(f"Expected a non-empty value for `id_or_name` but received {id_or_name!r}")
if not watch_id:
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(
- path_template("/browsers/{id}/fs/watch/{watch_id}", id=id, watch_id=watch_id),
+ path_template("/browsers/{id_or_name}/fs/watch/{watch_id}", id_or_name=id_or_name, watch_id=watch_id),
options=make_request_options(
extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
),
@@ -194,7 +196,7 @@ async def events(
self,
watch_id: str,
*,
- id: str,
+ id_or_name: str,
# Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
# The extra values given here take precedence over values defined on the client or passed to this method.
extra_headers: Headers | None = None,
@@ -214,13 +216,15 @@ async def events(
timeout: Override the client-level default timeout for this request, in seconds
"""
- if not id:
- raise ValueError(f"Expected a non-empty value for `id` but received {id!r}")
+ if not id_or_name:
+ raise ValueError(f"Expected a non-empty value for `id_or_name` but received {id_or_name!r}")
if not watch_id:
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(
- path_template("/browsers/{id}/fs/watch/{watch_id}/events", id=id, watch_id=watch_id),
+ path_template(
+ "/browsers/{id_or_name}/fs/watch/{watch_id}/events", id_or_name=id_or_name, watch_id=watch_id
+ ),
options=make_request_options(
extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
),
@@ -231,7 +235,7 @@ async def events(
async def start(
self,
- id: str,
+ id_or_name: str,
*,
path: str,
recursive: bool | Omit = omit,
@@ -258,10 +262,10 @@ async def start(
timeout: Override the client-level default timeout for this request, in seconds
"""
- if not id:
- raise ValueError(f"Expected a non-empty value for `id` but received {id!r}")
+ if not 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(
- path_template("/browsers/{id}/fs/watch", id=id),
+ path_template("/browsers/{id_or_name}/fs/watch", id_or_name=id_or_name),
body=await async_maybe_transform(
{
"path": path,
@@ -279,7 +283,7 @@ async def stop(
self,
watch_id: str,
*,
- id: str,
+ id_or_name: str,
# Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
# The extra values given here take precedence over values defined on the client or passed to this method.
extra_headers: Headers | None = None,
@@ -299,13 +303,13 @@ async def stop(
timeout: Override the client-level default timeout for this request, in seconds
"""
- if not id:
- raise ValueError(f"Expected a non-empty value for `id` but received {id!r}")
+ if not id_or_name:
+ raise ValueError(f"Expected a non-empty value for `id_or_name` but received {id_or_name!r}")
if not watch_id:
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(
- path_template("/browsers/{id}/fs/watch/{watch_id}", id=id, watch_id=watch_id),
+ path_template("/browsers/{id_or_name}/fs/watch/{watch_id}", id_or_name=id_or_name, 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 35ee66dc..db65f29b 100644
--- a/src/kernel/resources/browsers/logs.py
+++ b/src/kernel/resources/browsers/logs.py
@@ -48,7 +48,7 @@ def with_streaming_response(self) -> LogsResourceWithStreamingResponse:
def stream(
self,
- id: str,
+ id_or_name: str,
*,
source: Literal["path", "supervisor"],
follow: bool | Omit = omit,
@@ -77,11 +77,11 @@ def stream(
timeout: Override the client-level default timeout for this request, in seconds
"""
- if not id:
- raise ValueError(f"Expected a non-empty value for `id` but received {id!r}")
+ if not id_or_name:
+ raise ValueError(f"Expected a non-empty value for `id_or_name` but received {id_or_name!r}")
extra_headers = {"Accept": "text/event-stream", **(extra_headers or {})}
return self._get(
- path_template("/browsers/{id}/logs/stream", id=id),
+ path_template("/browsers/{id_or_name}/logs/stream", id_or_name=id_or_name),
options=make_request_options(
extra_headers=extra_headers,
extra_query=extra_query,
@@ -127,7 +127,7 @@ def with_streaming_response(self) -> AsyncLogsResourceWithStreamingResponse:
async def stream(
self,
- id: str,
+ id_or_name: str,
*,
source: Literal["path", "supervisor"],
follow: bool | Omit = omit,
@@ -156,11 +156,11 @@ async def stream(
timeout: Override the client-level default timeout for this request, in seconds
"""
- if not id:
- raise ValueError(f"Expected a non-empty value for `id` but received {id!r}")
+ if not id_or_name:
+ raise ValueError(f"Expected a non-empty value for `id_or_name` but received {id_or_name!r}")
extra_headers = {"Accept": "text/event-stream", **(extra_headers or {})}
return await self._get(
- path_template("/browsers/{id}/logs/stream", id=id),
+ path_template("/browsers/{id_or_name}/logs/stream", id_or_name=id_or_name),
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 8d261ed5..c1f9cea5 100644
--- a/src/kernel/resources/browsers/playwright.py
+++ b/src/kernel/resources/browsers/playwright.py
@@ -45,7 +45,7 @@ def with_streaming_response(self) -> PlaywrightResourceWithStreamingResponse:
def execute(
self,
- id: str,
+ id_or_name: str,
*,
code: str,
timeout_sec: int | Omit = omit,
@@ -59,8 +59,11 @@ def execute(
"""
Execute arbitrary Playwright code in a fresh execution context against the
browser. The code runs in the same VM as the browser, minimizing latency and
- maximizing throughput. It has access to 'page', 'context', and 'browser'
- variables. It can `return` a value, and this value is returned in the response.
+ maximizing throughput. It has access to 'page', 'context', 'browser', and
+ 'webmcp' variables. Use 'webmcp.listTools()' to discover browser-wide WebMCP
+ tools and 'webmcp.invokeTool(toolRef, input?, { timeoutSec? })' to invoke an
+ exact registration. It can `return` a value, and this value is returned in the
+ response.
Args:
code: TypeScript/JavaScript code to execute. The code has access to 'page', 'context',
@@ -79,10 +82,10 @@ def execute(
timeout: Override the client-level default timeout for this request, in seconds
"""
- if not id:
- raise ValueError(f"Expected a non-empty value for `id` but received {id!r}")
+ if not id_or_name:
+ raise ValueError(f"Expected a non-empty value for `id_or_name` but received {id_or_name!r}")
return self._post(
- path_template("/browsers/{id}/playwright/execute", id=id),
+ path_template("/browsers/{id_or_name}/playwright/execute", id_or_name=id_or_name),
body=maybe_transform(
{
"code": code,
@@ -121,7 +124,7 @@ def with_streaming_response(self) -> AsyncPlaywrightResourceWithStreamingRespons
async def execute(
self,
- id: str,
+ id_or_name: str,
*,
code: str,
timeout_sec: int | Omit = omit,
@@ -135,8 +138,11 @@ async def execute(
"""
Execute arbitrary Playwright code in a fresh execution context against the
browser. The code runs in the same VM as the browser, minimizing latency and
- maximizing throughput. It has access to 'page', 'context', and 'browser'
- variables. It can `return` a value, and this value is returned in the response.
+ maximizing throughput. It has access to 'page', 'context', 'browser', and
+ 'webmcp' variables. Use 'webmcp.listTools()' to discover browser-wide WebMCP
+ tools and 'webmcp.invokeTool(toolRef, input?, { timeoutSec? })' to invoke an
+ exact registration. It can `return` a value, and this value is returned in the
+ response.
Args:
code: TypeScript/JavaScript code to execute. The code has access to 'page', 'context',
@@ -155,10 +161,10 @@ async def execute(
timeout: Override the client-level default timeout for this request, in seconds
"""
- if not id:
- raise ValueError(f"Expected a non-empty value for `id` but received {id!r}")
+ if not 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(
- path_template("/browsers/{id}/playwright/execute", id=id),
+ path_template("/browsers/{id_or_name}/playwright/execute", id_or_name=id_or_name),
body=await async_maybe_transform(
{
"code": code,
diff --git a/src/kernel/resources/browsers/process.py b/src/kernel/resources/browsers/process.py
index 83827d38..c792148d 100644
--- a/src/kernel/resources/browsers/process.py
+++ b/src/kernel/resources/browsers/process.py
@@ -61,7 +61,7 @@ def with_streaming_response(self) -> ProcessResourceWithStreamingResponse:
def exec(
self,
- id: str,
+ id_or_name: str,
*,
command: str,
args: SequenceNotStr[str] | Omit = omit,
@@ -103,10 +103,10 @@ def exec(
timeout: Override the client-level default timeout for this request, in seconds
"""
- if not id:
- raise ValueError(f"Expected a non-empty value for `id` but received {id!r}")
+ if not id_or_name:
+ raise ValueError(f"Expected a non-empty value for `id_or_name` but received {id_or_name!r}")
return self._post(
- path_template("/browsers/{id}/process/exec", id=id),
+ path_template("/browsers/{id_or_name}/process/exec", id_or_name=id_or_name),
body=maybe_transform(
{
"command": command,
@@ -129,7 +129,7 @@ def kill(
self,
process_id: str,
*,
- id: str,
+ id_or_name: str,
signal: Literal["TERM", "KILL", "INT", "HUP"],
# 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.
@@ -152,12 +152,14 @@ def kill(
timeout: Override the client-level default timeout for this request, in seconds
"""
- if not id:
- raise ValueError(f"Expected a non-empty value for `id` but received {id!r}")
+ if not id_or_name:
+ raise ValueError(f"Expected a non-empty value for `id_or_name` but received {id_or_name!r}")
if not process_id:
raise ValueError(f"Expected a non-empty value for `process_id` but received {process_id!r}")
return self._post(
- path_template("/browsers/{id}/process/{process_id}/kill", id=id, process_id=process_id),
+ path_template(
+ "/browsers/{id_or_name}/process/{process_id}/kill", id_or_name=id_or_name, 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
@@ -169,7 +171,7 @@ def resize(
self,
process_id: str,
*,
- id: str,
+ id_or_name: str,
cols: int,
rows: int,
# Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
@@ -195,12 +197,14 @@ def resize(
timeout: Override the client-level default timeout for this request, in seconds
"""
- if not id:
- raise ValueError(f"Expected a non-empty value for `id` but received {id!r}")
+ if not id_or_name:
+ raise ValueError(f"Expected a non-empty value for `id_or_name` but received {id_or_name!r}")
if not process_id:
raise ValueError(f"Expected a non-empty value for `process_id` but received {process_id!r}")
return self._post(
- path_template("/browsers/{id}/process/{process_id}/resize", id=id, process_id=process_id),
+ path_template(
+ "/browsers/{id_or_name}/process/{process_id}/resize", id_or_name=id_or_name, process_id=process_id
+ ),
body=maybe_transform(
{
"cols": cols,
@@ -216,7 +220,7 @@ def resize(
def spawn(
self,
- id: str,
+ id_or_name: str,
*,
command: str,
allocate_tty: bool | Omit = omit,
@@ -267,10 +271,10 @@ def spawn(
timeout: Override the client-level default timeout for this request, in seconds
"""
- if not id:
- raise ValueError(f"Expected a non-empty value for `id` but received {id!r}")
+ if not id_or_name:
+ raise ValueError(f"Expected a non-empty value for `id_or_name` but received {id_or_name!r}")
return self._post(
- path_template("/browsers/{id}/process/spawn", id=id),
+ path_template("/browsers/{id_or_name}/process/spawn", id_or_name=id_or_name),
body=maybe_transform(
{
"command": command,
@@ -296,7 +300,7 @@ def status(
self,
process_id: str,
*,
- id: str,
+ id_or_name: str,
# Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
# The extra values given here take precedence over values defined on the client or passed to this method.
extra_headers: Headers | None = None,
@@ -316,12 +320,14 @@ def status(
timeout: Override the client-level default timeout for this request, in seconds
"""
- if not id:
- raise ValueError(f"Expected a non-empty value for `id` but received {id!r}")
+ if not id_or_name:
+ raise ValueError(f"Expected a non-empty value for `id_or_name` but received {id_or_name!r}")
if not process_id:
raise ValueError(f"Expected a non-empty value for `process_id` but received {process_id!r}")
return self._get(
- path_template("/browsers/{id}/process/{process_id}/status", id=id, process_id=process_id),
+ path_template(
+ "/browsers/{id_or_name}/process/{process_id}/status", id_or_name=id_or_name, process_id=process_id
+ ),
options=make_request_options(
extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
),
@@ -332,7 +338,7 @@ def stdin(
self,
process_id: str,
*,
- id: str,
+ id_or_name: str,
data_b64: str,
# Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
# The extra values given here take precedence over values defined on the client or passed to this method.
@@ -355,12 +361,14 @@ def stdin(
timeout: Override the client-level default timeout for this request, in seconds
"""
- if not id:
- raise ValueError(f"Expected a non-empty value for `id` but received {id!r}")
+ if not id_or_name:
+ raise ValueError(f"Expected a non-empty value for `id_or_name` but received {id_or_name!r}")
if not process_id:
raise ValueError(f"Expected a non-empty value for `process_id` but received {process_id!r}")
return self._post(
- path_template("/browsers/{id}/process/{process_id}/stdin", id=id, process_id=process_id),
+ path_template(
+ "/browsers/{id_or_name}/process/{process_id}/stdin", id_or_name=id_or_name, 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
@@ -372,7 +380,7 @@ def stdout_stream(
self,
process_id: str,
*,
- id: str,
+ id_or_name: str,
# Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
# The extra values given here take precedence over values defined on the client or passed to this method.
extra_headers: Headers | None = None,
@@ -392,13 +400,17 @@ def stdout_stream(
timeout: Override the client-level default timeout for this request, in seconds
"""
- if not id:
- raise ValueError(f"Expected a non-empty value for `id` but received {id!r}")
+ if not id_or_name:
+ raise ValueError(f"Expected a non-empty value for `id_or_name` but received {id_or_name!r}")
if not process_id:
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(
- path_template("/browsers/{id}/process/{process_id}/stdout/stream", id=id, process_id=process_id),
+ path_template(
+ "/browsers/{id_or_name}/process/{process_id}/stdout/stream",
+ id_or_name=id_or_name,
+ process_id=process_id,
+ ),
options=make_request_options(
extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
),
@@ -432,7 +444,7 @@ def with_streaming_response(self) -> AsyncProcessResourceWithStreamingResponse:
async def exec(
self,
- id: str,
+ id_or_name: str,
*,
command: str,
args: SequenceNotStr[str] | Omit = omit,
@@ -474,10 +486,10 @@ async def exec(
timeout: Override the client-level default timeout for this request, in seconds
"""
- if not id:
- raise ValueError(f"Expected a non-empty value for `id` but received {id!r}")
+ if not 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(
- path_template("/browsers/{id}/process/exec", id=id),
+ path_template("/browsers/{id_or_name}/process/exec", id_or_name=id_or_name),
body=await async_maybe_transform(
{
"command": command,
@@ -500,7 +512,7 @@ async def kill(
self,
process_id: str,
*,
- id: str,
+ id_or_name: str,
signal: Literal["TERM", "KILL", "INT", "HUP"],
# 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.
@@ -523,12 +535,14 @@ async def kill(
timeout: Override the client-level default timeout for this request, in seconds
"""
- if not id:
- raise ValueError(f"Expected a non-empty value for `id` but received {id!r}")
+ if not id_or_name:
+ raise ValueError(f"Expected a non-empty value for `id_or_name` but received {id_or_name!r}")
if not process_id:
raise ValueError(f"Expected a non-empty value for `process_id` but received {process_id!r}")
return await self._post(
- path_template("/browsers/{id}/process/{process_id}/kill", id=id, process_id=process_id),
+ path_template(
+ "/browsers/{id_or_name}/process/{process_id}/kill", id_or_name=id_or_name, 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
@@ -540,7 +554,7 @@ async def resize(
self,
process_id: str,
*,
- id: str,
+ id_or_name: str,
cols: int,
rows: int,
# Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
@@ -566,12 +580,14 @@ async def resize(
timeout: Override the client-level default timeout for this request, in seconds
"""
- if not id:
- raise ValueError(f"Expected a non-empty value for `id` but received {id!r}")
+ if not id_or_name:
+ raise ValueError(f"Expected a non-empty value for `id_or_name` but received {id_or_name!r}")
if not process_id:
raise ValueError(f"Expected a non-empty value for `process_id` but received {process_id!r}")
return await self._post(
- path_template("/browsers/{id}/process/{process_id}/resize", id=id, process_id=process_id),
+ path_template(
+ "/browsers/{id_or_name}/process/{process_id}/resize", id_or_name=id_or_name, process_id=process_id
+ ),
body=await async_maybe_transform(
{
"cols": cols,
@@ -587,7 +603,7 @@ async def resize(
async def spawn(
self,
- id: str,
+ id_or_name: str,
*,
command: str,
allocate_tty: bool | Omit = omit,
@@ -638,10 +654,10 @@ async def spawn(
timeout: Override the client-level default timeout for this request, in seconds
"""
- if not id:
- raise ValueError(f"Expected a non-empty value for `id` but received {id!r}")
+ if not 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(
- path_template("/browsers/{id}/process/spawn", id=id),
+ path_template("/browsers/{id_or_name}/process/spawn", id_or_name=id_or_name),
body=await async_maybe_transform(
{
"command": command,
@@ -667,7 +683,7 @@ async def status(
self,
process_id: str,
*,
- id: str,
+ id_or_name: str,
# Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
# The extra values given here take precedence over values defined on the client or passed to this method.
extra_headers: Headers | None = None,
@@ -687,12 +703,14 @@ async def status(
timeout: Override the client-level default timeout for this request, in seconds
"""
- if not id:
- raise ValueError(f"Expected a non-empty value for `id` but received {id!r}")
+ if not id_or_name:
+ raise ValueError(f"Expected a non-empty value for `id_or_name` but received {id_or_name!r}")
if not process_id:
raise ValueError(f"Expected a non-empty value for `process_id` but received {process_id!r}")
return await self._get(
- path_template("/browsers/{id}/process/{process_id}/status", id=id, process_id=process_id),
+ path_template(
+ "/browsers/{id_or_name}/process/{process_id}/status", id_or_name=id_or_name, process_id=process_id
+ ),
options=make_request_options(
extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
),
@@ -703,7 +721,7 @@ async def stdin(
self,
process_id: str,
*,
- id: str,
+ id_or_name: str,
data_b64: str,
# Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
# The extra values given here take precedence over values defined on the client or passed to this method.
@@ -726,12 +744,14 @@ async def stdin(
timeout: Override the client-level default timeout for this request, in seconds
"""
- if not id:
- raise ValueError(f"Expected a non-empty value for `id` but received {id!r}")
+ if not id_or_name:
+ raise ValueError(f"Expected a non-empty value for `id_or_name` but received {id_or_name!r}")
if not process_id:
raise ValueError(f"Expected a non-empty value for `process_id` but received {process_id!r}")
return await self._post(
- path_template("/browsers/{id}/process/{process_id}/stdin", id=id, process_id=process_id),
+ path_template(
+ "/browsers/{id_or_name}/process/{process_id}/stdin", id_or_name=id_or_name, 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
@@ -743,7 +763,7 @@ async def stdout_stream(
self,
process_id: str,
*,
- id: str,
+ id_or_name: str,
# Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
# The extra values given here take precedence over values defined on the client or passed to this method.
extra_headers: Headers | None = None,
@@ -763,13 +783,17 @@ async def stdout_stream(
timeout: Override the client-level default timeout for this request, in seconds
"""
- if not id:
- raise ValueError(f"Expected a non-empty value for `id` but received {id!r}")
+ if not id_or_name:
+ raise ValueError(f"Expected a non-empty value for `id_or_name` but received {id_or_name!r}")
if not process_id:
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(
- path_template("/browsers/{id}/process/{process_id}/stdout/stream", id=id, process_id=process_id),
+ path_template(
+ "/browsers/{id_or_name}/process/{process_id}/stdout/stream",
+ id_or_name=id_or_name,
+ 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 e58f8333..d74ce81c 100644
--- a/src/kernel/resources/browsers/replays.py
+++ b/src/kernel/resources/browsers/replays.py
@@ -54,7 +54,7 @@ def with_streaming_response(self) -> ReplaysResourceWithStreamingResponse:
def list(
self,
- id: str,
+ id_or_name: str,
*,
# Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
# The extra values given here take precedence over values defined on the client or passed to this method.
@@ -75,10 +75,10 @@ def list(
timeout: Override the client-level default timeout for this request, in seconds
"""
- if not id:
- raise ValueError(f"Expected a non-empty value for `id` but received {id!r}")
+ if not id_or_name:
+ raise ValueError(f"Expected a non-empty value for `id_or_name` but received {id_or_name!r}")
return self._get(
- path_template("/browsers/{id}/replays", id=id),
+ path_template("/browsers/{id_or_name}/replays", id_or_name=id_or_name),
options=make_request_options(
extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
),
@@ -89,7 +89,7 @@ def download(
self,
replay_id: str,
*,
- id: str,
+ id_or_name: str,
# Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
# The extra values given here take precedence over values defined on the client or passed to this method.
extra_headers: Headers | None = None,
@@ -109,13 +109,13 @@ def download(
timeout: Override the client-level default timeout for this request, in seconds
"""
- if not id:
- raise ValueError(f"Expected a non-empty value for `id` but received {id!r}")
+ if not id_or_name:
+ raise ValueError(f"Expected a non-empty value for `id_or_name` but received {id_or_name!r}")
if not replay_id:
raise ValueError(f"Expected a non-empty value for `replay_id` but received {replay_id!r}")
extra_headers = {"Accept": "video/mp4", **(extra_headers or {})}
return self._get(
- path_template("/browsers/{id}/replays/{replay_id}", id=id, replay_id=replay_id),
+ path_template("/browsers/{id_or_name}/replays/{replay_id}", id_or_name=id_or_name, replay_id=replay_id),
options=make_request_options(
extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
),
@@ -124,7 +124,7 @@ def download(
def start(
self,
- id: str,
+ id_or_name: str,
*,
framerate: int | Omit = omit,
max_duration_in_seconds: int | Omit = omit,
@@ -156,10 +156,10 @@ def start(
timeout: Override the client-level default timeout for this request, in seconds
"""
- if not id:
- raise ValueError(f"Expected a non-empty value for `id` but received {id!r}")
+ if not id_or_name:
+ raise ValueError(f"Expected a non-empty value for `id_or_name` but received {id_or_name!r}")
return self._post(
- path_template("/browsers/{id}/replays", id=id),
+ path_template("/browsers/{id_or_name}/replays", id_or_name=id_or_name),
body=maybe_transform(
{
"framerate": framerate,
@@ -178,7 +178,7 @@ def stop(
self,
replay_id: str,
*,
- id: str,
+ id_or_name: str,
# Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
# The extra values given here take precedence over values defined on the client or passed to this method.
extra_headers: Headers | None = None,
@@ -198,13 +198,15 @@ def stop(
timeout: Override the client-level default timeout for this request, in seconds
"""
- if not id:
- raise ValueError(f"Expected a non-empty value for `id` but received {id!r}")
+ if not id_or_name:
+ raise ValueError(f"Expected a non-empty value for `id_or_name` but received {id_or_name!r}")
if not replay_id:
raise ValueError(f"Expected a non-empty value for `replay_id` but received {replay_id!r}")
extra_headers = {"Accept": "*/*", **(extra_headers or {})}
return self._post(
- path_template("/browsers/{id}/replays/{replay_id}/stop", id=id, replay_id=replay_id),
+ path_template(
+ "/browsers/{id_or_name}/replays/{replay_id}/stop", id_or_name=id_or_name, replay_id=replay_id
+ ),
options=make_request_options(
extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
),
@@ -236,7 +238,7 @@ def with_streaming_response(self) -> AsyncReplaysResourceWithStreamingResponse:
async def list(
self,
- id: str,
+ id_or_name: str,
*,
# Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
# The extra values given here take precedence over values defined on the client or passed to this method.
@@ -257,10 +259,10 @@ async def list(
timeout: Override the client-level default timeout for this request, in seconds
"""
- if not id:
- raise ValueError(f"Expected a non-empty value for `id` but received {id!r}")
+ if not id_or_name:
+ raise ValueError(f"Expected a non-empty value for `id_or_name` but received {id_or_name!r}")
return await self._get(
- path_template("/browsers/{id}/replays", id=id),
+ path_template("/browsers/{id_or_name}/replays", id_or_name=id_or_name),
options=make_request_options(
extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
),
@@ -271,7 +273,7 @@ async def download(
self,
replay_id: str,
*,
- id: str,
+ id_or_name: str,
# Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
# The extra values given here take precedence over values defined on the client or passed to this method.
extra_headers: Headers | None = None,
@@ -291,13 +293,13 @@ async def download(
timeout: Override the client-level default timeout for this request, in seconds
"""
- if not id:
- raise ValueError(f"Expected a non-empty value for `id` but received {id!r}")
+ if not id_or_name:
+ raise ValueError(f"Expected a non-empty value for `id_or_name` but received {id_or_name!r}")
if not replay_id:
raise ValueError(f"Expected a non-empty value for `replay_id` but received {replay_id!r}")
extra_headers = {"Accept": "video/mp4", **(extra_headers or {})}
return await self._get(
- path_template("/browsers/{id}/replays/{replay_id}", id=id, replay_id=replay_id),
+ path_template("/browsers/{id_or_name}/replays/{replay_id}", id_or_name=id_or_name, replay_id=replay_id),
options=make_request_options(
extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
),
@@ -306,7 +308,7 @@ async def download(
async def start(
self,
- id: str,
+ id_or_name: str,
*,
framerate: int | Omit = omit,
max_duration_in_seconds: int | Omit = omit,
@@ -338,10 +340,10 @@ async def start(
timeout: Override the client-level default timeout for this request, in seconds
"""
- if not id:
- raise ValueError(f"Expected a non-empty value for `id` but received {id!r}")
+ if not 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(
- path_template("/browsers/{id}/replays", id=id),
+ path_template("/browsers/{id_or_name}/replays", id_or_name=id_or_name),
body=await async_maybe_transform(
{
"framerate": framerate,
@@ -360,7 +362,7 @@ async def stop(
self,
replay_id: str,
*,
- id: str,
+ id_or_name: str,
# Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
# The extra values given here take precedence over values defined on the client or passed to this method.
extra_headers: Headers | None = None,
@@ -380,13 +382,15 @@ async def stop(
timeout: Override the client-level default timeout for this request, in seconds
"""
- if not id:
- raise ValueError(f"Expected a non-empty value for `id` but received {id!r}")
+ if not id_or_name:
+ raise ValueError(f"Expected a non-empty value for `id_or_name` but received {id_or_name!r}")
if not replay_id:
raise ValueError(f"Expected a non-empty value for `replay_id` but received {replay_id!r}")
extra_headers = {"Accept": "*/*", **(extra_headers or {})}
return await self._post(
- path_template("/browsers/{id}/replays/{replay_id}/stop", id=id, replay_id=replay_id),
+ path_template(
+ "/browsers/{id_or_name}/replays/{replay_id}/stop", id_or_name=id_or_name, 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/browsers/telemetry.py b/src/kernel/resources/browsers/telemetry.py
index c6fa04ea..367ac734 100644
--- a/src/kernel/resources/browsers/telemetry.py
+++ b/src/kernel/resources/browsers/telemetry.py
@@ -53,7 +53,7 @@ def with_streaming_response(self) -> TelemetryResourceWithStreamingResponse:
def events(
self,
- id: str,
+ id_or_name: str,
*,
category: List[
Literal[
@@ -122,10 +122,10 @@ def events(
timeout: Override the client-level default timeout for this request, in seconds
"""
- if not id:
- raise ValueError(f"Expected a non-empty value for `id` but received {id!r}")
+ if not id_or_name:
+ raise ValueError(f"Expected a non-empty value for `id_or_name` but received {id_or_name!r}")
return self._get_api_list(
- path_template("/browsers/{id}/telemetry/events", id=id),
+ path_template("/browsers/{id_or_name}/telemetry/events", id_or_name=id_or_name),
page=SyncOffsetPagination[TelemetryEventsResponse],
options=make_request_options(
extra_headers=extra_headers,
@@ -149,7 +149,7 @@ def events(
def stream(
self,
- id: str,
+ id_or_name: str,
*,
replay: str | Omit = omit,
last_event_id: str | Omit = omit,
@@ -185,12 +185,12 @@ def stream(
timeout: Override the client-level default timeout for this request, in seconds
"""
- if not id:
- raise ValueError(f"Expected a non-empty value for `id` but received {id!r}")
+ if not id_or_name:
+ raise ValueError(f"Expected a non-empty value for `id_or_name` but received {id_or_name!r}")
extra_headers = {"Accept": "text/event-stream", **(extra_headers or {})}
extra_headers = {**strip_not_given({"Last-Event-ID": last_event_id}), **(extra_headers or {})}
return self._get(
- path_template("/browsers/{id}/telemetry/stream", id=id),
+ path_template("/browsers/{id_or_name}/telemetry/stream", id_or_name=id_or_name),
options=make_request_options(
extra_headers=extra_headers,
extra_query=extra_query,
@@ -230,7 +230,7 @@ def with_streaming_response(self) -> AsyncTelemetryResourceWithStreamingResponse
def events(
self,
- id: str,
+ id_or_name: str,
*,
category: List[
Literal[
@@ -299,10 +299,10 @@ def events(
timeout: Override the client-level default timeout for this request, in seconds
"""
- if not id:
- raise ValueError(f"Expected a non-empty value for `id` but received {id!r}")
+ if not id_or_name:
+ raise ValueError(f"Expected a non-empty value for `id_or_name` but received {id_or_name!r}")
return self._get_api_list(
- path_template("/browsers/{id}/telemetry/events", id=id),
+ path_template("/browsers/{id_or_name}/telemetry/events", id_or_name=id_or_name),
page=AsyncOffsetPagination[TelemetryEventsResponse],
options=make_request_options(
extra_headers=extra_headers,
@@ -326,7 +326,7 @@ def events(
async def stream(
self,
- id: str,
+ id_or_name: str,
*,
replay: str | Omit = omit,
last_event_id: str | Omit = omit,
@@ -362,12 +362,12 @@ async def stream(
timeout: Override the client-level default timeout for this request, in seconds
"""
- if not id:
- raise ValueError(f"Expected a non-empty value for `id` but received {id!r}")
+ if not id_or_name:
+ raise ValueError(f"Expected a non-empty value for `id_or_name` but received {id_or_name!r}")
extra_headers = {"Accept": "text/event-stream", **(extra_headers or {})}
extra_headers = {**strip_not_given({"Last-Event-ID": last_event_id}), **(extra_headers or {})}
return await self._get(
- path_template("/browsers/{id}/telemetry/stream", id=id),
+ path_template("/browsers/{id_or_name}/telemetry/stream", id_or_name=id_or_name),
options=make_request_options(
extra_headers=extra_headers,
extra_query=extra_query,
diff --git a/src/kernel/resources/browsers/webmcp.py b/src/kernel/resources/browsers/webmcp.py
new file mode 100644
index 00000000..00f571de
--- /dev/null
+++ b/src/kernel/resources/browsers/webmcp.py
@@ -0,0 +1,288 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from __future__ import annotations
+
+from typing import Dict
+
+import httpx
+
+from ..._types import Body, Omit, Query, Headers, NotGiven, omit, not_given
+from ..._utils import path_template, maybe_transform, async_maybe_transform
+from ..._compat import cached_property
+from ..._resource import SyncAPIResource, AsyncAPIResource
+from ..._response import (
+ to_raw_response_wrapper,
+ to_streamed_response_wrapper,
+ async_to_raw_response_wrapper,
+ async_to_streamed_response_wrapper,
+)
+from ..._base_client import make_request_options
+from ...types.browsers import webmcp_invoke_tool_params
+from ...types.browsers.tools_response import ToolsResponse
+from ...types.browsers.invocation_result import InvocationResult
+
+__all__ = ["WebmcpResource", "AsyncWebmcpResource"]
+
+
+class WebmcpResource(SyncAPIResource):
+ """Discover and invoke native page tools across the browser instance."""
+
+ @cached_property
+ def with_raw_response(self) -> WebmcpResourceWithRawResponse:
+ """
+ This property can be used as a prefix for any HTTP method call to return
+ the raw response object instead of the parsed content.
+
+ For more information, see https://www.github.com/kernel/kernel-python-sdk#accessing-raw-response-data-eg-headers
+ """
+ return WebmcpResourceWithRawResponse(self)
+
+ @cached_property
+ def with_streaming_response(self) -> WebmcpResourceWithStreamingResponse:
+ """
+ An alternative to `.with_raw_response` that doesn't eagerly read the response body.
+
+ For more information, see https://www.github.com/kernel/kernel-python-sdk#with_streaming_response
+ """
+ return WebmcpResourceWithStreamingResponse(self)
+
+ def invoke_tool(
+ self,
+ id_or_name: str,
+ *,
+ input: Dict[str, object],
+ tool_ref: str,
+ timeout_sec: int | Omit = omit,
+ # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
+ # The extra values given here take precedence over values defined on the client or passed to this method.
+ extra_headers: Headers | None = None,
+ extra_query: Query | None = None,
+ extra_body: Body | None = None,
+ timeout: float | httpx.Timeout | None | NotGiven = not_given,
+ ) -> InvocationResult:
+ """
+ Invokes the exact live registration identified by tool_ref and waits
+ synchronously for its result. Navigation during execution is allowed. If the tab
+ or embedded frame disappears, or the request times out after invocation begins,
+ the response reports outcome_unknown and the tool is not retried.
+
+ Args:
+ input: Tool input, limited to 1 MiB after JSON serialization.
+
+ extra_headers: Send extra headers
+
+ extra_query: Add additional query parameters to the request
+
+ extra_body: Add additional JSON properties to the request
+
+ timeout: Override the client-level default timeout for this request, in seconds
+ """
+ if not id_or_name:
+ raise ValueError(f"Expected a non-empty value for `id_or_name` but received {id_or_name!r}")
+ return self._post(
+ path_template("/browsers/{id_or_name}/webmcp/invoke", id_or_name=id_or_name),
+ body=maybe_transform(
+ {
+ "input": input,
+ "tool_ref": tool_ref,
+ "timeout_sec": timeout_sec,
+ },
+ webmcp_invoke_tool_params.WebmcpInvokeToolParams,
+ ),
+ options=make_request_options(
+ extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
+ ),
+ cast_to=InvocationResult,
+ )
+
+ def list_tools(
+ self,
+ id_or_name: str,
+ *,
+ # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
+ # The extra values given here take precedence over values defined on the client or passed to this method.
+ extra_headers: Headers | None = None,
+ extra_query: Query | None = None,
+ extra_body: Body | None = None,
+ timeout: float | httpx.Timeout | None | NotGiven = not_given,
+ ) -> ToolsResponse:
+ """
+ Returns a snapshot of native WebMCP tools available across every open tab and
+ embedded frame in the browser. Each tool includes an opaque tool_ref for
+ invoking that exact live registration. Tools disappear when their document
+ closes or navigates away.
+
+ Args:
+ extra_headers: Send extra headers
+
+ extra_query: Add additional query parameters to the request
+
+ extra_body: Add additional JSON properties to the request
+
+ timeout: Override the client-level default timeout for this request, in seconds
+ """
+ if not id_or_name:
+ raise ValueError(f"Expected a non-empty value for `id_or_name` but received {id_or_name!r}")
+ return self._get(
+ path_template("/browsers/{id_or_name}/webmcp/tools", id_or_name=id_or_name),
+ options=make_request_options(
+ extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
+ ),
+ cast_to=ToolsResponse,
+ )
+
+
+class AsyncWebmcpResource(AsyncAPIResource):
+ """Discover and invoke native page tools across the browser instance."""
+
+ @cached_property
+ def with_raw_response(self) -> AsyncWebmcpResourceWithRawResponse:
+ """
+ This property can be used as a prefix for any HTTP method call to return
+ the raw response object instead of the parsed content.
+
+ For more information, see https://www.github.com/kernel/kernel-python-sdk#accessing-raw-response-data-eg-headers
+ """
+ return AsyncWebmcpResourceWithRawResponse(self)
+
+ @cached_property
+ def with_streaming_response(self) -> AsyncWebmcpResourceWithStreamingResponse:
+ """
+ An alternative to `.with_raw_response` that doesn't eagerly read the response body.
+
+ For more information, see https://www.github.com/kernel/kernel-python-sdk#with_streaming_response
+ """
+ return AsyncWebmcpResourceWithStreamingResponse(self)
+
+ async def invoke_tool(
+ self,
+ id_or_name: str,
+ *,
+ input: Dict[str, object],
+ tool_ref: str,
+ timeout_sec: int | Omit = omit,
+ # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
+ # The extra values given here take precedence over values defined on the client or passed to this method.
+ extra_headers: Headers | None = None,
+ extra_query: Query | None = None,
+ extra_body: Body | None = None,
+ timeout: float | httpx.Timeout | None | NotGiven = not_given,
+ ) -> InvocationResult:
+ """
+ Invokes the exact live registration identified by tool_ref and waits
+ synchronously for its result. Navigation during execution is allowed. If the tab
+ or embedded frame disappears, or the request times out after invocation begins,
+ the response reports outcome_unknown and the tool is not retried.
+
+ Args:
+ input: Tool input, limited to 1 MiB after JSON serialization.
+
+ extra_headers: Send extra headers
+
+ extra_query: Add additional query parameters to the request
+
+ extra_body: Add additional JSON properties to the request
+
+ timeout: Override the client-level default timeout for this request, in seconds
+ """
+ if not id_or_name:
+ raise ValueError(f"Expected a non-empty value for `id_or_name` but received {id_or_name!r}")
+ return await self._post(
+ path_template("/browsers/{id_or_name}/webmcp/invoke", id_or_name=id_or_name),
+ body=await async_maybe_transform(
+ {
+ "input": input,
+ "tool_ref": tool_ref,
+ "timeout_sec": timeout_sec,
+ },
+ webmcp_invoke_tool_params.WebmcpInvokeToolParams,
+ ),
+ options=make_request_options(
+ extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
+ ),
+ cast_to=InvocationResult,
+ )
+
+ async def list_tools(
+ self,
+ id_or_name: str,
+ *,
+ # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
+ # The extra values given here take precedence over values defined on the client or passed to this method.
+ extra_headers: Headers | None = None,
+ extra_query: Query | None = None,
+ extra_body: Body | None = None,
+ timeout: float | httpx.Timeout | None | NotGiven = not_given,
+ ) -> ToolsResponse:
+ """
+ Returns a snapshot of native WebMCP tools available across every open tab and
+ embedded frame in the browser. Each tool includes an opaque tool_ref for
+ invoking that exact live registration. Tools disappear when their document
+ closes or navigates away.
+
+ Args:
+ extra_headers: Send extra headers
+
+ extra_query: Add additional query parameters to the request
+
+ extra_body: Add additional JSON properties to the request
+
+ timeout: Override the client-level default timeout for this request, in seconds
+ """
+ if not id_or_name:
+ raise ValueError(f"Expected a non-empty value for `id_or_name` but received {id_or_name!r}")
+ return await self._get(
+ path_template("/browsers/{id_or_name}/webmcp/tools", id_or_name=id_or_name),
+ options=make_request_options(
+ extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
+ ),
+ cast_to=ToolsResponse,
+ )
+
+
+class WebmcpResourceWithRawResponse:
+ def __init__(self, webmcp: WebmcpResource) -> None:
+ self._webmcp = webmcp
+
+ self.invoke_tool = to_raw_response_wrapper(
+ webmcp.invoke_tool,
+ )
+ self.list_tools = to_raw_response_wrapper(
+ webmcp.list_tools,
+ )
+
+
+class AsyncWebmcpResourceWithRawResponse:
+ def __init__(self, webmcp: AsyncWebmcpResource) -> None:
+ self._webmcp = webmcp
+
+ self.invoke_tool = async_to_raw_response_wrapper(
+ webmcp.invoke_tool,
+ )
+ self.list_tools = async_to_raw_response_wrapper(
+ webmcp.list_tools,
+ )
+
+
+class WebmcpResourceWithStreamingResponse:
+ def __init__(self, webmcp: WebmcpResource) -> None:
+ self._webmcp = webmcp
+
+ self.invoke_tool = to_streamed_response_wrapper(
+ webmcp.invoke_tool,
+ )
+ self.list_tools = to_streamed_response_wrapper(
+ webmcp.list_tools,
+ )
+
+
+class AsyncWebmcpResourceWithStreamingResponse:
+ def __init__(self, webmcp: AsyncWebmcpResource) -> None:
+ self._webmcp = webmcp
+
+ self.invoke_tool = async_to_streamed_response_wrapper(
+ webmcp.invoke_tool,
+ )
+ self.list_tools = async_to_streamed_response_wrapper(
+ webmcp.list_tools,
+ )
diff --git a/src/kernel/resources/telemetry/destinations.py b/src/kernel/resources/telemetry/destinations.py
index 09442435..4d6592fc 100644
--- a/src/kernel/resources/telemetry/destinations.py
+++ b/src/kernel/resources/telemetry/destinations.py
@@ -62,10 +62,11 @@ def create(
extra_body: Body | None = None,
timeout: float | httpx.Timeout | None | NotGiven = not_given,
) -> OtlpDestination:
- """Create an OTLP export destination in the resolved project.
+ """Create an OTLP export destination in the authenticated organization.
- Names must be unique
- within the project.
+ Names must
+ be unique within the organization. Requires an organization-scoped credential or
+ dashboard authentication; project-scoped credentials receive a 403.
Args:
endpoint: Base endpoint of the OTLP/HTTP collector, without a signal path. Kernel appends
@@ -80,7 +81,7 @@ def create(
`https://otlp.datadoghq.com` (Datadog's OTLP intake for US1, not its logs
intake).
- name: Unique within the project.
+ name: Unique within the organization.
headers: Headers sent with each export request, typically an ingestion key. Encrypted at
rest and returned redacted. Names and values must be valid HTTP header tokens,
@@ -125,7 +126,10 @@ def retrieve(
timeout: float | httpx.Timeout | None | NotGiven = not_given,
) -> OtlpDestination:
"""
- Retrieve a single OTLP destination in the resolved project by its ID or name.
+ Retrieve a customer-visible OTLP destination in the authenticated organization
+ by its ID or name. Project-scoped credentials can retrieve these destinations
+ for selection by workloads in their project. Non-dashboard reads return header
+ values redacted.
Args:
extra_headers: Send extra headers
@@ -167,10 +171,11 @@ def update(
values without restarting, which makes this the way to rotate credentials
without interrupting export.
- Names must be unique within the project. Renaming is refused with a 409 while a
- managed auth connection selects this destination by name, since that connection
- resolves the name on every login. Every other field, including `headers`, stays
- editable.
+ Names must be unique within the organization. Renaming is refused with a 409
+ while a managed auth connection selects this destination by name, since that
+ connection resolves the name on every login. Every other field, including
+ `headers`, stays editable. Requires an organization-scoped credential or
+ dashboard authentication; project-scoped credentials receive a 403.
Args:
endpoint: Base endpoint of the OTLP/HTTP collector, without a signal path. Same rules as
@@ -225,8 +230,12 @@ def list(
extra_body: Body | None = None,
timeout: float | httpx.Timeout | None | NotGiven = not_given,
) -> SyncOffsetPagination[OtlpDestination]:
- """
- List OTLP export destinations in the resolved project.
+ """List customer-visible OTLP export destinations in the authenticated
+ organization.
+
+ Project-scoped credentials can list these destinations for
+ selection by workloads in their project. Non-dashboard reads return header
+ values redacted.
Args:
limit: Limit the number of destinations to return.
@@ -286,6 +295,8 @@ def delete(
end or delete them first. It is refused the same way while a managed auth
connection still selects it, because that connection re-resolves the destination
on every login, and while a managed auth login using it is still in progress.
+ Requires an organization-scoped credential or dashboard authentication;
+ project-scoped credentials receive a 403.
Args:
extra_headers: Send extra headers
@@ -346,10 +357,11 @@ async def create(
extra_body: Body | None = None,
timeout: float | httpx.Timeout | None | NotGiven = not_given,
) -> OtlpDestination:
- """Create an OTLP export destination in the resolved project.
+ """Create an OTLP export destination in the authenticated organization.
- Names must be unique
- within the project.
+ Names must
+ be unique within the organization. Requires an organization-scoped credential or
+ dashboard authentication; project-scoped credentials receive a 403.
Args:
endpoint: Base endpoint of the OTLP/HTTP collector, without a signal path. Kernel appends
@@ -364,7 +376,7 @@ async def create(
`https://otlp.datadoghq.com` (Datadog's OTLP intake for US1, not its logs
intake).
- name: Unique within the project.
+ name: Unique within the organization.
headers: Headers sent with each export request, typically an ingestion key. Encrypted at
rest and returned redacted. Names and values must be valid HTTP header tokens,
@@ -409,7 +421,10 @@ async def retrieve(
timeout: float | httpx.Timeout | None | NotGiven = not_given,
) -> OtlpDestination:
"""
- Retrieve a single OTLP destination in the resolved project by its ID or name.
+ Retrieve a customer-visible OTLP destination in the authenticated organization
+ by its ID or name. Project-scoped credentials can retrieve these destinations
+ for selection by workloads in their project. Non-dashboard reads return header
+ values redacted.
Args:
extra_headers: Send extra headers
@@ -451,10 +466,11 @@ async def update(
values without restarting, which makes this the way to rotate credentials
without interrupting export.
- Names must be unique within the project. Renaming is refused with a 409 while a
- managed auth connection selects this destination by name, since that connection
- resolves the name on every login. Every other field, including `headers`, stays
- editable.
+ Names must be unique within the organization. Renaming is refused with a 409
+ while a managed auth connection selects this destination by name, since that
+ connection resolves the name on every login. Every other field, including
+ `headers`, stays editable. Requires an organization-scoped credential or
+ dashboard authentication; project-scoped credentials receive a 403.
Args:
endpoint: Base endpoint of the OTLP/HTTP collector, without a signal path. Same rules as
@@ -509,8 +525,12 @@ def list(
extra_body: Body | None = None,
timeout: float | httpx.Timeout | None | NotGiven = not_given,
) -> AsyncPaginator[OtlpDestination, AsyncOffsetPagination[OtlpDestination]]:
- """
- List OTLP export destinations in the resolved project.
+ """List customer-visible OTLP export destinations in the authenticated
+ organization.
+
+ Project-scoped credentials can list these destinations for
+ selection by workloads in their project. Non-dashboard reads return header
+ values redacted.
Args:
limit: Limit the number of destinations to return.
@@ -570,6 +590,8 @@ async def delete(
end or delete them first. It is refused the same way while a managed auth
connection still selects it, because that connection re-resolves the destination
on every login, and while a managed auth login using it is still in progress.
+ Requires an organization-scoped credential or dashboard authentication;
+ project-scoped credentials receive a 403.
Args:
extra_headers: Send extra headers
diff --git a/src/kernel/types/browsers/__init__.py b/src/kernel/types/browsers/__init__.py
index 927f8a40..34ed9b73 100644
--- a/src/kernel/types/browsers/__init__.py
+++ b/src/kernel/types/browsers/__init__.py
@@ -2,8 +2,14 @@
from __future__ import annotations
+from .tool import Tool as Tool
+from .tool_frame import ToolFrame as ToolFrame
+from .tool_source import ToolSource as ToolSource
from .f_move_params import FMoveParams as FMoveParams
+from .tools_response import ToolsResponse as ToolsResponse
from .f_upload_params import FUploadParams as FUploadParams
+from .tool_annotations import ToolAnnotations as ToolAnnotations
+from .invocation_result import InvocationResult as InvocationResult
from .log_stream_params import LogStreamParams as LogStreamParams
from .browser_call_stack import BrowserCallStack as BrowserCallStack
from .f_file_info_params import FFileInfoParams as FFileInfoParams
@@ -52,6 +58,7 @@
from .playwright_execute_params import PlaywrightExecuteParams as PlaywrightExecuteParams
from .telemetry_events_response import TelemetryEventsResponse as TelemetryEventsResponse
from .telemetry_stream_response import TelemetryStreamResponse as TelemetryStreamResponse
+from .webmcp_invoke_tool_params import WebmcpInvokeToolParams as WebmcpInvokeToolParams
from .browser_cdp_command_method import BrowserCdpCommandMethod as BrowserCdpCommandMethod
from .browser_network_idle_event import BrowserNetworkIdleEvent as BrowserNetworkIdleEvent
from .browser_page_crashed_event import BrowserPageCrashedEvent as BrowserPageCrashedEvent
diff --git a/src/kernel/types/browsers/invocation_result.py b/src/kernel/types/browsers/invocation_result.py
new file mode 100644
index 00000000..7ecd69ce
--- /dev/null
+++ b/src/kernel/types/browsers/invocation_result.py
@@ -0,0 +1,22 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from typing import Optional
+from typing_extensions import Literal
+
+from ..._models import BaseModel
+
+__all__ = ["InvocationResult"]
+
+
+class InvocationResult(BaseModel):
+ invocation_id: str
+
+ status: Literal["completed", "canceled", "error"]
+
+ error_text: Optional[str] = None
+
+ output: Optional[object] = None
+ """Untrusted page-provided output.
+
+ Callers must treat it as potentially malicious input.
+ """
diff --git a/src/kernel/types/browsers/process_kill_params.py b/src/kernel/types/browsers/process_kill_params.py
index 84be2770..def8a7c1 100644
--- a/src/kernel/types/browsers/process_kill_params.py
+++ b/src/kernel/types/browsers/process_kill_params.py
@@ -8,7 +8,7 @@
class ProcessKillParams(TypedDict, total=False):
- id: Required[str]
+ id_or_name: Required[str]
signal: Required[Literal["TERM", "KILL", "INT", "HUP"]]
"""Signal to send."""
diff --git a/src/kernel/types/browsers/process_resize_params.py b/src/kernel/types/browsers/process_resize_params.py
index 4fdb8ad5..5a7532ed 100644
--- a/src/kernel/types/browsers/process_resize_params.py
+++ b/src/kernel/types/browsers/process_resize_params.py
@@ -8,7 +8,7 @@
class ProcessResizeParams(TypedDict, total=False):
- id: Required[str]
+ id_or_name: Required[str]
cols: Required[int]
"""New terminal columns."""
diff --git a/src/kernel/types/browsers/process_stdin_params.py b/src/kernel/types/browsers/process_stdin_params.py
index 9ece9a57..c999fa64 100644
--- a/src/kernel/types/browsers/process_stdin_params.py
+++ b/src/kernel/types/browsers/process_stdin_params.py
@@ -8,7 +8,7 @@
class ProcessStdinParams(TypedDict, total=False):
- id: Required[str]
+ id_or_name: Required[str]
data_b64: Required[str]
"""Base64-encoded data to write."""
diff --git a/src/kernel/types/browsers/tool.py b/src/kernel/types/browsers/tool.py
new file mode 100644
index 00000000..12e63767
--- /dev/null
+++ b/src/kernel/types/browsers/tool.py
@@ -0,0 +1,31 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from typing import Dict, Optional
+
+from ..._models import BaseModel
+from .tool_source import ToolSource
+from .tool_annotations import ToolAnnotations
+
+__all__ = ["Tool"]
+
+
+class Tool(BaseModel):
+ description: str
+
+ input_schema: Dict[str, object]
+
+ name: str
+
+ source: ToolSource
+
+ tool_ref: str
+ """Opaque reference for invoking this exact live registration.
+
+ It becomes invalid when its document or browser process is replaced.
+ """
+
+ annotations: Optional[ToolAnnotations] = None
+ """Page-provided behavioral hints.
+
+ These values are untrusted and are not enforced by Kernel.
+ """
diff --git a/src/kernel/types/browsers/tool_annotations.py b/src/kernel/types/browsers/tool_annotations.py
new file mode 100644
index 00000000..9f9b097d
--- /dev/null
+++ b/src/kernel/types/browsers/tool_annotations.py
@@ -0,0 +1,20 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from ..._models import BaseModel
+
+__all__ = ["ToolAnnotations"]
+
+
+class ToolAnnotations(BaseModel):
+ """Page-provided behavioral hints.
+
+ These values are untrusted and are not enforced by Kernel.
+ """
+
+ autosubmit: bool
+
+ consequential: bool
+
+ read_only: bool
+
+ untrusted_content: bool
diff --git a/src/kernel/types/browsers/tool_frame.py b/src/kernel/types/browsers/tool_frame.py
new file mode 100644
index 00000000..687994f1
--- /dev/null
+++ b/src/kernel/types/browsers/tool_frame.py
@@ -0,0 +1,16 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from ..._models import BaseModel
+
+__all__ = ["ToolFrame"]
+
+
+class ToolFrame(BaseModel):
+ frame_id: int
+ """
+ Monotonically increasing identifier for this embedded frame during the current
+ browser process.
+ """
+
+ url: str
+ """Current frame URL with the fragment omitted."""
diff --git a/src/kernel/types/browsers/tool_source.py b/src/kernel/types/browsers/tool_source.py
new file mode 100644
index 00000000..ed95f856
--- /dev/null
+++ b/src/kernel/types/browsers/tool_source.py
@@ -0,0 +1,34 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from typing import Optional
+
+from ..._models import BaseModel
+from .tool_frame import ToolFrame
+
+__all__ = ["ToolSource"]
+
+
+class ToolSource(BaseModel):
+ frame: Optional[ToolFrame] = None
+ """
+ Embedded frame that registered the tool, or null when the top-level page
+ registered it.
+ """
+
+ page_title: str
+ """Current title of the top-level page."""
+
+ page_url: str
+ """Current URL of the top-level page with the fragment omitted."""
+
+ tab_id: int
+ """
+ Monotonically increasing identifier for the tab during the current browser
+ process.
+ """
+
+ window_id: int
+ """
+ Monotonically increasing identifier for the browser window during the current
+ browser process.
+ """
diff --git a/src/kernel/types/browsers/tools_response.py b/src/kernel/types/browsers/tools_response.py
new file mode 100644
index 00000000..85952306
--- /dev/null
+++ b/src/kernel/types/browsers/tools_response.py
@@ -0,0 +1,12 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from typing import List
+
+from .tool import Tool
+from ..._models import BaseModel
+
+__all__ = ["ToolsResponse"]
+
+
+class ToolsResponse(BaseModel):
+ tools: List[Tool]
diff --git a/src/kernel/types/browsers/webmcp_invoke_tool_params.py b/src/kernel/types/browsers/webmcp_invoke_tool_params.py
new file mode 100644
index 00000000..99f0f25f
--- /dev/null
+++ b/src/kernel/types/browsers/webmcp_invoke_tool_params.py
@@ -0,0 +1,17 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from __future__ import annotations
+
+from typing import Dict
+from typing_extensions import Required, TypedDict
+
+__all__ = ["WebmcpInvokeToolParams"]
+
+
+class WebmcpInvokeToolParams(TypedDict, total=False):
+ input: Required[Dict[str, object]]
+ """Tool input, limited to 1 MiB after JSON serialization."""
+
+ tool_ref: Required[str]
+
+ timeout_sec: int
diff --git a/src/kernel/types/telemetry/destination_create_params.py b/src/kernel/types/telemetry/destination_create_params.py
index 882f7cc6..5d10b71f 100644
--- a/src/kernel/types/telemetry/destination_create_params.py
+++ b/src/kernel/types/telemetry/destination_create_params.py
@@ -25,7 +25,7 @@ class DestinationCreateParams(TypedDict, total=False):
"""
name: Required[str]
- """Unique within the project."""
+ """Unique within the organization."""
description: str
diff --git a/src/kernel/types/telemetry/otlp_destination.py b/src/kernel/types/telemetry/otlp_destination.py
index c6e6f2bc..0afe6ae8 100644
--- a/src/kernel/types/telemetry/otlp_destination.py
+++ b/src/kernel/types/telemetry/otlp_destination.py
@@ -25,12 +25,12 @@ class OtlpDestination(BaseModel):
"""Headers sent with each export request.
Names are returned in canonical form (`Authorization`, not `authorization`).
- Values are returned redacted as empty strings, so the keys are visible but the
- credentials are not.
+ Non-dashboard reads return values redacted as empty strings, so the keys are
+ visible but the credentials are not. Dashboard reads return the stored values.
"""
name: str
- """Unique within the project.
+ """Unique within the organization.
Usable in place of the ID when selecting a destination, so it cannot be shaped
like an ID.
diff --git a/tests/api_resources/browsers/fs/test_watch.py b/tests/api_resources/browsers/fs/test_watch.py
index b28086ba..e81b2ade 100644
--- a/tests/api_resources/browsers/fs/test_watch.py
+++ b/tests/api_resources/browsers/fs/test_watch.py
@@ -22,7 +22,7 @@ class TestWatch:
def test_method_events(self, client: Kernel) -> None:
watch_stream = client.browsers.fs.watch.events(
watch_id="watch_id",
- id="id",
+ id_or_name="htzv5orfit78e1m2biiifpbv",
)
watch_stream.response.close()
@@ -31,7 +31,7 @@ def test_method_events(self, client: Kernel) -> None:
def test_raw_response_events(self, client: Kernel) -> None:
response = client.browsers.fs.watch.with_raw_response.events(
watch_id="watch_id",
- id="id",
+ id_or_name="htzv5orfit78e1m2biiifpbv",
)
assert response.http_request.headers.get("X-Stainless-Lang") == "python"
@@ -43,7 +43,7 @@ def test_raw_response_events(self, client: Kernel) -> None:
def test_streaming_response_events(self, client: Kernel) -> None:
with client.browsers.fs.watch.with_streaming_response.events(
watch_id="watch_id",
- id="id",
+ id_or_name="htzv5orfit78e1m2biiifpbv",
) as response:
assert not response.is_closed
assert response.http_request.headers.get("X-Stainless-Lang") == "python"
@@ -56,23 +56,23 @@ def test_streaming_response_events(self, client: Kernel) -> None:
@pytest.mark.skip(reason="Mock server tests are disabled")
@parametrize
def test_path_params_events(self, client: Kernel) -> None:
- with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"):
+ with pytest.raises(ValueError, match=r"Expected a non-empty value for `id_or_name` but received ''"):
client.browsers.fs.watch.with_raw_response.events(
watch_id="watch_id",
- id="",
+ id_or_name="",
)
with pytest.raises(ValueError, match=r"Expected a non-empty value for `watch_id` but received ''"):
client.browsers.fs.watch.with_raw_response.events(
watch_id="",
- id="id",
+ id_or_name="htzv5orfit78e1m2biiifpbv",
)
@pytest.mark.skip(reason="Mock server tests are disabled")
@parametrize
def test_method_start(self, client: Kernel) -> None:
watch = client.browsers.fs.watch.start(
- id="id",
+ id_or_name="htzv5orfit78e1m2biiifpbv",
path="path",
)
assert_matches_type(WatchStartResponse, watch, path=["response"])
@@ -81,7 +81,7 @@ def test_method_start(self, client: Kernel) -> None:
@parametrize
def test_method_start_with_all_params(self, client: Kernel) -> None:
watch = client.browsers.fs.watch.start(
- id="id",
+ id_or_name="htzv5orfit78e1m2biiifpbv",
path="path",
recursive=True,
)
@@ -91,7 +91,7 @@ def test_method_start_with_all_params(self, client: Kernel) -> None:
@parametrize
def test_raw_response_start(self, client: Kernel) -> None:
response = client.browsers.fs.watch.with_raw_response.start(
- id="id",
+ id_or_name="htzv5orfit78e1m2biiifpbv",
path="path",
)
@@ -104,7 +104,7 @@ def test_raw_response_start(self, client: Kernel) -> None:
@parametrize
def test_streaming_response_start(self, client: Kernel) -> None:
with client.browsers.fs.watch.with_streaming_response.start(
- id="id",
+ id_or_name="htzv5orfit78e1m2biiifpbv",
path="path",
) as response:
assert not response.is_closed
@@ -118,9 +118,9 @@ def test_streaming_response_start(self, client: Kernel) -> None:
@pytest.mark.skip(reason="Mock server tests are disabled")
@parametrize
def test_path_params_start(self, client: Kernel) -> None:
- with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"):
+ with pytest.raises(ValueError, match=r"Expected a non-empty value for `id_or_name` but received ''"):
client.browsers.fs.watch.with_raw_response.start(
- id="",
+ id_or_name="",
path="path",
)
@@ -129,7 +129,7 @@ def test_path_params_start(self, client: Kernel) -> None:
def test_method_stop(self, client: Kernel) -> None:
watch = client.browsers.fs.watch.stop(
watch_id="watch_id",
- id="id",
+ id_or_name="htzv5orfit78e1m2biiifpbv",
)
assert watch is None
@@ -138,7 +138,7 @@ def test_method_stop(self, client: Kernel) -> None:
def test_raw_response_stop(self, client: Kernel) -> None:
response = client.browsers.fs.watch.with_raw_response.stop(
watch_id="watch_id",
- id="id",
+ id_or_name="htzv5orfit78e1m2biiifpbv",
)
assert response.is_closed is True
@@ -151,7 +151,7 @@ def test_raw_response_stop(self, client: Kernel) -> None:
def test_streaming_response_stop(self, client: Kernel) -> None:
with client.browsers.fs.watch.with_streaming_response.stop(
watch_id="watch_id",
- id="id",
+ id_or_name="htzv5orfit78e1m2biiifpbv",
) as response:
assert not response.is_closed
assert response.http_request.headers.get("X-Stainless-Lang") == "python"
@@ -164,16 +164,16 @@ def test_streaming_response_stop(self, client: Kernel) -> None:
@pytest.mark.skip(reason="Mock server tests are disabled")
@parametrize
def test_path_params_stop(self, client: Kernel) -> None:
- with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"):
+ with pytest.raises(ValueError, match=r"Expected a non-empty value for `id_or_name` but received ''"):
client.browsers.fs.watch.with_raw_response.stop(
watch_id="watch_id",
- id="",
+ id_or_name="",
)
with pytest.raises(ValueError, match=r"Expected a non-empty value for `watch_id` but received ''"):
client.browsers.fs.watch.with_raw_response.stop(
watch_id="",
- id="id",
+ id_or_name="htzv5orfit78e1m2biiifpbv",
)
@@ -187,7 +187,7 @@ class TestAsyncWatch:
async def test_method_events(self, async_client: AsyncKernel) -> None:
watch_stream = await async_client.browsers.fs.watch.events(
watch_id="watch_id",
- id="id",
+ id_or_name="htzv5orfit78e1m2biiifpbv",
)
await watch_stream.response.aclose()
@@ -196,7 +196,7 @@ async def test_method_events(self, async_client: AsyncKernel) -> None:
async def test_raw_response_events(self, async_client: AsyncKernel) -> None:
response = await async_client.browsers.fs.watch.with_raw_response.events(
watch_id="watch_id",
- id="id",
+ id_or_name="htzv5orfit78e1m2biiifpbv",
)
assert response.http_request.headers.get("X-Stainless-Lang") == "python"
@@ -208,7 +208,7 @@ async def test_raw_response_events(self, async_client: AsyncKernel) -> None:
async def test_streaming_response_events(self, async_client: AsyncKernel) -> None:
async with async_client.browsers.fs.watch.with_streaming_response.events(
watch_id="watch_id",
- id="id",
+ id_or_name="htzv5orfit78e1m2biiifpbv",
) as response:
assert not response.is_closed
assert response.http_request.headers.get("X-Stainless-Lang") == "python"
@@ -221,23 +221,23 @@ async def test_streaming_response_events(self, async_client: AsyncKernel) -> Non
@pytest.mark.skip(reason="Mock server tests are disabled")
@parametrize
async def test_path_params_events(self, async_client: AsyncKernel) -> None:
- with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"):
+ with pytest.raises(ValueError, match=r"Expected a non-empty value for `id_or_name` but received ''"):
await async_client.browsers.fs.watch.with_raw_response.events(
watch_id="watch_id",
- id="",
+ id_or_name="",
)
with pytest.raises(ValueError, match=r"Expected a non-empty value for `watch_id` but received ''"):
await async_client.browsers.fs.watch.with_raw_response.events(
watch_id="",
- id="id",
+ id_or_name="htzv5orfit78e1m2biiifpbv",
)
@pytest.mark.skip(reason="Mock server tests are disabled")
@parametrize
async def test_method_start(self, async_client: AsyncKernel) -> None:
watch = await async_client.browsers.fs.watch.start(
- id="id",
+ id_or_name="htzv5orfit78e1m2biiifpbv",
path="path",
)
assert_matches_type(WatchStartResponse, watch, path=["response"])
@@ -246,7 +246,7 @@ async def test_method_start(self, async_client: AsyncKernel) -> None:
@parametrize
async def test_method_start_with_all_params(self, async_client: AsyncKernel) -> None:
watch = await async_client.browsers.fs.watch.start(
- id="id",
+ id_or_name="htzv5orfit78e1m2biiifpbv",
path="path",
recursive=True,
)
@@ -256,7 +256,7 @@ async def test_method_start_with_all_params(self, async_client: AsyncKernel) ->
@parametrize
async def test_raw_response_start(self, async_client: AsyncKernel) -> None:
response = await async_client.browsers.fs.watch.with_raw_response.start(
- id="id",
+ id_or_name="htzv5orfit78e1m2biiifpbv",
path="path",
)
@@ -269,7 +269,7 @@ async def test_raw_response_start(self, async_client: AsyncKernel) -> None:
@parametrize
async def test_streaming_response_start(self, async_client: AsyncKernel) -> None:
async with async_client.browsers.fs.watch.with_streaming_response.start(
- id="id",
+ id_or_name="htzv5orfit78e1m2biiifpbv",
path="path",
) as response:
assert not response.is_closed
@@ -283,9 +283,9 @@ async def test_streaming_response_start(self, async_client: AsyncKernel) -> None
@pytest.mark.skip(reason="Mock server tests are disabled")
@parametrize
async def test_path_params_start(self, async_client: AsyncKernel) -> None:
- with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"):
+ with pytest.raises(ValueError, match=r"Expected a non-empty value for `id_or_name` but received ''"):
await async_client.browsers.fs.watch.with_raw_response.start(
- id="",
+ id_or_name="",
path="path",
)
@@ -294,7 +294,7 @@ async def test_path_params_start(self, async_client: AsyncKernel) -> None:
async def test_method_stop(self, async_client: AsyncKernel) -> None:
watch = await async_client.browsers.fs.watch.stop(
watch_id="watch_id",
- id="id",
+ id_or_name="htzv5orfit78e1m2biiifpbv",
)
assert watch is None
@@ -303,7 +303,7 @@ async def test_method_stop(self, async_client: AsyncKernel) -> None:
async def test_raw_response_stop(self, async_client: AsyncKernel) -> None:
response = await async_client.browsers.fs.watch.with_raw_response.stop(
watch_id="watch_id",
- id="id",
+ id_or_name="htzv5orfit78e1m2biiifpbv",
)
assert response.is_closed is True
@@ -316,7 +316,7 @@ async def test_raw_response_stop(self, async_client: AsyncKernel) -> None:
async def test_streaming_response_stop(self, async_client: AsyncKernel) -> None:
async with async_client.browsers.fs.watch.with_streaming_response.stop(
watch_id="watch_id",
- id="id",
+ id_or_name="htzv5orfit78e1m2biiifpbv",
) as response:
assert not response.is_closed
assert response.http_request.headers.get("X-Stainless-Lang") == "python"
@@ -329,14 +329,14 @@ async def test_streaming_response_stop(self, async_client: AsyncKernel) -> None:
@pytest.mark.skip(reason="Mock server tests are disabled")
@parametrize
async def test_path_params_stop(self, async_client: AsyncKernel) -> None:
- with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"):
+ with pytest.raises(ValueError, match=r"Expected a non-empty value for `id_or_name` but received ''"):
await async_client.browsers.fs.watch.with_raw_response.stop(
watch_id="watch_id",
- id="",
+ id_or_name="",
)
with pytest.raises(ValueError, match=r"Expected a non-empty value for `watch_id` but received ''"):
await async_client.browsers.fs.watch.with_raw_response.stop(
watch_id="",
- id="id",
+ id_or_name="htzv5orfit78e1m2biiifpbv",
)
diff --git a/tests/api_resources/browsers/test_computer.py b/tests/api_resources/browsers/test_computer.py
index 31974d5b..916dbcb0 100644
--- a/tests/api_resources/browsers/test_computer.py
+++ b/tests/api_resources/browsers/test_computer.py
@@ -33,7 +33,7 @@ class TestComputer:
@parametrize
def test_method_batch(self, client: Kernel) -> None:
computer = client.browsers.computer.batch(
- id="id",
+ id_or_name="htzv5orfit78e1m2biiifpbv",
actions=[{"type": "click_mouse"}],
)
assert computer is None
@@ -42,7 +42,7 @@ def test_method_batch(self, client: Kernel) -> None:
@parametrize
def test_raw_response_batch(self, client: Kernel) -> None:
response = client.browsers.computer.with_raw_response.batch(
- id="id",
+ id_or_name="htzv5orfit78e1m2biiifpbv",
actions=[{"type": "click_mouse"}],
)
@@ -55,7 +55,7 @@ def test_raw_response_batch(self, client: Kernel) -> None:
@parametrize
def test_streaming_response_batch(self, client: Kernel) -> None:
with client.browsers.computer.with_streaming_response.batch(
- id="id",
+ id_or_name="htzv5orfit78e1m2biiifpbv",
actions=[{"type": "click_mouse"}],
) as response:
assert not response.is_closed
@@ -69,18 +69,20 @@ def test_streaming_response_batch(self, client: Kernel) -> None:
@pytest.mark.skip(reason="Mock server tests are disabled")
@parametrize
def test_path_params_batch(self, client: Kernel) -> None:
- with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"):
+ with pytest.raises(ValueError, match=r"Expected a non-empty value for `id_or_name` but received ''"):
client.browsers.computer.with_raw_response.batch(
- id="",
+ id_or_name="",
actions=[{"type": "click_mouse"}],
)
@parametrize
@pytest.mark.respx(base_url=base_url)
def test_method_capture_screenshot(self, client: Kernel, respx_mock: MockRouter) -> None:
- respx_mock.post("/browsers/id/computer/screenshot").mock(return_value=httpx.Response(200, json={"foo": "bar"}))
+ respx_mock.post("/browsers/htzv5orfit78e1m2biiifpbv/computer/screenshot").mock(
+ return_value=httpx.Response(200, json={"foo": "bar"})
+ )
computer = client.browsers.computer.capture_screenshot(
- id="id",
+ id_or_name="htzv5orfit78e1m2biiifpbv",
)
assert computer.is_closed
assert computer.json() == {"foo": "bar"}
@@ -90,9 +92,11 @@ def test_method_capture_screenshot(self, client: Kernel, respx_mock: MockRouter)
@parametrize
@pytest.mark.respx(base_url=base_url)
def test_method_capture_screenshot_with_all_params(self, client: Kernel, respx_mock: MockRouter) -> None:
- respx_mock.post("/browsers/id/computer/screenshot").mock(return_value=httpx.Response(200, json={"foo": "bar"}))
+ respx_mock.post("/browsers/htzv5orfit78e1m2biiifpbv/computer/screenshot").mock(
+ return_value=httpx.Response(200, json={"foo": "bar"})
+ )
computer = client.browsers.computer.capture_screenshot(
- id="id",
+ id_or_name="htzv5orfit78e1m2biiifpbv",
region={
"height": 0,
"width": 0,
@@ -108,10 +112,12 @@ def test_method_capture_screenshot_with_all_params(self, client: Kernel, respx_m
@parametrize
@pytest.mark.respx(base_url=base_url)
def test_raw_response_capture_screenshot(self, client: Kernel, respx_mock: MockRouter) -> None:
- respx_mock.post("/browsers/id/computer/screenshot").mock(return_value=httpx.Response(200, json={"foo": "bar"}))
+ respx_mock.post("/browsers/htzv5orfit78e1m2biiifpbv/computer/screenshot").mock(
+ return_value=httpx.Response(200, json={"foo": "bar"})
+ )
computer = client.browsers.computer.with_raw_response.capture_screenshot(
- id="id",
+ id_or_name="htzv5orfit78e1m2biiifpbv",
)
assert computer.is_closed is True
@@ -122,9 +128,11 @@ def test_raw_response_capture_screenshot(self, client: Kernel, respx_mock: MockR
@parametrize
@pytest.mark.respx(base_url=base_url)
def test_streaming_response_capture_screenshot(self, client: Kernel, respx_mock: MockRouter) -> None:
- respx_mock.post("/browsers/id/computer/screenshot").mock(return_value=httpx.Response(200, json={"foo": "bar"}))
+ respx_mock.post("/browsers/htzv5orfit78e1m2biiifpbv/computer/screenshot").mock(
+ return_value=httpx.Response(200, json={"foo": "bar"})
+ )
with client.browsers.computer.with_streaming_response.capture_screenshot(
- id="id",
+ id_or_name="htzv5orfit78e1m2biiifpbv",
) as computer:
assert not computer.is_closed
assert computer.http_request.headers.get("X-Stainless-Lang") == "python"
@@ -138,16 +146,16 @@ def test_streaming_response_capture_screenshot(self, client: Kernel, respx_mock:
@parametrize
@pytest.mark.respx(base_url=base_url)
def test_path_params_capture_screenshot(self, client: Kernel) -> None:
- with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"):
+ with pytest.raises(ValueError, match=r"Expected a non-empty value for `id_or_name` but received ''"):
client.browsers.computer.with_raw_response.capture_screenshot(
- id="",
+ id_or_name="",
)
@pytest.mark.skip(reason="Mock server tests are disabled")
@parametrize
def test_method_click_mouse(self, client: Kernel) -> None:
computer = client.browsers.computer.click_mouse(
- id="id",
+ id_or_name="htzv5orfit78e1m2biiifpbv",
x=0,
y=0,
)
@@ -157,7 +165,7 @@ def test_method_click_mouse(self, client: Kernel) -> None:
@parametrize
def test_method_click_mouse_with_all_params(self, client: Kernel) -> None:
computer = client.browsers.computer.click_mouse(
- id="id",
+ id_or_name="htzv5orfit78e1m2biiifpbv",
x=0,
y=0,
button="left",
@@ -171,7 +179,7 @@ def test_method_click_mouse_with_all_params(self, client: Kernel) -> None:
@parametrize
def test_raw_response_click_mouse(self, client: Kernel) -> None:
response = client.browsers.computer.with_raw_response.click_mouse(
- id="id",
+ id_or_name="htzv5orfit78e1m2biiifpbv",
x=0,
y=0,
)
@@ -185,7 +193,7 @@ def test_raw_response_click_mouse(self, client: Kernel) -> None:
@parametrize
def test_streaming_response_click_mouse(self, client: Kernel) -> None:
with client.browsers.computer.with_streaming_response.click_mouse(
- id="id",
+ id_or_name="htzv5orfit78e1m2biiifpbv",
x=0,
y=0,
) as response:
@@ -200,9 +208,9 @@ def test_streaming_response_click_mouse(self, client: Kernel) -> None:
@pytest.mark.skip(reason="Mock server tests are disabled")
@parametrize
def test_path_params_click_mouse(self, client: Kernel) -> None:
- with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"):
+ with pytest.raises(ValueError, match=r"Expected a non-empty value for `id_or_name` but received ''"):
client.browsers.computer.with_raw_response.click_mouse(
- id="",
+ id_or_name="",
x=0,
y=0,
)
@@ -211,7 +219,7 @@ def test_path_params_click_mouse(self, client: Kernel) -> None:
@parametrize
def test_method_drag_mouse(self, client: Kernel) -> None:
computer = client.browsers.computer.drag_mouse(
- id="id",
+ id_or_name="htzv5orfit78e1m2biiifpbv",
path=[[0, 0], [0, 0]],
)
assert computer is None
@@ -220,7 +228,7 @@ def test_method_drag_mouse(self, client: Kernel) -> None:
@parametrize
def test_method_drag_mouse_with_all_params(self, client: Kernel) -> None:
computer = client.browsers.computer.drag_mouse(
- id="id",
+ id_or_name="htzv5orfit78e1m2biiifpbv",
path=[[0, 0], [0, 0]],
button="left",
delay=0,
@@ -236,7 +244,7 @@ def test_method_drag_mouse_with_all_params(self, client: Kernel) -> None:
@parametrize
def test_raw_response_drag_mouse(self, client: Kernel) -> None:
response = client.browsers.computer.with_raw_response.drag_mouse(
- id="id",
+ id_or_name="htzv5orfit78e1m2biiifpbv",
path=[[0, 0], [0, 0]],
)
@@ -249,7 +257,7 @@ def test_raw_response_drag_mouse(self, client: Kernel) -> None:
@parametrize
def test_streaming_response_drag_mouse(self, client: Kernel) -> None:
with client.browsers.computer.with_streaming_response.drag_mouse(
- id="id",
+ id_or_name="htzv5orfit78e1m2biiifpbv",
path=[[0, 0], [0, 0]],
) as response:
assert not response.is_closed
@@ -263,9 +271,9 @@ def test_streaming_response_drag_mouse(self, client: Kernel) -> None:
@pytest.mark.skip(reason="Mock server tests are disabled")
@parametrize
def test_path_params_drag_mouse(self, client: Kernel) -> None:
- with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"):
+ with pytest.raises(ValueError, match=r"Expected a non-empty value for `id_or_name` but received ''"):
client.browsers.computer.with_raw_response.drag_mouse(
- id="",
+ id_or_name="",
path=[[0, 0], [0, 0]],
)
@@ -273,7 +281,7 @@ def test_path_params_drag_mouse(self, client: Kernel) -> None:
@parametrize
def test_method_get_mouse_position(self, client: Kernel) -> None:
computer = client.browsers.computer.get_mouse_position(
- "id",
+ "htzv5orfit78e1m2biiifpbv",
)
assert_matches_type(ComputerGetMousePositionResponse, computer, path=["response"])
@@ -281,7 +289,7 @@ def test_method_get_mouse_position(self, client: Kernel) -> None:
@parametrize
def test_raw_response_get_mouse_position(self, client: Kernel) -> None:
response = client.browsers.computer.with_raw_response.get_mouse_position(
- "id",
+ "htzv5orfit78e1m2biiifpbv",
)
assert response.is_closed is True
@@ -293,7 +301,7 @@ def test_raw_response_get_mouse_position(self, client: Kernel) -> None:
@parametrize
def test_streaming_response_get_mouse_position(self, client: Kernel) -> None:
with client.browsers.computer.with_streaming_response.get_mouse_position(
- "id",
+ "htzv5orfit78e1m2biiifpbv",
) as response:
assert not response.is_closed
assert response.http_request.headers.get("X-Stainless-Lang") == "python"
@@ -306,7 +314,7 @@ def test_streaming_response_get_mouse_position(self, client: Kernel) -> None:
@pytest.mark.skip(reason="Mock server tests are disabled")
@parametrize
def test_path_params_get_mouse_position(self, client: Kernel) -> None:
- with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"):
+ with pytest.raises(ValueError, match=r"Expected a non-empty value for `id_or_name` but received ''"):
client.browsers.computer.with_raw_response.get_mouse_position(
"",
)
@@ -315,7 +323,7 @@ def test_path_params_get_mouse_position(self, client: Kernel) -> None:
@parametrize
def test_method_move_mouse(self, client: Kernel) -> None:
computer = client.browsers.computer.move_mouse(
- id="id",
+ id_or_name="htzv5orfit78e1m2biiifpbv",
x=0,
y=0,
)
@@ -325,7 +333,7 @@ def test_method_move_mouse(self, client: Kernel) -> None:
@parametrize
def test_method_move_mouse_with_all_params(self, client: Kernel) -> None:
computer = client.browsers.computer.move_mouse(
- id="id",
+ id_or_name="htzv5orfit78e1m2biiifpbv",
x=0,
y=0,
duration_ms=50,
@@ -338,7 +346,7 @@ def test_method_move_mouse_with_all_params(self, client: Kernel) -> None:
@parametrize
def test_raw_response_move_mouse(self, client: Kernel) -> None:
response = client.browsers.computer.with_raw_response.move_mouse(
- id="id",
+ id_or_name="htzv5orfit78e1m2biiifpbv",
x=0,
y=0,
)
@@ -352,7 +360,7 @@ def test_raw_response_move_mouse(self, client: Kernel) -> None:
@parametrize
def test_streaming_response_move_mouse(self, client: Kernel) -> None:
with client.browsers.computer.with_streaming_response.move_mouse(
- id="id",
+ id_or_name="htzv5orfit78e1m2biiifpbv",
x=0,
y=0,
) as response:
@@ -367,9 +375,9 @@ def test_streaming_response_move_mouse(self, client: Kernel) -> None:
@pytest.mark.skip(reason="Mock server tests are disabled")
@parametrize
def test_path_params_move_mouse(self, client: Kernel) -> None:
- with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"):
+ with pytest.raises(ValueError, match=r"Expected a non-empty value for `id_or_name` but received ''"):
client.browsers.computer.with_raw_response.move_mouse(
- id="",
+ id_or_name="",
x=0,
y=0,
)
@@ -378,7 +386,7 @@ def test_path_params_move_mouse(self, client: Kernel) -> None:
@parametrize
def test_method_press_key(self, client: Kernel) -> None:
computer = client.browsers.computer.press_key(
- id="id",
+ id_or_name="htzv5orfit78e1m2biiifpbv",
keys=["string"],
)
assert computer is None
@@ -387,7 +395,7 @@ def test_method_press_key(self, client: Kernel) -> None:
@parametrize
def test_method_press_key_with_all_params(self, client: Kernel) -> None:
computer = client.browsers.computer.press_key(
- id="id",
+ id_or_name="htzv5orfit78e1m2biiifpbv",
keys=["string"],
duration=0,
hold_keys=["string"],
@@ -398,7 +406,7 @@ def test_method_press_key_with_all_params(self, client: Kernel) -> None:
@parametrize
def test_raw_response_press_key(self, client: Kernel) -> None:
response = client.browsers.computer.with_raw_response.press_key(
- id="id",
+ id_or_name="htzv5orfit78e1m2biiifpbv",
keys=["string"],
)
@@ -411,7 +419,7 @@ def test_raw_response_press_key(self, client: Kernel) -> None:
@parametrize
def test_streaming_response_press_key(self, client: Kernel) -> None:
with client.browsers.computer.with_streaming_response.press_key(
- id="id",
+ id_or_name="htzv5orfit78e1m2biiifpbv",
keys=["string"],
) as response:
assert not response.is_closed
@@ -425,9 +433,9 @@ def test_streaming_response_press_key(self, client: Kernel) -> None:
@pytest.mark.skip(reason="Mock server tests are disabled")
@parametrize
def test_path_params_press_key(self, client: Kernel) -> None:
- with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"):
+ with pytest.raises(ValueError, match=r"Expected a non-empty value for `id_or_name` but received ''"):
client.browsers.computer.with_raw_response.press_key(
- id="",
+ id_or_name="",
keys=["string"],
)
@@ -435,7 +443,7 @@ def test_path_params_press_key(self, client: Kernel) -> None:
@parametrize
def test_method_read_clipboard(self, client: Kernel) -> None:
computer = client.browsers.computer.read_clipboard(
- "id",
+ "htzv5orfit78e1m2biiifpbv",
)
assert_matches_type(ComputerReadClipboardResponse, computer, path=["response"])
@@ -443,7 +451,7 @@ def test_method_read_clipboard(self, client: Kernel) -> None:
@parametrize
def test_raw_response_read_clipboard(self, client: Kernel) -> None:
response = client.browsers.computer.with_raw_response.read_clipboard(
- "id",
+ "htzv5orfit78e1m2biiifpbv",
)
assert response.is_closed is True
@@ -455,7 +463,7 @@ def test_raw_response_read_clipboard(self, client: Kernel) -> None:
@parametrize
def test_streaming_response_read_clipboard(self, client: Kernel) -> None:
with client.browsers.computer.with_streaming_response.read_clipboard(
- "id",
+ "htzv5orfit78e1m2biiifpbv",
) as response:
assert not response.is_closed
assert response.http_request.headers.get("X-Stainless-Lang") == "python"
@@ -468,7 +476,7 @@ def test_streaming_response_read_clipboard(self, client: Kernel) -> None:
@pytest.mark.skip(reason="Mock server tests are disabled")
@parametrize
def test_path_params_read_clipboard(self, client: Kernel) -> None:
- with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"):
+ with pytest.raises(ValueError, match=r"Expected a non-empty value for `id_or_name` but received ''"):
client.browsers.computer.with_raw_response.read_clipboard(
"",
)
@@ -477,7 +485,7 @@ def test_path_params_read_clipboard(self, client: Kernel) -> None:
@parametrize
def test_method_scroll(self, client: Kernel) -> None:
computer = client.browsers.computer.scroll(
- id="id",
+ id_or_name="htzv5orfit78e1m2biiifpbv",
x=0,
y=0,
)
@@ -487,7 +495,7 @@ def test_method_scroll(self, client: Kernel) -> None:
@parametrize
def test_method_scroll_with_all_params(self, client: Kernel) -> None:
computer = client.browsers.computer.scroll(
- id="id",
+ id_or_name="htzv5orfit78e1m2biiifpbv",
x=0,
y=0,
delta_x=0,
@@ -500,7 +508,7 @@ def test_method_scroll_with_all_params(self, client: Kernel) -> None:
@parametrize
def test_raw_response_scroll(self, client: Kernel) -> None:
response = client.browsers.computer.with_raw_response.scroll(
- id="id",
+ id_or_name="htzv5orfit78e1m2biiifpbv",
x=0,
y=0,
)
@@ -514,7 +522,7 @@ def test_raw_response_scroll(self, client: Kernel) -> None:
@parametrize
def test_streaming_response_scroll(self, client: Kernel) -> None:
with client.browsers.computer.with_streaming_response.scroll(
- id="id",
+ id_or_name="htzv5orfit78e1m2biiifpbv",
x=0,
y=0,
) as response:
@@ -529,9 +537,9 @@ def test_streaming_response_scroll(self, client: Kernel) -> None:
@pytest.mark.skip(reason="Mock server tests are disabled")
@parametrize
def test_path_params_scroll(self, client: Kernel) -> None:
- with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"):
+ with pytest.raises(ValueError, match=r"Expected a non-empty value for `id_or_name` but received ''"):
client.browsers.computer.with_raw_response.scroll(
- id="",
+ id_or_name="",
x=0,
y=0,
)
@@ -540,7 +548,7 @@ def test_path_params_scroll(self, client: Kernel) -> None:
@parametrize
def test_method_set_cursor_visibility(self, client: Kernel) -> None:
computer = client.browsers.computer.set_cursor_visibility(
- id="id",
+ id_or_name="htzv5orfit78e1m2biiifpbv",
hidden=True,
)
assert_matches_type(ComputerSetCursorVisibilityResponse, computer, path=["response"])
@@ -549,7 +557,7 @@ def test_method_set_cursor_visibility(self, client: Kernel) -> None:
@parametrize
def test_raw_response_set_cursor_visibility(self, client: Kernel) -> None:
response = client.browsers.computer.with_raw_response.set_cursor_visibility(
- id="id",
+ id_or_name="htzv5orfit78e1m2biiifpbv",
hidden=True,
)
@@ -562,7 +570,7 @@ def test_raw_response_set_cursor_visibility(self, client: Kernel) -> None:
@parametrize
def test_streaming_response_set_cursor_visibility(self, client: Kernel) -> None:
with client.browsers.computer.with_streaming_response.set_cursor_visibility(
- id="id",
+ id_or_name="htzv5orfit78e1m2biiifpbv",
hidden=True,
) as response:
assert not response.is_closed
@@ -576,9 +584,9 @@ def test_streaming_response_set_cursor_visibility(self, client: Kernel) -> None:
@pytest.mark.skip(reason="Mock server tests are disabled")
@parametrize
def test_path_params_set_cursor_visibility(self, client: Kernel) -> None:
- with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"):
+ with pytest.raises(ValueError, match=r"Expected a non-empty value for `id_or_name` but received ''"):
client.browsers.computer.with_raw_response.set_cursor_visibility(
- id="",
+ id_or_name="",
hidden=True,
)
@@ -586,7 +594,7 @@ def test_path_params_set_cursor_visibility(self, client: Kernel) -> None:
@parametrize
def test_method_type_text(self, client: Kernel) -> None:
computer = client.browsers.computer.type_text(
- id="id",
+ id_or_name="htzv5orfit78e1m2biiifpbv",
text="text",
)
assert computer is None
@@ -595,7 +603,7 @@ def test_method_type_text(self, client: Kernel) -> None:
@parametrize
def test_method_type_text_with_all_params(self, client: Kernel) -> None:
computer = client.browsers.computer.type_text(
- id="id",
+ id_or_name="htzv5orfit78e1m2biiifpbv",
text="text",
delay=0,
)
@@ -605,7 +613,7 @@ def test_method_type_text_with_all_params(self, client: Kernel) -> None:
@parametrize
def test_raw_response_type_text(self, client: Kernel) -> None:
response = client.browsers.computer.with_raw_response.type_text(
- id="id",
+ id_or_name="htzv5orfit78e1m2biiifpbv",
text="text",
)
@@ -618,7 +626,7 @@ def test_raw_response_type_text(self, client: Kernel) -> None:
@parametrize
def test_streaming_response_type_text(self, client: Kernel) -> None:
with client.browsers.computer.with_streaming_response.type_text(
- id="id",
+ id_or_name="htzv5orfit78e1m2biiifpbv",
text="text",
) as response:
assert not response.is_closed
@@ -632,9 +640,9 @@ def test_streaming_response_type_text(self, client: Kernel) -> None:
@pytest.mark.skip(reason="Mock server tests are disabled")
@parametrize
def test_path_params_type_text(self, client: Kernel) -> None:
- with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"):
+ with pytest.raises(ValueError, match=r"Expected a non-empty value for `id_or_name` but received ''"):
client.browsers.computer.with_raw_response.type_text(
- id="",
+ id_or_name="",
text="text",
)
@@ -642,7 +650,7 @@ def test_path_params_type_text(self, client: Kernel) -> None:
@parametrize
def test_method_write_clipboard(self, client: Kernel) -> None:
computer = client.browsers.computer.write_clipboard(
- id="id",
+ id_or_name="htzv5orfit78e1m2biiifpbv",
text="text",
)
assert computer is None
@@ -651,7 +659,7 @@ def test_method_write_clipboard(self, client: Kernel) -> None:
@parametrize
def test_raw_response_write_clipboard(self, client: Kernel) -> None:
response = client.browsers.computer.with_raw_response.write_clipboard(
- id="id",
+ id_or_name="htzv5orfit78e1m2biiifpbv",
text="text",
)
@@ -664,7 +672,7 @@ def test_raw_response_write_clipboard(self, client: Kernel) -> None:
@parametrize
def test_streaming_response_write_clipboard(self, client: Kernel) -> None:
with client.browsers.computer.with_streaming_response.write_clipboard(
- id="id",
+ id_or_name="htzv5orfit78e1m2biiifpbv",
text="text",
) as response:
assert not response.is_closed
@@ -678,9 +686,9 @@ def test_streaming_response_write_clipboard(self, client: Kernel) -> None:
@pytest.mark.skip(reason="Mock server tests are disabled")
@parametrize
def test_path_params_write_clipboard(self, client: Kernel) -> None:
- with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"):
+ with pytest.raises(ValueError, match=r"Expected a non-empty value for `id_or_name` but received ''"):
client.browsers.computer.with_raw_response.write_clipboard(
- id="",
+ id_or_name="",
text="text",
)
@@ -694,7 +702,7 @@ class TestAsyncComputer:
@parametrize
async def test_method_batch(self, async_client: AsyncKernel) -> None:
computer = await async_client.browsers.computer.batch(
- id="id",
+ id_or_name="htzv5orfit78e1m2biiifpbv",
actions=[{"type": "click_mouse"}],
)
assert computer is None
@@ -703,7 +711,7 @@ async def test_method_batch(self, async_client: AsyncKernel) -> None:
@parametrize
async def test_raw_response_batch(self, async_client: AsyncKernel) -> None:
response = await async_client.browsers.computer.with_raw_response.batch(
- id="id",
+ id_or_name="htzv5orfit78e1m2biiifpbv",
actions=[{"type": "click_mouse"}],
)
@@ -716,7 +724,7 @@ async def test_raw_response_batch(self, async_client: AsyncKernel) -> None:
@parametrize
async def test_streaming_response_batch(self, async_client: AsyncKernel) -> None:
async with async_client.browsers.computer.with_streaming_response.batch(
- id="id",
+ id_or_name="htzv5orfit78e1m2biiifpbv",
actions=[{"type": "click_mouse"}],
) as response:
assert not response.is_closed
@@ -730,18 +738,20 @@ async def test_streaming_response_batch(self, async_client: AsyncKernel) -> None
@pytest.mark.skip(reason="Mock server tests are disabled")
@parametrize
async def test_path_params_batch(self, async_client: AsyncKernel) -> None:
- with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"):
+ with pytest.raises(ValueError, match=r"Expected a non-empty value for `id_or_name` but received ''"):
await async_client.browsers.computer.with_raw_response.batch(
- id="",
+ id_or_name="",
actions=[{"type": "click_mouse"}],
)
@parametrize
@pytest.mark.respx(base_url=base_url)
async def test_method_capture_screenshot(self, async_client: AsyncKernel, respx_mock: MockRouter) -> None:
- respx_mock.post("/browsers/id/computer/screenshot").mock(return_value=httpx.Response(200, json={"foo": "bar"}))
+ respx_mock.post("/browsers/htzv5orfit78e1m2biiifpbv/computer/screenshot").mock(
+ return_value=httpx.Response(200, json={"foo": "bar"})
+ )
computer = await async_client.browsers.computer.capture_screenshot(
- id="id",
+ id_or_name="htzv5orfit78e1m2biiifpbv",
)
assert computer.is_closed
assert await computer.json() == {"foo": "bar"}
@@ -753,9 +763,11 @@ async def test_method_capture_screenshot(self, async_client: AsyncKernel, respx_
async def test_method_capture_screenshot_with_all_params(
self, async_client: AsyncKernel, respx_mock: MockRouter
) -> None:
- respx_mock.post("/browsers/id/computer/screenshot").mock(return_value=httpx.Response(200, json={"foo": "bar"}))
+ respx_mock.post("/browsers/htzv5orfit78e1m2biiifpbv/computer/screenshot").mock(
+ return_value=httpx.Response(200, json={"foo": "bar"})
+ )
computer = await async_client.browsers.computer.capture_screenshot(
- id="id",
+ id_or_name="htzv5orfit78e1m2biiifpbv",
region={
"height": 0,
"width": 0,
@@ -771,10 +783,12 @@ async def test_method_capture_screenshot_with_all_params(
@parametrize
@pytest.mark.respx(base_url=base_url)
async def test_raw_response_capture_screenshot(self, async_client: AsyncKernel, respx_mock: MockRouter) -> None:
- respx_mock.post("/browsers/id/computer/screenshot").mock(return_value=httpx.Response(200, json={"foo": "bar"}))
+ respx_mock.post("/browsers/htzv5orfit78e1m2biiifpbv/computer/screenshot").mock(
+ return_value=httpx.Response(200, json={"foo": "bar"})
+ )
computer = await async_client.browsers.computer.with_raw_response.capture_screenshot(
- id="id",
+ id_or_name="htzv5orfit78e1m2biiifpbv",
)
assert computer.is_closed is True
@@ -787,9 +801,11 @@ async def test_raw_response_capture_screenshot(self, async_client: AsyncKernel,
async def test_streaming_response_capture_screenshot(
self, async_client: AsyncKernel, respx_mock: MockRouter
) -> None:
- respx_mock.post("/browsers/id/computer/screenshot").mock(return_value=httpx.Response(200, json={"foo": "bar"}))
+ respx_mock.post("/browsers/htzv5orfit78e1m2biiifpbv/computer/screenshot").mock(
+ return_value=httpx.Response(200, json={"foo": "bar"})
+ )
async with async_client.browsers.computer.with_streaming_response.capture_screenshot(
- id="id",
+ id_or_name="htzv5orfit78e1m2biiifpbv",
) as computer:
assert not computer.is_closed
assert computer.http_request.headers.get("X-Stainless-Lang") == "python"
@@ -803,16 +819,16 @@ async def test_streaming_response_capture_screenshot(
@parametrize
@pytest.mark.respx(base_url=base_url)
async def test_path_params_capture_screenshot(self, async_client: AsyncKernel) -> None:
- with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"):
+ with pytest.raises(ValueError, match=r"Expected a non-empty value for `id_or_name` but received ''"):
await async_client.browsers.computer.with_raw_response.capture_screenshot(
- id="",
+ id_or_name="",
)
@pytest.mark.skip(reason="Mock server tests are disabled")
@parametrize
async def test_method_click_mouse(self, async_client: AsyncKernel) -> None:
computer = await async_client.browsers.computer.click_mouse(
- id="id",
+ id_or_name="htzv5orfit78e1m2biiifpbv",
x=0,
y=0,
)
@@ -822,7 +838,7 @@ async def test_method_click_mouse(self, async_client: AsyncKernel) -> None:
@parametrize
async def test_method_click_mouse_with_all_params(self, async_client: AsyncKernel) -> None:
computer = await async_client.browsers.computer.click_mouse(
- id="id",
+ id_or_name="htzv5orfit78e1m2biiifpbv",
x=0,
y=0,
button="left",
@@ -836,7 +852,7 @@ async def test_method_click_mouse_with_all_params(self, async_client: AsyncKerne
@parametrize
async def test_raw_response_click_mouse(self, async_client: AsyncKernel) -> None:
response = await async_client.browsers.computer.with_raw_response.click_mouse(
- id="id",
+ id_or_name="htzv5orfit78e1m2biiifpbv",
x=0,
y=0,
)
@@ -850,7 +866,7 @@ async def test_raw_response_click_mouse(self, async_client: AsyncKernel) -> None
@parametrize
async def test_streaming_response_click_mouse(self, async_client: AsyncKernel) -> None:
async with async_client.browsers.computer.with_streaming_response.click_mouse(
- id="id",
+ id_or_name="htzv5orfit78e1m2biiifpbv",
x=0,
y=0,
) as response:
@@ -865,9 +881,9 @@ async def test_streaming_response_click_mouse(self, async_client: AsyncKernel) -
@pytest.mark.skip(reason="Mock server tests are disabled")
@parametrize
async def test_path_params_click_mouse(self, async_client: AsyncKernel) -> None:
- with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"):
+ with pytest.raises(ValueError, match=r"Expected a non-empty value for `id_or_name` but received ''"):
await async_client.browsers.computer.with_raw_response.click_mouse(
- id="",
+ id_or_name="",
x=0,
y=0,
)
@@ -876,7 +892,7 @@ async def test_path_params_click_mouse(self, async_client: AsyncKernel) -> None:
@parametrize
async def test_method_drag_mouse(self, async_client: AsyncKernel) -> None:
computer = await async_client.browsers.computer.drag_mouse(
- id="id",
+ id_or_name="htzv5orfit78e1m2biiifpbv",
path=[[0, 0], [0, 0]],
)
assert computer is None
@@ -885,7 +901,7 @@ async def test_method_drag_mouse(self, async_client: AsyncKernel) -> None:
@parametrize
async def test_method_drag_mouse_with_all_params(self, async_client: AsyncKernel) -> None:
computer = await async_client.browsers.computer.drag_mouse(
- id="id",
+ id_or_name="htzv5orfit78e1m2biiifpbv",
path=[[0, 0], [0, 0]],
button="left",
delay=0,
@@ -901,7 +917,7 @@ async def test_method_drag_mouse_with_all_params(self, async_client: AsyncKernel
@parametrize
async def test_raw_response_drag_mouse(self, async_client: AsyncKernel) -> None:
response = await async_client.browsers.computer.with_raw_response.drag_mouse(
- id="id",
+ id_or_name="htzv5orfit78e1m2biiifpbv",
path=[[0, 0], [0, 0]],
)
@@ -914,7 +930,7 @@ async def test_raw_response_drag_mouse(self, async_client: AsyncKernel) -> None:
@parametrize
async def test_streaming_response_drag_mouse(self, async_client: AsyncKernel) -> None:
async with async_client.browsers.computer.with_streaming_response.drag_mouse(
- id="id",
+ id_or_name="htzv5orfit78e1m2biiifpbv",
path=[[0, 0], [0, 0]],
) as response:
assert not response.is_closed
@@ -928,9 +944,9 @@ async def test_streaming_response_drag_mouse(self, async_client: AsyncKernel) ->
@pytest.mark.skip(reason="Mock server tests are disabled")
@parametrize
async def test_path_params_drag_mouse(self, async_client: AsyncKernel) -> None:
- with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"):
+ with pytest.raises(ValueError, match=r"Expected a non-empty value for `id_or_name` but received ''"):
await async_client.browsers.computer.with_raw_response.drag_mouse(
- id="",
+ id_or_name="",
path=[[0, 0], [0, 0]],
)
@@ -938,7 +954,7 @@ async def test_path_params_drag_mouse(self, async_client: AsyncKernel) -> None:
@parametrize
async def test_method_get_mouse_position(self, async_client: AsyncKernel) -> None:
computer = await async_client.browsers.computer.get_mouse_position(
- "id",
+ "htzv5orfit78e1m2biiifpbv",
)
assert_matches_type(ComputerGetMousePositionResponse, computer, path=["response"])
@@ -946,7 +962,7 @@ async def test_method_get_mouse_position(self, async_client: AsyncKernel) -> Non
@parametrize
async def test_raw_response_get_mouse_position(self, async_client: AsyncKernel) -> None:
response = await async_client.browsers.computer.with_raw_response.get_mouse_position(
- "id",
+ "htzv5orfit78e1m2biiifpbv",
)
assert response.is_closed is True
@@ -958,7 +974,7 @@ async def test_raw_response_get_mouse_position(self, async_client: AsyncKernel)
@parametrize
async def test_streaming_response_get_mouse_position(self, async_client: AsyncKernel) -> None:
async with async_client.browsers.computer.with_streaming_response.get_mouse_position(
- "id",
+ "htzv5orfit78e1m2biiifpbv",
) as response:
assert not response.is_closed
assert response.http_request.headers.get("X-Stainless-Lang") == "python"
@@ -971,7 +987,7 @@ async def test_streaming_response_get_mouse_position(self, async_client: AsyncKe
@pytest.mark.skip(reason="Mock server tests are disabled")
@parametrize
async def test_path_params_get_mouse_position(self, async_client: AsyncKernel) -> None:
- with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"):
+ with pytest.raises(ValueError, match=r"Expected a non-empty value for `id_or_name` but received ''"):
await async_client.browsers.computer.with_raw_response.get_mouse_position(
"",
)
@@ -980,7 +996,7 @@ async def test_path_params_get_mouse_position(self, async_client: AsyncKernel) -
@parametrize
async def test_method_move_mouse(self, async_client: AsyncKernel) -> None:
computer = await async_client.browsers.computer.move_mouse(
- id="id",
+ id_or_name="htzv5orfit78e1m2biiifpbv",
x=0,
y=0,
)
@@ -990,7 +1006,7 @@ async def test_method_move_mouse(self, async_client: AsyncKernel) -> None:
@parametrize
async def test_method_move_mouse_with_all_params(self, async_client: AsyncKernel) -> None:
computer = await async_client.browsers.computer.move_mouse(
- id="id",
+ id_or_name="htzv5orfit78e1m2biiifpbv",
x=0,
y=0,
duration_ms=50,
@@ -1003,7 +1019,7 @@ async def test_method_move_mouse_with_all_params(self, async_client: AsyncKernel
@parametrize
async def test_raw_response_move_mouse(self, async_client: AsyncKernel) -> None:
response = await async_client.browsers.computer.with_raw_response.move_mouse(
- id="id",
+ id_or_name="htzv5orfit78e1m2biiifpbv",
x=0,
y=0,
)
@@ -1017,7 +1033,7 @@ async def test_raw_response_move_mouse(self, async_client: AsyncKernel) -> None:
@parametrize
async def test_streaming_response_move_mouse(self, async_client: AsyncKernel) -> None:
async with async_client.browsers.computer.with_streaming_response.move_mouse(
- id="id",
+ id_or_name="htzv5orfit78e1m2biiifpbv",
x=0,
y=0,
) as response:
@@ -1032,9 +1048,9 @@ async def test_streaming_response_move_mouse(self, async_client: AsyncKernel) ->
@pytest.mark.skip(reason="Mock server tests are disabled")
@parametrize
async def test_path_params_move_mouse(self, async_client: AsyncKernel) -> None:
- with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"):
+ with pytest.raises(ValueError, match=r"Expected a non-empty value for `id_or_name` but received ''"):
await async_client.browsers.computer.with_raw_response.move_mouse(
- id="",
+ id_or_name="",
x=0,
y=0,
)
@@ -1043,7 +1059,7 @@ async def test_path_params_move_mouse(self, async_client: AsyncKernel) -> None:
@parametrize
async def test_method_press_key(self, async_client: AsyncKernel) -> None:
computer = await async_client.browsers.computer.press_key(
- id="id",
+ id_or_name="htzv5orfit78e1m2biiifpbv",
keys=["string"],
)
assert computer is None
@@ -1052,7 +1068,7 @@ async def test_method_press_key(self, async_client: AsyncKernel) -> None:
@parametrize
async def test_method_press_key_with_all_params(self, async_client: AsyncKernel) -> None:
computer = await async_client.browsers.computer.press_key(
- id="id",
+ id_or_name="htzv5orfit78e1m2biiifpbv",
keys=["string"],
duration=0,
hold_keys=["string"],
@@ -1063,7 +1079,7 @@ async def test_method_press_key_with_all_params(self, async_client: AsyncKernel)
@parametrize
async def test_raw_response_press_key(self, async_client: AsyncKernel) -> None:
response = await async_client.browsers.computer.with_raw_response.press_key(
- id="id",
+ id_or_name="htzv5orfit78e1m2biiifpbv",
keys=["string"],
)
@@ -1076,7 +1092,7 @@ async def test_raw_response_press_key(self, async_client: AsyncKernel) -> None:
@parametrize
async def test_streaming_response_press_key(self, async_client: AsyncKernel) -> None:
async with async_client.browsers.computer.with_streaming_response.press_key(
- id="id",
+ id_or_name="htzv5orfit78e1m2biiifpbv",
keys=["string"],
) as response:
assert not response.is_closed
@@ -1090,9 +1106,9 @@ async def test_streaming_response_press_key(self, async_client: AsyncKernel) ->
@pytest.mark.skip(reason="Mock server tests are disabled")
@parametrize
async def test_path_params_press_key(self, async_client: AsyncKernel) -> None:
- with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"):
+ with pytest.raises(ValueError, match=r"Expected a non-empty value for `id_or_name` but received ''"):
await async_client.browsers.computer.with_raw_response.press_key(
- id="",
+ id_or_name="",
keys=["string"],
)
@@ -1100,7 +1116,7 @@ async def test_path_params_press_key(self, async_client: AsyncKernel) -> None:
@parametrize
async def test_method_read_clipboard(self, async_client: AsyncKernel) -> None:
computer = await async_client.browsers.computer.read_clipboard(
- "id",
+ "htzv5orfit78e1m2biiifpbv",
)
assert_matches_type(ComputerReadClipboardResponse, computer, path=["response"])
@@ -1108,7 +1124,7 @@ async def test_method_read_clipboard(self, async_client: AsyncKernel) -> None:
@parametrize
async def test_raw_response_read_clipboard(self, async_client: AsyncKernel) -> None:
response = await async_client.browsers.computer.with_raw_response.read_clipboard(
- "id",
+ "htzv5orfit78e1m2biiifpbv",
)
assert response.is_closed is True
@@ -1120,7 +1136,7 @@ async def test_raw_response_read_clipboard(self, async_client: AsyncKernel) -> N
@parametrize
async def test_streaming_response_read_clipboard(self, async_client: AsyncKernel) -> None:
async with async_client.browsers.computer.with_streaming_response.read_clipboard(
- "id",
+ "htzv5orfit78e1m2biiifpbv",
) as response:
assert not response.is_closed
assert response.http_request.headers.get("X-Stainless-Lang") == "python"
@@ -1133,7 +1149,7 @@ async def test_streaming_response_read_clipboard(self, async_client: AsyncKernel
@pytest.mark.skip(reason="Mock server tests are disabled")
@parametrize
async def test_path_params_read_clipboard(self, async_client: AsyncKernel) -> None:
- with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"):
+ with pytest.raises(ValueError, match=r"Expected a non-empty value for `id_or_name` but received ''"):
await async_client.browsers.computer.with_raw_response.read_clipboard(
"",
)
@@ -1142,7 +1158,7 @@ async def test_path_params_read_clipboard(self, async_client: AsyncKernel) -> No
@parametrize
async def test_method_scroll(self, async_client: AsyncKernel) -> None:
computer = await async_client.browsers.computer.scroll(
- id="id",
+ id_or_name="htzv5orfit78e1m2biiifpbv",
x=0,
y=0,
)
@@ -1152,7 +1168,7 @@ async def test_method_scroll(self, async_client: AsyncKernel) -> None:
@parametrize
async def test_method_scroll_with_all_params(self, async_client: AsyncKernel) -> None:
computer = await async_client.browsers.computer.scroll(
- id="id",
+ id_or_name="htzv5orfit78e1m2biiifpbv",
x=0,
y=0,
delta_x=0,
@@ -1165,7 +1181,7 @@ async def test_method_scroll_with_all_params(self, async_client: AsyncKernel) ->
@parametrize
async def test_raw_response_scroll(self, async_client: AsyncKernel) -> None:
response = await async_client.browsers.computer.with_raw_response.scroll(
- id="id",
+ id_or_name="htzv5orfit78e1m2biiifpbv",
x=0,
y=0,
)
@@ -1179,7 +1195,7 @@ async def test_raw_response_scroll(self, async_client: AsyncKernel) -> None:
@parametrize
async def test_streaming_response_scroll(self, async_client: AsyncKernel) -> None:
async with async_client.browsers.computer.with_streaming_response.scroll(
- id="id",
+ id_or_name="htzv5orfit78e1m2biiifpbv",
x=0,
y=0,
) as response:
@@ -1194,9 +1210,9 @@ async def test_streaming_response_scroll(self, async_client: AsyncKernel) -> Non
@pytest.mark.skip(reason="Mock server tests are disabled")
@parametrize
async def test_path_params_scroll(self, async_client: AsyncKernel) -> None:
- with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"):
+ with pytest.raises(ValueError, match=r"Expected a non-empty value for `id_or_name` but received ''"):
await async_client.browsers.computer.with_raw_response.scroll(
- id="",
+ id_or_name="",
x=0,
y=0,
)
@@ -1205,7 +1221,7 @@ async def test_path_params_scroll(self, async_client: AsyncKernel) -> None:
@parametrize
async def test_method_set_cursor_visibility(self, async_client: AsyncKernel) -> None:
computer = await async_client.browsers.computer.set_cursor_visibility(
- id="id",
+ id_or_name="htzv5orfit78e1m2biiifpbv",
hidden=True,
)
assert_matches_type(ComputerSetCursorVisibilityResponse, computer, path=["response"])
@@ -1214,7 +1230,7 @@ async def test_method_set_cursor_visibility(self, async_client: AsyncKernel) ->
@parametrize
async def test_raw_response_set_cursor_visibility(self, async_client: AsyncKernel) -> None:
response = await async_client.browsers.computer.with_raw_response.set_cursor_visibility(
- id="id",
+ id_or_name="htzv5orfit78e1m2biiifpbv",
hidden=True,
)
@@ -1227,7 +1243,7 @@ async def test_raw_response_set_cursor_visibility(self, async_client: AsyncKerne
@parametrize
async def test_streaming_response_set_cursor_visibility(self, async_client: AsyncKernel) -> None:
async with async_client.browsers.computer.with_streaming_response.set_cursor_visibility(
- id="id",
+ id_or_name="htzv5orfit78e1m2biiifpbv",
hidden=True,
) as response:
assert not response.is_closed
@@ -1241,9 +1257,9 @@ async def test_streaming_response_set_cursor_visibility(self, async_client: Asyn
@pytest.mark.skip(reason="Mock server tests are disabled")
@parametrize
async def test_path_params_set_cursor_visibility(self, async_client: AsyncKernel) -> None:
- with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"):
+ with pytest.raises(ValueError, match=r"Expected a non-empty value for `id_or_name` but received ''"):
await async_client.browsers.computer.with_raw_response.set_cursor_visibility(
- id="",
+ id_or_name="",
hidden=True,
)
@@ -1251,7 +1267,7 @@ async def test_path_params_set_cursor_visibility(self, async_client: AsyncKernel
@parametrize
async def test_method_type_text(self, async_client: AsyncKernel) -> None:
computer = await async_client.browsers.computer.type_text(
- id="id",
+ id_or_name="htzv5orfit78e1m2biiifpbv",
text="text",
)
assert computer is None
@@ -1260,7 +1276,7 @@ async def test_method_type_text(self, async_client: AsyncKernel) -> None:
@parametrize
async def test_method_type_text_with_all_params(self, async_client: AsyncKernel) -> None:
computer = await async_client.browsers.computer.type_text(
- id="id",
+ id_or_name="htzv5orfit78e1m2biiifpbv",
text="text",
delay=0,
)
@@ -1270,7 +1286,7 @@ async def test_method_type_text_with_all_params(self, async_client: AsyncKernel)
@parametrize
async def test_raw_response_type_text(self, async_client: AsyncKernel) -> None:
response = await async_client.browsers.computer.with_raw_response.type_text(
- id="id",
+ id_or_name="htzv5orfit78e1m2biiifpbv",
text="text",
)
@@ -1283,7 +1299,7 @@ async def test_raw_response_type_text(self, async_client: AsyncKernel) -> None:
@parametrize
async def test_streaming_response_type_text(self, async_client: AsyncKernel) -> None:
async with async_client.browsers.computer.with_streaming_response.type_text(
- id="id",
+ id_or_name="htzv5orfit78e1m2biiifpbv",
text="text",
) as response:
assert not response.is_closed
@@ -1297,9 +1313,9 @@ async def test_streaming_response_type_text(self, async_client: AsyncKernel) ->
@pytest.mark.skip(reason="Mock server tests are disabled")
@parametrize
async def test_path_params_type_text(self, async_client: AsyncKernel) -> None:
- with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"):
+ with pytest.raises(ValueError, match=r"Expected a non-empty value for `id_or_name` but received ''"):
await async_client.browsers.computer.with_raw_response.type_text(
- id="",
+ id_or_name="",
text="text",
)
@@ -1307,7 +1323,7 @@ async def test_path_params_type_text(self, async_client: AsyncKernel) -> None:
@parametrize
async def test_method_write_clipboard(self, async_client: AsyncKernel) -> None:
computer = await async_client.browsers.computer.write_clipboard(
- id="id",
+ id_or_name="htzv5orfit78e1m2biiifpbv",
text="text",
)
assert computer is None
@@ -1316,7 +1332,7 @@ async def test_method_write_clipboard(self, async_client: AsyncKernel) -> None:
@parametrize
async def test_raw_response_write_clipboard(self, async_client: AsyncKernel) -> None:
response = await async_client.browsers.computer.with_raw_response.write_clipboard(
- id="id",
+ id_or_name="htzv5orfit78e1m2biiifpbv",
text="text",
)
@@ -1329,7 +1345,7 @@ async def test_raw_response_write_clipboard(self, async_client: AsyncKernel) ->
@parametrize
async def test_streaming_response_write_clipboard(self, async_client: AsyncKernel) -> None:
async with async_client.browsers.computer.with_streaming_response.write_clipboard(
- id="id",
+ id_or_name="htzv5orfit78e1m2biiifpbv",
text="text",
) as response:
assert not response.is_closed
@@ -1343,8 +1359,8 @@ async def test_streaming_response_write_clipboard(self, async_client: AsyncKerne
@pytest.mark.skip(reason="Mock server tests are disabled")
@parametrize
async def test_path_params_write_clipboard(self, async_client: AsyncKernel) -> None:
- with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"):
+ with pytest.raises(ValueError, match=r"Expected a non-empty value for `id_or_name` but received ''"):
await async_client.browsers.computer.with_raw_response.write_clipboard(
- id="",
+ id_or_name="",
text="text",
)
diff --git a/tests/api_resources/browsers/test_fs.py b/tests/api_resources/browsers/test_fs.py
index ceb179a0..f980777a 100644
--- a/tests/api_resources/browsers/test_fs.py
+++ b/tests/api_resources/browsers/test_fs.py
@@ -32,7 +32,7 @@ class TestFs:
@parametrize
def test_method_create_directory(self, client: Kernel) -> None:
f = client.browsers.fs.create_directory(
- id="id",
+ id_or_name="htzv5orfit78e1m2biiifpbv",
path="/J!",
)
assert f is None
@@ -41,7 +41,7 @@ def test_method_create_directory(self, client: Kernel) -> None:
@parametrize
def test_method_create_directory_with_all_params(self, client: Kernel) -> None:
f = client.browsers.fs.create_directory(
- id="id",
+ id_or_name="htzv5orfit78e1m2biiifpbv",
path="/J!",
mode="0611",
)
@@ -51,7 +51,7 @@ def test_method_create_directory_with_all_params(self, client: Kernel) -> None:
@parametrize
def test_raw_response_create_directory(self, client: Kernel) -> None:
response = client.browsers.fs.with_raw_response.create_directory(
- id="id",
+ id_or_name="htzv5orfit78e1m2biiifpbv",
path="/J!",
)
@@ -64,7 +64,7 @@ def test_raw_response_create_directory(self, client: Kernel) -> None:
@parametrize
def test_streaming_response_create_directory(self, client: Kernel) -> None:
with client.browsers.fs.with_streaming_response.create_directory(
- id="id",
+ id_or_name="htzv5orfit78e1m2biiifpbv",
path="/J!",
) as response:
assert not response.is_closed
@@ -78,9 +78,9 @@ def test_streaming_response_create_directory(self, client: Kernel) -> None:
@pytest.mark.skip(reason="Mock server tests are disabled")
@parametrize
def test_path_params_create_directory(self, client: Kernel) -> None:
- with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"):
+ with pytest.raises(ValueError, match=r"Expected a non-empty value for `id_or_name` but received ''"):
client.browsers.fs.with_raw_response.create_directory(
- id="",
+ id_or_name="",
path="/J!",
)
@@ -88,7 +88,7 @@ def test_path_params_create_directory(self, client: Kernel) -> None:
@parametrize
def test_method_delete_directory(self, client: Kernel) -> None:
f = client.browsers.fs.delete_directory(
- id="id",
+ id_or_name="htzv5orfit78e1m2biiifpbv",
path="/J!",
)
assert f is None
@@ -97,7 +97,7 @@ def test_method_delete_directory(self, client: Kernel) -> None:
@parametrize
def test_raw_response_delete_directory(self, client: Kernel) -> None:
response = client.browsers.fs.with_raw_response.delete_directory(
- id="id",
+ id_or_name="htzv5orfit78e1m2biiifpbv",
path="/J!",
)
@@ -110,7 +110,7 @@ def test_raw_response_delete_directory(self, client: Kernel) -> None:
@parametrize
def test_streaming_response_delete_directory(self, client: Kernel) -> None:
with client.browsers.fs.with_streaming_response.delete_directory(
- id="id",
+ id_or_name="htzv5orfit78e1m2biiifpbv",
path="/J!",
) as response:
assert not response.is_closed
@@ -124,9 +124,9 @@ def test_streaming_response_delete_directory(self, client: Kernel) -> None:
@pytest.mark.skip(reason="Mock server tests are disabled")
@parametrize
def test_path_params_delete_directory(self, client: Kernel) -> None:
- with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"):
+ with pytest.raises(ValueError, match=r"Expected a non-empty value for `id_or_name` but received ''"):
client.browsers.fs.with_raw_response.delete_directory(
- id="",
+ id_or_name="",
path="/J!",
)
@@ -134,7 +134,7 @@ def test_path_params_delete_directory(self, client: Kernel) -> None:
@parametrize
def test_method_delete_file(self, client: Kernel) -> None:
f = client.browsers.fs.delete_file(
- id="id",
+ id_or_name="htzv5orfit78e1m2biiifpbv",
path="/J!",
)
assert f is None
@@ -143,7 +143,7 @@ def test_method_delete_file(self, client: Kernel) -> None:
@parametrize
def test_raw_response_delete_file(self, client: Kernel) -> None:
response = client.browsers.fs.with_raw_response.delete_file(
- id="id",
+ id_or_name="htzv5orfit78e1m2biiifpbv",
path="/J!",
)
@@ -156,7 +156,7 @@ def test_raw_response_delete_file(self, client: Kernel) -> None:
@parametrize
def test_streaming_response_delete_file(self, client: Kernel) -> None:
with client.browsers.fs.with_streaming_response.delete_file(
- id="id",
+ id_or_name="htzv5orfit78e1m2biiifpbv",
path="/J!",
) as response:
assert not response.is_closed
@@ -170,18 +170,20 @@ def test_streaming_response_delete_file(self, client: Kernel) -> None:
@pytest.mark.skip(reason="Mock server tests are disabled")
@parametrize
def test_path_params_delete_file(self, client: Kernel) -> None:
- with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"):
+ with pytest.raises(ValueError, match=r"Expected a non-empty value for `id_or_name` but received ''"):
client.browsers.fs.with_raw_response.delete_file(
- id="",
+ id_or_name="",
path="/J!",
)
@parametrize
@pytest.mark.respx(base_url=base_url)
def test_method_download_dir_zip(self, client: Kernel, respx_mock: MockRouter) -> None:
- respx_mock.get("/browsers/id/fs/download_dir_zip").mock(return_value=httpx.Response(200, json={"foo": "bar"}))
+ respx_mock.get("/browsers/htzv5orfit78e1m2biiifpbv/fs/download_dir_zip").mock(
+ return_value=httpx.Response(200, json={"foo": "bar"})
+ )
f = client.browsers.fs.download_dir_zip(
- id="id",
+ id_or_name="htzv5orfit78e1m2biiifpbv",
path="/J!",
)
assert f.is_closed
@@ -192,10 +194,12 @@ def test_method_download_dir_zip(self, client: Kernel, respx_mock: MockRouter) -
@parametrize
@pytest.mark.respx(base_url=base_url)
def test_raw_response_download_dir_zip(self, client: Kernel, respx_mock: MockRouter) -> None:
- respx_mock.get("/browsers/id/fs/download_dir_zip").mock(return_value=httpx.Response(200, json={"foo": "bar"}))
+ respx_mock.get("/browsers/htzv5orfit78e1m2biiifpbv/fs/download_dir_zip").mock(
+ return_value=httpx.Response(200, json={"foo": "bar"})
+ )
f = client.browsers.fs.with_raw_response.download_dir_zip(
- id="id",
+ id_or_name="htzv5orfit78e1m2biiifpbv",
path="/J!",
)
@@ -207,9 +211,11 @@ def test_raw_response_download_dir_zip(self, client: Kernel, respx_mock: MockRou
@parametrize
@pytest.mark.respx(base_url=base_url)
def test_streaming_response_download_dir_zip(self, client: Kernel, respx_mock: MockRouter) -> None:
- respx_mock.get("/browsers/id/fs/download_dir_zip").mock(return_value=httpx.Response(200, json={"foo": "bar"}))
+ respx_mock.get("/browsers/htzv5orfit78e1m2biiifpbv/fs/download_dir_zip").mock(
+ return_value=httpx.Response(200, json={"foo": "bar"})
+ )
with client.browsers.fs.with_streaming_response.download_dir_zip(
- id="id",
+ id_or_name="htzv5orfit78e1m2biiifpbv",
path="/J!",
) as f:
assert not f.is_closed
@@ -224,9 +230,9 @@ def test_streaming_response_download_dir_zip(self, client: Kernel, respx_mock: M
@parametrize
@pytest.mark.respx(base_url=base_url)
def test_path_params_download_dir_zip(self, client: Kernel) -> None:
- with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"):
+ with pytest.raises(ValueError, match=r"Expected a non-empty value for `id_or_name` but received ''"):
client.browsers.fs.with_raw_response.download_dir_zip(
- id="",
+ id_or_name="",
path="/J!",
)
@@ -234,7 +240,7 @@ def test_path_params_download_dir_zip(self, client: Kernel) -> None:
@parametrize
def test_method_file_info(self, client: Kernel) -> None:
f = client.browsers.fs.file_info(
- id="id",
+ id_or_name="htzv5orfit78e1m2biiifpbv",
path="/J!",
)
assert_matches_type(FFileInfoResponse, f, path=["response"])
@@ -243,7 +249,7 @@ def test_method_file_info(self, client: Kernel) -> None:
@parametrize
def test_raw_response_file_info(self, client: Kernel) -> None:
response = client.browsers.fs.with_raw_response.file_info(
- id="id",
+ id_or_name="htzv5orfit78e1m2biiifpbv",
path="/J!",
)
@@ -256,7 +262,7 @@ def test_raw_response_file_info(self, client: Kernel) -> None:
@parametrize
def test_streaming_response_file_info(self, client: Kernel) -> None:
with client.browsers.fs.with_streaming_response.file_info(
- id="id",
+ id_or_name="htzv5orfit78e1m2biiifpbv",
path="/J!",
) as response:
assert not response.is_closed
@@ -270,9 +276,9 @@ def test_streaming_response_file_info(self, client: Kernel) -> None:
@pytest.mark.skip(reason="Mock server tests are disabled")
@parametrize
def test_path_params_file_info(self, client: Kernel) -> None:
- with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"):
+ with pytest.raises(ValueError, match=r"Expected a non-empty value for `id_or_name` but received ''"):
client.browsers.fs.with_raw_response.file_info(
- id="",
+ id_or_name="",
path="/J!",
)
@@ -280,7 +286,7 @@ def test_path_params_file_info(self, client: Kernel) -> None:
@parametrize
def test_method_list_files(self, client: Kernel) -> None:
f = client.browsers.fs.list_files(
- id="id",
+ id_or_name="htzv5orfit78e1m2biiifpbv",
path="/J!",
)
assert_matches_type(FListFilesResponse, f, path=["response"])
@@ -289,7 +295,7 @@ def test_method_list_files(self, client: Kernel) -> None:
@parametrize
def test_raw_response_list_files(self, client: Kernel) -> None:
response = client.browsers.fs.with_raw_response.list_files(
- id="id",
+ id_or_name="htzv5orfit78e1m2biiifpbv",
path="/J!",
)
@@ -302,7 +308,7 @@ def test_raw_response_list_files(self, client: Kernel) -> None:
@parametrize
def test_streaming_response_list_files(self, client: Kernel) -> None:
with client.browsers.fs.with_streaming_response.list_files(
- id="id",
+ id_or_name="htzv5orfit78e1m2biiifpbv",
path="/J!",
) as response:
assert not response.is_closed
@@ -316,9 +322,9 @@ def test_streaming_response_list_files(self, client: Kernel) -> None:
@pytest.mark.skip(reason="Mock server tests are disabled")
@parametrize
def test_path_params_list_files(self, client: Kernel) -> None:
- with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"):
+ with pytest.raises(ValueError, match=r"Expected a non-empty value for `id_or_name` but received ''"):
client.browsers.fs.with_raw_response.list_files(
- id="",
+ id_or_name="",
path="/J!",
)
@@ -326,7 +332,7 @@ def test_path_params_list_files(self, client: Kernel) -> None:
@parametrize
def test_method_move(self, client: Kernel) -> None:
f = client.browsers.fs.move(
- id="id",
+ id_or_name="htzv5orfit78e1m2biiifpbv",
dest_path="/J!",
src_path="/J!",
)
@@ -336,7 +342,7 @@ def test_method_move(self, client: Kernel) -> None:
@parametrize
def test_raw_response_move(self, client: Kernel) -> None:
response = client.browsers.fs.with_raw_response.move(
- id="id",
+ id_or_name="htzv5orfit78e1m2biiifpbv",
dest_path="/J!",
src_path="/J!",
)
@@ -350,7 +356,7 @@ def test_raw_response_move(self, client: Kernel) -> None:
@parametrize
def test_streaming_response_move(self, client: Kernel) -> None:
with client.browsers.fs.with_streaming_response.move(
- id="id",
+ id_or_name="htzv5orfit78e1m2biiifpbv",
dest_path="/J!",
src_path="/J!",
) as response:
@@ -365,9 +371,9 @@ def test_streaming_response_move(self, client: Kernel) -> None:
@pytest.mark.skip(reason="Mock server tests are disabled")
@parametrize
def test_path_params_move(self, client: Kernel) -> None:
- with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"):
+ with pytest.raises(ValueError, match=r"Expected a non-empty value for `id_or_name` but received ''"):
client.browsers.fs.with_raw_response.move(
- id="",
+ id_or_name="",
dest_path="/J!",
src_path="/J!",
)
@@ -375,9 +381,11 @@ def test_path_params_move(self, client: Kernel) -> None:
@parametrize
@pytest.mark.respx(base_url=base_url)
def test_method_read_file(self, client: Kernel, respx_mock: MockRouter) -> None:
- respx_mock.get("/browsers/id/fs/read_file").mock(return_value=httpx.Response(200, json={"foo": "bar"}))
+ respx_mock.get("/browsers/htzv5orfit78e1m2biiifpbv/fs/read_file").mock(
+ return_value=httpx.Response(200, json={"foo": "bar"})
+ )
f = client.browsers.fs.read_file(
- id="id",
+ id_or_name="htzv5orfit78e1m2biiifpbv",
path="/J!",
)
assert f.is_closed
@@ -388,10 +396,12 @@ def test_method_read_file(self, client: Kernel, respx_mock: MockRouter) -> None:
@parametrize
@pytest.mark.respx(base_url=base_url)
def test_raw_response_read_file(self, client: Kernel, respx_mock: MockRouter) -> None:
- respx_mock.get("/browsers/id/fs/read_file").mock(return_value=httpx.Response(200, json={"foo": "bar"}))
+ respx_mock.get("/browsers/htzv5orfit78e1m2biiifpbv/fs/read_file").mock(
+ return_value=httpx.Response(200, json={"foo": "bar"})
+ )
f = client.browsers.fs.with_raw_response.read_file(
- id="id",
+ id_or_name="htzv5orfit78e1m2biiifpbv",
path="/J!",
)
@@ -403,9 +413,11 @@ def test_raw_response_read_file(self, client: Kernel, respx_mock: MockRouter) ->
@parametrize
@pytest.mark.respx(base_url=base_url)
def test_streaming_response_read_file(self, client: Kernel, respx_mock: MockRouter) -> None:
- respx_mock.get("/browsers/id/fs/read_file").mock(return_value=httpx.Response(200, json={"foo": "bar"}))
+ respx_mock.get("/browsers/htzv5orfit78e1m2biiifpbv/fs/read_file").mock(
+ return_value=httpx.Response(200, json={"foo": "bar"})
+ )
with client.browsers.fs.with_streaming_response.read_file(
- id="id",
+ id_or_name="htzv5orfit78e1m2biiifpbv",
path="/J!",
) as f:
assert not f.is_closed
@@ -420,9 +432,9 @@ def test_streaming_response_read_file(self, client: Kernel, respx_mock: MockRout
@parametrize
@pytest.mark.respx(base_url=base_url)
def test_path_params_read_file(self, client: Kernel) -> None:
- with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"):
+ with pytest.raises(ValueError, match=r"Expected a non-empty value for `id_or_name` but received ''"):
client.browsers.fs.with_raw_response.read_file(
- id="",
+ id_or_name="",
path="/J!",
)
@@ -430,7 +442,7 @@ def test_path_params_read_file(self, client: Kernel) -> None:
@parametrize
def test_method_set_file_permissions(self, client: Kernel) -> None:
f = client.browsers.fs.set_file_permissions(
- id="id",
+ id_or_name="htzv5orfit78e1m2biiifpbv",
mode="0611",
path="/J!",
)
@@ -440,7 +452,7 @@ def test_method_set_file_permissions(self, client: Kernel) -> None:
@parametrize
def test_method_set_file_permissions_with_all_params(self, client: Kernel) -> None:
f = client.browsers.fs.set_file_permissions(
- id="id",
+ id_or_name="htzv5orfit78e1m2biiifpbv",
mode="0611",
path="/J!",
group="group",
@@ -452,7 +464,7 @@ def test_method_set_file_permissions_with_all_params(self, client: Kernel) -> No
@parametrize
def test_raw_response_set_file_permissions(self, client: Kernel) -> None:
response = client.browsers.fs.with_raw_response.set_file_permissions(
- id="id",
+ id_or_name="htzv5orfit78e1m2biiifpbv",
mode="0611",
path="/J!",
)
@@ -466,7 +478,7 @@ def test_raw_response_set_file_permissions(self, client: Kernel) -> None:
@parametrize
def test_streaming_response_set_file_permissions(self, client: Kernel) -> None:
with client.browsers.fs.with_streaming_response.set_file_permissions(
- id="id",
+ id_or_name="htzv5orfit78e1m2biiifpbv",
mode="0611",
path="/J!",
) as response:
@@ -481,9 +493,9 @@ def test_streaming_response_set_file_permissions(self, client: Kernel) -> None:
@pytest.mark.skip(reason="Mock server tests are disabled")
@parametrize
def test_path_params_set_file_permissions(self, client: Kernel) -> None:
- with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"):
+ with pytest.raises(ValueError, match=r"Expected a non-empty value for `id_or_name` but received ''"):
client.browsers.fs.with_raw_response.set_file_permissions(
- id="",
+ id_or_name="",
mode="0611",
path="/J!",
)
@@ -492,7 +504,7 @@ def test_path_params_set_file_permissions(self, client: Kernel) -> None:
@parametrize
def test_method_upload(self, client: Kernel) -> None:
f = client.browsers.fs.upload(
- id="id",
+ id_or_name="htzv5orfit78e1m2biiifpbv",
files=[
{
"dest_path": "/J!",
@@ -506,7 +518,7 @@ def test_method_upload(self, client: Kernel) -> None:
@parametrize
def test_raw_response_upload(self, client: Kernel) -> None:
response = client.browsers.fs.with_raw_response.upload(
- id="id",
+ id_or_name="htzv5orfit78e1m2biiifpbv",
files=[
{
"dest_path": "/J!",
@@ -524,7 +536,7 @@ def test_raw_response_upload(self, client: Kernel) -> None:
@parametrize
def test_streaming_response_upload(self, client: Kernel) -> None:
with client.browsers.fs.with_streaming_response.upload(
- id="id",
+ id_or_name="htzv5orfit78e1m2biiifpbv",
files=[
{
"dest_path": "/J!",
@@ -543,9 +555,9 @@ def test_streaming_response_upload(self, client: Kernel) -> None:
@pytest.mark.skip(reason="Mock server tests are disabled")
@parametrize
def test_path_params_upload(self, client: Kernel) -> None:
- with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"):
+ with pytest.raises(ValueError, match=r"Expected a non-empty value for `id_or_name` but received ''"):
client.browsers.fs.with_raw_response.upload(
- id="",
+ id_or_name="",
files=[
{
"dest_path": "/J!",
@@ -558,7 +570,7 @@ def test_path_params_upload(self, client: Kernel) -> None:
@parametrize
def test_method_upload_zip(self, client: Kernel) -> None:
f = client.browsers.fs.upload_zip(
- id="id",
+ id_or_name="htzv5orfit78e1m2biiifpbv",
dest_path="/J!",
zip_file=b"Example data",
)
@@ -568,7 +580,7 @@ def test_method_upload_zip(self, client: Kernel) -> None:
@parametrize
def test_raw_response_upload_zip(self, client: Kernel) -> None:
response = client.browsers.fs.with_raw_response.upload_zip(
- id="id",
+ id_or_name="htzv5orfit78e1m2biiifpbv",
dest_path="/J!",
zip_file=b"Example data",
)
@@ -582,7 +594,7 @@ def test_raw_response_upload_zip(self, client: Kernel) -> None:
@parametrize
def test_streaming_response_upload_zip(self, client: Kernel) -> None:
with client.browsers.fs.with_streaming_response.upload_zip(
- id="id",
+ id_or_name="htzv5orfit78e1m2biiifpbv",
dest_path="/J!",
zip_file=b"Example data",
) as response:
@@ -597,9 +609,9 @@ def test_streaming_response_upload_zip(self, client: Kernel) -> None:
@pytest.mark.skip(reason="Mock server tests are disabled")
@parametrize
def test_path_params_upload_zip(self, client: Kernel) -> None:
- with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"):
+ with pytest.raises(ValueError, match=r"Expected a non-empty value for `id_or_name` but received ''"):
client.browsers.fs.with_raw_response.upload_zip(
- id="",
+ id_or_name="",
dest_path="/J!",
zip_file=b"Example data",
)
@@ -608,7 +620,7 @@ def test_path_params_upload_zip(self, client: Kernel) -> None:
@parametrize
def test_method_write_file(self, client: Kernel) -> None:
f = client.browsers.fs.write_file(
- id="id",
+ id_or_name="htzv5orfit78e1m2biiifpbv",
contents=b"Example data",
path="/J!",
)
@@ -618,7 +630,7 @@ def test_method_write_file(self, client: Kernel) -> None:
@parametrize
def test_method_write_file_with_all_params(self, client: Kernel) -> None:
f = client.browsers.fs.write_file(
- id="id",
+ id_or_name="htzv5orfit78e1m2biiifpbv",
contents=b"Example data",
path="/J!",
mode="0611",
@@ -629,7 +641,7 @@ def test_method_write_file_with_all_params(self, client: Kernel) -> None:
@parametrize
def test_raw_response_write_file(self, client: Kernel) -> None:
response = client.browsers.fs.with_raw_response.write_file(
- id="id",
+ id_or_name="htzv5orfit78e1m2biiifpbv",
contents=b"Example data",
path="/J!",
)
@@ -643,7 +655,7 @@ def test_raw_response_write_file(self, client: Kernel) -> None:
@parametrize
def test_streaming_response_write_file(self, client: Kernel) -> None:
with client.browsers.fs.with_streaming_response.write_file(
- id="id",
+ id_or_name="htzv5orfit78e1m2biiifpbv",
contents=b"Example data",
path="/J!",
) as response:
@@ -658,9 +670,9 @@ def test_streaming_response_write_file(self, client: Kernel) -> None:
@pytest.mark.skip(reason="Mock server tests are disabled")
@parametrize
def test_path_params_write_file(self, client: Kernel) -> None:
- with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"):
+ with pytest.raises(ValueError, match=r"Expected a non-empty value for `id_or_name` but received ''"):
client.browsers.fs.with_raw_response.write_file(
- id="",
+ id_or_name="",
contents=b"Example data",
path="/J!",
)
@@ -675,7 +687,7 @@ class TestAsyncFs:
@parametrize
async def test_method_create_directory(self, async_client: AsyncKernel) -> None:
f = await async_client.browsers.fs.create_directory(
- id="id",
+ id_or_name="htzv5orfit78e1m2biiifpbv",
path="/J!",
)
assert f is None
@@ -684,7 +696,7 @@ async def test_method_create_directory(self, async_client: AsyncKernel) -> None:
@parametrize
async def test_method_create_directory_with_all_params(self, async_client: AsyncKernel) -> None:
f = await async_client.browsers.fs.create_directory(
- id="id",
+ id_or_name="htzv5orfit78e1m2biiifpbv",
path="/J!",
mode="0611",
)
@@ -694,7 +706,7 @@ async def test_method_create_directory_with_all_params(self, async_client: Async
@parametrize
async def test_raw_response_create_directory(self, async_client: AsyncKernel) -> None:
response = await async_client.browsers.fs.with_raw_response.create_directory(
- id="id",
+ id_or_name="htzv5orfit78e1m2biiifpbv",
path="/J!",
)
@@ -707,7 +719,7 @@ async def test_raw_response_create_directory(self, async_client: AsyncKernel) ->
@parametrize
async def test_streaming_response_create_directory(self, async_client: AsyncKernel) -> None:
async with async_client.browsers.fs.with_streaming_response.create_directory(
- id="id",
+ id_or_name="htzv5orfit78e1m2biiifpbv",
path="/J!",
) as response:
assert not response.is_closed
@@ -721,9 +733,9 @@ async def test_streaming_response_create_directory(self, async_client: AsyncKern
@pytest.mark.skip(reason="Mock server tests are disabled")
@parametrize
async def test_path_params_create_directory(self, async_client: AsyncKernel) -> None:
- with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"):
+ with pytest.raises(ValueError, match=r"Expected a non-empty value for `id_or_name` but received ''"):
await async_client.browsers.fs.with_raw_response.create_directory(
- id="",
+ id_or_name="",
path="/J!",
)
@@ -731,7 +743,7 @@ async def test_path_params_create_directory(self, async_client: AsyncKernel) ->
@parametrize
async def test_method_delete_directory(self, async_client: AsyncKernel) -> None:
f = await async_client.browsers.fs.delete_directory(
- id="id",
+ id_or_name="htzv5orfit78e1m2biiifpbv",
path="/J!",
)
assert f is None
@@ -740,7 +752,7 @@ async def test_method_delete_directory(self, async_client: AsyncKernel) -> None:
@parametrize
async def test_raw_response_delete_directory(self, async_client: AsyncKernel) -> None:
response = await async_client.browsers.fs.with_raw_response.delete_directory(
- id="id",
+ id_or_name="htzv5orfit78e1m2biiifpbv",
path="/J!",
)
@@ -753,7 +765,7 @@ async def test_raw_response_delete_directory(self, async_client: AsyncKernel) ->
@parametrize
async def test_streaming_response_delete_directory(self, async_client: AsyncKernel) -> None:
async with async_client.browsers.fs.with_streaming_response.delete_directory(
- id="id",
+ id_or_name="htzv5orfit78e1m2biiifpbv",
path="/J!",
) as response:
assert not response.is_closed
@@ -767,9 +779,9 @@ async def test_streaming_response_delete_directory(self, async_client: AsyncKern
@pytest.mark.skip(reason="Mock server tests are disabled")
@parametrize
async def test_path_params_delete_directory(self, async_client: AsyncKernel) -> None:
- with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"):
+ with pytest.raises(ValueError, match=r"Expected a non-empty value for `id_or_name` but received ''"):
await async_client.browsers.fs.with_raw_response.delete_directory(
- id="",
+ id_or_name="",
path="/J!",
)
@@ -777,7 +789,7 @@ async def test_path_params_delete_directory(self, async_client: AsyncKernel) ->
@parametrize
async def test_method_delete_file(self, async_client: AsyncKernel) -> None:
f = await async_client.browsers.fs.delete_file(
- id="id",
+ id_or_name="htzv5orfit78e1m2biiifpbv",
path="/J!",
)
assert f is None
@@ -786,7 +798,7 @@ async def test_method_delete_file(self, async_client: AsyncKernel) -> None:
@parametrize
async def test_raw_response_delete_file(self, async_client: AsyncKernel) -> None:
response = await async_client.browsers.fs.with_raw_response.delete_file(
- id="id",
+ id_or_name="htzv5orfit78e1m2biiifpbv",
path="/J!",
)
@@ -799,7 +811,7 @@ async def test_raw_response_delete_file(self, async_client: AsyncKernel) -> None
@parametrize
async def test_streaming_response_delete_file(self, async_client: AsyncKernel) -> None:
async with async_client.browsers.fs.with_streaming_response.delete_file(
- id="id",
+ id_or_name="htzv5orfit78e1m2biiifpbv",
path="/J!",
) as response:
assert not response.is_closed
@@ -813,18 +825,20 @@ async def test_streaming_response_delete_file(self, async_client: AsyncKernel) -
@pytest.mark.skip(reason="Mock server tests are disabled")
@parametrize
async def test_path_params_delete_file(self, async_client: AsyncKernel) -> None:
- with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"):
+ with pytest.raises(ValueError, match=r"Expected a non-empty value for `id_or_name` but received ''"):
await async_client.browsers.fs.with_raw_response.delete_file(
- id="",
+ id_or_name="",
path="/J!",
)
@parametrize
@pytest.mark.respx(base_url=base_url)
async def test_method_download_dir_zip(self, async_client: AsyncKernel, respx_mock: MockRouter) -> None:
- respx_mock.get("/browsers/id/fs/download_dir_zip").mock(return_value=httpx.Response(200, json={"foo": "bar"}))
+ respx_mock.get("/browsers/htzv5orfit78e1m2biiifpbv/fs/download_dir_zip").mock(
+ return_value=httpx.Response(200, json={"foo": "bar"})
+ )
f = await async_client.browsers.fs.download_dir_zip(
- id="id",
+ id_or_name="htzv5orfit78e1m2biiifpbv",
path="/J!",
)
assert f.is_closed
@@ -835,10 +849,12 @@ async def test_method_download_dir_zip(self, async_client: AsyncKernel, respx_mo
@parametrize
@pytest.mark.respx(base_url=base_url)
async def test_raw_response_download_dir_zip(self, async_client: AsyncKernel, respx_mock: MockRouter) -> None:
- respx_mock.get("/browsers/id/fs/download_dir_zip").mock(return_value=httpx.Response(200, json={"foo": "bar"}))
+ respx_mock.get("/browsers/htzv5orfit78e1m2biiifpbv/fs/download_dir_zip").mock(
+ return_value=httpx.Response(200, json={"foo": "bar"})
+ )
f = await async_client.browsers.fs.with_raw_response.download_dir_zip(
- id="id",
+ id_or_name="htzv5orfit78e1m2biiifpbv",
path="/J!",
)
@@ -850,9 +866,11 @@ async def test_raw_response_download_dir_zip(self, async_client: AsyncKernel, re
@parametrize
@pytest.mark.respx(base_url=base_url)
async def test_streaming_response_download_dir_zip(self, async_client: AsyncKernel, respx_mock: MockRouter) -> None:
- respx_mock.get("/browsers/id/fs/download_dir_zip").mock(return_value=httpx.Response(200, json={"foo": "bar"}))
+ respx_mock.get("/browsers/htzv5orfit78e1m2biiifpbv/fs/download_dir_zip").mock(
+ return_value=httpx.Response(200, json={"foo": "bar"})
+ )
async with async_client.browsers.fs.with_streaming_response.download_dir_zip(
- id="id",
+ id_or_name="htzv5orfit78e1m2biiifpbv",
path="/J!",
) as f:
assert not f.is_closed
@@ -867,9 +885,9 @@ async def test_streaming_response_download_dir_zip(self, async_client: AsyncKern
@parametrize
@pytest.mark.respx(base_url=base_url)
async def test_path_params_download_dir_zip(self, async_client: AsyncKernel) -> None:
- with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"):
+ with pytest.raises(ValueError, match=r"Expected a non-empty value for `id_or_name` but received ''"):
await async_client.browsers.fs.with_raw_response.download_dir_zip(
- id="",
+ id_or_name="",
path="/J!",
)
@@ -877,7 +895,7 @@ async def test_path_params_download_dir_zip(self, async_client: AsyncKernel) ->
@parametrize
async def test_method_file_info(self, async_client: AsyncKernel) -> None:
f = await async_client.browsers.fs.file_info(
- id="id",
+ id_or_name="htzv5orfit78e1m2biiifpbv",
path="/J!",
)
assert_matches_type(FFileInfoResponse, f, path=["response"])
@@ -886,7 +904,7 @@ async def test_method_file_info(self, async_client: AsyncKernel) -> None:
@parametrize
async def test_raw_response_file_info(self, async_client: AsyncKernel) -> None:
response = await async_client.browsers.fs.with_raw_response.file_info(
- id="id",
+ id_or_name="htzv5orfit78e1m2biiifpbv",
path="/J!",
)
@@ -899,7 +917,7 @@ async def test_raw_response_file_info(self, async_client: AsyncKernel) -> None:
@parametrize
async def test_streaming_response_file_info(self, async_client: AsyncKernel) -> None:
async with async_client.browsers.fs.with_streaming_response.file_info(
- id="id",
+ id_or_name="htzv5orfit78e1m2biiifpbv",
path="/J!",
) as response:
assert not response.is_closed
@@ -913,9 +931,9 @@ async def test_streaming_response_file_info(self, async_client: AsyncKernel) ->
@pytest.mark.skip(reason="Mock server tests are disabled")
@parametrize
async def test_path_params_file_info(self, async_client: AsyncKernel) -> None:
- with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"):
+ with pytest.raises(ValueError, match=r"Expected a non-empty value for `id_or_name` but received ''"):
await async_client.browsers.fs.with_raw_response.file_info(
- id="",
+ id_or_name="",
path="/J!",
)
@@ -923,7 +941,7 @@ async def test_path_params_file_info(self, async_client: AsyncKernel) -> None:
@parametrize
async def test_method_list_files(self, async_client: AsyncKernel) -> None:
f = await async_client.browsers.fs.list_files(
- id="id",
+ id_or_name="htzv5orfit78e1m2biiifpbv",
path="/J!",
)
assert_matches_type(FListFilesResponse, f, path=["response"])
@@ -932,7 +950,7 @@ async def test_method_list_files(self, async_client: AsyncKernel) -> None:
@parametrize
async def test_raw_response_list_files(self, async_client: AsyncKernel) -> None:
response = await async_client.browsers.fs.with_raw_response.list_files(
- id="id",
+ id_or_name="htzv5orfit78e1m2biiifpbv",
path="/J!",
)
@@ -945,7 +963,7 @@ async def test_raw_response_list_files(self, async_client: AsyncKernel) -> None:
@parametrize
async def test_streaming_response_list_files(self, async_client: AsyncKernel) -> None:
async with async_client.browsers.fs.with_streaming_response.list_files(
- id="id",
+ id_or_name="htzv5orfit78e1m2biiifpbv",
path="/J!",
) as response:
assert not response.is_closed
@@ -959,9 +977,9 @@ async def test_streaming_response_list_files(self, async_client: AsyncKernel) ->
@pytest.mark.skip(reason="Mock server tests are disabled")
@parametrize
async def test_path_params_list_files(self, async_client: AsyncKernel) -> None:
- with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"):
+ with pytest.raises(ValueError, match=r"Expected a non-empty value for `id_or_name` but received ''"):
await async_client.browsers.fs.with_raw_response.list_files(
- id="",
+ id_or_name="",
path="/J!",
)
@@ -969,7 +987,7 @@ async def test_path_params_list_files(self, async_client: AsyncKernel) -> None:
@parametrize
async def test_method_move(self, async_client: AsyncKernel) -> None:
f = await async_client.browsers.fs.move(
- id="id",
+ id_or_name="htzv5orfit78e1m2biiifpbv",
dest_path="/J!",
src_path="/J!",
)
@@ -979,7 +997,7 @@ async def test_method_move(self, async_client: AsyncKernel) -> None:
@parametrize
async def test_raw_response_move(self, async_client: AsyncKernel) -> None:
response = await async_client.browsers.fs.with_raw_response.move(
- id="id",
+ id_or_name="htzv5orfit78e1m2biiifpbv",
dest_path="/J!",
src_path="/J!",
)
@@ -993,7 +1011,7 @@ async def test_raw_response_move(self, async_client: AsyncKernel) -> None:
@parametrize
async def test_streaming_response_move(self, async_client: AsyncKernel) -> None:
async with async_client.browsers.fs.with_streaming_response.move(
- id="id",
+ id_or_name="htzv5orfit78e1m2biiifpbv",
dest_path="/J!",
src_path="/J!",
) as response:
@@ -1008,9 +1026,9 @@ async def test_streaming_response_move(self, async_client: AsyncKernel) -> None:
@pytest.mark.skip(reason="Mock server tests are disabled")
@parametrize
async def test_path_params_move(self, async_client: AsyncKernel) -> None:
- with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"):
+ with pytest.raises(ValueError, match=r"Expected a non-empty value for `id_or_name` but received ''"):
await async_client.browsers.fs.with_raw_response.move(
- id="",
+ id_or_name="",
dest_path="/J!",
src_path="/J!",
)
@@ -1018,9 +1036,11 @@ async def test_path_params_move(self, async_client: AsyncKernel) -> None:
@parametrize
@pytest.mark.respx(base_url=base_url)
async def test_method_read_file(self, async_client: AsyncKernel, respx_mock: MockRouter) -> None:
- respx_mock.get("/browsers/id/fs/read_file").mock(return_value=httpx.Response(200, json={"foo": "bar"}))
+ respx_mock.get("/browsers/htzv5orfit78e1m2biiifpbv/fs/read_file").mock(
+ return_value=httpx.Response(200, json={"foo": "bar"})
+ )
f = await async_client.browsers.fs.read_file(
- id="id",
+ id_or_name="htzv5orfit78e1m2biiifpbv",
path="/J!",
)
assert f.is_closed
@@ -1031,10 +1051,12 @@ async def test_method_read_file(self, async_client: AsyncKernel, respx_mock: Moc
@parametrize
@pytest.mark.respx(base_url=base_url)
async def test_raw_response_read_file(self, async_client: AsyncKernel, respx_mock: MockRouter) -> None:
- respx_mock.get("/browsers/id/fs/read_file").mock(return_value=httpx.Response(200, json={"foo": "bar"}))
+ respx_mock.get("/browsers/htzv5orfit78e1m2biiifpbv/fs/read_file").mock(
+ return_value=httpx.Response(200, json={"foo": "bar"})
+ )
f = await async_client.browsers.fs.with_raw_response.read_file(
- id="id",
+ id_or_name="htzv5orfit78e1m2biiifpbv",
path="/J!",
)
@@ -1046,9 +1068,11 @@ async def test_raw_response_read_file(self, async_client: AsyncKernel, respx_moc
@parametrize
@pytest.mark.respx(base_url=base_url)
async def test_streaming_response_read_file(self, async_client: AsyncKernel, respx_mock: MockRouter) -> None:
- respx_mock.get("/browsers/id/fs/read_file").mock(return_value=httpx.Response(200, json={"foo": "bar"}))
+ respx_mock.get("/browsers/htzv5orfit78e1m2biiifpbv/fs/read_file").mock(
+ return_value=httpx.Response(200, json={"foo": "bar"})
+ )
async with async_client.browsers.fs.with_streaming_response.read_file(
- id="id",
+ id_or_name="htzv5orfit78e1m2biiifpbv",
path="/J!",
) as f:
assert not f.is_closed
@@ -1063,9 +1087,9 @@ async def test_streaming_response_read_file(self, async_client: AsyncKernel, res
@parametrize
@pytest.mark.respx(base_url=base_url)
async def test_path_params_read_file(self, async_client: AsyncKernel) -> None:
- with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"):
+ with pytest.raises(ValueError, match=r"Expected a non-empty value for `id_or_name` but received ''"):
await async_client.browsers.fs.with_raw_response.read_file(
- id="",
+ id_or_name="",
path="/J!",
)
@@ -1073,7 +1097,7 @@ async def test_path_params_read_file(self, async_client: AsyncKernel) -> None:
@parametrize
async def test_method_set_file_permissions(self, async_client: AsyncKernel) -> None:
f = await async_client.browsers.fs.set_file_permissions(
- id="id",
+ id_or_name="htzv5orfit78e1m2biiifpbv",
mode="0611",
path="/J!",
)
@@ -1083,7 +1107,7 @@ async def test_method_set_file_permissions(self, async_client: AsyncKernel) -> N
@parametrize
async def test_method_set_file_permissions_with_all_params(self, async_client: AsyncKernel) -> None:
f = await async_client.browsers.fs.set_file_permissions(
- id="id",
+ id_or_name="htzv5orfit78e1m2biiifpbv",
mode="0611",
path="/J!",
group="group",
@@ -1095,7 +1119,7 @@ async def test_method_set_file_permissions_with_all_params(self, async_client: A
@parametrize
async def test_raw_response_set_file_permissions(self, async_client: AsyncKernel) -> None:
response = await async_client.browsers.fs.with_raw_response.set_file_permissions(
- id="id",
+ id_or_name="htzv5orfit78e1m2biiifpbv",
mode="0611",
path="/J!",
)
@@ -1109,7 +1133,7 @@ async def test_raw_response_set_file_permissions(self, async_client: AsyncKernel
@parametrize
async def test_streaming_response_set_file_permissions(self, async_client: AsyncKernel) -> None:
async with async_client.browsers.fs.with_streaming_response.set_file_permissions(
- id="id",
+ id_or_name="htzv5orfit78e1m2biiifpbv",
mode="0611",
path="/J!",
) as response:
@@ -1124,9 +1148,9 @@ async def test_streaming_response_set_file_permissions(self, async_client: Async
@pytest.mark.skip(reason="Mock server tests are disabled")
@parametrize
async def test_path_params_set_file_permissions(self, async_client: AsyncKernel) -> None:
- with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"):
+ with pytest.raises(ValueError, match=r"Expected a non-empty value for `id_or_name` but received ''"):
await async_client.browsers.fs.with_raw_response.set_file_permissions(
- id="",
+ id_or_name="",
mode="0611",
path="/J!",
)
@@ -1135,7 +1159,7 @@ async def test_path_params_set_file_permissions(self, async_client: AsyncKernel)
@parametrize
async def test_method_upload(self, async_client: AsyncKernel) -> None:
f = await async_client.browsers.fs.upload(
- id="id",
+ id_or_name="htzv5orfit78e1m2biiifpbv",
files=[
{
"dest_path": "/J!",
@@ -1149,7 +1173,7 @@ async def test_method_upload(self, async_client: AsyncKernel) -> None:
@parametrize
async def test_raw_response_upload(self, async_client: AsyncKernel) -> None:
response = await async_client.browsers.fs.with_raw_response.upload(
- id="id",
+ id_or_name="htzv5orfit78e1m2biiifpbv",
files=[
{
"dest_path": "/J!",
@@ -1167,7 +1191,7 @@ async def test_raw_response_upload(self, async_client: AsyncKernel) -> None:
@parametrize
async def test_streaming_response_upload(self, async_client: AsyncKernel) -> None:
async with async_client.browsers.fs.with_streaming_response.upload(
- id="id",
+ id_or_name="htzv5orfit78e1m2biiifpbv",
files=[
{
"dest_path": "/J!",
@@ -1186,9 +1210,9 @@ async def test_streaming_response_upload(self, async_client: AsyncKernel) -> Non
@pytest.mark.skip(reason="Mock server tests are disabled")
@parametrize
async def test_path_params_upload(self, async_client: AsyncKernel) -> None:
- with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"):
+ with pytest.raises(ValueError, match=r"Expected a non-empty value for `id_or_name` but received ''"):
await async_client.browsers.fs.with_raw_response.upload(
- id="",
+ id_or_name="",
files=[
{
"dest_path": "/J!",
@@ -1201,7 +1225,7 @@ async def test_path_params_upload(self, async_client: AsyncKernel) -> None:
@parametrize
async def test_method_upload_zip(self, async_client: AsyncKernel) -> None:
f = await async_client.browsers.fs.upload_zip(
- id="id",
+ id_or_name="htzv5orfit78e1m2biiifpbv",
dest_path="/J!",
zip_file=b"Example data",
)
@@ -1211,7 +1235,7 @@ async def test_method_upload_zip(self, async_client: AsyncKernel) -> None:
@parametrize
async def test_raw_response_upload_zip(self, async_client: AsyncKernel) -> None:
response = await async_client.browsers.fs.with_raw_response.upload_zip(
- id="id",
+ id_or_name="htzv5orfit78e1m2biiifpbv",
dest_path="/J!",
zip_file=b"Example data",
)
@@ -1225,7 +1249,7 @@ async def test_raw_response_upload_zip(self, async_client: AsyncKernel) -> None:
@parametrize
async def test_streaming_response_upload_zip(self, async_client: AsyncKernel) -> None:
async with async_client.browsers.fs.with_streaming_response.upload_zip(
- id="id",
+ id_or_name="htzv5orfit78e1m2biiifpbv",
dest_path="/J!",
zip_file=b"Example data",
) as response:
@@ -1240,9 +1264,9 @@ async def test_streaming_response_upload_zip(self, async_client: AsyncKernel) ->
@pytest.mark.skip(reason="Mock server tests are disabled")
@parametrize
async def test_path_params_upload_zip(self, async_client: AsyncKernel) -> None:
- with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"):
+ with pytest.raises(ValueError, match=r"Expected a non-empty value for `id_or_name` but received ''"):
await async_client.browsers.fs.with_raw_response.upload_zip(
- id="",
+ id_or_name="",
dest_path="/J!",
zip_file=b"Example data",
)
@@ -1251,7 +1275,7 @@ async def test_path_params_upload_zip(self, async_client: AsyncKernel) -> None:
@parametrize
async def test_method_write_file(self, async_client: AsyncKernel) -> None:
f = await async_client.browsers.fs.write_file(
- id="id",
+ id_or_name="htzv5orfit78e1m2biiifpbv",
contents=b"Example data",
path="/J!",
)
@@ -1261,7 +1285,7 @@ async def test_method_write_file(self, async_client: AsyncKernel) -> None:
@parametrize
async def test_method_write_file_with_all_params(self, async_client: AsyncKernel) -> None:
f = await async_client.browsers.fs.write_file(
- id="id",
+ id_or_name="htzv5orfit78e1m2biiifpbv",
contents=b"Example data",
path="/J!",
mode="0611",
@@ -1272,7 +1296,7 @@ async def test_method_write_file_with_all_params(self, async_client: AsyncKernel
@parametrize
async def test_raw_response_write_file(self, async_client: AsyncKernel) -> None:
response = await async_client.browsers.fs.with_raw_response.write_file(
- id="id",
+ id_or_name="htzv5orfit78e1m2biiifpbv",
contents=b"Example data",
path="/J!",
)
@@ -1286,7 +1310,7 @@ async def test_raw_response_write_file(self, async_client: AsyncKernel) -> None:
@parametrize
async def test_streaming_response_write_file(self, async_client: AsyncKernel) -> None:
async with async_client.browsers.fs.with_streaming_response.write_file(
- id="id",
+ id_or_name="htzv5orfit78e1m2biiifpbv",
contents=b"Example data",
path="/J!",
) as response:
@@ -1301,9 +1325,9 @@ async def test_streaming_response_write_file(self, async_client: AsyncKernel) ->
@pytest.mark.skip(reason="Mock server tests are disabled")
@parametrize
async def test_path_params_write_file(self, async_client: AsyncKernel) -> None:
- with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"):
+ with pytest.raises(ValueError, match=r"Expected a non-empty value for `id_or_name` but received ''"):
await async_client.browsers.fs.with_raw_response.write_file(
- id="",
+ id_or_name="",
contents=b"Example data",
path="/J!",
)
diff --git a/tests/api_resources/browsers/test_logs.py b/tests/api_resources/browsers/test_logs.py
index 27268b82..0c4bb3ea 100644
--- a/tests/api_resources/browsers/test_logs.py
+++ b/tests/api_resources/browsers/test_logs.py
@@ -19,7 +19,7 @@ class TestLogs:
@parametrize
def test_method_stream(self, client: Kernel) -> None:
log_stream = client.browsers.logs.stream(
- id="id",
+ id_or_name="htzv5orfit78e1m2biiifpbv",
source="path",
)
log_stream.response.close()
@@ -28,7 +28,7 @@ def test_method_stream(self, client: Kernel) -> None:
@parametrize
def test_method_stream_with_all_params(self, client: Kernel) -> None:
log_stream = client.browsers.logs.stream(
- id="id",
+ id_or_name="htzv5orfit78e1m2biiifpbv",
source="path",
follow=True,
path="path",
@@ -40,7 +40,7 @@ def test_method_stream_with_all_params(self, client: Kernel) -> None:
@parametrize
def test_raw_response_stream(self, client: Kernel) -> None:
response = client.browsers.logs.with_raw_response.stream(
- id="id",
+ id_or_name="htzv5orfit78e1m2biiifpbv",
source="path",
)
@@ -52,7 +52,7 @@ def test_raw_response_stream(self, client: Kernel) -> None:
@parametrize
def test_streaming_response_stream(self, client: Kernel) -> None:
with client.browsers.logs.with_streaming_response.stream(
- id="id",
+ id_or_name="htzv5orfit78e1m2biiifpbv",
source="path",
) as response:
assert not response.is_closed
@@ -66,9 +66,9 @@ def test_streaming_response_stream(self, client: Kernel) -> None:
@pytest.mark.skip(reason="Mock server tests are disabled")
@parametrize
def test_path_params_stream(self, client: Kernel) -> None:
- with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"):
+ with pytest.raises(ValueError, match=r"Expected a non-empty value for `id_or_name` but received ''"):
client.browsers.logs.with_raw_response.stream(
- id="",
+ id_or_name="",
source="path",
)
@@ -82,7 +82,7 @@ class TestAsyncLogs:
@parametrize
async def test_method_stream(self, async_client: AsyncKernel) -> None:
log_stream = await async_client.browsers.logs.stream(
- id="id",
+ id_or_name="htzv5orfit78e1m2biiifpbv",
source="path",
)
await log_stream.response.aclose()
@@ -91,7 +91,7 @@ async def test_method_stream(self, async_client: AsyncKernel) -> None:
@parametrize
async def test_method_stream_with_all_params(self, async_client: AsyncKernel) -> None:
log_stream = await async_client.browsers.logs.stream(
- id="id",
+ id_or_name="htzv5orfit78e1m2biiifpbv",
source="path",
follow=True,
path="path",
@@ -103,7 +103,7 @@ async def test_method_stream_with_all_params(self, async_client: AsyncKernel) ->
@parametrize
async def test_raw_response_stream(self, async_client: AsyncKernel) -> None:
response = await async_client.browsers.logs.with_raw_response.stream(
- id="id",
+ id_or_name="htzv5orfit78e1m2biiifpbv",
source="path",
)
@@ -115,7 +115,7 @@ async def test_raw_response_stream(self, async_client: AsyncKernel) -> None:
@parametrize
async def test_streaming_response_stream(self, async_client: AsyncKernel) -> None:
async with async_client.browsers.logs.with_streaming_response.stream(
- id="id",
+ id_or_name="htzv5orfit78e1m2biiifpbv",
source="path",
) as response:
assert not response.is_closed
@@ -129,8 +129,8 @@ async def test_streaming_response_stream(self, async_client: AsyncKernel) -> Non
@pytest.mark.skip(reason="Mock server tests are disabled")
@parametrize
async def test_path_params_stream(self, async_client: AsyncKernel) -> None:
- with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"):
+ with pytest.raises(ValueError, match=r"Expected a non-empty value for `id_or_name` but received ''"):
await async_client.browsers.logs.with_raw_response.stream(
- id="",
+ id_or_name="",
source="path",
)
diff --git a/tests/api_resources/browsers/test_playwright.py b/tests/api_resources/browsers/test_playwright.py
index fd91b832..5adc553f 100644
--- a/tests/api_resources/browsers/test_playwright.py
+++ b/tests/api_resources/browsers/test_playwright.py
@@ -21,7 +21,7 @@ class TestPlaywright:
@parametrize
def test_method_execute(self, client: Kernel) -> None:
playwright = client.browsers.playwright.execute(
- id="id",
+ id_or_name="htzv5orfit78e1m2biiifpbv",
code="code",
)
assert_matches_type(PlaywrightExecuteResponse, playwright, path=["response"])
@@ -30,7 +30,7 @@ def test_method_execute(self, client: Kernel) -> None:
@parametrize
def test_method_execute_with_all_params(self, client: Kernel) -> None:
playwright = client.browsers.playwright.execute(
- id="id",
+ id_or_name="htzv5orfit78e1m2biiifpbv",
code="code",
timeout_sec=1,
)
@@ -40,7 +40,7 @@ def test_method_execute_with_all_params(self, client: Kernel) -> None:
@parametrize
def test_raw_response_execute(self, client: Kernel) -> None:
response = client.browsers.playwright.with_raw_response.execute(
- id="id",
+ id_or_name="htzv5orfit78e1m2biiifpbv",
code="code",
)
@@ -53,7 +53,7 @@ def test_raw_response_execute(self, client: Kernel) -> None:
@parametrize
def test_streaming_response_execute(self, client: Kernel) -> None:
with client.browsers.playwright.with_streaming_response.execute(
- id="id",
+ id_or_name="htzv5orfit78e1m2biiifpbv",
code="code",
) as response:
assert not response.is_closed
@@ -67,9 +67,9 @@ def test_streaming_response_execute(self, client: Kernel) -> None:
@pytest.mark.skip(reason="Mock server tests are disabled")
@parametrize
def test_path_params_execute(self, client: Kernel) -> None:
- with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"):
+ with pytest.raises(ValueError, match=r"Expected a non-empty value for `id_or_name` but received ''"):
client.browsers.playwright.with_raw_response.execute(
- id="",
+ id_or_name="",
code="code",
)
@@ -83,7 +83,7 @@ class TestAsyncPlaywright:
@parametrize
async def test_method_execute(self, async_client: AsyncKernel) -> None:
playwright = await async_client.browsers.playwright.execute(
- id="id",
+ id_or_name="htzv5orfit78e1m2biiifpbv",
code="code",
)
assert_matches_type(PlaywrightExecuteResponse, playwright, path=["response"])
@@ -92,7 +92,7 @@ async def test_method_execute(self, async_client: AsyncKernel) -> None:
@parametrize
async def test_method_execute_with_all_params(self, async_client: AsyncKernel) -> None:
playwright = await async_client.browsers.playwright.execute(
- id="id",
+ id_or_name="htzv5orfit78e1m2biiifpbv",
code="code",
timeout_sec=1,
)
@@ -102,7 +102,7 @@ async def test_method_execute_with_all_params(self, async_client: AsyncKernel) -
@parametrize
async def test_raw_response_execute(self, async_client: AsyncKernel) -> None:
response = await async_client.browsers.playwright.with_raw_response.execute(
- id="id",
+ id_or_name="htzv5orfit78e1m2biiifpbv",
code="code",
)
@@ -115,7 +115,7 @@ async def test_raw_response_execute(self, async_client: AsyncKernel) -> None:
@parametrize
async def test_streaming_response_execute(self, async_client: AsyncKernel) -> None:
async with async_client.browsers.playwright.with_streaming_response.execute(
- id="id",
+ id_or_name="htzv5orfit78e1m2biiifpbv",
code="code",
) as response:
assert not response.is_closed
@@ -129,8 +129,8 @@ async def test_streaming_response_execute(self, async_client: AsyncKernel) -> No
@pytest.mark.skip(reason="Mock server tests are disabled")
@parametrize
async def test_path_params_execute(self, async_client: AsyncKernel) -> None:
- with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"):
+ with pytest.raises(ValueError, match=r"Expected a non-empty value for `id_or_name` but received ''"):
await async_client.browsers.playwright.with_raw_response.execute(
- id="",
+ id_or_name="",
code="code",
)
diff --git a/tests/api_resources/browsers/test_process.py b/tests/api_resources/browsers/test_process.py
index 0418d219..bbcd7860 100644
--- a/tests/api_resources/browsers/test_process.py
+++ b/tests/api_resources/browsers/test_process.py
@@ -28,7 +28,7 @@ class TestProcess:
@parametrize
def test_method_exec(self, client: Kernel) -> None:
process = client.browsers.process.exec(
- id="id",
+ id_or_name="htzv5orfit78e1m2biiifpbv",
command="command",
)
assert_matches_type(ProcessExecResponse, process, path=["response"])
@@ -37,7 +37,7 @@ def test_method_exec(self, client: Kernel) -> None:
@parametrize
def test_method_exec_with_all_params(self, client: Kernel) -> None:
process = client.browsers.process.exec(
- id="id",
+ id_or_name="htzv5orfit78e1m2biiifpbv",
command="command",
args=["string"],
as_root=True,
@@ -52,7 +52,7 @@ def test_method_exec_with_all_params(self, client: Kernel) -> None:
@parametrize
def test_raw_response_exec(self, client: Kernel) -> None:
response = client.browsers.process.with_raw_response.exec(
- id="id",
+ id_or_name="htzv5orfit78e1m2biiifpbv",
command="command",
)
@@ -65,7 +65,7 @@ def test_raw_response_exec(self, client: Kernel) -> None:
@parametrize
def test_streaming_response_exec(self, client: Kernel) -> None:
with client.browsers.process.with_streaming_response.exec(
- id="id",
+ id_or_name="htzv5orfit78e1m2biiifpbv",
command="command",
) as response:
assert not response.is_closed
@@ -79,9 +79,9 @@ def test_streaming_response_exec(self, client: Kernel) -> None:
@pytest.mark.skip(reason="Mock server tests are disabled")
@parametrize
def test_path_params_exec(self, client: Kernel) -> None:
- with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"):
+ with pytest.raises(ValueError, match=r"Expected a non-empty value for `id_or_name` but received ''"):
client.browsers.process.with_raw_response.exec(
- id="",
+ id_or_name="",
command="command",
)
@@ -90,7 +90,7 @@ def test_path_params_exec(self, client: Kernel) -> None:
def test_method_kill(self, client: Kernel) -> None:
process = client.browsers.process.kill(
process_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e",
- id="id",
+ id_or_name="htzv5orfit78e1m2biiifpbv",
signal="TERM",
)
assert_matches_type(ProcessKillResponse, process, path=["response"])
@@ -100,7 +100,7 @@ def test_method_kill(self, client: Kernel) -> None:
def test_raw_response_kill(self, client: Kernel) -> None:
response = client.browsers.process.with_raw_response.kill(
process_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e",
- id="id",
+ id_or_name="htzv5orfit78e1m2biiifpbv",
signal="TERM",
)
@@ -114,7 +114,7 @@ def test_raw_response_kill(self, client: Kernel) -> None:
def test_streaming_response_kill(self, client: Kernel) -> None:
with client.browsers.process.with_streaming_response.kill(
process_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e",
- id="id",
+ id_or_name="htzv5orfit78e1m2biiifpbv",
signal="TERM",
) as response:
assert not response.is_closed
@@ -128,17 +128,17 @@ def test_streaming_response_kill(self, client: Kernel) -> None:
@pytest.mark.skip(reason="Mock server tests are disabled")
@parametrize
def test_path_params_kill(self, client: Kernel) -> None:
- with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"):
+ with pytest.raises(ValueError, match=r"Expected a non-empty value for `id_or_name` but received ''"):
client.browsers.process.with_raw_response.kill(
process_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e",
- id="",
+ id_or_name="",
signal="TERM",
)
with pytest.raises(ValueError, match=r"Expected a non-empty value for `process_id` but received ''"):
client.browsers.process.with_raw_response.kill(
process_id="",
- id="id",
+ id_or_name="htzv5orfit78e1m2biiifpbv",
signal="TERM",
)
@@ -147,7 +147,7 @@ def test_path_params_kill(self, client: Kernel) -> None:
def test_method_resize(self, client: Kernel) -> None:
process = client.browsers.process.resize(
process_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e",
- id="id",
+ id_or_name="htzv5orfit78e1m2biiifpbv",
cols=1,
rows=1,
)
@@ -158,7 +158,7 @@ def test_method_resize(self, client: Kernel) -> None:
def test_raw_response_resize(self, client: Kernel) -> None:
response = client.browsers.process.with_raw_response.resize(
process_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e",
- id="id",
+ id_or_name="htzv5orfit78e1m2biiifpbv",
cols=1,
rows=1,
)
@@ -173,7 +173,7 @@ def test_raw_response_resize(self, client: Kernel) -> None:
def test_streaming_response_resize(self, client: Kernel) -> None:
with client.browsers.process.with_streaming_response.resize(
process_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e",
- id="id",
+ id_or_name="htzv5orfit78e1m2biiifpbv",
cols=1,
rows=1,
) as response:
@@ -188,10 +188,10 @@ def test_streaming_response_resize(self, client: Kernel) -> None:
@pytest.mark.skip(reason="Mock server tests are disabled")
@parametrize
def test_path_params_resize(self, client: Kernel) -> None:
- with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"):
+ with pytest.raises(ValueError, match=r"Expected a non-empty value for `id_or_name` but received ''"):
client.browsers.process.with_raw_response.resize(
process_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e",
- id="",
+ id_or_name="",
cols=1,
rows=1,
)
@@ -199,7 +199,7 @@ def test_path_params_resize(self, client: Kernel) -> None:
with pytest.raises(ValueError, match=r"Expected a non-empty value for `process_id` but received ''"):
client.browsers.process.with_raw_response.resize(
process_id="",
- id="id",
+ id_or_name="htzv5orfit78e1m2biiifpbv",
cols=1,
rows=1,
)
@@ -208,7 +208,7 @@ def test_path_params_resize(self, client: Kernel) -> None:
@parametrize
def test_method_spawn(self, client: Kernel) -> None:
process = client.browsers.process.spawn(
- id="id",
+ id_or_name="htzv5orfit78e1m2biiifpbv",
command="command",
)
assert_matches_type(ProcessSpawnResponse, process, path=["response"])
@@ -217,7 +217,7 @@ def test_method_spawn(self, client: Kernel) -> None:
@parametrize
def test_method_spawn_with_all_params(self, client: Kernel) -> None:
process = client.browsers.process.spawn(
- id="id",
+ id_or_name="htzv5orfit78e1m2biiifpbv",
command="command",
allocate_tty=True,
args=["string"],
@@ -235,7 +235,7 @@ def test_method_spawn_with_all_params(self, client: Kernel) -> None:
@parametrize
def test_raw_response_spawn(self, client: Kernel) -> None:
response = client.browsers.process.with_raw_response.spawn(
- id="id",
+ id_or_name="htzv5orfit78e1m2biiifpbv",
command="command",
)
@@ -248,7 +248,7 @@ def test_raw_response_spawn(self, client: Kernel) -> None:
@parametrize
def test_streaming_response_spawn(self, client: Kernel) -> None:
with client.browsers.process.with_streaming_response.spawn(
- id="id",
+ id_or_name="htzv5orfit78e1m2biiifpbv",
command="command",
) as response:
assert not response.is_closed
@@ -262,9 +262,9 @@ def test_streaming_response_spawn(self, client: Kernel) -> None:
@pytest.mark.skip(reason="Mock server tests are disabled")
@parametrize
def test_path_params_spawn(self, client: Kernel) -> None:
- with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"):
+ with pytest.raises(ValueError, match=r"Expected a non-empty value for `id_or_name` but received ''"):
client.browsers.process.with_raw_response.spawn(
- id="",
+ id_or_name="",
command="command",
)
@@ -273,7 +273,7 @@ def test_path_params_spawn(self, client: Kernel) -> None:
def test_method_status(self, client: Kernel) -> None:
process = client.browsers.process.status(
process_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e",
- id="id",
+ id_or_name="htzv5orfit78e1m2biiifpbv",
)
assert_matches_type(ProcessStatusResponse, process, path=["response"])
@@ -282,7 +282,7 @@ def test_method_status(self, client: Kernel) -> None:
def test_raw_response_status(self, client: Kernel) -> None:
response = client.browsers.process.with_raw_response.status(
process_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e",
- id="id",
+ id_or_name="htzv5orfit78e1m2biiifpbv",
)
assert response.is_closed is True
@@ -295,7 +295,7 @@ def test_raw_response_status(self, client: Kernel) -> None:
def test_streaming_response_status(self, client: Kernel) -> None:
with client.browsers.process.with_streaming_response.status(
process_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e",
- id="id",
+ id_or_name="htzv5orfit78e1m2biiifpbv",
) as response:
assert not response.is_closed
assert response.http_request.headers.get("X-Stainless-Lang") == "python"
@@ -308,16 +308,16 @@ def test_streaming_response_status(self, client: Kernel) -> None:
@pytest.mark.skip(reason="Mock server tests are disabled")
@parametrize
def test_path_params_status(self, client: Kernel) -> None:
- with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"):
+ with pytest.raises(ValueError, match=r"Expected a non-empty value for `id_or_name` but received ''"):
client.browsers.process.with_raw_response.status(
process_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e",
- id="",
+ id_or_name="",
)
with pytest.raises(ValueError, match=r"Expected a non-empty value for `process_id` but received ''"):
client.browsers.process.with_raw_response.status(
process_id="",
- id="id",
+ id_or_name="htzv5orfit78e1m2biiifpbv",
)
@pytest.mark.skip(reason="Mock server tests are disabled")
@@ -325,7 +325,7 @@ def test_path_params_status(self, client: Kernel) -> None:
def test_method_stdin(self, client: Kernel) -> None:
process = client.browsers.process.stdin(
process_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e",
- id="id",
+ id_or_name="htzv5orfit78e1m2biiifpbv",
data_b64="data_b64",
)
assert_matches_type(ProcessStdinResponse, process, path=["response"])
@@ -335,7 +335,7 @@ def test_method_stdin(self, client: Kernel) -> None:
def test_raw_response_stdin(self, client: Kernel) -> None:
response = client.browsers.process.with_raw_response.stdin(
process_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e",
- id="id",
+ id_or_name="htzv5orfit78e1m2biiifpbv",
data_b64="data_b64",
)
@@ -349,7 +349,7 @@ def test_raw_response_stdin(self, client: Kernel) -> None:
def test_streaming_response_stdin(self, client: Kernel) -> None:
with client.browsers.process.with_streaming_response.stdin(
process_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e",
- id="id",
+ id_or_name="htzv5orfit78e1m2biiifpbv",
data_b64="data_b64",
) as response:
assert not response.is_closed
@@ -363,17 +363,17 @@ def test_streaming_response_stdin(self, client: Kernel) -> None:
@pytest.mark.skip(reason="Mock server tests are disabled")
@parametrize
def test_path_params_stdin(self, client: Kernel) -> None:
- with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"):
+ with pytest.raises(ValueError, match=r"Expected a non-empty value for `id_or_name` but received ''"):
client.browsers.process.with_raw_response.stdin(
process_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e",
- id="",
+ id_or_name="",
data_b64="data_b64",
)
with pytest.raises(ValueError, match=r"Expected a non-empty value for `process_id` but received ''"):
client.browsers.process.with_raw_response.stdin(
process_id="",
- id="id",
+ id_or_name="htzv5orfit78e1m2biiifpbv",
data_b64="data_b64",
)
@@ -382,7 +382,7 @@ def test_path_params_stdin(self, client: Kernel) -> None:
def test_method_stdout_stream(self, client: Kernel) -> None:
process_stream = client.browsers.process.stdout_stream(
process_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e",
- id="id",
+ id_or_name="htzv5orfit78e1m2biiifpbv",
)
process_stream.response.close()
@@ -391,7 +391,7 @@ def test_method_stdout_stream(self, client: Kernel) -> None:
def test_raw_response_stdout_stream(self, client: Kernel) -> None:
response = client.browsers.process.with_raw_response.stdout_stream(
process_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e",
- id="id",
+ id_or_name="htzv5orfit78e1m2biiifpbv",
)
assert response.http_request.headers.get("X-Stainless-Lang") == "python"
@@ -403,7 +403,7 @@ def test_raw_response_stdout_stream(self, client: Kernel) -> None:
def test_streaming_response_stdout_stream(self, client: Kernel) -> None:
with client.browsers.process.with_streaming_response.stdout_stream(
process_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e",
- id="id",
+ id_or_name="htzv5orfit78e1m2biiifpbv",
) as response:
assert not response.is_closed
assert response.http_request.headers.get("X-Stainless-Lang") == "python"
@@ -416,16 +416,16 @@ def test_streaming_response_stdout_stream(self, client: Kernel) -> None:
@pytest.mark.skip(reason="Mock server tests are disabled")
@parametrize
def test_path_params_stdout_stream(self, client: Kernel) -> None:
- with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"):
+ with pytest.raises(ValueError, match=r"Expected a non-empty value for `id_or_name` but received ''"):
client.browsers.process.with_raw_response.stdout_stream(
process_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e",
- id="",
+ id_or_name="",
)
with pytest.raises(ValueError, match=r"Expected a non-empty value for `process_id` but received ''"):
client.browsers.process.with_raw_response.stdout_stream(
process_id="",
- id="id",
+ id_or_name="htzv5orfit78e1m2biiifpbv",
)
@@ -438,7 +438,7 @@ class TestAsyncProcess:
@parametrize
async def test_method_exec(self, async_client: AsyncKernel) -> None:
process = await async_client.browsers.process.exec(
- id="id",
+ id_or_name="htzv5orfit78e1m2biiifpbv",
command="command",
)
assert_matches_type(ProcessExecResponse, process, path=["response"])
@@ -447,7 +447,7 @@ async def test_method_exec(self, async_client: AsyncKernel) -> None:
@parametrize
async def test_method_exec_with_all_params(self, async_client: AsyncKernel) -> None:
process = await async_client.browsers.process.exec(
- id="id",
+ id_or_name="htzv5orfit78e1m2biiifpbv",
command="command",
args=["string"],
as_root=True,
@@ -462,7 +462,7 @@ async def test_method_exec_with_all_params(self, async_client: AsyncKernel) -> N
@parametrize
async def test_raw_response_exec(self, async_client: AsyncKernel) -> None:
response = await async_client.browsers.process.with_raw_response.exec(
- id="id",
+ id_or_name="htzv5orfit78e1m2biiifpbv",
command="command",
)
@@ -475,7 +475,7 @@ async def test_raw_response_exec(self, async_client: AsyncKernel) -> None:
@parametrize
async def test_streaming_response_exec(self, async_client: AsyncKernel) -> None:
async with async_client.browsers.process.with_streaming_response.exec(
- id="id",
+ id_or_name="htzv5orfit78e1m2biiifpbv",
command="command",
) as response:
assert not response.is_closed
@@ -489,9 +489,9 @@ async def test_streaming_response_exec(self, async_client: AsyncKernel) -> None:
@pytest.mark.skip(reason="Mock server tests are disabled")
@parametrize
async def test_path_params_exec(self, async_client: AsyncKernel) -> None:
- with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"):
+ with pytest.raises(ValueError, match=r"Expected a non-empty value for `id_or_name` but received ''"):
await async_client.browsers.process.with_raw_response.exec(
- id="",
+ id_or_name="",
command="command",
)
@@ -500,7 +500,7 @@ async def test_path_params_exec(self, async_client: AsyncKernel) -> None:
async def test_method_kill(self, async_client: AsyncKernel) -> None:
process = await async_client.browsers.process.kill(
process_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e",
- id="id",
+ id_or_name="htzv5orfit78e1m2biiifpbv",
signal="TERM",
)
assert_matches_type(ProcessKillResponse, process, path=["response"])
@@ -510,7 +510,7 @@ async def test_method_kill(self, async_client: AsyncKernel) -> None:
async def test_raw_response_kill(self, async_client: AsyncKernel) -> None:
response = await async_client.browsers.process.with_raw_response.kill(
process_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e",
- id="id",
+ id_or_name="htzv5orfit78e1m2biiifpbv",
signal="TERM",
)
@@ -524,7 +524,7 @@ async def test_raw_response_kill(self, async_client: AsyncKernel) -> None:
async def test_streaming_response_kill(self, async_client: AsyncKernel) -> None:
async with async_client.browsers.process.with_streaming_response.kill(
process_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e",
- id="id",
+ id_or_name="htzv5orfit78e1m2biiifpbv",
signal="TERM",
) as response:
assert not response.is_closed
@@ -538,17 +538,17 @@ async def test_streaming_response_kill(self, async_client: AsyncKernel) -> None:
@pytest.mark.skip(reason="Mock server tests are disabled")
@parametrize
async def test_path_params_kill(self, async_client: AsyncKernel) -> None:
- with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"):
+ with pytest.raises(ValueError, match=r"Expected a non-empty value for `id_or_name` but received ''"):
await async_client.browsers.process.with_raw_response.kill(
process_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e",
- id="",
+ id_or_name="",
signal="TERM",
)
with pytest.raises(ValueError, match=r"Expected a non-empty value for `process_id` but received ''"):
await async_client.browsers.process.with_raw_response.kill(
process_id="",
- id="id",
+ id_or_name="htzv5orfit78e1m2biiifpbv",
signal="TERM",
)
@@ -557,7 +557,7 @@ async def test_path_params_kill(self, async_client: AsyncKernel) -> None:
async def test_method_resize(self, async_client: AsyncKernel) -> None:
process = await async_client.browsers.process.resize(
process_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e",
- id="id",
+ id_or_name="htzv5orfit78e1m2biiifpbv",
cols=1,
rows=1,
)
@@ -568,7 +568,7 @@ async def test_method_resize(self, async_client: AsyncKernel) -> None:
async def test_raw_response_resize(self, async_client: AsyncKernel) -> None:
response = await async_client.browsers.process.with_raw_response.resize(
process_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e",
- id="id",
+ id_or_name="htzv5orfit78e1m2biiifpbv",
cols=1,
rows=1,
)
@@ -583,7 +583,7 @@ async def test_raw_response_resize(self, async_client: AsyncKernel) -> None:
async def test_streaming_response_resize(self, async_client: AsyncKernel) -> None:
async with async_client.browsers.process.with_streaming_response.resize(
process_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e",
- id="id",
+ id_or_name="htzv5orfit78e1m2biiifpbv",
cols=1,
rows=1,
) as response:
@@ -598,10 +598,10 @@ async def test_streaming_response_resize(self, async_client: AsyncKernel) -> Non
@pytest.mark.skip(reason="Mock server tests are disabled")
@parametrize
async def test_path_params_resize(self, async_client: AsyncKernel) -> None:
- with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"):
+ with pytest.raises(ValueError, match=r"Expected a non-empty value for `id_or_name` but received ''"):
await async_client.browsers.process.with_raw_response.resize(
process_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e",
- id="",
+ id_or_name="",
cols=1,
rows=1,
)
@@ -609,7 +609,7 @@ async def test_path_params_resize(self, async_client: AsyncKernel) -> None:
with pytest.raises(ValueError, match=r"Expected a non-empty value for `process_id` but received ''"):
await async_client.browsers.process.with_raw_response.resize(
process_id="",
- id="id",
+ id_or_name="htzv5orfit78e1m2biiifpbv",
cols=1,
rows=1,
)
@@ -618,7 +618,7 @@ async def test_path_params_resize(self, async_client: AsyncKernel) -> None:
@parametrize
async def test_method_spawn(self, async_client: AsyncKernel) -> None:
process = await async_client.browsers.process.spawn(
- id="id",
+ id_or_name="htzv5orfit78e1m2biiifpbv",
command="command",
)
assert_matches_type(ProcessSpawnResponse, process, path=["response"])
@@ -627,7 +627,7 @@ async def test_method_spawn(self, async_client: AsyncKernel) -> None:
@parametrize
async def test_method_spawn_with_all_params(self, async_client: AsyncKernel) -> None:
process = await async_client.browsers.process.spawn(
- id="id",
+ id_or_name="htzv5orfit78e1m2biiifpbv",
command="command",
allocate_tty=True,
args=["string"],
@@ -645,7 +645,7 @@ async def test_method_spawn_with_all_params(self, async_client: AsyncKernel) ->
@parametrize
async def test_raw_response_spawn(self, async_client: AsyncKernel) -> None:
response = await async_client.browsers.process.with_raw_response.spawn(
- id="id",
+ id_or_name="htzv5orfit78e1m2biiifpbv",
command="command",
)
@@ -658,7 +658,7 @@ async def test_raw_response_spawn(self, async_client: AsyncKernel) -> None:
@parametrize
async def test_streaming_response_spawn(self, async_client: AsyncKernel) -> None:
async with async_client.browsers.process.with_streaming_response.spawn(
- id="id",
+ id_or_name="htzv5orfit78e1m2biiifpbv",
command="command",
) as response:
assert not response.is_closed
@@ -672,9 +672,9 @@ async def test_streaming_response_spawn(self, async_client: AsyncKernel) -> None
@pytest.mark.skip(reason="Mock server tests are disabled")
@parametrize
async def test_path_params_spawn(self, async_client: AsyncKernel) -> None:
- with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"):
+ with pytest.raises(ValueError, match=r"Expected a non-empty value for `id_or_name` but received ''"):
await async_client.browsers.process.with_raw_response.spawn(
- id="",
+ id_or_name="",
command="command",
)
@@ -683,7 +683,7 @@ async def test_path_params_spawn(self, async_client: AsyncKernel) -> None:
async def test_method_status(self, async_client: AsyncKernel) -> None:
process = await async_client.browsers.process.status(
process_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e",
- id="id",
+ id_or_name="htzv5orfit78e1m2biiifpbv",
)
assert_matches_type(ProcessStatusResponse, process, path=["response"])
@@ -692,7 +692,7 @@ async def test_method_status(self, async_client: AsyncKernel) -> None:
async def test_raw_response_status(self, async_client: AsyncKernel) -> None:
response = await async_client.browsers.process.with_raw_response.status(
process_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e",
- id="id",
+ id_or_name="htzv5orfit78e1m2biiifpbv",
)
assert response.is_closed is True
@@ -705,7 +705,7 @@ async def test_raw_response_status(self, async_client: AsyncKernel) -> None:
async def test_streaming_response_status(self, async_client: AsyncKernel) -> None:
async with async_client.browsers.process.with_streaming_response.status(
process_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e",
- id="id",
+ id_or_name="htzv5orfit78e1m2biiifpbv",
) as response:
assert not response.is_closed
assert response.http_request.headers.get("X-Stainless-Lang") == "python"
@@ -718,16 +718,16 @@ async def test_streaming_response_status(self, async_client: AsyncKernel) -> Non
@pytest.mark.skip(reason="Mock server tests are disabled")
@parametrize
async def test_path_params_status(self, async_client: AsyncKernel) -> None:
- with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"):
+ with pytest.raises(ValueError, match=r"Expected a non-empty value for `id_or_name` but received ''"):
await async_client.browsers.process.with_raw_response.status(
process_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e",
- id="",
+ id_or_name="",
)
with pytest.raises(ValueError, match=r"Expected a non-empty value for `process_id` but received ''"):
await async_client.browsers.process.with_raw_response.status(
process_id="",
- id="id",
+ id_or_name="htzv5orfit78e1m2biiifpbv",
)
@pytest.mark.skip(reason="Mock server tests are disabled")
@@ -735,7 +735,7 @@ async def test_path_params_status(self, async_client: AsyncKernel) -> None:
async def test_method_stdin(self, async_client: AsyncKernel) -> None:
process = await async_client.browsers.process.stdin(
process_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e",
- id="id",
+ id_or_name="htzv5orfit78e1m2biiifpbv",
data_b64="data_b64",
)
assert_matches_type(ProcessStdinResponse, process, path=["response"])
@@ -745,7 +745,7 @@ async def test_method_stdin(self, async_client: AsyncKernel) -> None:
async def test_raw_response_stdin(self, async_client: AsyncKernel) -> None:
response = await async_client.browsers.process.with_raw_response.stdin(
process_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e",
- id="id",
+ id_or_name="htzv5orfit78e1m2biiifpbv",
data_b64="data_b64",
)
@@ -759,7 +759,7 @@ async def test_raw_response_stdin(self, async_client: AsyncKernel) -> None:
async def test_streaming_response_stdin(self, async_client: AsyncKernel) -> None:
async with async_client.browsers.process.with_streaming_response.stdin(
process_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e",
- id="id",
+ id_or_name="htzv5orfit78e1m2biiifpbv",
data_b64="data_b64",
) as response:
assert not response.is_closed
@@ -773,17 +773,17 @@ async def test_streaming_response_stdin(self, async_client: AsyncKernel) -> None
@pytest.mark.skip(reason="Mock server tests are disabled")
@parametrize
async def test_path_params_stdin(self, async_client: AsyncKernel) -> None:
- with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"):
+ with pytest.raises(ValueError, match=r"Expected a non-empty value for `id_or_name` but received ''"):
await async_client.browsers.process.with_raw_response.stdin(
process_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e",
- id="",
+ id_or_name="",
data_b64="data_b64",
)
with pytest.raises(ValueError, match=r"Expected a non-empty value for `process_id` but received ''"):
await async_client.browsers.process.with_raw_response.stdin(
process_id="",
- id="id",
+ id_or_name="htzv5orfit78e1m2biiifpbv",
data_b64="data_b64",
)
@@ -792,7 +792,7 @@ async def test_path_params_stdin(self, async_client: AsyncKernel) -> None:
async def test_method_stdout_stream(self, async_client: AsyncKernel) -> None:
process_stream = await async_client.browsers.process.stdout_stream(
process_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e",
- id="id",
+ id_or_name="htzv5orfit78e1m2biiifpbv",
)
await process_stream.response.aclose()
@@ -801,7 +801,7 @@ async def test_method_stdout_stream(self, async_client: AsyncKernel) -> None:
async def test_raw_response_stdout_stream(self, async_client: AsyncKernel) -> None:
response = await async_client.browsers.process.with_raw_response.stdout_stream(
process_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e",
- id="id",
+ id_or_name="htzv5orfit78e1m2biiifpbv",
)
assert response.http_request.headers.get("X-Stainless-Lang") == "python"
@@ -813,7 +813,7 @@ async def test_raw_response_stdout_stream(self, async_client: AsyncKernel) -> No
async def test_streaming_response_stdout_stream(self, async_client: AsyncKernel) -> None:
async with async_client.browsers.process.with_streaming_response.stdout_stream(
process_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e",
- id="id",
+ id_or_name="htzv5orfit78e1m2biiifpbv",
) as response:
assert not response.is_closed
assert response.http_request.headers.get("X-Stainless-Lang") == "python"
@@ -826,14 +826,14 @@ async def test_streaming_response_stdout_stream(self, async_client: AsyncKernel)
@pytest.mark.skip(reason="Mock server tests are disabled")
@parametrize
async def test_path_params_stdout_stream(self, async_client: AsyncKernel) -> None:
- with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"):
+ with pytest.raises(ValueError, match=r"Expected a non-empty value for `id_or_name` but received ''"):
await async_client.browsers.process.with_raw_response.stdout_stream(
process_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e",
- id="",
+ id_or_name="",
)
with pytest.raises(ValueError, match=r"Expected a non-empty value for `process_id` but received ''"):
await async_client.browsers.process.with_raw_response.stdout_stream(
process_id="",
- id="id",
+ id_or_name="htzv5orfit78e1m2biiifpbv",
)
diff --git a/tests/api_resources/browsers/test_replays.py b/tests/api_resources/browsers/test_replays.py
index 0b450ff8..bb6ddbdd 100644
--- a/tests/api_resources/browsers/test_replays.py
+++ b/tests/api_resources/browsers/test_replays.py
@@ -29,7 +29,7 @@ class TestReplays:
@parametrize
def test_method_list(self, client: Kernel) -> None:
replay = client.browsers.replays.list(
- "id",
+ "htzv5orfit78e1m2biiifpbv",
)
assert_matches_type(ReplayListResponse, replay, path=["response"])
@@ -37,7 +37,7 @@ def test_method_list(self, client: Kernel) -> None:
@parametrize
def test_raw_response_list(self, client: Kernel) -> None:
response = client.browsers.replays.with_raw_response.list(
- "id",
+ "htzv5orfit78e1m2biiifpbv",
)
assert response.is_closed is True
@@ -49,7 +49,7 @@ def test_raw_response_list(self, client: Kernel) -> None:
@parametrize
def test_streaming_response_list(self, client: Kernel) -> None:
with client.browsers.replays.with_streaming_response.list(
- "id",
+ "htzv5orfit78e1m2biiifpbv",
) as response:
assert not response.is_closed
assert response.http_request.headers.get("X-Stainless-Lang") == "python"
@@ -62,7 +62,7 @@ def test_streaming_response_list(self, client: Kernel) -> None:
@pytest.mark.skip(reason="Mock server tests are disabled")
@parametrize
def test_path_params_list(self, client: Kernel) -> None:
- with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"):
+ with pytest.raises(ValueError, match=r"Expected a non-empty value for `id_or_name` but received ''"):
client.browsers.replays.with_raw_response.list(
"",
)
@@ -70,10 +70,12 @@ def test_path_params_list(self, client: Kernel) -> None:
@parametrize
@pytest.mark.respx(base_url=base_url)
def test_method_download(self, client: Kernel, respx_mock: MockRouter) -> None:
- respx_mock.get("/browsers/id/replays/replay_id").mock(return_value=httpx.Response(200, json={"foo": "bar"}))
+ respx_mock.get("/browsers/htzv5orfit78e1m2biiifpbv/replays/replay_id").mock(
+ return_value=httpx.Response(200, json={"foo": "bar"})
+ )
replay = client.browsers.replays.download(
replay_id="replay_id",
- id="id",
+ id_or_name="htzv5orfit78e1m2biiifpbv",
)
assert replay.is_closed
assert replay.json() == {"foo": "bar"}
@@ -83,11 +85,13 @@ def test_method_download(self, client: Kernel, respx_mock: MockRouter) -> None:
@parametrize
@pytest.mark.respx(base_url=base_url)
def test_raw_response_download(self, client: Kernel, respx_mock: MockRouter) -> None:
- respx_mock.get("/browsers/id/replays/replay_id").mock(return_value=httpx.Response(200, json={"foo": "bar"}))
+ respx_mock.get("/browsers/htzv5orfit78e1m2biiifpbv/replays/replay_id").mock(
+ return_value=httpx.Response(200, json={"foo": "bar"})
+ )
replay = client.browsers.replays.with_raw_response.download(
replay_id="replay_id",
- id="id",
+ id_or_name="htzv5orfit78e1m2biiifpbv",
)
assert replay.is_closed is True
@@ -98,10 +102,12 @@ def test_raw_response_download(self, client: Kernel, respx_mock: MockRouter) ->
@parametrize
@pytest.mark.respx(base_url=base_url)
def test_streaming_response_download(self, client: Kernel, respx_mock: MockRouter) -> None:
- respx_mock.get("/browsers/id/replays/replay_id").mock(return_value=httpx.Response(200, json={"foo": "bar"}))
+ respx_mock.get("/browsers/htzv5orfit78e1m2biiifpbv/replays/replay_id").mock(
+ return_value=httpx.Response(200, json={"foo": "bar"})
+ )
with client.browsers.replays.with_streaming_response.download(
replay_id="replay_id",
- id="id",
+ id_or_name="htzv5orfit78e1m2biiifpbv",
) as replay:
assert not replay.is_closed
assert replay.http_request.headers.get("X-Stainless-Lang") == "python"
@@ -115,23 +121,23 @@ def test_streaming_response_download(self, client: Kernel, respx_mock: MockRoute
@parametrize
@pytest.mark.respx(base_url=base_url)
def test_path_params_download(self, client: Kernel) -> None:
- with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"):
+ with pytest.raises(ValueError, match=r"Expected a non-empty value for `id_or_name` but received ''"):
client.browsers.replays.with_raw_response.download(
replay_id="replay_id",
- id="",
+ id_or_name="",
)
with pytest.raises(ValueError, match=r"Expected a non-empty value for `replay_id` but received ''"):
client.browsers.replays.with_raw_response.download(
replay_id="",
- id="id",
+ id_or_name="htzv5orfit78e1m2biiifpbv",
)
@pytest.mark.skip(reason="Mock server tests are disabled")
@parametrize
def test_method_start(self, client: Kernel) -> None:
replay = client.browsers.replays.start(
- id="id",
+ id_or_name="htzv5orfit78e1m2biiifpbv",
)
assert_matches_type(ReplayStartResponse, replay, path=["response"])
@@ -139,7 +145,7 @@ def test_method_start(self, client: Kernel) -> None:
@parametrize
def test_method_start_with_all_params(self, client: Kernel) -> None:
replay = client.browsers.replays.start(
- id="id",
+ id_or_name="htzv5orfit78e1m2biiifpbv",
framerate=1,
max_duration_in_seconds=1,
record_audio=True,
@@ -150,7 +156,7 @@ def test_method_start_with_all_params(self, client: Kernel) -> None:
@parametrize
def test_raw_response_start(self, client: Kernel) -> None:
response = client.browsers.replays.with_raw_response.start(
- id="id",
+ id_or_name="htzv5orfit78e1m2biiifpbv",
)
assert response.is_closed is True
@@ -162,7 +168,7 @@ def test_raw_response_start(self, client: Kernel) -> None:
@parametrize
def test_streaming_response_start(self, client: Kernel) -> None:
with client.browsers.replays.with_streaming_response.start(
- id="id",
+ id_or_name="htzv5orfit78e1m2biiifpbv",
) as response:
assert not response.is_closed
assert response.http_request.headers.get("X-Stainless-Lang") == "python"
@@ -175,9 +181,9 @@ def test_streaming_response_start(self, client: Kernel) -> None:
@pytest.mark.skip(reason="Mock server tests are disabled")
@parametrize
def test_path_params_start(self, client: Kernel) -> None:
- with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"):
+ with pytest.raises(ValueError, match=r"Expected a non-empty value for `id_or_name` but received ''"):
client.browsers.replays.with_raw_response.start(
- id="",
+ id_or_name="",
)
@pytest.mark.skip(reason="Mock server tests are disabled")
@@ -185,7 +191,7 @@ def test_path_params_start(self, client: Kernel) -> None:
def test_method_stop(self, client: Kernel) -> None:
replay = client.browsers.replays.stop(
replay_id="replay_id",
- id="id",
+ id_or_name="htzv5orfit78e1m2biiifpbv",
)
assert replay is None
@@ -194,7 +200,7 @@ def test_method_stop(self, client: Kernel) -> None:
def test_raw_response_stop(self, client: Kernel) -> None:
response = client.browsers.replays.with_raw_response.stop(
replay_id="replay_id",
- id="id",
+ id_or_name="htzv5orfit78e1m2biiifpbv",
)
assert response.is_closed is True
@@ -207,7 +213,7 @@ def test_raw_response_stop(self, client: Kernel) -> None:
def test_streaming_response_stop(self, client: Kernel) -> None:
with client.browsers.replays.with_streaming_response.stop(
replay_id="replay_id",
- id="id",
+ id_or_name="htzv5orfit78e1m2biiifpbv",
) as response:
assert not response.is_closed
assert response.http_request.headers.get("X-Stainless-Lang") == "python"
@@ -220,16 +226,16 @@ def test_streaming_response_stop(self, client: Kernel) -> None:
@pytest.mark.skip(reason="Mock server tests are disabled")
@parametrize
def test_path_params_stop(self, client: Kernel) -> None:
- with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"):
+ with pytest.raises(ValueError, match=r"Expected a non-empty value for `id_or_name` but received ''"):
client.browsers.replays.with_raw_response.stop(
replay_id="replay_id",
- id="",
+ id_or_name="",
)
with pytest.raises(ValueError, match=r"Expected a non-empty value for `replay_id` but received ''"):
client.browsers.replays.with_raw_response.stop(
replay_id="",
- id="id",
+ id_or_name="htzv5orfit78e1m2biiifpbv",
)
@@ -242,7 +248,7 @@ class TestAsyncReplays:
@parametrize
async def test_method_list(self, async_client: AsyncKernel) -> None:
replay = await async_client.browsers.replays.list(
- "id",
+ "htzv5orfit78e1m2biiifpbv",
)
assert_matches_type(ReplayListResponse, replay, path=["response"])
@@ -250,7 +256,7 @@ async def test_method_list(self, async_client: AsyncKernel) -> None:
@parametrize
async def test_raw_response_list(self, async_client: AsyncKernel) -> None:
response = await async_client.browsers.replays.with_raw_response.list(
- "id",
+ "htzv5orfit78e1m2biiifpbv",
)
assert response.is_closed is True
@@ -262,7 +268,7 @@ async def test_raw_response_list(self, async_client: AsyncKernel) -> None:
@parametrize
async def test_streaming_response_list(self, async_client: AsyncKernel) -> None:
async with async_client.browsers.replays.with_streaming_response.list(
- "id",
+ "htzv5orfit78e1m2biiifpbv",
) as response:
assert not response.is_closed
assert response.http_request.headers.get("X-Stainless-Lang") == "python"
@@ -275,7 +281,7 @@ async def test_streaming_response_list(self, async_client: AsyncKernel) -> None:
@pytest.mark.skip(reason="Mock server tests are disabled")
@parametrize
async def test_path_params_list(self, async_client: AsyncKernel) -> None:
- with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"):
+ with pytest.raises(ValueError, match=r"Expected a non-empty value for `id_or_name` but received ''"):
await async_client.browsers.replays.with_raw_response.list(
"",
)
@@ -283,10 +289,12 @@ async def test_path_params_list(self, async_client: AsyncKernel) -> None:
@parametrize
@pytest.mark.respx(base_url=base_url)
async def test_method_download(self, async_client: AsyncKernel, respx_mock: MockRouter) -> None:
- respx_mock.get("/browsers/id/replays/replay_id").mock(return_value=httpx.Response(200, json={"foo": "bar"}))
+ respx_mock.get("/browsers/htzv5orfit78e1m2biiifpbv/replays/replay_id").mock(
+ return_value=httpx.Response(200, json={"foo": "bar"})
+ )
replay = await async_client.browsers.replays.download(
replay_id="replay_id",
- id="id",
+ id_or_name="htzv5orfit78e1m2biiifpbv",
)
assert replay.is_closed
assert await replay.json() == {"foo": "bar"}
@@ -296,11 +304,13 @@ async def test_method_download(self, async_client: AsyncKernel, respx_mock: Mock
@parametrize
@pytest.mark.respx(base_url=base_url)
async def test_raw_response_download(self, async_client: AsyncKernel, respx_mock: MockRouter) -> None:
- respx_mock.get("/browsers/id/replays/replay_id").mock(return_value=httpx.Response(200, json={"foo": "bar"}))
+ respx_mock.get("/browsers/htzv5orfit78e1m2biiifpbv/replays/replay_id").mock(
+ return_value=httpx.Response(200, json={"foo": "bar"})
+ )
replay = await async_client.browsers.replays.with_raw_response.download(
replay_id="replay_id",
- id="id",
+ id_or_name="htzv5orfit78e1m2biiifpbv",
)
assert replay.is_closed is True
@@ -311,10 +321,12 @@ async def test_raw_response_download(self, async_client: AsyncKernel, respx_mock
@parametrize
@pytest.mark.respx(base_url=base_url)
async def test_streaming_response_download(self, async_client: AsyncKernel, respx_mock: MockRouter) -> None:
- respx_mock.get("/browsers/id/replays/replay_id").mock(return_value=httpx.Response(200, json={"foo": "bar"}))
+ respx_mock.get("/browsers/htzv5orfit78e1m2biiifpbv/replays/replay_id").mock(
+ return_value=httpx.Response(200, json={"foo": "bar"})
+ )
async with async_client.browsers.replays.with_streaming_response.download(
replay_id="replay_id",
- id="id",
+ id_or_name="htzv5orfit78e1m2biiifpbv",
) as replay:
assert not replay.is_closed
assert replay.http_request.headers.get("X-Stainless-Lang") == "python"
@@ -328,23 +340,23 @@ async def test_streaming_response_download(self, async_client: AsyncKernel, resp
@parametrize
@pytest.mark.respx(base_url=base_url)
async def test_path_params_download(self, async_client: AsyncKernel) -> None:
- with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"):
+ with pytest.raises(ValueError, match=r"Expected a non-empty value for `id_or_name` but received ''"):
await async_client.browsers.replays.with_raw_response.download(
replay_id="replay_id",
- id="",
+ id_or_name="",
)
with pytest.raises(ValueError, match=r"Expected a non-empty value for `replay_id` but received ''"):
await async_client.browsers.replays.with_raw_response.download(
replay_id="",
- id="id",
+ id_or_name="htzv5orfit78e1m2biiifpbv",
)
@pytest.mark.skip(reason="Mock server tests are disabled")
@parametrize
async def test_method_start(self, async_client: AsyncKernel) -> None:
replay = await async_client.browsers.replays.start(
- id="id",
+ id_or_name="htzv5orfit78e1m2biiifpbv",
)
assert_matches_type(ReplayStartResponse, replay, path=["response"])
@@ -352,7 +364,7 @@ async def test_method_start(self, async_client: AsyncKernel) -> None:
@parametrize
async def test_method_start_with_all_params(self, async_client: AsyncKernel) -> None:
replay = await async_client.browsers.replays.start(
- id="id",
+ id_or_name="htzv5orfit78e1m2biiifpbv",
framerate=1,
max_duration_in_seconds=1,
record_audio=True,
@@ -363,7 +375,7 @@ async def test_method_start_with_all_params(self, async_client: AsyncKernel) ->
@parametrize
async def test_raw_response_start(self, async_client: AsyncKernel) -> None:
response = await async_client.browsers.replays.with_raw_response.start(
- id="id",
+ id_or_name="htzv5orfit78e1m2biiifpbv",
)
assert response.is_closed is True
@@ -375,7 +387,7 @@ async def test_raw_response_start(self, async_client: AsyncKernel) -> None:
@parametrize
async def test_streaming_response_start(self, async_client: AsyncKernel) -> None:
async with async_client.browsers.replays.with_streaming_response.start(
- id="id",
+ id_or_name="htzv5orfit78e1m2biiifpbv",
) as response:
assert not response.is_closed
assert response.http_request.headers.get("X-Stainless-Lang") == "python"
@@ -388,9 +400,9 @@ async def test_streaming_response_start(self, async_client: AsyncKernel) -> None
@pytest.mark.skip(reason="Mock server tests are disabled")
@parametrize
async def test_path_params_start(self, async_client: AsyncKernel) -> None:
- with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"):
+ with pytest.raises(ValueError, match=r"Expected a non-empty value for `id_or_name` but received ''"):
await async_client.browsers.replays.with_raw_response.start(
- id="",
+ id_or_name="",
)
@pytest.mark.skip(reason="Mock server tests are disabled")
@@ -398,7 +410,7 @@ async def test_path_params_start(self, async_client: AsyncKernel) -> None:
async def test_method_stop(self, async_client: AsyncKernel) -> None:
replay = await async_client.browsers.replays.stop(
replay_id="replay_id",
- id="id",
+ id_or_name="htzv5orfit78e1m2biiifpbv",
)
assert replay is None
@@ -407,7 +419,7 @@ async def test_method_stop(self, async_client: AsyncKernel) -> None:
async def test_raw_response_stop(self, async_client: AsyncKernel) -> None:
response = await async_client.browsers.replays.with_raw_response.stop(
replay_id="replay_id",
- id="id",
+ id_or_name="htzv5orfit78e1m2biiifpbv",
)
assert response.is_closed is True
@@ -420,7 +432,7 @@ async def test_raw_response_stop(self, async_client: AsyncKernel) -> None:
async def test_streaming_response_stop(self, async_client: AsyncKernel) -> None:
async with async_client.browsers.replays.with_streaming_response.stop(
replay_id="replay_id",
- id="id",
+ id_or_name="htzv5orfit78e1m2biiifpbv",
) as response:
assert not response.is_closed
assert response.http_request.headers.get("X-Stainless-Lang") == "python"
@@ -433,14 +445,14 @@ async def test_streaming_response_stop(self, async_client: AsyncKernel) -> None:
@pytest.mark.skip(reason="Mock server tests are disabled")
@parametrize
async def test_path_params_stop(self, async_client: AsyncKernel) -> None:
- with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"):
+ with pytest.raises(ValueError, match=r"Expected a non-empty value for `id_or_name` but received ''"):
await async_client.browsers.replays.with_raw_response.stop(
replay_id="replay_id",
- id="",
+ id_or_name="",
)
with pytest.raises(ValueError, match=r"Expected a non-empty value for `replay_id` but received ''"):
await async_client.browsers.replays.with_raw_response.stop(
replay_id="",
- id="id",
+ id_or_name="htzv5orfit78e1m2biiifpbv",
)
diff --git a/tests/api_resources/browsers/test_telemetry.py b/tests/api_resources/browsers/test_telemetry.py
index 3c5dad44..b54dfbc1 100644
--- a/tests/api_resources/browsers/test_telemetry.py
+++ b/tests/api_resources/browsers/test_telemetry.py
@@ -22,7 +22,7 @@ class TestTelemetry:
@parametrize
def test_method_events(self, client: Kernel) -> None:
telemetry = client.browsers.telemetry.events(
- id="id",
+ id_or_name="htzv5orfit78e1m2biiifpbv",
)
assert_matches_type(SyncOffsetPagination[TelemetryEventsResponse], telemetry, path=["response"])
@@ -30,7 +30,7 @@ def test_method_events(self, client: Kernel) -> None:
@parametrize
def test_method_events_with_all_params(self, client: Kernel) -> None:
telemetry = client.browsers.telemetry.events(
- id="id",
+ id_or_name="htzv5orfit78e1m2biiifpbv",
category=["console"],
limit=1,
offset=0,
@@ -44,7 +44,7 @@ def test_method_events_with_all_params(self, client: Kernel) -> None:
@parametrize
def test_raw_response_events(self, client: Kernel) -> None:
response = client.browsers.telemetry.with_raw_response.events(
- id="id",
+ id_or_name="htzv5orfit78e1m2biiifpbv",
)
assert response.is_closed is True
@@ -56,7 +56,7 @@ def test_raw_response_events(self, client: Kernel) -> None:
@parametrize
def test_streaming_response_events(self, client: Kernel) -> None:
with client.browsers.telemetry.with_streaming_response.events(
- id="id",
+ id_or_name="htzv5orfit78e1m2biiifpbv",
) as response:
assert not response.is_closed
assert response.http_request.headers.get("X-Stainless-Lang") == "python"
@@ -69,16 +69,16 @@ def test_streaming_response_events(self, client: Kernel) -> None:
@pytest.mark.skip(reason="Mock server tests are disabled")
@parametrize
def test_path_params_events(self, client: Kernel) -> None:
- with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"):
+ with pytest.raises(ValueError, match=r"Expected a non-empty value for `id_or_name` but received ''"):
client.browsers.telemetry.with_raw_response.events(
- id="",
+ id_or_name="",
)
@pytest.mark.skip(reason="Mock server tests are disabled")
@parametrize
def test_method_stream(self, client: Kernel) -> None:
telemetry_stream = client.browsers.telemetry.stream(
- id="id",
+ id_or_name="htzv5orfit78e1m2biiifpbv",
)
telemetry_stream.response.close()
@@ -86,7 +86,7 @@ def test_method_stream(self, client: Kernel) -> None:
@parametrize
def test_method_stream_with_all_params(self, client: Kernel) -> None:
telemetry_stream = client.browsers.telemetry.stream(
- id="id",
+ id_or_name="htzv5orfit78e1m2biiifpbv",
replay="replay",
last_event_id="Last-Event-ID",
)
@@ -96,7 +96,7 @@ def test_method_stream_with_all_params(self, client: Kernel) -> None:
@parametrize
def test_raw_response_stream(self, client: Kernel) -> None:
response = client.browsers.telemetry.with_raw_response.stream(
- id="id",
+ id_or_name="htzv5orfit78e1m2biiifpbv",
)
assert response.http_request.headers.get("X-Stainless-Lang") == "python"
@@ -107,7 +107,7 @@ def test_raw_response_stream(self, client: Kernel) -> None:
@parametrize
def test_streaming_response_stream(self, client: Kernel) -> None:
with client.browsers.telemetry.with_streaming_response.stream(
- id="id",
+ id_or_name="htzv5orfit78e1m2biiifpbv",
) as response:
assert not response.is_closed
assert response.http_request.headers.get("X-Stainless-Lang") == "python"
@@ -120,9 +120,9 @@ def test_streaming_response_stream(self, client: Kernel) -> None:
@pytest.mark.skip(reason="Mock server tests are disabled")
@parametrize
def test_path_params_stream(self, client: Kernel) -> None:
- with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"):
+ with pytest.raises(ValueError, match=r"Expected a non-empty value for `id_or_name` but received ''"):
client.browsers.telemetry.with_raw_response.stream(
- id="",
+ id_or_name="",
)
@@ -135,7 +135,7 @@ class TestAsyncTelemetry:
@parametrize
async def test_method_events(self, async_client: AsyncKernel) -> None:
telemetry = await async_client.browsers.telemetry.events(
- id="id",
+ id_or_name="htzv5orfit78e1m2biiifpbv",
)
assert_matches_type(AsyncOffsetPagination[TelemetryEventsResponse], telemetry, path=["response"])
@@ -143,7 +143,7 @@ async def test_method_events(self, async_client: AsyncKernel) -> None:
@parametrize
async def test_method_events_with_all_params(self, async_client: AsyncKernel) -> None:
telemetry = await async_client.browsers.telemetry.events(
- id="id",
+ id_or_name="htzv5orfit78e1m2biiifpbv",
category=["console"],
limit=1,
offset=0,
@@ -157,7 +157,7 @@ async def test_method_events_with_all_params(self, async_client: AsyncKernel) ->
@parametrize
async def test_raw_response_events(self, async_client: AsyncKernel) -> None:
response = await async_client.browsers.telemetry.with_raw_response.events(
- id="id",
+ id_or_name="htzv5orfit78e1m2biiifpbv",
)
assert response.is_closed is True
@@ -169,7 +169,7 @@ async def test_raw_response_events(self, async_client: AsyncKernel) -> None:
@parametrize
async def test_streaming_response_events(self, async_client: AsyncKernel) -> None:
async with async_client.browsers.telemetry.with_streaming_response.events(
- id="id",
+ id_or_name="htzv5orfit78e1m2biiifpbv",
) as response:
assert not response.is_closed
assert response.http_request.headers.get("X-Stainless-Lang") == "python"
@@ -182,16 +182,16 @@ async def test_streaming_response_events(self, async_client: AsyncKernel) -> Non
@pytest.mark.skip(reason="Mock server tests are disabled")
@parametrize
async def test_path_params_events(self, async_client: AsyncKernel) -> None:
- with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"):
+ with pytest.raises(ValueError, match=r"Expected a non-empty value for `id_or_name` but received ''"):
await async_client.browsers.telemetry.with_raw_response.events(
- id="",
+ id_or_name="",
)
@pytest.mark.skip(reason="Mock server tests are disabled")
@parametrize
async def test_method_stream(self, async_client: AsyncKernel) -> None:
telemetry_stream = await async_client.browsers.telemetry.stream(
- id="id",
+ id_or_name="htzv5orfit78e1m2biiifpbv",
)
await telemetry_stream.response.aclose()
@@ -199,7 +199,7 @@ async def test_method_stream(self, async_client: AsyncKernel) -> None:
@parametrize
async def test_method_stream_with_all_params(self, async_client: AsyncKernel) -> None:
telemetry_stream = await async_client.browsers.telemetry.stream(
- id="id",
+ id_or_name="htzv5orfit78e1m2biiifpbv",
replay="replay",
last_event_id="Last-Event-ID",
)
@@ -209,7 +209,7 @@ async def test_method_stream_with_all_params(self, async_client: AsyncKernel) ->
@parametrize
async def test_raw_response_stream(self, async_client: AsyncKernel) -> None:
response = await async_client.browsers.telemetry.with_raw_response.stream(
- id="id",
+ id_or_name="htzv5orfit78e1m2biiifpbv",
)
assert response.http_request.headers.get("X-Stainless-Lang") == "python"
@@ -220,7 +220,7 @@ async def test_raw_response_stream(self, async_client: AsyncKernel) -> None:
@parametrize
async def test_streaming_response_stream(self, async_client: AsyncKernel) -> None:
async with async_client.browsers.telemetry.with_streaming_response.stream(
- id="id",
+ id_or_name="htzv5orfit78e1m2biiifpbv",
) as response:
assert not response.is_closed
assert response.http_request.headers.get("X-Stainless-Lang") == "python"
@@ -233,7 +233,7 @@ async def test_streaming_response_stream(self, async_client: AsyncKernel) -> Non
@pytest.mark.skip(reason="Mock server tests are disabled")
@parametrize
async def test_path_params_stream(self, async_client: AsyncKernel) -> None:
- with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"):
+ with pytest.raises(ValueError, match=r"Expected a non-empty value for `id_or_name` but received ''"):
await async_client.browsers.telemetry.with_raw_response.stream(
- id="",
+ id_or_name="",
)
diff --git a/tests/api_resources/browsers/test_webmcp.py b/tests/api_resources/browsers/test_webmcp.py
new file mode 100644
index 00000000..a55d692c
--- /dev/null
+++ b/tests/api_resources/browsers/test_webmcp.py
@@ -0,0 +1,230 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from __future__ import annotations
+
+import os
+from typing import Any, cast
+
+import pytest
+
+from kernel import Kernel, AsyncKernel
+from tests.utils import assert_matches_type
+from kernel.types.browsers import ToolsResponse, InvocationResult
+
+base_url = os.environ.get("TEST_API_BASE_URL", "http://127.0.0.1:4010")
+
+
+class TestWebmcp:
+ parametrize = pytest.mark.parametrize("client", [False, True], indirect=True, ids=["loose", "strict"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ def test_method_invoke_tool(self, client: Kernel) -> None:
+ webmcp = client.browsers.webmcp.invoke_tool(
+ id_or_name="htzv5orfit78e1m2biiifpbv",
+ input={"foo": "bar"},
+ tool_ref="x",
+ )
+ assert_matches_type(InvocationResult, webmcp, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ def test_method_invoke_tool_with_all_params(self, client: Kernel) -> None:
+ webmcp = client.browsers.webmcp.invoke_tool(
+ id_or_name="htzv5orfit78e1m2biiifpbv",
+ input={"foo": "bar"},
+ tool_ref="x",
+ timeout_sec=1,
+ )
+ assert_matches_type(InvocationResult, webmcp, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ def test_raw_response_invoke_tool(self, client: Kernel) -> None:
+ response = client.browsers.webmcp.with_raw_response.invoke_tool(
+ id_or_name="htzv5orfit78e1m2biiifpbv",
+ input={"foo": "bar"},
+ tool_ref="x",
+ )
+
+ assert response.is_closed is True
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+ webmcp = response.parse()
+ assert_matches_type(InvocationResult, webmcp, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ def test_streaming_response_invoke_tool(self, client: Kernel) -> None:
+ with client.browsers.webmcp.with_streaming_response.invoke_tool(
+ id_or_name="htzv5orfit78e1m2biiifpbv",
+ input={"foo": "bar"},
+ tool_ref="x",
+ ) as response:
+ assert not response.is_closed
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+
+ webmcp = response.parse()
+ assert_matches_type(InvocationResult, webmcp, path=["response"])
+
+ assert cast(Any, response.is_closed) is True
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ def test_path_params_invoke_tool(self, client: Kernel) -> None:
+ with pytest.raises(ValueError, match=r"Expected a non-empty value for `id_or_name` but received ''"):
+ client.browsers.webmcp.with_raw_response.invoke_tool(
+ id_or_name="",
+ input={"foo": "bar"},
+ tool_ref="x",
+ )
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ def test_method_list_tools(self, client: Kernel) -> None:
+ webmcp = client.browsers.webmcp.list_tools(
+ "htzv5orfit78e1m2biiifpbv",
+ )
+ assert_matches_type(ToolsResponse, webmcp, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ def test_raw_response_list_tools(self, client: Kernel) -> None:
+ response = client.browsers.webmcp.with_raw_response.list_tools(
+ "htzv5orfit78e1m2biiifpbv",
+ )
+
+ assert response.is_closed is True
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+ webmcp = response.parse()
+ assert_matches_type(ToolsResponse, webmcp, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ def test_streaming_response_list_tools(self, client: Kernel) -> None:
+ with client.browsers.webmcp.with_streaming_response.list_tools(
+ "htzv5orfit78e1m2biiifpbv",
+ ) as response:
+ assert not response.is_closed
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+
+ webmcp = response.parse()
+ assert_matches_type(ToolsResponse, webmcp, path=["response"])
+
+ assert cast(Any, response.is_closed) is True
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ def test_path_params_list_tools(self, client: Kernel) -> None:
+ with pytest.raises(ValueError, match=r"Expected a non-empty value for `id_or_name` but received ''"):
+ client.browsers.webmcp.with_raw_response.list_tools(
+ "",
+ )
+
+
+class TestAsyncWebmcp:
+ parametrize = pytest.mark.parametrize(
+ "async_client", [False, True, {"http_client": "aiohttp"}], indirect=True, ids=["loose", "strict", "aiohttp"]
+ )
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ async def test_method_invoke_tool(self, async_client: AsyncKernel) -> None:
+ webmcp = await async_client.browsers.webmcp.invoke_tool(
+ id_or_name="htzv5orfit78e1m2biiifpbv",
+ input={"foo": "bar"},
+ tool_ref="x",
+ )
+ assert_matches_type(InvocationResult, webmcp, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ async def test_method_invoke_tool_with_all_params(self, async_client: AsyncKernel) -> None:
+ webmcp = await async_client.browsers.webmcp.invoke_tool(
+ id_or_name="htzv5orfit78e1m2biiifpbv",
+ input={"foo": "bar"},
+ tool_ref="x",
+ timeout_sec=1,
+ )
+ assert_matches_type(InvocationResult, webmcp, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ async def test_raw_response_invoke_tool(self, async_client: AsyncKernel) -> None:
+ response = await async_client.browsers.webmcp.with_raw_response.invoke_tool(
+ id_or_name="htzv5orfit78e1m2biiifpbv",
+ input={"foo": "bar"},
+ tool_ref="x",
+ )
+
+ assert response.is_closed is True
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+ webmcp = await response.parse()
+ assert_matches_type(InvocationResult, webmcp, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ async def test_streaming_response_invoke_tool(self, async_client: AsyncKernel) -> None:
+ async with async_client.browsers.webmcp.with_streaming_response.invoke_tool(
+ id_or_name="htzv5orfit78e1m2biiifpbv",
+ input={"foo": "bar"},
+ tool_ref="x",
+ ) as response:
+ assert not response.is_closed
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+
+ webmcp = await response.parse()
+ assert_matches_type(InvocationResult, webmcp, 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_invoke_tool(self, async_client: AsyncKernel) -> None:
+ with pytest.raises(ValueError, match=r"Expected a non-empty value for `id_or_name` but received ''"):
+ await async_client.browsers.webmcp.with_raw_response.invoke_tool(
+ id_or_name="",
+ input={"foo": "bar"},
+ tool_ref="x",
+ )
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ async def test_method_list_tools(self, async_client: AsyncKernel) -> None:
+ webmcp = await async_client.browsers.webmcp.list_tools(
+ "htzv5orfit78e1m2biiifpbv",
+ )
+ assert_matches_type(ToolsResponse, webmcp, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ async def test_raw_response_list_tools(self, async_client: AsyncKernel) -> None:
+ response = await async_client.browsers.webmcp.with_raw_response.list_tools(
+ "htzv5orfit78e1m2biiifpbv",
+ )
+
+ assert response.is_closed is True
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+ webmcp = await response.parse()
+ assert_matches_type(ToolsResponse, webmcp, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ async def test_streaming_response_list_tools(self, async_client: AsyncKernel) -> None:
+ async with async_client.browsers.webmcp.with_streaming_response.list_tools(
+ "htzv5orfit78e1m2biiifpbv",
+ ) as response:
+ assert not response.is_closed
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+
+ webmcp = await response.parse()
+ assert_matches_type(ToolsResponse, webmcp, 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_list_tools(self, async_client: AsyncKernel) -> None:
+ with pytest.raises(ValueError, match=r"Expected a non-empty value for `id_or_name` but received ''"):
+ await async_client.browsers.webmcp.with_raw_response.list_tools(
+ "",
+ )
diff --git a/tests/api_resources/test_browsers.py b/tests/api_resources/test_browsers.py
index 55a7e66f..3d0a3eb6 100644
--- a/tests/api_resources/test_browsers.py
+++ b/tests/api_resources/test_browsers.py
@@ -321,7 +321,7 @@ def test_streaming_response_list(self, client: Kernel) -> None:
@parametrize
def test_method_curl(self, client: Kernel) -> None:
browser = client.browsers.curl(
- id="id",
+ id_or_name="htzv5orfit78e1m2biiifpbv",
url="url",
)
assert_matches_type(BrowserCurlResponse, browser, path=["response"])
@@ -330,7 +330,7 @@ def test_method_curl(self, client: Kernel) -> None:
@parametrize
def test_method_curl_with_all_params(self, client: Kernel) -> None:
browser = client.browsers.curl(
- id="id",
+ id_or_name="htzv5orfit78e1m2biiifpbv",
url="url",
body="body",
headers={"foo": "string"},
@@ -344,7 +344,7 @@ def test_method_curl_with_all_params(self, client: Kernel) -> None:
@parametrize
def test_raw_response_curl(self, client: Kernel) -> None:
response = client.browsers.with_raw_response.curl(
- id="id",
+ id_or_name="htzv5orfit78e1m2biiifpbv",
url="url",
)
@@ -357,7 +357,7 @@ def test_raw_response_curl(self, client: Kernel) -> None:
@parametrize
def test_streaming_response_curl(self, client: Kernel) -> None:
with client.browsers.with_streaming_response.curl(
- id="id",
+ id_or_name="htzv5orfit78e1m2biiifpbv",
url="url",
) as response:
assert not response.is_closed
@@ -371,9 +371,9 @@ def test_streaming_response_curl(self, client: Kernel) -> None:
@pytest.mark.skip(reason="Mock server tests are disabled")
@parametrize
def test_path_params_curl(self, client: Kernel) -> None:
- with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"):
+ with pytest.raises(ValueError, match=r"Expected a non-empty value for `id_or_name` but received ''"):
client.browsers.with_raw_response.curl(
- id="",
+ id_or_name="",
url="url",
)
@@ -423,7 +423,7 @@ def test_path_params_delete_by_id(self, client: Kernel) -> None:
@parametrize
def test_method_load_extensions(self, client: Kernel) -> None:
browser = client.browsers.load_extensions(
- id="id",
+ id_or_name="htzv5orfit78e1m2biiifpbv",
extensions=[
{
"name": "name",
@@ -437,7 +437,7 @@ def test_method_load_extensions(self, client: Kernel) -> None:
@parametrize
def test_raw_response_load_extensions(self, client: Kernel) -> None:
response = client.browsers.with_raw_response.load_extensions(
- id="id",
+ id_or_name="htzv5orfit78e1m2biiifpbv",
extensions=[
{
"name": "name",
@@ -455,7 +455,7 @@ def test_raw_response_load_extensions(self, client: Kernel) -> None:
@parametrize
def test_streaming_response_load_extensions(self, client: Kernel) -> None:
with client.browsers.with_streaming_response.load_extensions(
- id="id",
+ id_or_name="htzv5orfit78e1m2biiifpbv",
extensions=[
{
"name": "name",
@@ -474,9 +474,9 @@ def test_streaming_response_load_extensions(self, client: Kernel) -> None:
@pytest.mark.skip(reason="Mock server tests are disabled")
@parametrize
def test_path_params_load_extensions(self, client: Kernel) -> None:
- with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"):
+ with pytest.raises(ValueError, match=r"Expected a non-empty value for `id_or_name` but received ''"):
client.browsers.with_raw_response.load_extensions(
- id="",
+ id_or_name="",
extensions=[
{
"name": "name",
@@ -788,7 +788,7 @@ async def test_streaming_response_list(self, async_client: AsyncKernel) -> None:
@parametrize
async def test_method_curl(self, async_client: AsyncKernel) -> None:
browser = await async_client.browsers.curl(
- id="id",
+ id_or_name="htzv5orfit78e1m2biiifpbv",
url="url",
)
assert_matches_type(BrowserCurlResponse, browser, path=["response"])
@@ -797,7 +797,7 @@ async def test_method_curl(self, async_client: AsyncKernel) -> None:
@parametrize
async def test_method_curl_with_all_params(self, async_client: AsyncKernel) -> None:
browser = await async_client.browsers.curl(
- id="id",
+ id_or_name="htzv5orfit78e1m2biiifpbv",
url="url",
body="body",
headers={"foo": "string"},
@@ -811,7 +811,7 @@ async def test_method_curl_with_all_params(self, async_client: AsyncKernel) -> N
@parametrize
async def test_raw_response_curl(self, async_client: AsyncKernel) -> None:
response = await async_client.browsers.with_raw_response.curl(
- id="id",
+ id_or_name="htzv5orfit78e1m2biiifpbv",
url="url",
)
@@ -824,7 +824,7 @@ async def test_raw_response_curl(self, async_client: AsyncKernel) -> None:
@parametrize
async def test_streaming_response_curl(self, async_client: AsyncKernel) -> None:
async with async_client.browsers.with_streaming_response.curl(
- id="id",
+ id_or_name="htzv5orfit78e1m2biiifpbv",
url="url",
) as response:
assert not response.is_closed
@@ -838,9 +838,9 @@ async def test_streaming_response_curl(self, async_client: AsyncKernel) -> None:
@pytest.mark.skip(reason="Mock server tests are disabled")
@parametrize
async def test_path_params_curl(self, async_client: AsyncKernel) -> None:
- with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"):
+ with pytest.raises(ValueError, match=r"Expected a non-empty value for `id_or_name` but received ''"):
await async_client.browsers.with_raw_response.curl(
- id="",
+ id_or_name="",
url="url",
)
@@ -890,7 +890,7 @@ async def test_path_params_delete_by_id(self, async_client: AsyncKernel) -> None
@parametrize
async def test_method_load_extensions(self, async_client: AsyncKernel) -> None:
browser = await async_client.browsers.load_extensions(
- id="id",
+ id_or_name="htzv5orfit78e1m2biiifpbv",
extensions=[
{
"name": "name",
@@ -904,7 +904,7 @@ async def test_method_load_extensions(self, async_client: AsyncKernel) -> None:
@parametrize
async def test_raw_response_load_extensions(self, async_client: AsyncKernel) -> None:
response = await async_client.browsers.with_raw_response.load_extensions(
- id="id",
+ id_or_name="htzv5orfit78e1m2biiifpbv",
extensions=[
{
"name": "name",
@@ -922,7 +922,7 @@ async def test_raw_response_load_extensions(self, async_client: AsyncKernel) ->
@parametrize
async def test_streaming_response_load_extensions(self, async_client: AsyncKernel) -> None:
async with async_client.browsers.with_streaming_response.load_extensions(
- id="id",
+ id_or_name="htzv5orfit78e1m2biiifpbv",
extensions=[
{
"name": "name",
@@ -941,9 +941,9 @@ async def test_streaming_response_load_extensions(self, async_client: AsyncKerne
@pytest.mark.skip(reason="Mock server tests are disabled")
@parametrize
async def test_path_params_load_extensions(self, async_client: AsyncKernel) -> None:
- with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"):
+ with pytest.raises(ValueError, match=r"Expected a non-empty value for `id_or_name` but received ''"):
await async_client.browsers.with_raw_response.load_extensions(
- id="",
+ id_or_name="",
extensions=[
{
"name": "name",
From 5aaaae65d3df8bbf2c9dfa4a5f7770115c383346 Mon Sep 17 00:00:00 2001
From: "kernel-internal[bot]"
<260533166+kernel-internal[bot]@users.noreply.github.com>
Date: Wed, 2 Sep 2026 19:06:27 +0000
Subject: [PATCH 223/366] release: 0.98.0
---
.release-please-manifest.json | 2 +-
CHANGELOG.md | 7 +++++++
pyproject.toml | 2 +-
src/kernel/_version.py | 2 +-
4 files changed, 10 insertions(+), 3 deletions(-)
diff --git a/.release-please-manifest.json b/.release-please-manifest.json
index ca93fdaa..a8124647 100644
--- a/.release-please-manifest.json
+++ b/.release-please-manifest.json
@@ -1,3 +1,3 @@
{
- ".": "0.97.0"
+ ".": "0.98.0"
}
\ No newline at end of file
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 01df1612..992c5f68 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,12 @@
# Changelog
+## [0.98.0](https://github.com/kernel/kernel-python-sdk/compare/v0.97.0...v0.98.0) (2026-09-02)
+
+
+### Features
+
+* Accept browser session name on all /browsers/{id_or_name} sub-resource routes ([a60fa2b](https://github.com/kernel/kernel-python-sdk/commit/a60fa2bd48b06c8ca61005c3e29c42918ee371f2))
+
## [0.97.0](https://github.com/kernel/kernel-python-sdk/compare/v0.96.0...v0.97.0) (2026-08-31)
diff --git a/pyproject.toml b/pyproject.toml
index 2400b318..fbe3ab05 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -1,6 +1,6 @@
[project]
name = "kernel"
-version = "0.97.0"
+version = "0.98.0"
description = "The official Python library for the kernel API"
dynamic = ["readme"]
license = "Apache-2.0"
diff --git a/src/kernel/_version.py b/src/kernel/_version.py
index f1214100..4adaa91f 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.97.0" # x-release-please-version
+__version__ = "0.98.0" # x-release-please-version
From 9088723937cde444617ed97d3188e78c705e5cdd Mon Sep 17 00:00:00 2001
From: tnsardesai <18272584+tnsardesai@users.noreply.github.com>
Date: Wed, 2 Sep 2026 06:17:24 +0000
Subject: [PATCH 224/366] Route browser fs and logs endpoints directly to the
VM
Add fs and logs to the default direct-to-VM subresource prefixes so
filesystem operations and log streaming use the cached browser base_url
and JWT instead of the control plane.
Serialize multipart array entries with indexed names so a file part stays
associated with the sibling fields of its array entry, which repeated
`files[][file]` names cannot express.
Only retry a stale direct-to-VM auth failure on the control plane when the
request body can be rebuilt byte for byte; a streamed body is consumed by
the direct attempt, so retrying would send a truncated body. The stale
route is evicted either way.
---
src/kernel/_base_client.py | 5 +-
src/kernel/_client.py | 15 +-
src/kernel/_utils/_utils.py | 6 +-
src/kernel/lib/browser_routing/routing.py | 56 ++-
tests/test_browser_routing.py | 435 +++++++++++++++++++++-
tests/test_client.py | 16 +-
tests/test_extract_files.py | 6 +-
7 files changed, 504 insertions(+), 35 deletions(-)
diff --git a/src/kernel/_base_client.py b/src/kernel/_base_client.py
index 2599dc41..50b190a5 100644
--- a/src/kernel/_base_client.py
+++ b/src/kernel/_base_client.py
@@ -587,7 +587,10 @@ def _serialize_multipartform(self, data: Mapping[object, object]) -> dict[str, o
# TODO: type ignore is required as stringify_items is well typed but we can't be
# well typed without heavy validation.
data, # type: ignore
- array_format="brackets",
+ # Indexed names (`files[0][dest_path]`) keep each array entry's fields
+ # grouped together; repeated `files[][dest_path]` parts cannot be
+ # matched back to their file part. `extract_files` uses the same format.
+ array_format="indices",
)
serialized: dict[str, object] = {}
for key, value in items:
diff --git a/src/kernel/_client.py b/src/kernel/_client.py
index 2c06cb04..995e2dad 100644
--- a/src/kernel/_client.py
+++ b/src/kernel/_client.py
@@ -41,6 +41,7 @@
strip_direct_vm_auth,
rewrite_direct_vm_options,
browser_routing_config_from_env,
+ is_stale_direct_vm_auth_response,
should_retry_stale_direct_vm_auth,
maybe_evict_browser_route_from_response,
maybe_populate_browser_route_cache_from_response,
@@ -365,9 +366,12 @@ def _prepare_request(self, request: httpx.Request) -> None:
@override
def _should_retry(self, response: httpx.Response) -> bool:
- if should_retry_stale_direct_vm_auth(response):
+ if is_stale_direct_vm_auth_response(response):
maybe_evict_browser_route_from_response(response, cache=self.browser_route_cache)
- return True
+ # The route is evicted either way; only retry when the body can be
+ # rebuilt, otherwise the caller sees the original auth failure and a
+ # later call goes to the control plane.
+ return should_retry_stale_direct_vm_auth(response)
return super()._should_retry(response)
@override
@@ -748,9 +752,12 @@ async def _prepare_request(self, request: httpx.Request) -> None:
@override
def _should_retry(self, response: httpx.Response) -> bool:
- if should_retry_stale_direct_vm_auth(response):
+ if is_stale_direct_vm_auth_response(response):
maybe_evict_browser_route_from_response(response, cache=self.browser_route_cache)
- return True
+ # The route is evicted either way; only retry when the body can be
+ # rebuilt, otherwise the caller sees the original auth failure and a
+ # later call goes to the control plane.
+ return should_retry_stale_direct_vm_auth(response)
return super()._should_retry(response)
@override
diff --git a/src/kernel/_utils/_utils.py b/src/kernel/_utils/_utils.py
index 199cd231..45116559 100644
--- a/src/kernel/_utils/_utils.py
+++ b/src/kernel/_utils/_utils.py
@@ -40,7 +40,7 @@ def extract_files(
query: Mapping[str, object],
*,
paths: Sequence[Sequence[str]],
- array_format: ArrayFormat = "brackets",
+ array_format: ArrayFormat = "indices",
) -> list[tuple[str, FileTypes]]:
"""Recursively extract files from the given dictionary based on specified paths.
@@ -48,7 +48,9 @@ def extract_files(
``array_format`` controls how ```` segments contribute to the emitted
field name. Supported values: ``"brackets"`` (``foo[]``), ``"repeat"`` and
- ``"comma"`` (``foo``), ``"indices"`` (``foo[0]``, ``foo[1]``).
+ ``"comma"`` (``foo``), ``"indices"`` (``foo[0]``, ``foo[1]``). Indexed names are
+ the default so that a file part stays associated with the sibling fields of the
+ same array entry, which repeated ``foo[]`` names cannot express.
Note: this mutates the given dictionary.
"""
diff --git a/src/kernel/lib/browser_routing/routing.py b/src/kernel/lib/browser_routing/routing.py
index bad5e4ea..1fa69f54 100644
--- a/src/kernel/lib/browser_routing/routing.py
+++ b/src/kernel/lib/browser_routing/routing.py
@@ -44,7 +44,17 @@ def browser_routing_config_from_env() -> BrowserRoutingConfig:
# Path prefixes eligible for direct-to-VM routing. "telemetry/stream" is
# the live SSE endpoint (VM); "telemetry/events" is a historical read
# served by the control plane (S2) and must NOT be here.
- return BrowserRoutingConfig(subresources=("curl", "telemetry/stream", "computer", "playwright", "process"))
+ return BrowserRoutingConfig(
+ subresources=(
+ "curl",
+ "telemetry/stream",
+ "computer",
+ "playwright",
+ "process",
+ "fs",
+ "logs",
+ )
+ )
if raw.strip() == "":
return BrowserRoutingConfig()
@@ -189,7 +199,49 @@ def is_stale_direct_vm_auth_response(response: httpx.Response) -> bool:
def should_retry_stale_direct_vm_auth(response: httpx.Response) -> bool:
- return is_stale_direct_vm_auth_response(response)
+ """Whether a stale direct-to-VM auth failure can be retried on the control plane.
+
+ A retry rebuilds the request from the original options, so it is only safe when
+ the body can be serialized again byte for byte. Streamed bodies (e.g. a file
+ object passed to fs.write_file) are consumed by the direct request, so retrying
+ would send a truncated or empty body to the control plane.
+ """
+ if not is_stale_direct_vm_auth_response(response):
+ return False
+ return direct_vm_request_body_is_replayable(response.request)
+
+
+def direct_vm_request_body_is_replayable(request: httpx.Request) -> bool:
+ try:
+ _ = request.content
+ except httpx.RequestNotRead:
+ pass
+ else:
+ # httpx already buffered the body, so rebuilding it yields the same bytes.
+ return True
+
+ # httpx encodes multipart bodies as a stream of fields it re-renders per attempt.
+ fields = getattr(request.stream, "fields", None)
+ if fields is None:
+ # A streamed body (file object, iterator or async iterator) cannot be replayed.
+ return False
+ return all(_multipart_field_is_replayable(field) for field in cast("list[Any]", fields))
+
+
+def _multipart_field_is_replayable(field: Any) -> bool:
+ file = getattr(field, "file", None)
+ if file is None:
+ # A data field renders from an in-memory value.
+ return True
+ if isinstance(file, (bytes, str)):
+ return True
+ if getattr(file, "closed", False):
+ return False
+ if not callable(getattr(file, "seek", None)):
+ return False
+ seekable = getattr(file, "seekable", None)
+ # httpx rewinds seekable file fields before rendering them again.
+ return bool(seekable()) if callable(seekable) else True
def _session_id_from_browser_delete_path(path: str) -> str | None:
diff --git a/tests/test_browser_routing.py b/tests/test_browser_routing.py
index eb47e040..4e154802 100644
--- a/tests/test_browser_routing.py
+++ b/tests/test_browser_routing.py
@@ -3,13 +3,14 @@
import os
import asyncio
from typing import Any, AsyncIterator, cast
+from pathlib import Path
from typing_extensions import override
import httpx
import respx
import pytest
-from kernel import Kernel, AsyncKernel, InternalServerError
+from kernel import Kernel, AsyncKernel, AuthenticationError, InternalServerError
from kernel.lib.browser_routing.util import jwt_from_cdp_ws_url
from kernel.lib.browser_routing.routing import (
BrowserRoute,
@@ -37,6 +38,30 @@ def _fake_browser() -> dict[str, object]:
}
+def _skip_retry_sleep(_self: object, **_kwargs: object) -> None:
+ return None
+
+
+class _UnseekableFile:
+ """A file-like upload body that cannot be rewound, e.g. a pipe."""
+
+ name = "one.txt"
+
+ def __init__(self, content: bytes) -> None:
+ self._content = content
+
+ def read(self, size: int = -1) -> bytes:
+ chunk = self._content if size < 0 else self._content[:size]
+ self._content = b"" if size < 0 else self._content[size:]
+ return chunk
+
+ def seekable(self) -> bool:
+ return False
+
+ def seek(self, _offset: int, _whence: int = 0) -> int:
+ raise OSError("not seekable")
+
+
def _cache_browser(client: Kernel) -> None:
route = browser_route_from_browser(_fake_browser())
assert route is not None
@@ -398,6 +423,8 @@ def test_browser_routing_config_from_env_defaults(monkeypatch: pytest.MonkeyPatc
"computer",
"playwright",
"process",
+ "fs",
+ "logs",
)
@@ -407,7 +434,7 @@ def test_direct_vm_routing_allowlist_segment_boundary() -> None:
# stream-prefixed-but-different path is not matched.
from kernel.lib.browser_routing.routing import _matches_direct_vm_prefix
- prefixes = ("curl", "telemetry/stream", "computer", "playwright", "process")
+ prefixes = ("curl", "telemetry/stream", "computer", "playwright", "process", "fs", "logs")
assert _matches_direct_vm_prefix("telemetry/stream", prefixes) is True
assert _matches_direct_vm_prefix("telemetry/stream/x", prefixes) is True
assert _matches_direct_vm_prefix("telemetry/events", prefixes) is False
@@ -418,7 +445,13 @@ def test_direct_vm_routing_allowlist_segment_boundary() -> None:
assert _matches_direct_vm_prefix("playwright/execute", prefixes) is True
assert _matches_direct_vm_prefix("process/exec", prefixes) is True
assert _matches_direct_vm_prefix("process/proc-1/stdout/stream", prefixes) is True
- assert _matches_direct_vm_prefix("fs/read", prefixes) is False
+ assert _matches_direct_vm_prefix("fs/read_file", prefixes) is True
+ assert _matches_direct_vm_prefix("fs/watch/watch-1/events", prefixes) is True
+ assert _matches_direct_vm_prefix("fsx/read_file", prefixes) is False
+ assert _matches_direct_vm_prefix("logs/stream", prefixes) is True
+ assert _matches_direct_vm_prefix("logstream", prefixes) is False
+ assert _matches_direct_vm_prefix("extensions", prefixes) is False
+ assert _matches_direct_vm_prefix("replays/rec-1", prefixes) is False
def test_rewrite_direct_vm_options_keeps_telemetry_events_on_control_plane() -> None:
@@ -435,7 +468,9 @@ def test_rewrite_direct_vm_options_keeps_telemetry_events_on_control_plane() ->
cache = BrowserRouteCache()
cache.set(BrowserRoute(session_id="sess-1", base_url="http://browser-session.test/browser/kernel", jwt="token-abc"))
- config = BrowserRoutingConfig(subresources=("curl", "telemetry/stream", "computer", "playwright", "process"))
+ config = BrowserRoutingConfig(
+ subresources=("curl", "telemetry/stream", "computer", "playwright", "process", "fs", "logs")
+ )
events = rewrite_direct_vm_options(
FinalRequestOptions(method="get", url="/browsers/sess-1/telemetry/events"), cache=cache, config=config
@@ -465,7 +500,22 @@ def test_rewrite_direct_vm_options_keeps_telemetry_events_on_control_plane() ->
fs_read = rewrite_direct_vm_options(
FinalRequestOptions(method="get", url="/browsers/sess-1/fs/read_file"), cache=cache, config=config
)
- assert fs_read.url == "/browsers/sess-1/fs/read_file"
+ assert str(fs_read.url).startswith("http://browser-session.test/browser/kernel/fs/read_file")
+
+ logs_stream = rewrite_direct_vm_options(
+ FinalRequestOptions(method="get", url="/browsers/sess-1/logs/stream"), cache=cache, config=config
+ )
+ assert str(logs_stream.url).startswith("http://browser-session.test/browser/kernel/logs/stream")
+
+ extensions = rewrite_direct_vm_options(
+ FinalRequestOptions(method="post", url="/browsers/sess-1/extensions"), cache=cache, config=config
+ )
+ assert extensions.url == "/browsers/sess-1/extensions"
+
+ replays = rewrite_direct_vm_options(
+ FinalRequestOptions(method="get", url="/browsers/sess-1/replays"), cache=cache, config=config
+ )
+ assert replays.url == "/browsers/sess-1/replays"
def test_browser_routing_config_from_env_empty_string_disables_routing(monkeypatch: pytest.MonkeyPatch) -> None:
@@ -510,21 +560,24 @@ def test_default_browser_subresources_route_to_vm(
@respx.mock
-def test_fs_and_telemetry_events_stay_on_api_origin_by_default(
+def test_control_plane_subresources_stay_on_api_origin_by_default(
monkeypatch: pytest.MonkeyPatch,
) -> None:
monkeypatch.delenv("KERNEL_BROWSER_ROUTING_SUBRESOURCES", raising=False)
- fs_read = respx.get(f"{base_url}/browsers/sess-1/fs/read_file").mock(
- return_value=httpx.Response(200, content=b"x", headers={"content-type": "application/octet-stream"})
- )
events = respx.get(f"{base_url}/browsers/sess-1/telemetry/events").mock(return_value=httpx.Response(200, json=[]))
+ replays = respx.get(f"{base_url}/browsers/sess-1/replays").mock(return_value=httpx.Response(200, json=[]))
+ extensions = respx.post(f"{base_url}/browsers/sess-1/extensions").mock(return_value=httpx.Response(204))
with Kernel(base_url=base_url, api_key=api_key, _strict_response_validation=True) as client:
_cache_browser(client)
- client.browsers.fs.read_file("sess-1", path="/tmp/x")
client.browsers.telemetry.events("sess-1")
+ client.browsers.replays.list("sess-1")
+ client.browsers.load_extensions("sess-1", extensions=[{"name": "ext", "zip_file": b"zip"}])
- assert fs_read.called
assert events.called
+ assert replays.called
+ assert extensions.called
+ extensions_req = cast(httpx.Request, cast(Any, extensions.calls[0]).request)
+ assert extensions_req.headers.get("Authorization") == f"Bearer {api_key}"
@respx.mock
@@ -532,10 +585,6 @@ def test_stale_direct_vm_jwt_evicts_cache_and_retries_control_plane(
monkeypatch: pytest.MonkeyPatch,
) -> None:
monkeypatch.delenv("KERNEL_BROWSER_ROUTING_SUBRESOURCES", raising=False)
-
- def _skip_retry_sleep(_self: object, **_kwargs: object) -> None:
- return None
-
monkeypatch.setattr("kernel._base_client.SyncAPIClient._sleep_for_retry", _skip_retry_sleep)
vm = respx.post("http://browser-session.test/browser/kernel/computer/screenshot").mock(
return_value=httpx.Response(401, text="Invalid JWT")
@@ -596,3 +645,359 @@ def test_stale_direct_vm_auth_retry_does_not_require_cached_route() -> None:
empty = BrowserRouteCache()
assert should_retry_stale_direct_vm_auth(response) is True
assert empty.get("sess-1") is None
+
+
+@respx.mock
+def test_fs_json_endpoints_route_to_vm(monkeypatch: pytest.MonkeyPatch) -> None:
+ monkeypatch.delenv("KERNEL_BROWSER_ROUTING_SUBRESOURCES", raising=False)
+ list_files = respx.get("http://browser-session.test/browser/kernel/fs/list_files").mock(
+ return_value=httpx.Response(200, json=[])
+ )
+ move = respx.put("http://browser-session.test/browser/kernel/fs/move").mock(return_value=httpx.Response(204))
+ with Kernel(base_url=base_url, api_key=api_key, _strict_response_validation=True) as client:
+ _cache_browser(client)
+ client.browsers.fs.list_files("sess-1", path="/tmp")
+ client.browsers.fs.move("sess-1", dest_path="/tmp/b", src_path="/tmp/a")
+
+ list_req = cast(httpx.Request, cast(Any, list_files.calls[0]).request)
+ assert list_req.url.params.get("path") == "/tmp"
+ assert list_req.url.params.get("jwt") == "token-abc"
+ assert list_req.headers.get("Authorization") is None
+
+ move_req = cast(httpx.Request, cast(Any, move.calls[0]).request)
+ assert move_req.url.path == "/browser/kernel/fs/move"
+ assert move_req.url.params.get("jwt") == "token-abc"
+ assert move_req.headers.get("Authorization") is None
+
+
+@respx.mock
+def test_fs_read_file_routes_binary_response_from_vm(monkeypatch: pytest.MonkeyPatch) -> None:
+ monkeypatch.delenv("KERNEL_BROWSER_ROUTING_SUBRESOURCES", raising=False)
+ read_file = respx.get("http://browser-session.test/browser/kernel/fs/read_file").mock(
+ return_value=httpx.Response(200, content=b"\x00binary", headers={"content-type": "application/octet-stream"})
+ )
+ with Kernel(base_url=base_url, api_key=api_key, _strict_response_validation=True) as client:
+ _cache_browser(client)
+ response = client.browsers.fs.read_file("sess-1", path="/tmp/x")
+
+ assert response.read() == b"\x00binary"
+ request = cast(httpx.Request, cast(Any, read_file.calls[0]).request)
+ assert request.url.params.get("path") == "/tmp/x"
+ assert request.url.params.get("jwt") == "token-abc"
+ assert request.headers.get("Authorization") is None
+
+
+@respx.mock
+def test_fs_write_file_routes_binary_body_to_vm(monkeypatch: pytest.MonkeyPatch) -> None:
+ monkeypatch.delenv("KERNEL_BROWSER_ROUTING_SUBRESOURCES", raising=False)
+ write_file = respx.put("http://browser-session.test/browser/kernel/fs/write_file").mock(
+ return_value=httpx.Response(204)
+ )
+ with Kernel(base_url=base_url, api_key=api_key, _strict_response_validation=True) as client:
+ _cache_browser(client)
+ client.browsers.fs.write_file("sess-1", b"\x00payload", path="/tmp/x", mode="600")
+
+ request = cast(httpx.Request, cast(Any, write_file.calls[0]).request)
+ assert request.content == b"\x00payload"
+ assert request.headers.get("content-type") == "application/octet-stream"
+ assert request.url.params.get("path") == "/tmp/x"
+ assert request.url.params.get("mode") == "600"
+ assert request.url.params.get("jwt") == "token-abc"
+ assert request.headers.get("Authorization") is None
+
+
+@respx.mock
+def test_fs_upload_routes_indexed_multipart_to_vm(monkeypatch: pytest.MonkeyPatch) -> None:
+ monkeypatch.delenv("KERNEL_BROWSER_ROUTING_SUBRESOURCES", raising=False)
+ upload = respx.post("http://browser-session.test/browser/kernel/fs/upload").mock(return_value=httpx.Response(204))
+ with Kernel(base_url=base_url, api_key=api_key, _strict_response_validation=True) as client:
+ _cache_browser(client)
+ client.browsers.fs.upload(
+ "sess-1",
+ files=[
+ {"dest_path": "/tmp/one", "file": b"one"},
+ {"dest_path": "/tmp/two", "file": b"two"},
+ ],
+ )
+
+ request = cast(httpx.Request, cast(Any, upload.calls[0]).request)
+ assert request.url.params.get("jwt") == "token-abc"
+ assert request.headers.get("Authorization") is None
+ body = request.read()
+ assert b'name="files[0][dest_path]"' in body
+ assert b'name="files[0][file]"' in body
+ assert b'name="files[1][dest_path]"' in body
+ assert b'name="files[1][file]"' in body
+ assert b"files[][" not in body
+
+
+@respx.mock
+def test_fs_watch_events_stream_routes_to_vm(monkeypatch: pytest.MonkeyPatch) -> None:
+ monkeypatch.delenv("KERNEL_BROWSER_ROUTING_SUBRESOURCES", raising=False)
+ events = respx.get("http://browser-session.test/browser/kernel/fs/watch/watch-1/events").mock(
+ return_value=httpx.Response(
+ 200,
+ headers={"content-type": "text/event-stream"},
+ content=b'data: {"type":"CREATE","path":"/tmp/x","is_dir":false,"name":"x"}\n\n',
+ )
+ )
+ with Kernel(base_url=base_url, api_key=api_key, _strict_response_validation=True) as client:
+ _cache_browser(client)
+ stream = client.browsers.fs.watch.events("watch-1", id_or_name="sess-1")
+ first = next(iter(stream))
+ stream.close()
+
+ assert first.path == "/tmp/x"
+ request = cast(httpx.Request, cast(Any, events.calls[0]).request)
+ assert request.url.path == "/browser/kernel/fs/watch/watch-1/events"
+ assert request.url.params.get("jwt") == "token-abc"
+ assert request.headers.get("Authorization") is None
+
+
+@respx.mock
+def test_logs_stream_routes_to_vm(monkeypatch: pytest.MonkeyPatch) -> None:
+ monkeypatch.delenv("KERNEL_BROWSER_ROUTING_SUBRESOURCES", raising=False)
+ logs = respx.get("http://browser-session.test/browser/kernel/logs/stream").mock(
+ return_value=httpx.Response(
+ 200,
+ headers={"content-type": "text/event-stream"},
+ content=b'data: {"event":"log","message":"hello","timestamp":"2020-01-01T00:00:00Z"}\n\n',
+ )
+ )
+ with Kernel(base_url=base_url, api_key=api_key, _strict_response_validation=True) as client:
+ _cache_browser(client)
+ stream = client.browsers.logs.stream("sess-1", source="path", path="/var/log/x", follow=True)
+ first = next(iter(stream))
+ stream.close()
+
+ assert first.message == "hello"
+ request = cast(httpx.Request, cast(Any, logs.calls[0]).request)
+ assert request.url.path == "/browser/kernel/logs/stream"
+ assert request.url.params.get("source") == "path"
+ assert request.url.params.get("path") == "/var/log/x"
+ assert request.url.params.get("follow") == "true"
+ assert request.url.params.get("jwt") == "token-abc"
+ assert request.headers.get("Authorization") is None
+
+
+@pytest.mark.asyncio
+async def test_async_logs_stream_cancellation_reaches_transport(
+ monkeypatch: pytest.MonkeyPatch,
+) -> None:
+ monkeypatch.delenv("KERNEL_BROWSER_ROUTING_SUBRESOURCES", raising=False)
+ read_started = asyncio.Event()
+ transport_cancelled = asyncio.Event()
+ chunks: asyncio.Queue[bytes] = asyncio.Queue()
+ requested: list[httpx.URL] = []
+
+ class BlockingSSEStream(httpx.AsyncByteStream):
+ @override
+ async def __aiter__(self) -> AsyncIterator[bytes]:
+ read_started.set()
+ try:
+ while True:
+ yield await chunks.get()
+ except asyncio.CancelledError:
+ transport_cancelled.set()
+ raise
+
+ @override
+ async def aclose(self) -> None:
+ pass
+
+ async def handle_request(request: httpx.Request) -> httpx.Response:
+ requested.append(request.url)
+ return httpx.Response(
+ 200,
+ headers={"content-type": "text/event-stream"},
+ stream=BlockingSSEStream(),
+ )
+
+ http_client = httpx.AsyncClient(transport=httpx.MockTransport(handle_request))
+ async with AsyncKernel(
+ base_url=base_url,
+ api_key=api_key,
+ http_client=http_client,
+ _strict_response_validation=True,
+ ) as client:
+ route = browser_route_from_browser(_fake_browser())
+ assert route is not None
+ client.browser_route_cache.set(route)
+ stream = await client.browsers.logs.stream("sess-1", source="supervisor", supervisor_process="chromium")
+ consumer = asyncio.create_task(stream.__anext__())
+ await asyncio.wait_for(read_started.wait(), timeout=1)
+
+ consumer.cancel()
+ with pytest.raises(asyncio.CancelledError):
+ await asyncio.wait_for(consumer, timeout=1)
+ await asyncio.wait_for(transport_cancelled.wait(), timeout=1)
+
+ assert requested
+ assert requested[0].path == "/browser/kernel/logs/stream"
+
+
+@respx.mock
+def test_stale_direct_vm_jwt_replays_buffered_fs_body_on_control_plane(
+ monkeypatch: pytest.MonkeyPatch,
+) -> None:
+ monkeypatch.delenv("KERNEL_BROWSER_ROUTING_SUBRESOURCES", raising=False)
+ monkeypatch.setattr("kernel._base_client.SyncAPIClient._sleep_for_retry", _skip_retry_sleep)
+ vm = respx.put("http://browser-session.test/browser/kernel/fs/write_file").mock(
+ return_value=httpx.Response(401, text="Invalid JWT")
+ )
+ api = respx.put(f"{base_url}/browsers/sess-1/fs/write_file").mock(return_value=httpx.Response(204))
+ with Kernel(base_url=base_url, api_key=api_key, _strict_response_validation=True) as client:
+ _cache_browser(client)
+ client.browsers.fs.write_file("sess-1", b"payload", path="/tmp/x")
+ assert client.browser_route_cache.get("sess-1") is None
+
+ assert vm.called
+ api_req = cast(httpx.Request, cast(Any, api.calls[0]).request)
+ assert api_req.content == b"payload"
+ assert api_req.url.params.get("path") == "/tmp/x"
+ assert api_req.url.params.get("jwt") is None
+ assert api_req.headers.get("Authorization") == f"Bearer {api_key}"
+
+
+@respx.mock
+def test_stale_direct_vm_jwt_does_not_replay_streamed_fs_body(
+ monkeypatch: pytest.MonkeyPatch,
+) -> None:
+ monkeypatch.delenv("KERNEL_BROWSER_ROUTING_SUBRESOURCES", raising=False)
+ monkeypatch.setattr("kernel._base_client.SyncAPIClient._sleep_for_retry", _skip_retry_sleep)
+ vm = respx.put("http://browser-session.test/browser/kernel/fs/write_file").mock(
+ return_value=httpx.Response(401, text="Invalid JWT")
+ )
+ api = respx.put(f"{base_url}/browsers/sess-1/fs/write_file").mock(return_value=httpx.Response(204))
+ with Kernel(base_url=base_url, api_key=api_key, _strict_response_validation=True) as client:
+ _cache_browser(client)
+ with pytest.raises(AuthenticationError):
+ client.browsers.fs.write_file("sess-1", iter([b"chunk-one", b"chunk-two"]), path="/tmp/x")
+ # The stale route is still evicted, so the caller's next attempt uses the control plane.
+ assert client.browser_route_cache.get("sess-1") is None
+
+ assert vm.call_count == 1
+ assert not api.called
+
+
+@pytest.mark.asyncio
+@respx.mock
+async def test_async_stale_direct_vm_jwt_does_not_replay_streamed_fs_body(
+ monkeypatch: pytest.MonkeyPatch,
+) -> None:
+ monkeypatch.delenv("KERNEL_BROWSER_ROUTING_SUBRESOURCES", raising=False)
+ monkeypatch.setattr("kernel._base_client.AsyncAPIClient._sleep_for_retry", _skip_retry_sleep)
+ vm = respx.put("http://browser-session.test/browser/kernel/fs/write_file").mock(
+ return_value=httpx.Response(401, text="Invalid JWT")
+ )
+ api = respx.put(f"{base_url}/browsers/sess-1/fs/write_file").mock(return_value=httpx.Response(204))
+
+ async def _chunks() -> AsyncIterator[bytes]:
+ yield b"chunk-one"
+ yield b"chunk-two"
+
+ async with AsyncKernel(base_url=base_url, api_key=api_key, _strict_response_validation=True) as client:
+ route = browser_route_from_browser(_fake_browser())
+ assert route is not None
+ client.browser_route_cache.set(route)
+ with pytest.raises(AuthenticationError):
+ await client.browsers.fs.write_file("sess-1", _chunks(), path="/tmp/x")
+ assert client.browser_route_cache.get("sess-1") is None
+
+ assert vm.call_count == 1
+ assert not api.called
+
+
+@respx.mock
+def test_stale_direct_vm_jwt_replays_multipart_upload_on_control_plane(
+ monkeypatch: pytest.MonkeyPatch,
+ tmp_path: Path,
+) -> None:
+ monkeypatch.delenv("KERNEL_BROWSER_ROUTING_SUBRESOURCES", raising=False)
+ monkeypatch.setattr("kernel._base_client.SyncAPIClient._sleep_for_retry", _skip_retry_sleep)
+ vm = respx.post("http://browser-session.test/browser/kernel/fs/upload").mock(
+ return_value=httpx.Response(403, text="Invalid JWT")
+ )
+ api = respx.post(f"{base_url}/browsers/sess-1/fs/upload").mock(return_value=httpx.Response(204))
+ upload = tmp_path / "one.txt"
+ upload.write_bytes(b"file-bytes")
+
+ with Kernel(base_url=base_url, api_key=api_key, _strict_response_validation=True) as client:
+ _cache_browser(client)
+ with upload.open("rb") as handle:
+ client.browsers.fs.upload("sess-1", files=[{"dest_path": "/tmp/one", "file": handle}])
+
+ assert vm.called
+ api_req = cast(httpx.Request, cast(Any, api.calls[0]).request)
+ body = api_req.read()
+ assert b"file-bytes" in body
+ assert b'name="files[0][dest_path]"' in body
+ assert api_req.headers.get("Authorization") == f"Bearer {api_key}"
+
+
+def test_direct_vm_request_body_is_replayable_classification(tmp_path: Path) -> None:
+ from kernel.lib.browser_routing.routing import direct_vm_request_body_is_replayable
+
+ assert direct_vm_request_body_is_replayable(httpx.Request("GET", "http://vm.test/fs/read_file")) is True
+ assert direct_vm_request_body_is_replayable(httpx.Request("PUT", "http://vm.test/fs/write_file", content=b"x"))
+ assert (
+ direct_vm_request_body_is_replayable(httpx.Request("PUT", "http://vm.test/fs/write_file", content=iter([b"x"])))
+ is False
+ )
+
+ path = tmp_path / "one.txt"
+ path.write_bytes(b"file-bytes")
+ with path.open("rb") as handle:
+ seekable = httpx.Request(
+ "POST",
+ "http://vm.test/fs/upload",
+ data={"files[0][dest_path]": "/tmp/one"},
+ files=[("files[0][file]", handle)],
+ )
+ assert direct_vm_request_body_is_replayable(seekable) is True
+
+ unseekable = httpx.Request(
+ "POST",
+ "http://vm.test/fs/upload",
+ files=[("files[0][file]", cast(Any, _UnseekableFile(b"file-bytes")))],
+ )
+ assert direct_vm_request_body_is_replayable(unseekable) is False
+
+
+@respx.mock
+def test_env_override_can_exclude_fs_and_logs(monkeypatch: pytest.MonkeyPatch) -> None:
+ monkeypatch.setenv("KERNEL_BROWSER_ROUTING_SUBRESOURCES", "computer")
+ fs_read = respx.get(f"{base_url}/browsers/sess-1/fs/read_file").mock(
+ return_value=httpx.Response(200, content=b"x", headers={"content-type": "application/octet-stream"})
+ )
+ logs = respx.get(f"{base_url}/browsers/sess-1/logs/stream").mock(
+ return_value=httpx.Response(
+ 200,
+ headers={"content-type": "text/event-stream"},
+ content=b'data: {"event":"log","message":"hello","timestamp":"2020-01-01T00:00:00Z"}\n\n',
+ )
+ )
+ with Kernel(base_url=base_url, api_key=api_key, _strict_response_validation=True) as client:
+ _cache_browser(client)
+ client.browsers.fs.read_file("sess-1", path="/tmp/x")
+ client.browsers.logs.stream("sess-1", source="path", path="/var/log/x").close()
+
+ assert fs_read.called
+ assert logs.called
+
+
+@respx.mock
+def test_empty_env_disables_fs_and_logs_routing(monkeypatch: pytest.MonkeyPatch) -> None:
+ monkeypatch.setenv("KERNEL_BROWSER_ROUTING_SUBRESOURCES", "")
+ fs_read = respx.get(f"{base_url}/browsers/sess-1/fs/read_file").mock(
+ return_value=httpx.Response(200, content=b"x", headers={"content-type": "application/octet-stream"})
+ )
+ with Kernel(base_url=base_url, api_key=api_key, _strict_response_validation=True) as client:
+ _cache_browser(client)
+ response = client.browsers.fs.read_file("sess-1", path="/tmp/x")
+
+ assert fs_read.called
+ request = cast(httpx.Request, cast(Any, fs_read.calls[0]).request)
+ assert request.url.params.get("jwt") is None
+ assert request.headers.get("Authorization") == f"Bearer {api_key}"
+ assert response.read() == b"x"
diff --git a/tests/test_client.py b/tests/test_client.py
index c3a1186c..3d5f915b 100644
--- a/tests/test_client.py
+++ b/tests/test_client.py
@@ -561,11 +561,11 @@ def test_multipart_repeating_array(self, client: Kernel) -> None:
assert request.read().split(b"\r\n") == [
b"--6b7ba517decee4a450543ea6ae821c82",
- b'Content-Disposition: form-data; name="array[]"',
+ b'Content-Disposition: form-data; name="array[0]"',
b"",
b"foo",
b"--6b7ba517decee4a450543ea6ae821c82",
- b'Content-Disposition: form-data; name="array[]"',
+ b'Content-Disposition: form-data; name="array[1]"',
b"",
b"bar",
b"--6b7ba517decee4a450543ea6ae821c82",
@@ -880,7 +880,7 @@ def test_parse_retry_after_header(
calculated = client._calculate_retry_timeout(remaining_retries, options, headers)
assert calculated == pytest.approx(timeout, 0.5 * 0.875) # pyright: ignore[reportUnknownMemberType]
- @pytest.mark.skip() # SDK-2615
+ @pytest.mark.skip() # SDK-2615
@mock.patch("kernel._base_client.BaseClient._calculate_retry_timeout", _low_retry_timeout)
@pytest.mark.respx(base_url=base_url)
def test_retrying_timeout_errors_doesnt_leak(self, respx_mock: MockRouter, client: Kernel) -> None:
@@ -891,7 +891,7 @@ def test_retrying_timeout_errors_doesnt_leak(self, respx_mock: MockRouter, clien
assert _get_open_connections(client) == 0
- @pytest.mark.skip() # SDK-2615
+ @pytest.mark.skip() # SDK-2615
@mock.patch("kernel._base_client.BaseClient._calculate_retry_timeout", _low_retry_timeout)
@pytest.mark.respx(base_url=base_url)
def test_retrying_status_errors_doesnt_leak(self, respx_mock: MockRouter, client: Kernel) -> None:
@@ -1488,11 +1488,11 @@ def test_multipart_repeating_array(self, async_client: AsyncKernel) -> None:
assert request.read().split(b"\r\n") == [
b"--6b7ba517decee4a450543ea6ae821c82",
- b'Content-Disposition: form-data; name="array[]"',
+ b'Content-Disposition: form-data; name="array[0]"',
b"",
b"foo",
b"--6b7ba517decee4a450543ea6ae821c82",
- b'Content-Disposition: form-data; name="array[]"',
+ b'Content-Disposition: form-data; name="array[1]"',
b"",
b"bar",
b"--6b7ba517decee4a450543ea6ae821c82",
@@ -1824,7 +1824,7 @@ async def test_parse_retry_after_header(
calculated = async_client._calculate_retry_timeout(remaining_retries, options, headers)
assert calculated == pytest.approx(timeout, 0.5 * 0.875) # pyright: ignore[reportUnknownMemberType]
- @pytest.mark.skip() # SDK-2615
+ @pytest.mark.skip() # SDK-2615
@mock.patch("kernel._base_client.BaseClient._calculate_retry_timeout", _low_retry_timeout)
@pytest.mark.respx(base_url=base_url)
async def test_retrying_timeout_errors_doesnt_leak(self, respx_mock: MockRouter, async_client: AsyncKernel) -> None:
@@ -1835,7 +1835,7 @@ async def test_retrying_timeout_errors_doesnt_leak(self, respx_mock: MockRouter,
assert _get_open_connections(async_client) == 0
- @pytest.mark.skip() # SDK-2615
+ @pytest.mark.skip() # SDK-2615
@mock.patch("kernel._base_client.BaseClient._calculate_retry_timeout", _low_retry_timeout)
@pytest.mark.respx(base_url=base_url)
async def test_retrying_status_errors_doesnt_leak(self, respx_mock: MockRouter, async_client: AsyncKernel) -> None:
diff --git a/tests/test_extract_files.py b/tests/test_extract_files.py
index 54ef03af..1325ec06 100644
--- a/tests/test_extract_files.py
+++ b/tests/test_extract_files.py
@@ -29,15 +29,15 @@ def test_removes_files_from_input() -> None:
def test_multiple_files() -> None:
query = {"documents": [{"file": b"My first file"}, {"file": b"My second file"}]}
assert extract_files(query, paths=[["documents", "", "file"]]) == [
- ("documents[][file]", b"My first file"),
- ("documents[][file]", b"My second file"),
+ ("documents[0][file]", b"My first file"),
+ ("documents[1][file]", b"My second file"),
]
assert query == {"documents": [{}, {}]}
def test_top_level_file_array() -> None:
query = {"files": [b"file one", b"file two"], "title": "hello"}
- assert extract_files(query, paths=[["files", ""]]) == [("files[]", b"file one"), ("files[]", b"file two")]
+ assert extract_files(query, paths=[["files", ""]]) == [("files[0]", b"file one"), ("files[1]", b"file two")]
assert query == {"title": "hello"}
From 96006001897a6f33da15b77eda0095fe0bb50ff3 Mon Sep 17 00:00:00 2001
From: tnsardesai <18272584+tnsardesai@users.noreply.github.com>
Date: Wed, 2 Sep 2026 06:51:37 +0000
Subject: [PATCH 225/366] Address review: narrow multipart change, prove
rewinds, always evict
Scope the indexed multipart array names to fs.upload instead of changing
the client's generic array encoding: the endpoint now flattens its own body
with indexed names and asks extract_files for matching file part names, so
load_extensions and any other multipart array keep their existing wire
format.
Prove a multipart file field can be rewound before treating a stale-JWT
failure as retryable. A wrapper can report seekable() while seek() raises,
which rendered the fallback body as an empty part.
Evict a stale direct-to-VM route from the terminal error path too. Retry
eligibility is only consulted when retries remain, so with max_retries=0 a
VM 401/403 previously left the dead route cached and wedged later calls.
Route only logs/stream rather than the whole logs subresource.
---
src/kernel/_base_client.py | 5 +-
src/kernel/_client.py | 28 ++-
src/kernel/_utils/_utils.py | 6 +-
src/kernel/lib/browser_routing/routing.py | 39 ++-
src/kernel/lib/multipart.py | 41 ++++
src/kernel/resources/browsers/fs/fs.py | 19 +-
tests/test_browser_routing.py | 274 +++++++++++++++++++++-
tests/test_client.py | 16 +-
tests/test_extract_files.py | 6 +-
9 files changed, 396 insertions(+), 38 deletions(-)
create mode 100644 src/kernel/lib/multipart.py
diff --git a/src/kernel/_base_client.py b/src/kernel/_base_client.py
index 50b190a5..2599dc41 100644
--- a/src/kernel/_base_client.py
+++ b/src/kernel/_base_client.py
@@ -587,10 +587,7 @@ def _serialize_multipartform(self, data: Mapping[object, object]) -> dict[str, o
# TODO: type ignore is required as stringify_items is well typed but we can't be
# well typed without heavy validation.
data, # type: ignore
- # Indexed names (`files[0][dest_path]`) keep each array entry's fields
- # grouped together; repeated `files[][dest_path]` parts cannot be
- # matched back to their file part. `extract_files` uses the same format.
- array_format="indices",
+ array_format="brackets",
)
serialized: dict[str, object] = {}
for key, value in items:
diff --git a/src/kernel/_client.py b/src/kernel/_client.py
index 995e2dad..07fd085b 100644
--- a/src/kernel/_client.py
+++ b/src/kernel/_client.py
@@ -364,16 +364,28 @@ def _prepare_options(self, options: Any) -> Any:
def _prepare_request(self, request: httpx.Request) -> None:
strip_direct_vm_auth(request, cache=self.browser_route_cache)
+ def _evict_stale_direct_vm_route(self, response: httpx.Response) -> None:
+ maybe_evict_browser_route_from_response(response, cache=self.browser_route_cache)
+
@override
def _should_retry(self, response: httpx.Response) -> bool:
if is_stale_direct_vm_auth_response(response):
- maybe_evict_browser_route_from_response(response, cache=self.browser_route_cache)
+ self._evict_stale_direct_vm_route(response)
# The route is evicted either way; only retry when the body can be
# rebuilt, otherwise the caller sees the original auth failure and a
# later call goes to the control plane.
return should_retry_stale_direct_vm_auth(response)
return super()._should_retry(response)
+ @override
+ def _make_status_error_from_response(self, response: httpx.Response) -> APIStatusError:
+ # `_should_retry` never runs when the request has no retries left, so this
+ # is the only place a stale direct-to-VM route gets evicted before the
+ # error surfaces to the caller.
+ if is_stale_direct_vm_auth_response(response):
+ self._evict_stale_direct_vm_route(response)
+ return super()._make_status_error_from_response(response)
+
@override
def _process_response(
self,
@@ -750,16 +762,28 @@ async def _prepare_options(self, options: Any) -> Any:
async def _prepare_request(self, request: httpx.Request) -> None:
strip_direct_vm_auth(request, cache=self.browser_route_cache)
+ def _evict_stale_direct_vm_route(self, response: httpx.Response) -> None:
+ maybe_evict_browser_route_from_response(response, cache=self.browser_route_cache)
+
@override
def _should_retry(self, response: httpx.Response) -> bool:
if is_stale_direct_vm_auth_response(response):
- maybe_evict_browser_route_from_response(response, cache=self.browser_route_cache)
+ self._evict_stale_direct_vm_route(response)
# The route is evicted either way; only retry when the body can be
# rebuilt, otherwise the caller sees the original auth failure and a
# later call goes to the control plane.
return should_retry_stale_direct_vm_auth(response)
return super()._should_retry(response)
+ @override
+ def _make_status_error_from_response(self, response: httpx.Response) -> APIStatusError:
+ # `_should_retry` never runs when the request has no retries left, so this
+ # is the only place a stale direct-to-VM route gets evicted before the
+ # error surfaces to the caller.
+ if is_stale_direct_vm_auth_response(response):
+ self._evict_stale_direct_vm_route(response)
+ return super()._make_status_error_from_response(response)
+
@override
async def _process_response(
self,
diff --git a/src/kernel/_utils/_utils.py b/src/kernel/_utils/_utils.py
index 45116559..199cd231 100644
--- a/src/kernel/_utils/_utils.py
+++ b/src/kernel/_utils/_utils.py
@@ -40,7 +40,7 @@ def extract_files(
query: Mapping[str, object],
*,
paths: Sequence[Sequence[str]],
- array_format: ArrayFormat = "indices",
+ array_format: ArrayFormat = "brackets",
) -> list[tuple[str, FileTypes]]:
"""Recursively extract files from the given dictionary based on specified paths.
@@ -48,9 +48,7 @@ def extract_files(
``array_format`` controls how ```` segments contribute to the emitted
field name. Supported values: ``"brackets"`` (``foo[]``), ``"repeat"`` and
- ``"comma"`` (``foo``), ``"indices"`` (``foo[0]``, ``foo[1]``). Indexed names are
- the default so that a file part stays associated with the sibling fields of the
- same array entry, which repeated ``foo[]`` names cannot express.
+ ``"comma"`` (``foo``), ``"indices"`` (``foo[0]``, ``foo[1]``).
Note: this mutates the given dictionary.
"""
diff --git a/src/kernel/lib/browser_routing/routing.py b/src/kernel/lib/browser_routing/routing.py
index 1fa69f54..f005644f 100644
--- a/src/kernel/lib/browser_routing/routing.py
+++ b/src/kernel/lib/browser_routing/routing.py
@@ -52,7 +52,7 @@ def browser_routing_config_from_env() -> BrowserRoutingConfig:
"playwright",
"process",
"fs",
- "logs",
+ "logs/stream",
)
)
if raw.strip() == "":
@@ -237,11 +237,40 @@ def _multipart_field_is_replayable(field: Any) -> bool:
return True
if getattr(file, "closed", False):
return False
- if not callable(getattr(file, "seek", None)):
+ return _rewind_succeeds(file)
+
+
+def _rewind_succeeds(file: Any) -> bool:
+ """Whether the file field can actually be rewound for another render.
+
+ `seekable()` is not proof: a wrapper can report True and still raise from
+ `seek()`, which would render the field as an empty part on the retry. The
+ only reliable check is to perform the rewind httpx would perform.
+ """
+ seek = getattr(file, "seek", None)
+ if not callable(seek):
+ return False
+
+ position: object = None
+ tell = getattr(file, "tell", None)
+ if callable(tell):
+ try:
+ position = tell()
+ except Exception:
+ position = None
+
+ try:
+ seek(0)
+ except Exception:
return False
- seekable = getattr(file, "seekable", None)
- # httpx rewinds seekable file fields before rendering them again.
- return bool(seekable()) if callable(seekable) else True
+
+ if isinstance(position, int) and position > 0:
+ try:
+ seek(position)
+ except Exception:
+ # The field is left rewound, which is where httpx renders it from anyway.
+ pass
+ return True
def _session_id_from_browser_delete_path(path: str) -> str | None:
diff --git a/src/kernel/lib/multipart.py b/src/kernel/lib/multipart.py
new file mode 100644
index 00000000..611c6d8c
--- /dev/null
+++ b/src/kernel/lib/multipart.py
@@ -0,0 +1,41 @@
+from __future__ import annotations
+
+from typing import Mapping, Sequence, cast
+
+from .._utils import is_given
+
+__all__ = ["indexed_multipart_body"]
+
+
+def indexed_multipart_body(body: object) -> dict[str, object]:
+ """Flatten a multipart body so that array entries carry their index.
+
+ Endpoints that take an array of objects with a file field need each entry's
+ fields grouped together: `files[0][dest_path]` pairs with the `files[0][file]`
+ part, while repeated `files[][dest_path]` names cannot be matched back to
+ their file. The returned mapping is already flat, so the client's generic
+ multipart serialization passes the names through untouched and every other
+ endpoint keeps its existing encoding.
+ """
+ flattened: dict[str, object] = {}
+ if isinstance(body, Mapping):
+ for key, value in cast(Mapping[object, object], body).items():
+ _flatten(str(key), value, flattened)
+ return flattened
+
+
+def _flatten(key: str, value: object, out: dict[str, object]) -> None:
+ if not is_given(value):
+ return
+
+ if isinstance(value, Mapping):
+ for child_key, child in cast(Mapping[object, object], value).items():
+ _flatten(f"{key}[{child_key}]", child, out)
+ return
+
+ if isinstance(value, (list, tuple)):
+ for index, child in enumerate(cast(Sequence[object], value)):
+ _flatten(f"{key}[{index}]", child, out)
+ return
+
+ out[key] = value
diff --git a/src/kernel/resources/browsers/fs/fs.py b/src/kernel/resources/browsers/fs/fs.py
index a89d86f2..ab6f572d 100644
--- a/src/kernel/resources/browsers/fs/fs.py
+++ b/src/kernel/resources/browsers/fs/fs.py
@@ -48,6 +48,7 @@
async_to_custom_streamed_response_wrapper,
)
from ...._base_client import make_request_options
+from ....lib.multipart import indexed_multipart_body
from ....types.browsers import (
f_move_params,
f_upload_params,
@@ -510,14 +511,19 @@ def upload(
raise ValueError(f"Expected a non-empty value for `id_or_name` but received {id_or_name!r}")
extra_headers = {"Accept": "*/*", **(extra_headers or {})}
body = deepcopy_with_paths({"files": files}, [["files", "", "file"]])
- extracted_files = extract_files(cast(Mapping[str, object], body), paths=[["files", "", "file"]])
+ # The remote filesystem pairs each file part with the sibling fields of the
+ # same array entry, so both halves of the form use indexed names
+ # (`files[0][file]`, `files[0][dest_path]`).
+ extracted_files = extract_files(
+ cast(Mapping[str, object], body), paths=[["files", "", "file"]], array_format="indices"
+ )
# It should be noted that the actual Content-Type header that will be
# sent to the server will contain a `boundary` parameter, e.g.
# multipart/form-data; boundary=---abc--
extra_headers["Content-Type"] = "multipart/form-data"
return self._post(
path_template("/browsers/{id_or_name}/fs/upload", id_or_name=id_or_name),
- body=maybe_transform(body, f_upload_params.FUploadParams),
+ body=indexed_multipart_body(maybe_transform(body, f_upload_params.FUploadParams)),
files=extracted_files,
options=make_request_options(
extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
@@ -1073,14 +1079,19 @@ async def upload(
raise ValueError(f"Expected a non-empty value for `id_or_name` but received {id_or_name!r}")
extra_headers = {"Accept": "*/*", **(extra_headers or {})}
body = deepcopy_with_paths({"files": files}, [["files", "", "file"]])
- extracted_files = extract_files(cast(Mapping[str, object], body), paths=[["files", "", "file"]])
+ # The remote filesystem pairs each file part with the sibling fields of the
+ # same array entry, so both halves of the form use indexed names
+ # (`files[0][file]`, `files[0][dest_path]`).
+ extracted_files = extract_files(
+ cast(Mapping[str, object], body), paths=[["files", "", "file"]], array_format="indices"
+ )
# It should be noted that the actual Content-Type header that will be
# sent to the server will contain a `boundary` parameter, e.g.
# multipart/form-data; boundary=---abc--
extra_headers["Content-Type"] = "multipart/form-data"
return await self._post(
path_template("/browsers/{id_or_name}/fs/upload", id_or_name=id_or_name),
- body=await async_maybe_transform(body, f_upload_params.FUploadParams),
+ body=indexed_multipart_body(await async_maybe_transform(body, f_upload_params.FUploadParams)),
files=extracted_files,
options=make_request_options(
extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
diff --git a/tests/test_browser_routing.py b/tests/test_browser_routing.py
index 4e154802..756fbcda 100644
--- a/tests/test_browser_routing.py
+++ b/tests/test_browser_routing.py
@@ -1,5 +1,6 @@
from __future__ import annotations
+import io
import os
import asyncio
from typing import Any, AsyncIterator, cast
@@ -10,7 +11,13 @@
import respx
import pytest
-from kernel import Kernel, AsyncKernel, AuthenticationError, InternalServerError
+from kernel import (
+ Kernel,
+ AsyncKernel,
+ AuthenticationError,
+ InternalServerError,
+ PermissionDeniedError,
+)
from kernel.lib.browser_routing.util import jwt_from_cdp_ws_url
from kernel.lib.browser_routing.routing import (
BrowserRoute,
@@ -42,22 +49,61 @@ def _skip_retry_sleep(_self: object, **_kwargs: object) -> None:
return None
-class _UnseekableFile:
- """A file-like upload body that cannot be rewound, e.g. a pipe."""
+class _UnseekableFile(io.RawIOBase):
+ """A file-like upload body that cannot be rewound, e.g. a pipe.
+
+ `claims_seekable` reproduces a wrapper whose `seekable()` says True while
+ `seek()` still raises, which would otherwise render as an empty part.
+ """
name = "one.txt"
- def __init__(self, content: bytes) -> None:
+ def __init__(self, content: bytes, *, claims_seekable: bool = False) -> None:
self._content = content
+ self._claims_seekable = claims_seekable
+ @override
+ def readable(self) -> bool:
+ return True
+
+ @override
def read(self, size: int = -1) -> bytes:
chunk = self._content if size < 0 else self._content[:size]
self._content = b"" if size < 0 else self._content[size:]
return chunk
+ @override
+ def tell(self) -> int:
+ return 0
+
+ @override
def seekable(self) -> bool:
- return False
+ return self._claims_seekable
+ @override
+ def seek(self, _offset: int, _whence: int = 0) -> int:
+ raise io.UnsupportedOperation("not seekable")
+
+
+class _NoSeekableAttrFile(io.RawIOBase):
+ """A file-like upload body whose `seek()` raises and reports no seekability."""
+
+ name = "one.txt"
+
+ def __init__(self, content: bytes) -> None:
+ self._content = content
+
+ @override
+ def readable(self) -> bool:
+ return True
+
+ @override
+ def read(self, size: int = -1) -> bytes:
+ chunk = self._content if size < 0 else self._content[:size]
+ self._content = b"" if size < 0 else self._content[size:]
+ return chunk
+
+ @override
def seek(self, _offset: int, _whence: int = 0) -> int:
raise OSError("not seekable")
@@ -424,7 +470,7 @@ def test_browser_routing_config_from_env_defaults(monkeypatch: pytest.MonkeyPatc
"playwright",
"process",
"fs",
- "logs",
+ "logs/stream",
)
@@ -434,7 +480,7 @@ def test_direct_vm_routing_allowlist_segment_boundary() -> None:
# stream-prefixed-but-different path is not matched.
from kernel.lib.browser_routing.routing import _matches_direct_vm_prefix
- prefixes = ("curl", "telemetry/stream", "computer", "playwright", "process", "fs", "logs")
+ prefixes = ("curl", "telemetry/stream", "computer", "playwright", "process", "fs", "logs/stream")
assert _matches_direct_vm_prefix("telemetry/stream", prefixes) is True
assert _matches_direct_vm_prefix("telemetry/stream/x", prefixes) is True
assert _matches_direct_vm_prefix("telemetry/events", prefixes) is False
@@ -449,6 +495,9 @@ def test_direct_vm_routing_allowlist_segment_boundary() -> None:
assert _matches_direct_vm_prefix("fs/watch/watch-1/events", prefixes) is True
assert _matches_direct_vm_prefix("fsx/read_file", prefixes) is False
assert _matches_direct_vm_prefix("logs/stream", prefixes) is True
+ assert _matches_direct_vm_prefix("logs/stream/x", prefixes) is True
+ assert _matches_direct_vm_prefix("logs", prefixes) is False
+ assert _matches_direct_vm_prefix("logs/history", prefixes) is False
assert _matches_direct_vm_prefix("logstream", prefixes) is False
assert _matches_direct_vm_prefix("extensions", prefixes) is False
assert _matches_direct_vm_prefix("replays/rec-1", prefixes) is False
@@ -469,7 +518,7 @@ def test_rewrite_direct_vm_options_keeps_telemetry_events_on_control_plane() ->
cache = BrowserRouteCache()
cache.set(BrowserRoute(session_id="sess-1", base_url="http://browser-session.test/browser/kernel", jwt="token-abc"))
config = BrowserRoutingConfig(
- subresources=("curl", "telemetry/stream", "computer", "playwright", "process", "fs", "logs")
+ subresources=("curl", "telemetry/stream", "computer", "playwright", "process", "fs", "logs/stream")
)
events = rewrite_direct_vm_options(
@@ -507,6 +556,16 @@ def test_rewrite_direct_vm_options_keeps_telemetry_events_on_control_plane() ->
)
assert str(logs_stream.url).startswith("http://browser-session.test/browser/kernel/logs/stream")
+ logs_root = rewrite_direct_vm_options(
+ FinalRequestOptions(method="get", url="/browsers/sess-1/logs"), cache=cache, config=config
+ )
+ assert logs_root.url == "/browsers/sess-1/logs"
+
+ logs_history = rewrite_direct_vm_options(
+ FinalRequestOptions(method="get", url="/browsers/sess-1/logs/history"), cache=cache, config=config
+ )
+ assert logs_history.url == "/browsers/sess-1/logs/history"
+
extensions = rewrite_direct_vm_options(
FinalRequestOptions(method="post", url="/browsers/sess-1/extensions"), cache=cache, config=config
)
@@ -963,6 +1022,28 @@ def test_direct_vm_request_body_is_replayable_classification(tmp_path: Path) ->
)
assert direct_vm_request_body_is_replayable(unseekable) is False
+ # seekable() is not proof: the rewind itself has to succeed.
+ lies_about_seekable = httpx.Request(
+ "POST",
+ "http://vm.test/fs/upload",
+ files=[("files[0][file]", cast(Any, _UnseekableFile(b"file-bytes", claims_seekable=True)))],
+ )
+ assert direct_vm_request_body_is_replayable(lies_about_seekable) is False
+
+ without_seekable_attr = httpx.Request(
+ "POST",
+ "http://vm.test/fs/upload",
+ files=[("files[0][file]", cast(Any, _NoSeekableAttrFile(b"file-bytes")))],
+ )
+ assert direct_vm_request_body_is_replayable(without_seekable_attr) is False
+
+ in_memory = io.BytesIO(b"file-bytes")
+ buffered = httpx.Request("POST", "http://vm.test/fs/upload", files=[("files[0][file]", in_memory)])
+ assert direct_vm_request_body_is_replayable(buffered) is True
+
+ in_memory.close()
+ assert direct_vm_request_body_is_replayable(buffered) is False
+
@respx.mock
def test_env_override_can_exclude_fs_and_logs(monkeypatch: pytest.MonkeyPatch) -> None:
@@ -1001,3 +1082,180 @@ def test_empty_env_disables_fs_and_logs_routing(monkeypatch: pytest.MonkeyPatch)
assert request.url.params.get("jwt") is None
assert request.headers.get("Authorization") == f"Bearer {api_key}"
assert response.read() == b"x"
+
+
+@respx.mock
+def test_stale_direct_vm_jwt_does_not_replay_multipart_that_cannot_rewind(
+ monkeypatch: pytest.MonkeyPatch,
+) -> None:
+ monkeypatch.delenv("KERNEL_BROWSER_ROUTING_SUBRESOURCES", raising=False)
+ monkeypatch.setattr("kernel._base_client.SyncAPIClient._sleep_for_retry", _skip_retry_sleep)
+ vm = respx.post("http://browser-session.test/browser/kernel/fs/upload").mock(
+ return_value=httpx.Response(401, text="Invalid JWT")
+ )
+ api = respx.post(f"{base_url}/browsers/sess-1/fs/upload").mock(return_value=httpx.Response(204))
+ with Kernel(base_url=base_url, api_key=api_key, _strict_response_validation=True) as client:
+ _cache_browser(client)
+ with pytest.raises(AuthenticationError):
+ client.browsers.fs.upload(
+ "sess-1",
+ files=[
+ {
+ "dest_path": "/tmp/one",
+ "file": cast(Any, _UnseekableFile(b"file-bytes", claims_seekable=True)),
+ }
+ ],
+ )
+ assert client.browser_route_cache.get("sess-1") is None
+
+ assert vm.call_count == 1
+ assert not api.called
+
+
+@pytest.mark.asyncio
+@respx.mock
+async def test_async_stale_direct_vm_jwt_does_not_replay_multipart_that_cannot_rewind(
+ monkeypatch: pytest.MonkeyPatch,
+) -> None:
+ monkeypatch.delenv("KERNEL_BROWSER_ROUTING_SUBRESOURCES", raising=False)
+ monkeypatch.setattr("kernel._base_client.AsyncAPIClient._sleep_for_retry", _skip_retry_sleep)
+ vm = respx.post("http://browser-session.test/browser/kernel/fs/upload").mock(
+ return_value=httpx.Response(403, text="Invalid JWT")
+ )
+ api = respx.post(f"{base_url}/browsers/sess-1/fs/upload").mock(return_value=httpx.Response(204))
+ async with AsyncKernel(base_url=base_url, api_key=api_key, _strict_response_validation=True) as client:
+ route = browser_route_from_browser(_fake_browser())
+ assert route is not None
+ client.browser_route_cache.set(route)
+ with pytest.raises(PermissionDeniedError):
+ await client.browsers.fs.upload(
+ "sess-1",
+ files=[
+ {
+ "dest_path": "/tmp/one",
+ "file": cast(Any, _UnseekableFile(b"file-bytes", claims_seekable=True)),
+ }
+ ],
+ )
+ assert client.browser_route_cache.get("sess-1") is None
+
+ assert vm.call_count == 1
+ assert not api.called
+
+
+@respx.mock
+def test_stale_direct_vm_jwt_evicts_route_without_retries_for_buffered_body(
+ monkeypatch: pytest.MonkeyPatch,
+) -> None:
+ monkeypatch.delenv("KERNEL_BROWSER_ROUTING_SUBRESOURCES", raising=False)
+ vm = respx.put("http://browser-session.test/browser/kernel/fs/write_file").mock(
+ return_value=httpx.Response(401, text="Invalid JWT")
+ )
+ api = respx.put(f"{base_url}/browsers/sess-1/fs/write_file").mock(return_value=httpx.Response(204))
+ with Kernel(base_url=base_url, api_key=api_key, max_retries=0, _strict_response_validation=True) as client:
+ _cache_browser(client)
+ with pytest.raises(AuthenticationError):
+ client.browsers.fs.write_file("sess-1", b"payload", path="/tmp/x")
+ assert client.browser_route_cache.get("sess-1") is None
+
+ # The caller's next attempt goes to the control plane.
+ client.browsers.fs.write_file("sess-1", b"payload", path="/tmp/x")
+
+ assert vm.call_count == 1
+ api_req = cast(httpx.Request, cast(Any, api.calls[0]).request)
+ assert api_req.content == b"payload"
+ assert api_req.url.params.get("jwt") is None
+ assert api_req.headers.get("Authorization") == f"Bearer {api_key}"
+
+
+@pytest.mark.asyncio
+@respx.mock
+async def test_async_stale_direct_vm_jwt_evicts_route_without_retries_for_streamed_body(
+ monkeypatch: pytest.MonkeyPatch,
+) -> None:
+ monkeypatch.delenv("KERNEL_BROWSER_ROUTING_SUBRESOURCES", raising=False)
+ vm = respx.put("http://browser-session.test/browser/kernel/fs/write_file").mock(
+ return_value=httpx.Response(403, text="Invalid JWT")
+ )
+ api = respx.put(f"{base_url}/browsers/sess-1/fs/write_file").mock(return_value=httpx.Response(204))
+
+ async def _chunks() -> AsyncIterator[bytes]:
+ yield b"chunk-one"
+
+ async with AsyncKernel(
+ base_url=base_url, api_key=api_key, max_retries=0, _strict_response_validation=True
+ ) as client:
+ route = browser_route_from_browser(_fake_browser())
+ assert route is not None
+ client.browser_route_cache.set(route)
+ with pytest.raises(PermissionDeniedError):
+ await client.browsers.fs.write_file("sess-1", _chunks(), path="/tmp/x")
+ assert client.browser_route_cache.get("sess-1") is None
+
+ await client.browsers.fs.write_file("sess-1", b"payload", path="/tmp/x")
+
+ assert vm.call_count == 1
+ api_req = cast(httpx.Request, cast(Any, api.calls[0]).request)
+ assert api_req.url.params.get("jwt") is None
+ assert api_req.headers.get("Authorization") == f"Bearer {api_key}"
+
+
+@respx.mock
+def test_load_extensions_multipart_encoding_is_unchanged(monkeypatch: pytest.MonkeyPatch) -> None:
+ # Indexed names are scoped to fs.upload; every other multipart endpoint keeps
+ # the client's generic array encoding.
+ monkeypatch.delenv("KERNEL_BROWSER_ROUTING_SUBRESOURCES", raising=False)
+ extensions = respx.post(f"{base_url}/browsers/sess-1/extensions").mock(return_value=httpx.Response(204))
+ with Kernel(base_url=base_url, api_key=api_key, _strict_response_validation=True) as client:
+ _cache_browser(client)
+ client.browsers.load_extensions(
+ "sess-1",
+ extensions=[
+ {"name": "one", "zip_file": b"zip-one"},
+ {"name": "two", "zip_file": b"zip-two"},
+ ],
+ )
+
+ body = cast(httpx.Request, cast(Any, extensions.calls[0]).request).read()
+ assert b'name="extensions[][name]"' in body
+ assert b'name="extensions[][zip_file]"' in body
+ assert b"extensions[0]" not in body
+
+
+def test_generic_multipart_array_encoding_is_unchanged() -> None:
+ from kernel._models import FinalRequestOptions
+
+ with Kernel(base_url=base_url, api_key=api_key, _strict_response_validation=True) as client:
+ request = client._build_request( # pyright: ignore[reportPrivateUsage]
+ FinalRequestOptions.construct(
+ method="post",
+ url="/foo",
+ headers={"Content-Type": "multipart/form-data; boundary=abc"},
+ json_data={"array": ["foo", "bar"]},
+ files=[("foo.txt", b"hello world")],
+ )
+ )
+
+ body = request.read()
+ assert b'name="array[]"' in body
+ assert b'name="array[0]"' not in body
+
+
+def test_indexed_multipart_body_flattens_only_given_values() -> None:
+ from kernel._types import omit
+ from kernel.lib.multipart import indexed_multipart_body
+
+ assert indexed_multipart_body(
+ {
+ "files": [
+ {"dest_path": "/tmp/one", "mode": omit},
+ {"dest_path": "/tmp/two"},
+ ],
+ "flag": True,
+ "skipped": omit,
+ }
+ ) == {
+ "files[0][dest_path]": "/tmp/one",
+ "files[1][dest_path]": "/tmp/two",
+ "flag": True,
+ }
diff --git a/tests/test_client.py b/tests/test_client.py
index 3d5f915b..c3a1186c 100644
--- a/tests/test_client.py
+++ b/tests/test_client.py
@@ -561,11 +561,11 @@ def test_multipart_repeating_array(self, client: Kernel) -> None:
assert request.read().split(b"\r\n") == [
b"--6b7ba517decee4a450543ea6ae821c82",
- b'Content-Disposition: form-data; name="array[0]"',
+ b'Content-Disposition: form-data; name="array[]"',
b"",
b"foo",
b"--6b7ba517decee4a450543ea6ae821c82",
- b'Content-Disposition: form-data; name="array[1]"',
+ b'Content-Disposition: form-data; name="array[]"',
b"",
b"bar",
b"--6b7ba517decee4a450543ea6ae821c82",
@@ -880,7 +880,7 @@ def test_parse_retry_after_header(
calculated = client._calculate_retry_timeout(remaining_retries, options, headers)
assert calculated == pytest.approx(timeout, 0.5 * 0.875) # pyright: ignore[reportUnknownMemberType]
- @pytest.mark.skip() # SDK-2615
+ @pytest.mark.skip() # SDK-2615
@mock.patch("kernel._base_client.BaseClient._calculate_retry_timeout", _low_retry_timeout)
@pytest.mark.respx(base_url=base_url)
def test_retrying_timeout_errors_doesnt_leak(self, respx_mock: MockRouter, client: Kernel) -> None:
@@ -891,7 +891,7 @@ def test_retrying_timeout_errors_doesnt_leak(self, respx_mock: MockRouter, clien
assert _get_open_connections(client) == 0
- @pytest.mark.skip() # SDK-2615
+ @pytest.mark.skip() # SDK-2615
@mock.patch("kernel._base_client.BaseClient._calculate_retry_timeout", _low_retry_timeout)
@pytest.mark.respx(base_url=base_url)
def test_retrying_status_errors_doesnt_leak(self, respx_mock: MockRouter, client: Kernel) -> None:
@@ -1488,11 +1488,11 @@ def test_multipart_repeating_array(self, async_client: AsyncKernel) -> None:
assert request.read().split(b"\r\n") == [
b"--6b7ba517decee4a450543ea6ae821c82",
- b'Content-Disposition: form-data; name="array[0]"',
+ b'Content-Disposition: form-data; name="array[]"',
b"",
b"foo",
b"--6b7ba517decee4a450543ea6ae821c82",
- b'Content-Disposition: form-data; name="array[1]"',
+ b'Content-Disposition: form-data; name="array[]"',
b"",
b"bar",
b"--6b7ba517decee4a450543ea6ae821c82",
@@ -1824,7 +1824,7 @@ async def test_parse_retry_after_header(
calculated = async_client._calculate_retry_timeout(remaining_retries, options, headers)
assert calculated == pytest.approx(timeout, 0.5 * 0.875) # pyright: ignore[reportUnknownMemberType]
- @pytest.mark.skip() # SDK-2615
+ @pytest.mark.skip() # SDK-2615
@mock.patch("kernel._base_client.BaseClient._calculate_retry_timeout", _low_retry_timeout)
@pytest.mark.respx(base_url=base_url)
async def test_retrying_timeout_errors_doesnt_leak(self, respx_mock: MockRouter, async_client: AsyncKernel) -> None:
@@ -1835,7 +1835,7 @@ async def test_retrying_timeout_errors_doesnt_leak(self, respx_mock: MockRouter,
assert _get_open_connections(async_client) == 0
- @pytest.mark.skip() # SDK-2615
+ @pytest.mark.skip() # SDK-2615
@mock.patch("kernel._base_client.BaseClient._calculate_retry_timeout", _low_retry_timeout)
@pytest.mark.respx(base_url=base_url)
async def test_retrying_status_errors_doesnt_leak(self, respx_mock: MockRouter, async_client: AsyncKernel) -> None:
diff --git a/tests/test_extract_files.py b/tests/test_extract_files.py
index 1325ec06..54ef03af 100644
--- a/tests/test_extract_files.py
+++ b/tests/test_extract_files.py
@@ -29,15 +29,15 @@ def test_removes_files_from_input() -> None:
def test_multiple_files() -> None:
query = {"documents": [{"file": b"My first file"}, {"file": b"My second file"}]}
assert extract_files(query, paths=[["documents", "", "file"]]) == [
- ("documents[0][file]", b"My first file"),
- ("documents[1][file]", b"My second file"),
+ ("documents[][file]", b"My first file"),
+ ("documents[][file]", b"My second file"),
]
assert query == {"documents": [{}, {}]}
def test_top_level_file_array() -> None:
query = {"files": [b"file one", b"file two"], "title": "hello"}
- assert extract_files(query, paths=[["files", ""]]) == [("files[0]", b"file one"), ("files[1]", b"file two")]
+ assert extract_files(query, paths=[["files", ""]]) == [("files[]", b"file one"), ("files[]", b"file two")]
assert query == {"title": "hello"}
From c01804d33d22237b6912d4479fddcfbed2c3cd72 Mon Sep 17 00:00:00 2001
From: tnsardesai <18272584+tnsardesai@users.noreply.github.com>
Date: Wed, 2 Sep 2026 07:08:10 +0000
Subject: [PATCH 226/366] Evict a stale direct-to-VM route before the error
body is read
httpx reads the body of a non-streamed response inside send(), so a 401 or
403 whose body read fails surfaces as a connection error and never reaches
the status-error path. The route eviction ran after that read, which left a
dead JWT cached and wedged every later call for the session.
Move eviction into an httpx response event hook, which runs once the status
is known and before any body is read, for both the sync and async clients.
`_should_retry` now only decides whether replaying the body on the control
plane is safe.
---
src/kernel/_client.py | 42 +++-------
src/kernel/lib/browser_routing/routing.py | 45 +++++++++++
tests/test_browser_routing.py | 96 ++++++++++++++++++++++-
3 files changed, 150 insertions(+), 33 deletions(-)
diff --git a/src/kernel/_client.py b/src/kernel/_client.py
index 07fd085b..dc506418 100644
--- a/src/kernel/_client.py
+++ b/src/kernel/_client.py
@@ -43,7 +43,9 @@
browser_routing_config_from_env,
is_stale_direct_vm_auth_response,
should_retry_stale_direct_vm_auth,
+ install_stale_direct_vm_auth_eviction,
maybe_evict_browser_route_from_response,
+ install_async_stale_direct_vm_auth_eviction,
maybe_populate_browser_route_cache_from_response,
)
@@ -204,6 +206,7 @@ def __init__(
)
self.browser_route_cache = _browser_route_cache or BrowserRouteCache()
self._browser_routing = browser_routing_config_from_env()
+ install_stale_direct_vm_auth_eviction(self._client, cache=self.browser_route_cache)
@cached_property
def deployments(self) -> DeploymentsResource:
@@ -364,28 +367,15 @@ def _prepare_options(self, options: Any) -> Any:
def _prepare_request(self, request: httpx.Request) -> None:
strip_direct_vm_auth(request, cache=self.browser_route_cache)
- def _evict_stale_direct_vm_route(self, response: httpx.Response) -> None:
- maybe_evict_browser_route_from_response(response, cache=self.browser_route_cache)
-
@override
def _should_retry(self, response: httpx.Response) -> bool:
if is_stale_direct_vm_auth_response(response):
- self._evict_stale_direct_vm_route(response)
- # The route is evicted either way; only retry when the body can be
- # rebuilt, otherwise the caller sees the original auth failure and a
- # later call goes to the control plane.
+ # The route was already evicted by the response hook; retry only when
+ # the body can be rebuilt, otherwise the caller sees the original auth
+ # failure and a later call goes to the control plane.
return should_retry_stale_direct_vm_auth(response)
return super()._should_retry(response)
- @override
- def _make_status_error_from_response(self, response: httpx.Response) -> APIStatusError:
- # `_should_retry` never runs when the request has no retries left, so this
- # is the only place a stale direct-to-VM route gets evicted before the
- # error surfaces to the caller.
- if is_stale_direct_vm_auth_response(response):
- self._evict_stale_direct_vm_route(response)
- return super()._make_status_error_from_response(response)
-
@override
def _process_response(
self,
@@ -602,6 +592,7 @@ def __init__(
)
self.browser_route_cache = _browser_route_cache or BrowserRouteCache()
self._browser_routing = browser_routing_config_from_env()
+ install_async_stale_direct_vm_auth_eviction(self._client, cache=self.browser_route_cache)
@cached_property
def deployments(self) -> AsyncDeploymentsResource:
@@ -762,28 +753,15 @@ async def _prepare_options(self, options: Any) -> Any:
async def _prepare_request(self, request: httpx.Request) -> None:
strip_direct_vm_auth(request, cache=self.browser_route_cache)
- def _evict_stale_direct_vm_route(self, response: httpx.Response) -> None:
- maybe_evict_browser_route_from_response(response, cache=self.browser_route_cache)
-
@override
def _should_retry(self, response: httpx.Response) -> bool:
if is_stale_direct_vm_auth_response(response):
- self._evict_stale_direct_vm_route(response)
- # The route is evicted either way; only retry when the body can be
- # rebuilt, otherwise the caller sees the original auth failure and a
- # later call goes to the control plane.
+ # The route was already evicted by the response hook; retry only when
+ # the body can be rebuilt, otherwise the caller sees the original auth
+ # failure and a later call goes to the control plane.
return should_retry_stale_direct_vm_auth(response)
return super()._should_retry(response)
- @override
- def _make_status_error_from_response(self, response: httpx.Response) -> APIStatusError:
- # `_should_retry` never runs when the request has no retries left, so this
- # is the only place a stale direct-to-VM route gets evicted before the
- # error surfaces to the caller.
- if is_stale_direct_vm_auth_response(response):
- self._evict_stale_direct_vm_route(response)
- return super()._make_status_error_from_response(response)
-
@override
async def _process_response(
self,
diff --git a/src/kernel/lib/browser_routing/routing.py b/src/kernel/lib/browser_routing/routing.py
index f005644f..0fbd5734 100644
--- a/src/kernel/lib/browser_routing/routing.py
+++ b/src/kernel/lib/browser_routing/routing.py
@@ -32,6 +32,9 @@ class BrowserRoutingConfig:
subresources: tuple[str, ...] = field(default_factory=tuple)
+_EVICTION_HOOK_CACHE_ATTR = "_kernel_browser_route_cache"
+
+
_BROWSER_ROUTE_CACHEABLE_PATH = re.compile(r"^/(?:v\d+/)?browsers(?:/[^/]+)?/?$")
_BROWSER_DELETE_BY_ID_PATH = re.compile(r"^/(?:v\d+/)?browsers/([^/]+)/?$")
_BROWSER_POOL_ACQUIRE_PATH = re.compile(r"^/(?:v\d+/)?browser_pools/[^/]+/acquire/?$")
@@ -198,6 +201,48 @@ def is_stale_direct_vm_auth_response(response: httpx.Response) -> bool:
return bool(response.request.url.params.get("jwt"))
+def install_stale_direct_vm_auth_eviction(client: httpx.Client, *, cache: BrowserRouteCache) -> None:
+ """Evict stale direct-to-VM routes as soon as the response status is known.
+
+ httpx reads the body of a non-streamed response inside `send()`, so a caller
+ that only inspects the returned response never learns the status of a 401/403
+ whose body read fails — the read error surfaces from `send()` instead and the
+ dead route would stay cached, wedging every later call for that session. A
+ response event hook runs after the status is known and before any body is
+ read, which keeps eviction independent of the body.
+ """
+ hooks = client.event_hooks.setdefault("response", [])
+ if _has_eviction_hook(hooks, cache):
+ return
+
+ def evict(response: httpx.Response) -> None:
+ if is_stale_direct_vm_auth_response(response):
+ maybe_evict_browser_route_from_response(response, cache=cache)
+
+ setattr(evict, _EVICTION_HOOK_CACHE_ATTR, cache)
+ hooks.append(evict)
+
+
+def install_async_stale_direct_vm_auth_eviction(client: httpx.AsyncClient, *, cache: BrowserRouteCache) -> None:
+ """Async counterpart of `install_stale_direct_vm_auth_eviction`."""
+ hooks = client.event_hooks.setdefault("response", [])
+ if _has_eviction_hook(hooks, cache):
+ return
+
+ async def evict(response: httpx.Response) -> None:
+ if is_stale_direct_vm_auth_response(response):
+ maybe_evict_browser_route_from_response(response, cache=cache)
+
+ setattr(evict, _EVICTION_HOOK_CACHE_ATTR, cache)
+ hooks.append(evict)
+
+
+def _has_eviction_hook(hooks: list[Any], cache: BrowserRouteCache) -> bool:
+ # A copied client shares both the httpx client and the route cache, so the
+ # hook is registered once per cache instead of once per client.
+ return any(getattr(hook, _EVICTION_HOOK_CACHE_ATTR, None) is cache for hook in hooks)
+
+
def should_retry_stale_direct_vm_auth(response: httpx.Response) -> bool:
"""Whether a stale direct-to-VM auth failure can be retried on the control plane.
diff --git a/tests/test_browser_routing.py b/tests/test_browser_routing.py
index 756fbcda..ed4db6ac 100644
--- a/tests/test_browser_routing.py
+++ b/tests/test_browser_routing.py
@@ -3,7 +3,7 @@
import io
import os
import asyncio
-from typing import Any, AsyncIterator, cast
+from typing import Any, Iterator, AsyncIterator, cast
from pathlib import Path
from typing_extensions import override
@@ -14,6 +14,7 @@
from kernel import (
Kernel,
AsyncKernel,
+ APIConnectionError,
AuthenticationError,
InternalServerError,
PermissionDeniedError,
@@ -1259,3 +1260,96 @@ def test_indexed_multipart_body_flattens_only_given_values() -> None:
"files[1][dest_path]": "/tmp/two",
"flag": True,
}
+
+
+class _FailingSyncStream(httpx.SyncByteStream):
+ """A response body that fails while it is being read."""
+
+ @override
+ def __iter__(self) -> Iterator[bytes]:
+ raise httpx.ReadError("connection reset while reading the error body")
+
+
+class _FailingAsyncStream(httpx.AsyncByteStream):
+ @override
+ async def __aiter__(self) -> AsyncIterator[bytes]:
+ raise httpx.ReadError("connection reset while reading the error body")
+ yield b"" # pragma: no cover - unreachable, keeps this an async generator
+
+
+def test_stale_direct_vm_jwt_evicts_route_when_error_body_read_fails(
+ monkeypatch: pytest.MonkeyPatch,
+) -> None:
+ monkeypatch.delenv("KERNEL_BROWSER_ROUTING_SUBRESOURCES", raising=False)
+ requests: list[httpx.Request] = []
+
+ def handle_request(request: httpx.Request) -> httpx.Response:
+ requests.append(request)
+ if "browser-session.test" in str(request.url):
+ return httpx.Response(401, stream=_FailingSyncStream(), headers={"content-type": "text/plain"})
+ return httpx.Response(200, content=b"png", headers={"content-type": "image/png"})
+
+ http_client = httpx.Client(transport=httpx.MockTransport(handle_request))
+ with Kernel(
+ base_url=base_url,
+ api_key=api_key,
+ max_retries=0,
+ http_client=http_client,
+ _strict_response_validation=True,
+ ) as client:
+ _cache_browser(client)
+ with pytest.raises(APIConnectionError):
+ client.browsers.computer.capture_screenshot("sess-1")
+ # The status was known before the body read failed, so the dead route is gone.
+ assert client.browser_route_cache.get("sess-1") is None
+
+ client.browsers.computer.capture_screenshot("sess-1")
+
+ assert str(requests[0].url).startswith("http://browser-session.test/browser/kernel/computer/screenshot")
+ assert requests[1].url == httpx.URL(f"{base_url}/browsers/sess-1/computer/screenshot")
+ assert requests[1].url.params.get("jwt") is None
+ assert requests[1].headers.get("Authorization") == f"Bearer {api_key}"
+
+
+@pytest.mark.asyncio
+async def test_async_stale_direct_vm_jwt_evicts_route_when_error_body_read_fails(
+ monkeypatch: pytest.MonkeyPatch,
+) -> None:
+ monkeypatch.delenv("KERNEL_BROWSER_ROUTING_SUBRESOURCES", raising=False)
+ requests: list[httpx.Request] = []
+
+ async def handle_request(request: httpx.Request) -> httpx.Response:
+ requests.append(request)
+ if "browser-session.test" in str(request.url):
+ return httpx.Response(403, stream=_FailingAsyncStream(), headers={"content-type": "text/plain"})
+ return httpx.Response(200, content=b"png", headers={"content-type": "image/png"})
+
+ http_client = httpx.AsyncClient(transport=httpx.MockTransport(handle_request))
+ async with AsyncKernel(
+ base_url=base_url,
+ api_key=api_key,
+ max_retries=0,
+ http_client=http_client,
+ _strict_response_validation=True,
+ ) as client:
+ route = browser_route_from_browser(_fake_browser())
+ assert route is not None
+ client.browser_route_cache.set(route)
+ with pytest.raises(APIConnectionError):
+ await client.browsers.computer.capture_screenshot("sess-1")
+ assert client.browser_route_cache.get("sess-1") is None
+
+ await client.browsers.computer.capture_screenshot("sess-1")
+
+ assert str(requests[0].url).startswith("http://browser-session.test/browser/kernel/computer/screenshot")
+ assert requests[1].url == httpx.URL(f"{base_url}/browsers/sess-1/computer/screenshot")
+ assert requests[1].url.params.get("jwt") is None
+ assert requests[1].headers.get("Authorization") == f"Bearer {api_key}"
+
+
+def test_copied_client_registers_one_route_eviction_hook() -> None:
+ with Kernel(base_url=base_url, api_key=api_key, _strict_response_validation=True) as client:
+ copied = client.copy(api_key="sk-456")
+ assert copied.browser_route_cache is client.browser_route_cache
+ hooks = client._client.event_hooks["response"] # pyright: ignore[reportPrivateUsage]
+ assert len(hooks) == 1
From 41ebedabdb271f775bb42f0ead4fc5daa24b4c63 Mon Sep 17 00:00:00 2001
From: tnsardesai <18272584+tnsardesai@users.noreply.github.com>
Date: Wed, 2 Sep 2026 07:15:13 +0000
Subject: [PATCH 227/366] Run the route eviction hook before caller response
hooks
A response hook registered by the caller runs in registration order, so one
that reads a failing 401/403 body or raises would skip the eviction hook
appended after it and leave the stale route cached. Prepend the hook in both
the sync and async installers; registration stays once per route cache.
---
src/kernel/lib/browser_routing/routing.py | 7 +-
tests/test_browser_routing.py | 110 ++++++++++++++++++++++
2 files changed, 114 insertions(+), 3 deletions(-)
diff --git a/src/kernel/lib/browser_routing/routing.py b/src/kernel/lib/browser_routing/routing.py
index 0fbd5734..c9120e60 100644
--- a/src/kernel/lib/browser_routing/routing.py
+++ b/src/kernel/lib/browser_routing/routing.py
@@ -209,7 +209,8 @@ def install_stale_direct_vm_auth_eviction(client: httpx.Client, *, cache: Browse
whose body read fails — the read error surfaces from `send()` instead and the
dead route would stay cached, wedging every later call for that session. A
response event hook runs after the status is known and before any body is
- read, which keeps eviction independent of the body.
+ read, which keeps eviction independent of the body. It is prepended so that a
+ caller-supplied hook cannot pre-empt it by reading a failing body or raising.
"""
hooks = client.event_hooks.setdefault("response", [])
if _has_eviction_hook(hooks, cache):
@@ -220,7 +221,7 @@ def evict(response: httpx.Response) -> None:
maybe_evict_browser_route_from_response(response, cache=cache)
setattr(evict, _EVICTION_HOOK_CACHE_ATTR, cache)
- hooks.append(evict)
+ hooks.insert(0, evict)
def install_async_stale_direct_vm_auth_eviction(client: httpx.AsyncClient, *, cache: BrowserRouteCache) -> None:
@@ -234,7 +235,7 @@ async def evict(response: httpx.Response) -> None:
maybe_evict_browser_route_from_response(response, cache=cache)
setattr(evict, _EVICTION_HOOK_CACHE_ATTR, cache)
- hooks.append(evict)
+ hooks.insert(0, evict)
def _has_eviction_hook(hooks: list[Any], cache: BrowserRouteCache) -> bool:
diff --git a/tests/test_browser_routing.py b/tests/test_browser_routing.py
index ed4db6ac..18cd9a20 100644
--- a/tests/test_browser_routing.py
+++ b/tests/test_browser_routing.py
@@ -1353,3 +1353,113 @@ def test_copied_client_registers_one_route_eviction_hook() -> None:
assert copied.browser_route_cache is client.browser_route_cache
hooks = client._client.event_hooks["response"] # pyright: ignore[reportPrivateUsage]
assert len(hooks) == 1
+
+
+def test_route_eviction_hook_runs_before_caller_response_hooks(
+ monkeypatch: pytest.MonkeyPatch,
+) -> None:
+ monkeypatch.delenv("KERNEL_BROWSER_ROUTING_SUBRESOURCES", raising=False)
+ requests: list[httpx.Request] = []
+ caller_hook_statuses: list[int] = []
+
+ def handle_request(request: httpx.Request) -> httpx.Response:
+ requests.append(request)
+ if "browser-session.test" in str(request.url):
+ return httpx.Response(401, stream=_FailingSyncStream(), headers={"content-type": "text/plain"})
+ return httpx.Response(200, content=b"png", headers={"content-type": "image/png"})
+
+ def caller_hook(response: httpx.Response) -> None:
+ caller_hook_statuses.append(response.status_code)
+ if response.status_code in {401, 403}:
+ # Reading a failing body raises out of the hook chain, which would
+ # skip any eviction hook registered after this one.
+ response.read()
+
+ http_client = httpx.Client(
+ transport=httpx.MockTransport(handle_request),
+ event_hooks={"response": [caller_hook]},
+ )
+ with Kernel(
+ base_url=base_url,
+ api_key=api_key,
+ max_retries=0,
+ http_client=http_client,
+ _strict_response_validation=True,
+ ) as client:
+ _cache_browser(client)
+ assert http_client.event_hooks["response"][-1] is caller_hook
+ with pytest.raises(APIConnectionError):
+ client.browsers.computer.capture_screenshot("sess-1")
+ assert caller_hook_statuses == [401]
+ assert client.browser_route_cache.get("sess-1") is None
+
+ client.browsers.computer.capture_screenshot("sess-1")
+
+ assert str(requests[0].url).startswith("http://browser-session.test/browser/kernel/computer/screenshot")
+ assert requests[1].url == httpx.URL(f"{base_url}/browsers/sess-1/computer/screenshot")
+ assert requests[1].url.params.get("jwt") is None
+ assert requests[1].headers.get("Authorization") == f"Bearer {api_key}"
+
+
+@pytest.mark.asyncio
+async def test_async_route_eviction_hook_runs_before_caller_response_hooks(
+ monkeypatch: pytest.MonkeyPatch,
+) -> None:
+ monkeypatch.delenv("KERNEL_BROWSER_ROUTING_SUBRESOURCES", raising=False)
+ requests: list[httpx.Request] = []
+ caller_hook_statuses: list[int] = []
+
+ async def handle_request(request: httpx.Request) -> httpx.Response:
+ requests.append(request)
+ if "browser-session.test" in str(request.url):
+ return httpx.Response(403, stream=_FailingAsyncStream(), headers={"content-type": "text/plain"})
+ return httpx.Response(200, content=b"png", headers={"content-type": "image/png"})
+
+ async def caller_hook(response: httpx.Response) -> None:
+ caller_hook_statuses.append(response.status_code)
+ if response.status_code in {401, 403}:
+ await response.aread()
+
+ http_client = httpx.AsyncClient(
+ transport=httpx.MockTransport(handle_request),
+ event_hooks={"response": [caller_hook]},
+ )
+ async with AsyncKernel(
+ base_url=base_url,
+ api_key=api_key,
+ max_retries=0,
+ http_client=http_client,
+ _strict_response_validation=True,
+ ) as client:
+ route = browser_route_from_browser(_fake_browser())
+ assert route is not None
+ client.browser_route_cache.set(route)
+ assert http_client.event_hooks["response"][-1] is caller_hook
+ with pytest.raises(APIConnectionError):
+ await client.browsers.computer.capture_screenshot("sess-1")
+ assert caller_hook_statuses == [403]
+ assert client.browser_route_cache.get("sess-1") is None
+
+ await client.browsers.computer.capture_screenshot("sess-1")
+
+ assert str(requests[0].url).startswith("http://browser-session.test/browser/kernel/computer/screenshot")
+ assert requests[1].url == httpx.URL(f"{base_url}/browsers/sess-1/computer/screenshot")
+ assert requests[1].url.params.get("jwt") is None
+ assert requests[1].headers.get("Authorization") == f"Bearer {api_key}"
+
+
+def test_route_eviction_hook_is_registered_once_before_caller_hooks() -> None:
+ def caller_hook(_response: httpx.Response) -> None: # pragma: no cover - never invoked
+ return None
+
+ http_client = httpx.Client(event_hooks={"response": [caller_hook]})
+ with Kernel(
+ base_url=base_url,
+ api_key=api_key,
+ http_client=http_client,
+ _strict_response_validation=True,
+ ) as client:
+ client.copy(api_key="sk-456")
+ hooks = http_client.event_hooks["response"]
+ assert len(hooks) == 2
+ assert hooks[1] is caller_hook
From 9ab1d583d7bb4dabba5a640883dda9f2ac248f18 Mon Sep 17 00:00:00 2001
From: "kernel-internal[bot]"
<260533166+kernel-internal[bot]@users.noreply.github.com>
Date: Thu, 3 Sep 2026 14:23:55 +0000
Subject: [PATCH 228/366] feat: Expose OTLP destination delivery health
Stainless-Generated-From: ae612ef5de5a9136bfc5f8cac5c6990b9d82e233
---
.../types/telemetry/otlp_destination.py | 25 +++++++++++++++++++
1 file changed, 25 insertions(+)
diff --git a/src/kernel/types/telemetry/otlp_destination.py b/src/kernel/types/telemetry/otlp_destination.py
index 0afe6ae8..293c2497 100644
--- a/src/kernel/types/telemetry/otlp_destination.py
+++ b/src/kernel/types/telemetry/otlp_destination.py
@@ -16,6 +16,13 @@ class OtlpDestination(BaseModel):
id: str
+ consecutive_failures: int
+ """
+ Failed deliveries since the last success, as observed by the relay process that
+ wrote the latest outcome. Zero means the most recently recorded outcome
+ succeeded.
+ """
+
created_at: datetime
endpoint: str
@@ -39,3 +46,21 @@ class OtlpDestination(BaseModel):
updated_at: datetime
description: Optional[str] = None
+
+ last_error: Optional[str] = None
+ """Sanitized class of the delivery failure recorded at `last_error_at`.
+
+ It is retained after a later success, so its presence does not mean the
+ destination is currently failing. Response bodies, endpoint URLs, credentials,
+ and raw transport errors are never returned.
+ """
+
+ last_error_at: Optional[datetime] = None
+ """Timestamp of the most recent failed delivery.
+
+ It is retained after a later success, so it can predate `last_export_at`. Read
+ `consecutive_failures` to tell whether the destination is currently failing.
+ """
+
+ last_export_at: Optional[datetime] = None
+ """Timestamp of the most recent successful delivery. Moves only on success."""
From fe23cc791631b414e68b83ebf9f0951ea379f9fc Mon Sep 17 00:00:00 2001
From: tnsardesai <18272584+tnsardesai@users.noreply.github.com>
Date: Fri, 4 Sep 2026 18:05:52 +0000
Subject: [PATCH 229/366] Align routing tests with filesystem responses
---
src/kernel/lib/browser_routing/routing.py | 6 ++++--
tests/test_browser_routing.py | 20 ++++++++++----------
2 files changed, 14 insertions(+), 12 deletions(-)
diff --git a/src/kernel/lib/browser_routing/routing.py b/src/kernel/lib/browser_routing/routing.py
index c9120e60..df5f6567 100644
--- a/src/kernel/lib/browser_routing/routing.py
+++ b/src/kernel/lib/browser_routing/routing.py
@@ -209,8 +209,10 @@ def install_stale_direct_vm_auth_eviction(client: httpx.Client, *, cache: Browse
whose body read fails — the read error surfaces from `send()` instead and the
dead route would stay cached, wedging every later call for that session. A
response event hook runs after the status is known and before any body is
- read, which keeps eviction independent of the body. It is prepended so that a
- caller-supplied hook cannot pre-empt it by reading a failing body or raising.
+ read, which keeps eviction independent of the body. For a caller-supplied
+ `http_client`, the hook is installed into that client's `event_hooks` and
+ prepended so an existing hook cannot pre-empt eviction by reading a failing
+ body or raising.
"""
hooks = client.event_hooks.setdefault("response", [])
if _has_eviction_hook(hooks, cache):
diff --git a/tests/test_browser_routing.py b/tests/test_browser_routing.py
index 18cd9a20..2ab6ad2a 100644
--- a/tests/test_browser_routing.py
+++ b/tests/test_browser_routing.py
@@ -751,7 +751,7 @@ def test_fs_read_file_routes_binary_response_from_vm(monkeypatch: pytest.MonkeyP
def test_fs_write_file_routes_binary_body_to_vm(monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.delenv("KERNEL_BROWSER_ROUTING_SUBRESOURCES", raising=False)
write_file = respx.put("http://browser-session.test/browser/kernel/fs/write_file").mock(
- return_value=httpx.Response(204)
+ return_value=httpx.Response(201)
)
with Kernel(base_url=base_url, api_key=api_key, _strict_response_validation=True) as client:
_cache_browser(client)
@@ -769,7 +769,7 @@ def test_fs_write_file_routes_binary_body_to_vm(monkeypatch: pytest.MonkeyPatch)
@respx.mock
def test_fs_upload_routes_indexed_multipart_to_vm(monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.delenv("KERNEL_BROWSER_ROUTING_SUBRESOURCES", raising=False)
- upload = respx.post("http://browser-session.test/browser/kernel/fs/upload").mock(return_value=httpx.Response(204))
+ upload = respx.post("http://browser-session.test/browser/kernel/fs/upload").mock(return_value=httpx.Response(201))
with Kernel(base_url=base_url, api_key=api_key, _strict_response_validation=True) as client:
_cache_browser(client)
client.browsers.fs.upload(
@@ -905,7 +905,7 @@ def test_stale_direct_vm_jwt_replays_buffered_fs_body_on_control_plane(
vm = respx.put("http://browser-session.test/browser/kernel/fs/write_file").mock(
return_value=httpx.Response(401, text="Invalid JWT")
)
- api = respx.put(f"{base_url}/browsers/sess-1/fs/write_file").mock(return_value=httpx.Response(204))
+ api = respx.put(f"{base_url}/browsers/sess-1/fs/write_file").mock(return_value=httpx.Response(201))
with Kernel(base_url=base_url, api_key=api_key, _strict_response_validation=True) as client:
_cache_browser(client)
client.browsers.fs.write_file("sess-1", b"payload", path="/tmp/x")
@@ -928,7 +928,7 @@ def test_stale_direct_vm_jwt_does_not_replay_streamed_fs_body(
vm = respx.put("http://browser-session.test/browser/kernel/fs/write_file").mock(
return_value=httpx.Response(401, text="Invalid JWT")
)
- api = respx.put(f"{base_url}/browsers/sess-1/fs/write_file").mock(return_value=httpx.Response(204))
+ api = respx.put(f"{base_url}/browsers/sess-1/fs/write_file").mock(return_value=httpx.Response(201))
with Kernel(base_url=base_url, api_key=api_key, _strict_response_validation=True) as client:
_cache_browser(client)
with pytest.raises(AuthenticationError):
@@ -950,7 +950,7 @@ async def test_async_stale_direct_vm_jwt_does_not_replay_streamed_fs_body(
vm = respx.put("http://browser-session.test/browser/kernel/fs/write_file").mock(
return_value=httpx.Response(401, text="Invalid JWT")
)
- api = respx.put(f"{base_url}/browsers/sess-1/fs/write_file").mock(return_value=httpx.Response(204))
+ api = respx.put(f"{base_url}/browsers/sess-1/fs/write_file").mock(return_value=httpx.Response(201))
async def _chunks() -> AsyncIterator[bytes]:
yield b"chunk-one"
@@ -978,7 +978,7 @@ def test_stale_direct_vm_jwt_replays_multipart_upload_on_control_plane(
vm = respx.post("http://browser-session.test/browser/kernel/fs/upload").mock(
return_value=httpx.Response(403, text="Invalid JWT")
)
- api = respx.post(f"{base_url}/browsers/sess-1/fs/upload").mock(return_value=httpx.Response(204))
+ api = respx.post(f"{base_url}/browsers/sess-1/fs/upload").mock(return_value=httpx.Response(201))
upload = tmp_path / "one.txt"
upload.write_bytes(b"file-bytes")
@@ -1094,7 +1094,7 @@ def test_stale_direct_vm_jwt_does_not_replay_multipart_that_cannot_rewind(
vm = respx.post("http://browser-session.test/browser/kernel/fs/upload").mock(
return_value=httpx.Response(401, text="Invalid JWT")
)
- api = respx.post(f"{base_url}/browsers/sess-1/fs/upload").mock(return_value=httpx.Response(204))
+ api = respx.post(f"{base_url}/browsers/sess-1/fs/upload").mock(return_value=httpx.Response(201))
with Kernel(base_url=base_url, api_key=api_key, _strict_response_validation=True) as client:
_cache_browser(client)
with pytest.raises(AuthenticationError):
@@ -1123,7 +1123,7 @@ async def test_async_stale_direct_vm_jwt_does_not_replay_multipart_that_cannot_r
vm = respx.post("http://browser-session.test/browser/kernel/fs/upload").mock(
return_value=httpx.Response(403, text="Invalid JWT")
)
- api = respx.post(f"{base_url}/browsers/sess-1/fs/upload").mock(return_value=httpx.Response(204))
+ api = respx.post(f"{base_url}/browsers/sess-1/fs/upload").mock(return_value=httpx.Response(201))
async with AsyncKernel(base_url=base_url, api_key=api_key, _strict_response_validation=True) as client:
route = browser_route_from_browser(_fake_browser())
assert route is not None
@@ -1152,7 +1152,7 @@ def test_stale_direct_vm_jwt_evicts_route_without_retries_for_buffered_body(
vm = respx.put("http://browser-session.test/browser/kernel/fs/write_file").mock(
return_value=httpx.Response(401, text="Invalid JWT")
)
- api = respx.put(f"{base_url}/browsers/sess-1/fs/write_file").mock(return_value=httpx.Response(204))
+ api = respx.put(f"{base_url}/browsers/sess-1/fs/write_file").mock(return_value=httpx.Response(201))
with Kernel(base_url=base_url, api_key=api_key, max_retries=0, _strict_response_validation=True) as client:
_cache_browser(client)
with pytest.raises(AuthenticationError):
@@ -1178,7 +1178,7 @@ async def test_async_stale_direct_vm_jwt_evicts_route_without_retries_for_stream
vm = respx.put("http://browser-session.test/browser/kernel/fs/write_file").mock(
return_value=httpx.Response(403, text="Invalid JWT")
)
- api = respx.put(f"{base_url}/browsers/sess-1/fs/write_file").mock(return_value=httpx.Response(204))
+ api = respx.put(f"{base_url}/browsers/sess-1/fs/write_file").mock(return_value=httpx.Response(201))
async def _chunks() -> AsyncIterator[bytes]:
yield b"chunk-one"
From 645ce00d68172a323d11b7e4dd4f3997cc287fe0 Mon Sep 17 00:00:00 2001
From: "kernel-internal[bot]"
<260533166+kernel-internal[bot]@users.noreply.github.com>
Date: Fri, 4 Sep 2026 18:18:47 +0000
Subject: [PATCH 230/366] feat: Repair Vault SDK custom-code seals
Stainless-Generated-From: d9abf430b03daa1f5c6058aef478bc7b4813b17d
---
.stats.yml | 2 +-
api.md | 47 +
src/kernel/_client.py | 38 +
src/kernel/resources/__init__.py | 14 +
src/kernel/resources/browsers/browsers.py | 11 +
.../config_registry/config_registry.py | 16 +-
src/kernel/resources/vaults/__init__.py | 33 +
src/kernel/resources/vaults/items.py | 928 +++++++++++
src/kernel/resources/vaults/vaults.py | 456 ++++++
src/kernel/types/__init__.py | 5 +
src/kernel/types/browser_create_params.py | 7 +
src/kernel/types/browser_create_response.py | 12 +-
src/kernel/types/browser_list_response.py | 12 +-
.../types/browser_pool_acquire_response.py | 12 +-
src/kernel/types/browser_retrieve_response.py | 12 +-
src/kernel/types/browser_update_response.py | 12 +-
.../types/config_registry_list_params.py | 6 +-
.../invocation_list_browsers_response.py | 10 +
src/kernel/types/recommendation_summary.py | 21 +-
src/kernel/types/vault.py | 18 +
src/kernel/types/vault_list_params.py | 13 +
src/kernel/types/vault_reference.py | 15 +
src/kernel/types/vault_reference_param.py | 15 +
src/kernel/types/vault_upsert_params.py | 12 +
src/kernel/types/vaults/__init__.py | 23 +
.../agentcard_checkout_authorization.py | 66 +
.../types/vaults/card_vault_item_spec.py | 125 ++
.../vaults/card_vault_item_spec_param.py | 122 ++
.../types/vaults/card_vault_item_state.py | 86 +
src/kernel/types/vaults/item_events_params.py | 17 +
.../types/vaults/item_events_response.py | 10 +
src/kernel/types/vaults/item_list_response.py | 10 +
.../vaults/item_perform_operation_params.py | 13 +
.../types/vaults/item_retrieve_params.py | 21 +
src/kernel/types/vaults/item_update_params.py | 20 +
src/kernel/types/vaults/item_upsert_params.py | 41 +
src/kernel/types/vaults/vault_card_aliases.py | 15 +
src/kernel/types/vaults/vault_item.py | 140 ++
src/kernel/types/vaults/vault_item_action.py | 66 +
src/kernel/types/vaults/vault_item_event.py | 21 +
.../types/vaults/vault_payment_method.py | 51 +
.../types/vaults/wallet_vault_item_spec.py | 47 +
.../vaults/wallet_vault_item_spec_param.py | 44 +
.../types/vaults/wallet_vault_item_state.py | 33 +
tests/api_resources/test_browsers.py | 12 +
tests/api_resources/test_vaults.py | 335 ++++
tests/api_resources/vaults/__init__.py | 1 +
tests/api_resources/vaults/test_items.py | 1432 +++++++++++++++++
48 files changed, 4455 insertions(+), 23 deletions(-)
create mode 100644 src/kernel/resources/vaults/__init__.py
create mode 100644 src/kernel/resources/vaults/items.py
create mode 100644 src/kernel/resources/vaults/vaults.py
create mode 100644 src/kernel/types/vault.py
create mode 100644 src/kernel/types/vault_list_params.py
create mode 100644 src/kernel/types/vault_reference.py
create mode 100644 src/kernel/types/vault_reference_param.py
create mode 100644 src/kernel/types/vault_upsert_params.py
create mode 100644 src/kernel/types/vaults/__init__.py
create mode 100644 src/kernel/types/vaults/agentcard_checkout_authorization.py
create mode 100644 src/kernel/types/vaults/card_vault_item_spec.py
create mode 100644 src/kernel/types/vaults/card_vault_item_spec_param.py
create mode 100644 src/kernel/types/vaults/card_vault_item_state.py
create mode 100644 src/kernel/types/vaults/item_events_params.py
create mode 100644 src/kernel/types/vaults/item_events_response.py
create mode 100644 src/kernel/types/vaults/item_list_response.py
create mode 100644 src/kernel/types/vaults/item_perform_operation_params.py
create mode 100644 src/kernel/types/vaults/item_retrieve_params.py
create mode 100644 src/kernel/types/vaults/item_update_params.py
create mode 100644 src/kernel/types/vaults/item_upsert_params.py
create mode 100644 src/kernel/types/vaults/vault_card_aliases.py
create mode 100644 src/kernel/types/vaults/vault_item.py
create mode 100644 src/kernel/types/vaults/vault_item_action.py
create mode 100644 src/kernel/types/vaults/vault_item_event.py
create mode 100644 src/kernel/types/vaults/vault_payment_method.py
create mode 100644 src/kernel/types/vaults/wallet_vault_item_spec.py
create mode 100644 src/kernel/types/vaults/wallet_vault_item_spec_param.py
create mode 100644 src/kernel/types/vaults/wallet_vault_item_state.py
create mode 100644 tests/api_resources/test_vaults.py
create mode 100644 tests/api_resources/vaults/__init__.py
create mode 100644 tests/api_resources/vaults/test_items.py
diff --git a/.stats.yml b/.stats.yml
index a867372d..5fe5772a 100644
--- a/.stats.yml
+++ b/.stats.yml
@@ -1 +1 @@
-configured_endpoints: 147
+configured_endpoints: 158
diff --git a/api.md b/api.md
index 4a09a5d9..797c5d9c 100644
--- a/api.md
+++ b/api.md
@@ -126,6 +126,7 @@ from kernel.types import (
BrowserUsage,
Profile,
Tags,
+ VaultReference,
BrowserCreateResponse,
BrowserRetrieveResponse,
BrowserUpdateResponse,
@@ -486,6 +487,52 @@ Methods:
- client.browser_pools.flush(id_or_name) -> None
- client.browser_pools.release(id_or_name, \*\*params) -> None
+# Vaults
+
+Types:
+
+```python
+from kernel.types import Vault
+```
+
+Methods:
+
+- client.vaults.retrieve(id_or_name) -> Vault
+- client.vaults.list(\*\*params) -> SyncOffsetPagination[Vault]
+- client.vaults.delete(id_or_name) -> None
+- client.vaults.upsert(\*\*params) -> Vault
+
+## Items
+
+Types:
+
+```python
+from kernel.types.vaults import (
+ AgentcardCheckoutAuthorization,
+ CardVaultItemSpec,
+ CardVaultItemState,
+ VaultCardAliases,
+ VaultItem,
+ VaultItemAction,
+ VaultItemEvent,
+ VaultPaymentMethod,
+ WalletVaultItemSpec,
+ WalletVaultItemState,
+ ItemListResponse,
+ ItemEventsResponse,
+)
+```
+
+Methods:
+
+- client.vaults.items.retrieve(key, \*, id_or_name, \*\*params) -> VaultItem
+- client.vaults.items.update(key, \*, id_or_name, \*\*params) -> VaultItem
+- client.vaults.items.list(id_or_name) -> ItemListResponse
+- client.vaults.items.delete(key, \*, id_or_name) -> None
+- client.vaults.items.events(key, \*, id_or_name, \*\*params) -> ItemEventsResponse
+- client.vaults.items.perform_operation(key, \*, id_or_name, \*\*params) -> VaultItem
+- client.vaults.items.upsert(key, \*, id_or_name, \*\*params) -> VaultItem
+
# Credentials
Types:
diff --git a/src/kernel/_client.py b/src/kernel/_client.py
index 2c06cb04..acdb7357 100644
--- a/src/kernel/_client.py
+++ b/src/kernel/_client.py
@@ -50,6 +50,7 @@
from .resources import (
apps,
auth,
+ vaults,
proxies,
api_keys,
browsers,
@@ -76,6 +77,7 @@
from .resources.deployments import DeploymentsResource, AsyncDeploymentsResource
from .resources.invocations import InvocationsResource, AsyncInvocationsResource
from .resources.browser_pools import BrowserPoolsResource, AsyncBrowserPoolsResource
+ from .resources.vaults.vaults import VaultsResource, AsyncVaultsResource
from .resources.browsers.browsers import BrowsersResource, AsyncBrowsersResource
from .resources.projects.projects import ProjectsResource, AsyncProjectsResource
from .resources.telemetry.telemetry import TelemetryResource, AsyncTelemetryResource
@@ -279,6 +281,12 @@ def browser_pools(self) -> BrowserPoolsResource:
return BrowserPoolsResource(self)
+ @cached_property
+ def vaults(self) -> VaultsResource:
+ from .resources.vaults import VaultsResource
+
+ return VaultsResource(self)
+
@cached_property
def credentials(self) -> CredentialsResource:
"""Create and manage credentials for authentication."""
@@ -662,6 +670,12 @@ def browser_pools(self) -> AsyncBrowserPoolsResource:
return AsyncBrowserPoolsResource(self)
+ @cached_property
+ def vaults(self) -> AsyncVaultsResource:
+ from .resources.vaults import AsyncVaultsResource
+
+ return AsyncVaultsResource(self)
+
@cached_property
def credentials(self) -> AsyncCredentialsResource:
"""Create and manage credentials for authentication."""
@@ -949,6 +963,12 @@ def browser_pools(self) -> browser_pools.BrowserPoolsResourceWithRawResponse:
return BrowserPoolsResourceWithRawResponse(self._client.browser_pools)
+ @cached_property
+ def vaults(self) -> vaults.VaultsResourceWithRawResponse:
+ from .resources.vaults import VaultsResourceWithRawResponse
+
+ return VaultsResourceWithRawResponse(self._client.vaults)
+
@cached_property
def credentials(self) -> credentials.CredentialsResourceWithRawResponse:
"""Create and manage credentials for authentication."""
@@ -1076,6 +1096,12 @@ def browser_pools(self) -> browser_pools.AsyncBrowserPoolsResourceWithRawRespons
return AsyncBrowserPoolsResourceWithRawResponse(self._client.browser_pools)
+ @cached_property
+ def vaults(self) -> vaults.AsyncVaultsResourceWithRawResponse:
+ from .resources.vaults import AsyncVaultsResourceWithRawResponse
+
+ return AsyncVaultsResourceWithRawResponse(self._client.vaults)
+
@cached_property
def credentials(self) -> credentials.AsyncCredentialsResourceWithRawResponse:
"""Create and manage credentials for authentication."""
@@ -1203,6 +1229,12 @@ def browser_pools(self) -> browser_pools.BrowserPoolsResourceWithStreamingRespon
return BrowserPoolsResourceWithStreamingResponse(self._client.browser_pools)
+ @cached_property
+ def vaults(self) -> vaults.VaultsResourceWithStreamingResponse:
+ from .resources.vaults import VaultsResourceWithStreamingResponse
+
+ return VaultsResourceWithStreamingResponse(self._client.vaults)
+
@cached_property
def credentials(self) -> credentials.CredentialsResourceWithStreamingResponse:
"""Create and manage credentials for authentication."""
@@ -1330,6 +1362,12 @@ def browser_pools(self) -> browser_pools.AsyncBrowserPoolsResourceWithStreamingR
return AsyncBrowserPoolsResourceWithStreamingResponse(self._client.browser_pools)
+ @cached_property
+ def vaults(self) -> vaults.AsyncVaultsResourceWithStreamingResponse:
+ from .resources.vaults import AsyncVaultsResourceWithStreamingResponse
+
+ return AsyncVaultsResourceWithStreamingResponse(self._client.vaults)
+
@cached_property
def credentials(self) -> credentials.AsyncCredentialsResourceWithStreamingResponse:
"""Create and manage credentials for authentication."""
diff --git a/src/kernel/resources/__init__.py b/src/kernel/resources/__init__.py
index c6496534..5bc0849c 100644
--- a/src/kernel/resources/__init__.py
+++ b/src/kernel/resources/__init__.py
@@ -16,6 +16,14 @@
AuthResourceWithStreamingResponse,
AsyncAuthResourceWithStreamingResponse,
)
+from .vaults import (
+ VaultsResource,
+ AsyncVaultsResource,
+ VaultsResourceWithRawResponse,
+ AsyncVaultsResourceWithRawResponse,
+ VaultsResourceWithStreamingResponse,
+ AsyncVaultsResourceWithStreamingResponse,
+)
from .proxies import (
ProxiesResource,
AsyncProxiesResource,
@@ -204,6 +212,12 @@
"AsyncBrowserPoolsResourceWithRawResponse",
"BrowserPoolsResourceWithStreamingResponse",
"AsyncBrowserPoolsResourceWithStreamingResponse",
+ "VaultsResource",
+ "AsyncVaultsResource",
+ "VaultsResourceWithRawResponse",
+ "AsyncVaultsResourceWithRawResponse",
+ "VaultsResourceWithStreamingResponse",
+ "AsyncVaultsResourceWithStreamingResponse",
"CredentialsResource",
"AsyncCredentialsResource",
"CredentialsResourceWithRawResponse",
diff --git a/src/kernel/resources/browsers/browsers.py b/src/kernel/resources/browsers/browsers.py
index d703df24..219dd462 100644
--- a/src/kernel/resources/browsers/browsers.py
+++ b/src/kernel/resources/browsers/browsers.py
@@ -97,6 +97,7 @@
from ...types.tags_param import TagsParam
from ...types.browser_curl_response import BrowserCurlResponse
from ...types.browser_list_response import BrowserListResponse
+from ...types.vault_reference_param import VaultReferenceParam
from ...lib.browser_routing.raw_http import (
stream_via_browser_route,
request_via_browser_route,
@@ -201,6 +202,7 @@ def create(
tags: TagsParam | Omit = omit,
telemetry: Optional[browser_create_params.Telemetry] | Omit = omit,
timeout_seconds: int | Omit = omit,
+ vaults: Iterable[VaultReferenceParam] | Omit = omit,
viewport: BrowserViewport | 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.
@@ -284,6 +286,9 @@ def create(
check for inactivity every 5 seconds, so the actual timeout behavior you will
see is +/- 5 seconds around the specified value.
+ vaults: Project-scoped vaults to link to the browser session. Links are immutable after
+ creation.
+
viewport: 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.
@@ -327,6 +332,7 @@ def create(
"tags": tags,
"telemetry": telemetry,
"timeout_seconds": timeout_seconds,
+ "vaults": vaults,
"viewport": viewport,
},
browser_create_params.BrowserCreateParams,
@@ -826,6 +832,7 @@ async def create(
tags: TagsParam | Omit = omit,
telemetry: Optional[browser_create_params.Telemetry] | Omit = omit,
timeout_seconds: int | Omit = omit,
+ vaults: Iterable[VaultReferenceParam] | Omit = omit,
viewport: BrowserViewport | 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.
@@ -909,6 +916,9 @@ async def create(
check for inactivity every 5 seconds, so the actual timeout behavior you will
see is +/- 5 seconds around the specified value.
+ vaults: Project-scoped vaults to link to the browser session. Links are immutable after
+ creation.
+
viewport: 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.
@@ -952,6 +962,7 @@ async def create(
"tags": tags,
"telemetry": telemetry,
"timeout_seconds": timeout_seconds,
+ "vaults": vaults,
"viewport": viewport,
},
browser_create_params.BrowserCreateParams,
diff --git a/src/kernel/resources/config_registry/config_registry.py b/src/kernel/resources/config_registry/config_registry.py
index e5aa40d6..0b9d1824 100644
--- a/src/kernel/resources/config_registry/config_registry.py
+++ b/src/kernel/resources/config_registry/config_registry.py
@@ -78,12 +78,12 @@ def list(
timeout: float | httpx.Timeout | None | NotGiven = not_given,
) -> SyncOffsetPagination[RecommendationSummary]:
"""
- Lists unique domains previously analyzed by the selected project with their
- current domain-level recommendations.
+ Lists unique exact targets previously analyzed by the selected project with the
+ recommendation produced by each target's latest analysis.
Args:
- search: Case-insensitive domain search. Full URLs are reduced to their registrable
- domain.
+ search: Case-insensitive substring search over normalized targets, including domain,
+ subdomain, and path.
extra_headers: Send extra headers
@@ -252,12 +252,12 @@ def list(
timeout: float | httpx.Timeout | None | NotGiven = not_given,
) -> AsyncPaginator[RecommendationSummary, AsyncOffsetPagination[RecommendationSummary]]:
"""
- Lists unique domains previously analyzed by the selected project with their
- current domain-level recommendations.
+ Lists unique exact targets previously analyzed by the selected project with the
+ recommendation produced by each target's latest analysis.
Args:
- search: Case-insensitive domain search. Full URLs are reduced to their registrable
- domain.
+ search: Case-insensitive substring search over normalized targets, including domain,
+ subdomain, and path.
extra_headers: Send extra headers
diff --git a/src/kernel/resources/vaults/__init__.py b/src/kernel/resources/vaults/__init__.py
new file mode 100644
index 00000000..a019b7ee
--- /dev/null
+++ b/src/kernel/resources/vaults/__init__.py
@@ -0,0 +1,33 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from .items import (
+ ItemsResource,
+ AsyncItemsResource,
+ ItemsResourceWithRawResponse,
+ AsyncItemsResourceWithRawResponse,
+ ItemsResourceWithStreamingResponse,
+ AsyncItemsResourceWithStreamingResponse,
+)
+from .vaults import (
+ VaultsResource,
+ AsyncVaultsResource,
+ VaultsResourceWithRawResponse,
+ AsyncVaultsResourceWithRawResponse,
+ VaultsResourceWithStreamingResponse,
+ AsyncVaultsResourceWithStreamingResponse,
+)
+
+__all__ = [
+ "ItemsResource",
+ "AsyncItemsResource",
+ "ItemsResourceWithRawResponse",
+ "AsyncItemsResourceWithRawResponse",
+ "ItemsResourceWithStreamingResponse",
+ "AsyncItemsResourceWithStreamingResponse",
+ "VaultsResource",
+ "AsyncVaultsResource",
+ "VaultsResourceWithRawResponse",
+ "AsyncVaultsResourceWithRawResponse",
+ "VaultsResourceWithStreamingResponse",
+ "AsyncVaultsResourceWithStreamingResponse",
+]
diff --git a/src/kernel/resources/vaults/items.py b/src/kernel/resources/vaults/items.py
new file mode 100644
index 00000000..17bef3c6
--- /dev/null
+++ b/src/kernel/resources/vaults/items.py
@@ -0,0 +1,928 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from __future__ import annotations
+
+from typing import Any, List, cast
+from typing_extensions import Literal, overload
+
+import httpx
+
+from ..._types import Body, Omit, Query, Headers, NoneType, NotGiven, omit, not_given
+from ..._utils import path_template, required_args, maybe_transform, async_maybe_transform
+from ..._compat import cached_property
+from ..._resource import SyncAPIResource, AsyncAPIResource
+from ..._response import (
+ to_raw_response_wrapper,
+ to_streamed_response_wrapper,
+ async_to_raw_response_wrapper,
+ async_to_streamed_response_wrapper,
+)
+from ..._base_client import make_request_options
+from ...types.vaults import (
+ item_events_params,
+ item_update_params,
+ item_upsert_params,
+ item_retrieve_params,
+ item_perform_operation_params,
+)
+from ...types.vaults.vault_item import VaultItem
+from ...types.vaults.item_list_response import ItemListResponse
+from ...types.vaults.item_events_response import ItemEventsResponse
+from ...types.vaults.card_vault_item_spec_param import CardVaultItemSpecParam
+from ...types.vaults.wallet_vault_item_spec_param import WalletVaultItemSpecParam
+
+__all__ = ["ItemsResource", "AsyncItemsResource"]
+
+
+class ItemsResource(SyncAPIResource):
+ @cached_property
+ def with_raw_response(self) -> ItemsResourceWithRawResponse:
+ """
+ This property can be used as a prefix for any HTTP method call to return
+ the raw response object instead of the parsed content.
+
+ For more information, see https://www.github.com/kernel/kernel-python-sdk#accessing-raw-response-data-eg-headers
+ """
+ return ItemsResourceWithRawResponse(self)
+
+ @cached_property
+ def with_streaming_response(self) -> ItemsResourceWithStreamingResponse:
+ """
+ An alternative to `.with_raw_response` that doesn't eagerly read the response body.
+
+ For more information, see https://www.github.com/kernel/kernel-python-sdk#with_streaming_response
+ """
+ return ItemsResourceWithStreamingResponse(self)
+
+ def retrieve(
+ self,
+ key: str,
+ *,
+ id_or_name: str,
+ expand: List[Literal["payment_methods"]] | Omit = omit,
+ wait: int | Omit = omit,
+ # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
+ # The extra values given here take precedence over values defined on the client or passed to this method.
+ extra_headers: Headers | None = None,
+ extra_query: Query | None = None,
+ extra_body: Body | None = None,
+ timeout: float | httpx.Timeout | None | NotGiven = not_given,
+ ) -> VaultItem:
+ """
+ The response advertises operations that are valid in the item's current state
+ and live data that can be requested through `expand`. Read each operation's
+ description before using it. Expanded data is fetched from the provider and is
+ not persisted in the vault item. Requesting an unavailable expansion returns 409
+ instead of a partial item.
+
+ Args:
+ expand: Live fields advertised by `available_expansions` to include in `expanded`.
+
+ wait: Hold for up to this many seconds while the item is pending authorization or
+ approval.
+
+ extra_headers: Send extra headers
+
+ extra_query: Add additional query parameters to the request
+
+ extra_body: Add additional JSON properties to the request
+
+ timeout: Override the client-level default timeout for this request, in seconds
+ """
+ if not id_or_name:
+ raise ValueError(f"Expected a non-empty value for `id_or_name` but received {id_or_name!r}")
+ if not key:
+ raise ValueError(f"Expected a non-empty value for `key` but received {key!r}")
+ return cast(
+ VaultItem,
+ self._get(
+ path_template("/vaults/{id_or_name}/items/{key}", id_or_name=id_or_name, key=key),
+ options=make_request_options(
+ extra_headers=extra_headers,
+ extra_query=extra_query,
+ extra_body=extra_body,
+ timeout=timeout,
+ query=maybe_transform(
+ {
+ "expand": expand,
+ "wait": wait,
+ },
+ item_retrieve_params.ItemRetrieveParams,
+ ),
+ ),
+ cast_to=cast(Any, VaultItem), # Union types cannot be passed in as arguments in the type system
+ ),
+ )
+
+ def update(
+ self,
+ key: str,
+ *,
+ id_or_name: str,
+ spec: CardVaultItemSpecParam,
+ # 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,
+ ) -> VaultItem:
+ """
+ Update a card specification before or between authorizations
+
+ Args:
+ spec: AgentCard reusable card. Each checkout creates an approval-gated authorization
+ for spec.merchant / spec.amount. The card stays ready after each authorization.
+
+ extra_headers: Send extra headers
+
+ extra_query: Add additional query parameters to the request
+
+ extra_body: Add additional JSON properties to the request
+
+ timeout: Override the client-level default timeout for this request, in seconds
+ """
+ if not id_or_name:
+ raise ValueError(f"Expected a non-empty value for `id_or_name` but received {id_or_name!r}")
+ if not key:
+ raise ValueError(f"Expected a non-empty value for `key` but received {key!r}")
+ return cast(
+ VaultItem,
+ self._patch(
+ path_template("/vaults/{id_or_name}/items/{key}", id_or_name=id_or_name, key=key),
+ body=maybe_transform({"spec": spec}, item_update_params.ItemUpdateParams),
+ options=make_request_options(
+ extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
+ ),
+ cast_to=cast(Any, VaultItem), # Union types cannot be passed in as arguments in the type system
+ ),
+ )
+
+ def list(
+ self,
+ id_or_name: str,
+ *,
+ # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
+ # The extra values given here take precedence over values defined on the client or passed to this method.
+ extra_headers: Headers | None = None,
+ extra_query: Query | None = None,
+ extra_body: Body | None = None,
+ timeout: float | httpx.Timeout | None | NotGiven = not_given,
+ ) -> ItemListResponse:
+ """
+ List vault items without secret values
+
+ Args:
+ extra_headers: Send extra headers
+
+ extra_query: Add additional query parameters to the request
+
+ extra_body: Add additional JSON properties to the request
+
+ timeout: Override the client-level default timeout for this request, in seconds
+ """
+ if not id_or_name:
+ raise ValueError(f"Expected a non-empty value for `id_or_name` but received {id_or_name!r}")
+ return self._get(
+ path_template("/vaults/{id_or_name}/items", id_or_name=id_or_name),
+ options=make_request_options(
+ extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
+ ),
+ cast_to=ItemListResponse,
+ )
+
+ def delete(
+ self,
+ key: str,
+ *,
+ id_or_name: str,
+ # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
+ # The extra values given here take precedence over values defined on the client or passed to this method.
+ extra_headers: Headers | None = None,
+ extra_query: Query | None = None,
+ extra_body: Body | None = None,
+ timeout: float | httpx.Timeout | None | NotGiven = not_given,
+ ) -> None:
+ """
+ Delete a vault item and invalidate its secret value
+
+ Args:
+ extra_headers: Send extra headers
+
+ extra_query: Add additional query parameters to the request
+
+ extra_body: Add additional JSON properties to the request
+
+ timeout: Override the client-level default timeout for this request, in seconds
+ """
+ if not id_or_name:
+ raise ValueError(f"Expected a non-empty value for `id_or_name` but received {id_or_name!r}")
+ if not key:
+ raise ValueError(f"Expected a non-empty value for `key` but received {key!r}")
+ extra_headers = {"Accept": "*/*", **(extra_headers or {})}
+ return self._delete(
+ path_template("/vaults/{id_or_name}/items/{key}", id_or_name=id_or_name, key=key),
+ options=make_request_options(
+ extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
+ ),
+ cast_to=NoneType,
+ )
+
+ def events(
+ self,
+ key: str,
+ *,
+ id_or_name: str,
+ after: str | Omit = omit,
+ wait: int | Omit = omit,
+ # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
+ # The extra values given here take precedence over values defined on the client or passed to this method.
+ extra_headers: Headers | None = None,
+ extra_query: Query | None = None,
+ extra_body: Body | None = None,
+ timeout: float | httpx.Timeout | None | NotGiven = not_given,
+ ) -> ItemEventsResponse:
+ """
+ List immutable audit events for a vault item
+
+ Args:
+ after: Return events after this event ID.
+
+ wait: Long-poll for new events for up to this many seconds.
+
+ extra_headers: Send extra headers
+
+ extra_query: Add additional query parameters to the request
+
+ extra_body: Add additional JSON properties to the request
+
+ timeout: Override the client-level default timeout for this request, in seconds
+ """
+ if not id_or_name:
+ raise ValueError(f"Expected a non-empty value for `id_or_name` but received {id_or_name!r}")
+ if not key:
+ raise ValueError(f"Expected a non-empty value for `key` but received {key!r}")
+ return self._get(
+ path_template("/vaults/{id_or_name}/items/{key}/events", id_or_name=id_or_name, key=key),
+ options=make_request_options(
+ extra_headers=extra_headers,
+ extra_query=extra_query,
+ extra_body=extra_body,
+ timeout=timeout,
+ query=maybe_transform(
+ {
+ "after": after,
+ "wait": wait,
+ },
+ item_events_params.ItemEventsParams,
+ ),
+ ),
+ cast_to=ItemEventsResponse,
+ )
+
+ def perform_operation(
+ self,
+ key: str,
+ *,
+ id_or_name: str,
+ type: Literal["authorize"],
+ # 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,
+ ) -> VaultItem:
+ """
+ Retrieve the item first and invoke only an operation listed in
+ `available_operations`, following its natural-language description. Operations
+ may call an external provider and can return the item's updated state.
+
+ Args:
+ extra_headers: Send extra headers
+
+ extra_query: Add additional query parameters to the request
+
+ extra_body: Add additional JSON properties to the request
+
+ timeout: Override the client-level default timeout for this request, in seconds
+ """
+ if not id_or_name:
+ raise ValueError(f"Expected a non-empty value for `id_or_name` but received {id_or_name!r}")
+ if not key:
+ raise ValueError(f"Expected a non-empty value for `key` but received {key!r}")
+ return cast(
+ VaultItem,
+ self._post(
+ path_template("/vaults/{id_or_name}/items/{key}/operations", id_or_name=id_or_name, key=key),
+ body=maybe_transform({"type": type}, item_perform_operation_params.ItemPerformOperationParams),
+ options=make_request_options(
+ extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
+ ),
+ cast_to=cast(Any, VaultItem), # Union types cannot be passed in as arguments in the type system
+ ),
+ )
+
+ @overload
+ def upsert(
+ self,
+ key: str,
+ *,
+ id_or_name: str,
+ spec: WalletVaultItemSpecParam,
+ type: Literal["wallet"],
+ # 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,
+ ) -> VaultItem:
+ """
+ Create or retrieve an identical vault item by immutable key
+
+ Args:
+ spec: AgentCard wallet. Mode (sandbox vs live) is fixed by the deployment's AgentCard
+ credential; there is no per-item test flag. user_id may only reference a user
+ already enrolled by a wallet in this organization.
+
+ 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
+ """
+ ...
+
+ @overload
+ def upsert(
+ self,
+ key: str,
+ *,
+ id_or_name: str,
+ spec: CardVaultItemSpecParam,
+ type: Literal["card"],
+ # 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,
+ ) -> VaultItem:
+ """
+ Create or retrieve an identical vault item by immutable key
+
+ Args:
+ spec: AgentCard reusable card. Each checkout creates an approval-gated authorization
+ for spec.merchant / spec.amount. The card stays ready after each authorization.
+
+ 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
+ """
+ ...
+
+ @required_args(["id_or_name", "spec", "type"])
+ def upsert(
+ self,
+ key: str,
+ *,
+ id_or_name: str,
+ spec: WalletVaultItemSpecParam | CardVaultItemSpecParam,
+ type: Literal["wallet"] | Literal["card"],
+ # 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,
+ ) -> VaultItem:
+ if not id_or_name:
+ raise ValueError(f"Expected a non-empty value for `id_or_name` but received {id_or_name!r}")
+ if not key:
+ raise ValueError(f"Expected a non-empty value for `key` but received {key!r}")
+ return cast(
+ VaultItem,
+ self._put(
+ path_template("/vaults/{id_or_name}/items/{key}", id_or_name=id_or_name, key=key),
+ body=maybe_transform(
+ {
+ "spec": spec,
+ "type": type,
+ },
+ item_upsert_params.ItemUpsertParams,
+ ),
+ options=make_request_options(
+ extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
+ ),
+ cast_to=cast(Any, VaultItem), # Union types cannot be passed in as arguments in the type system
+ ),
+ )
+
+
+class AsyncItemsResource(AsyncAPIResource):
+ @cached_property
+ def with_raw_response(self) -> AsyncItemsResourceWithRawResponse:
+ """
+ This property can be used as a prefix for any HTTP method call to return
+ the raw response object instead of the parsed content.
+
+ For more information, see https://www.github.com/kernel/kernel-python-sdk#accessing-raw-response-data-eg-headers
+ """
+ return AsyncItemsResourceWithRawResponse(self)
+
+ @cached_property
+ def with_streaming_response(self) -> AsyncItemsResourceWithStreamingResponse:
+ """
+ An alternative to `.with_raw_response` that doesn't eagerly read the response body.
+
+ For more information, see https://www.github.com/kernel/kernel-python-sdk#with_streaming_response
+ """
+ return AsyncItemsResourceWithStreamingResponse(self)
+
+ async def retrieve(
+ self,
+ key: str,
+ *,
+ id_or_name: str,
+ expand: List[Literal["payment_methods"]] | Omit = omit,
+ wait: int | Omit = omit,
+ # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
+ # The extra values given here take precedence over values defined on the client or passed to this method.
+ extra_headers: Headers | None = None,
+ extra_query: Query | None = None,
+ extra_body: Body | None = None,
+ timeout: float | httpx.Timeout | None | NotGiven = not_given,
+ ) -> VaultItem:
+ """
+ The response advertises operations that are valid in the item's current state
+ and live data that can be requested through `expand`. Read each operation's
+ description before using it. Expanded data is fetched from the provider and is
+ not persisted in the vault item. Requesting an unavailable expansion returns 409
+ instead of a partial item.
+
+ Args:
+ expand: Live fields advertised by `available_expansions` to include in `expanded`.
+
+ wait: Hold for up to this many seconds while the item is pending authorization or
+ approval.
+
+ extra_headers: Send extra headers
+
+ extra_query: Add additional query parameters to the request
+
+ extra_body: Add additional JSON properties to the request
+
+ timeout: Override the client-level default timeout for this request, in seconds
+ """
+ if not id_or_name:
+ raise ValueError(f"Expected a non-empty value for `id_or_name` but received {id_or_name!r}")
+ if not key:
+ raise ValueError(f"Expected a non-empty value for `key` but received {key!r}")
+ return cast(
+ VaultItem,
+ await self._get(
+ path_template("/vaults/{id_or_name}/items/{key}", id_or_name=id_or_name, key=key),
+ options=make_request_options(
+ extra_headers=extra_headers,
+ extra_query=extra_query,
+ extra_body=extra_body,
+ timeout=timeout,
+ query=await async_maybe_transform(
+ {
+ "expand": expand,
+ "wait": wait,
+ },
+ item_retrieve_params.ItemRetrieveParams,
+ ),
+ ),
+ cast_to=cast(Any, VaultItem), # Union types cannot be passed in as arguments in the type system
+ ),
+ )
+
+ async def update(
+ self,
+ key: str,
+ *,
+ id_or_name: str,
+ spec: CardVaultItemSpecParam,
+ # 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,
+ ) -> VaultItem:
+ """
+ Update a card specification before or between authorizations
+
+ Args:
+ spec: AgentCard reusable card. Each checkout creates an approval-gated authorization
+ for spec.merchant / spec.amount. The card stays ready after each authorization.
+
+ extra_headers: Send extra headers
+
+ extra_query: Add additional query parameters to the request
+
+ extra_body: Add additional JSON properties to the request
+
+ timeout: Override the client-level default timeout for this request, in seconds
+ """
+ if not id_or_name:
+ raise ValueError(f"Expected a non-empty value for `id_or_name` but received {id_or_name!r}")
+ if not key:
+ raise ValueError(f"Expected a non-empty value for `key` but received {key!r}")
+ return cast(
+ VaultItem,
+ await self._patch(
+ path_template("/vaults/{id_or_name}/items/{key}", id_or_name=id_or_name, key=key),
+ body=await async_maybe_transform({"spec": spec}, item_update_params.ItemUpdateParams),
+ options=make_request_options(
+ extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
+ ),
+ cast_to=cast(Any, VaultItem), # Union types cannot be passed in as arguments in the type system
+ ),
+ )
+
+ async def list(
+ self,
+ id_or_name: str,
+ *,
+ # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
+ # The extra values given here take precedence over values defined on the client or passed to this method.
+ extra_headers: Headers | None = None,
+ extra_query: Query | None = None,
+ extra_body: Body | None = None,
+ timeout: float | httpx.Timeout | None | NotGiven = not_given,
+ ) -> ItemListResponse:
+ """
+ List vault items without secret values
+
+ Args:
+ extra_headers: Send extra headers
+
+ extra_query: Add additional query parameters to the request
+
+ extra_body: Add additional JSON properties to the request
+
+ timeout: Override the client-level default timeout for this request, in seconds
+ """
+ if not id_or_name:
+ raise ValueError(f"Expected a non-empty value for `id_or_name` but received {id_or_name!r}")
+ return await self._get(
+ path_template("/vaults/{id_or_name}/items", id_or_name=id_or_name),
+ options=make_request_options(
+ extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
+ ),
+ cast_to=ItemListResponse,
+ )
+
+ async def delete(
+ self,
+ key: str,
+ *,
+ id_or_name: str,
+ # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
+ # The extra values given here take precedence over values defined on the client or passed to this method.
+ extra_headers: Headers | None = None,
+ extra_query: Query | None = None,
+ extra_body: Body | None = None,
+ timeout: float | httpx.Timeout | None | NotGiven = not_given,
+ ) -> None:
+ """
+ Delete a vault item and invalidate its secret value
+
+ Args:
+ extra_headers: Send extra headers
+
+ extra_query: Add additional query parameters to the request
+
+ extra_body: Add additional JSON properties to the request
+
+ timeout: Override the client-level default timeout for this request, in seconds
+ """
+ if not id_or_name:
+ raise ValueError(f"Expected a non-empty value for `id_or_name` but received {id_or_name!r}")
+ if not key:
+ raise ValueError(f"Expected a non-empty value for `key` but received {key!r}")
+ extra_headers = {"Accept": "*/*", **(extra_headers or {})}
+ return await self._delete(
+ path_template("/vaults/{id_or_name}/items/{key}", id_or_name=id_or_name, key=key),
+ options=make_request_options(
+ extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
+ ),
+ cast_to=NoneType,
+ )
+
+ async def events(
+ self,
+ key: str,
+ *,
+ id_or_name: str,
+ after: str | Omit = omit,
+ wait: int | Omit = omit,
+ # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
+ # The extra values given here take precedence over values defined on the client or passed to this method.
+ extra_headers: Headers | None = None,
+ extra_query: Query | None = None,
+ extra_body: Body | None = None,
+ timeout: float | httpx.Timeout | None | NotGiven = not_given,
+ ) -> ItemEventsResponse:
+ """
+ List immutable audit events for a vault item
+
+ Args:
+ after: Return events after this event ID.
+
+ wait: Long-poll for new events for up to this many seconds.
+
+ extra_headers: Send extra headers
+
+ extra_query: Add additional query parameters to the request
+
+ extra_body: Add additional JSON properties to the request
+
+ timeout: Override the client-level default timeout for this request, in seconds
+ """
+ if not id_or_name:
+ raise ValueError(f"Expected a non-empty value for `id_or_name` but received {id_or_name!r}")
+ if not key:
+ raise ValueError(f"Expected a non-empty value for `key` but received {key!r}")
+ return await self._get(
+ path_template("/vaults/{id_or_name}/items/{key}/events", id_or_name=id_or_name, key=key),
+ options=make_request_options(
+ extra_headers=extra_headers,
+ extra_query=extra_query,
+ extra_body=extra_body,
+ timeout=timeout,
+ query=await async_maybe_transform(
+ {
+ "after": after,
+ "wait": wait,
+ },
+ item_events_params.ItemEventsParams,
+ ),
+ ),
+ cast_to=ItemEventsResponse,
+ )
+
+ async def perform_operation(
+ self,
+ key: str,
+ *,
+ id_or_name: str,
+ type: Literal["authorize"],
+ # 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,
+ ) -> VaultItem:
+ """
+ Retrieve the item first and invoke only an operation listed in
+ `available_operations`, following its natural-language description. Operations
+ may call an external provider and can return the item's updated state.
+
+ Args:
+ extra_headers: Send extra headers
+
+ extra_query: Add additional query parameters to the request
+
+ extra_body: Add additional JSON properties to the request
+
+ timeout: Override the client-level default timeout for this request, in seconds
+ """
+ if not id_or_name:
+ raise ValueError(f"Expected a non-empty value for `id_or_name` but received {id_or_name!r}")
+ if not key:
+ raise ValueError(f"Expected a non-empty value for `key` but received {key!r}")
+ return cast(
+ VaultItem,
+ await self._post(
+ path_template("/vaults/{id_or_name}/items/{key}/operations", id_or_name=id_or_name, key=key),
+ body=await async_maybe_transform(
+ {"type": type}, item_perform_operation_params.ItemPerformOperationParams
+ ),
+ options=make_request_options(
+ extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
+ ),
+ cast_to=cast(Any, VaultItem), # Union types cannot be passed in as arguments in the type system
+ ),
+ )
+
+ @overload
+ async def upsert(
+ self,
+ key: str,
+ *,
+ id_or_name: str,
+ spec: WalletVaultItemSpecParam,
+ type: Literal["wallet"],
+ # 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,
+ ) -> VaultItem:
+ """
+ Create or retrieve an identical vault item by immutable key
+
+ Args:
+ spec: AgentCard wallet. Mode (sandbox vs live) is fixed by the deployment's AgentCard
+ credential; there is no per-item test flag. user_id may only reference a user
+ already enrolled by a wallet in this organization.
+
+ 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
+ """
+ ...
+
+ @overload
+ async def upsert(
+ self,
+ key: str,
+ *,
+ id_or_name: str,
+ spec: CardVaultItemSpecParam,
+ type: Literal["card"],
+ # 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,
+ ) -> VaultItem:
+ """
+ Create or retrieve an identical vault item by immutable key
+
+ Args:
+ spec: AgentCard reusable card. Each checkout creates an approval-gated authorization
+ for spec.merchant / spec.amount. The card stays ready after each authorization.
+
+ 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
+ """
+ ...
+
+ @required_args(["id_or_name", "spec", "type"])
+ async def upsert(
+ self,
+ key: str,
+ *,
+ id_or_name: str,
+ spec: WalletVaultItemSpecParam | CardVaultItemSpecParam,
+ type: Literal["wallet"] | Literal["card"],
+ # 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,
+ ) -> VaultItem:
+ if not id_or_name:
+ raise ValueError(f"Expected a non-empty value for `id_or_name` but received {id_or_name!r}")
+ if not key:
+ raise ValueError(f"Expected a non-empty value for `key` but received {key!r}")
+ return cast(
+ VaultItem,
+ await self._put(
+ path_template("/vaults/{id_or_name}/items/{key}", id_or_name=id_or_name, key=key),
+ body=await async_maybe_transform(
+ {
+ "spec": spec,
+ "type": type,
+ },
+ item_upsert_params.ItemUpsertParams,
+ ),
+ options=make_request_options(
+ extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
+ ),
+ cast_to=cast(Any, VaultItem), # Union types cannot be passed in as arguments in the type system
+ ),
+ )
+
+
+class ItemsResourceWithRawResponse:
+ def __init__(self, items: ItemsResource) -> None:
+ self._items = items
+
+ self.retrieve = to_raw_response_wrapper(
+ items.retrieve,
+ )
+ self.update = to_raw_response_wrapper(
+ items.update,
+ )
+ self.list = to_raw_response_wrapper(
+ items.list,
+ )
+ self.delete = to_raw_response_wrapper(
+ items.delete,
+ )
+ self.events = to_raw_response_wrapper(
+ items.events,
+ )
+ self.perform_operation = to_raw_response_wrapper(
+ items.perform_operation,
+ )
+ self.upsert = to_raw_response_wrapper(
+ items.upsert,
+ )
+
+
+class AsyncItemsResourceWithRawResponse:
+ def __init__(self, items: AsyncItemsResource) -> None:
+ self._items = items
+
+ self.retrieve = async_to_raw_response_wrapper(
+ items.retrieve,
+ )
+ self.update = async_to_raw_response_wrapper(
+ items.update,
+ )
+ self.list = async_to_raw_response_wrapper(
+ items.list,
+ )
+ self.delete = async_to_raw_response_wrapper(
+ items.delete,
+ )
+ self.events = async_to_raw_response_wrapper(
+ items.events,
+ )
+ self.perform_operation = async_to_raw_response_wrapper(
+ items.perform_operation,
+ )
+ self.upsert = async_to_raw_response_wrapper(
+ items.upsert,
+ )
+
+
+class ItemsResourceWithStreamingResponse:
+ def __init__(self, items: ItemsResource) -> None:
+ self._items = items
+
+ self.retrieve = to_streamed_response_wrapper(
+ items.retrieve,
+ )
+ self.update = to_streamed_response_wrapper(
+ items.update,
+ )
+ self.list = to_streamed_response_wrapper(
+ items.list,
+ )
+ self.delete = to_streamed_response_wrapper(
+ items.delete,
+ )
+ self.events = to_streamed_response_wrapper(
+ items.events,
+ )
+ self.perform_operation = to_streamed_response_wrapper(
+ items.perform_operation,
+ )
+ self.upsert = to_streamed_response_wrapper(
+ items.upsert,
+ )
+
+
+class AsyncItemsResourceWithStreamingResponse:
+ def __init__(self, items: AsyncItemsResource) -> None:
+ self._items = items
+
+ self.retrieve = async_to_streamed_response_wrapper(
+ items.retrieve,
+ )
+ self.update = async_to_streamed_response_wrapper(
+ items.update,
+ )
+ self.list = async_to_streamed_response_wrapper(
+ items.list,
+ )
+ self.delete = async_to_streamed_response_wrapper(
+ items.delete,
+ )
+ self.events = async_to_streamed_response_wrapper(
+ items.events,
+ )
+ self.perform_operation = async_to_streamed_response_wrapper(
+ items.perform_operation,
+ )
+ self.upsert = async_to_streamed_response_wrapper(
+ items.upsert,
+ )
diff --git a/src/kernel/resources/vaults/vaults.py b/src/kernel/resources/vaults/vaults.py
new file mode 100644
index 00000000..ec33277c
--- /dev/null
+++ b/src/kernel/resources/vaults/vaults.py
@@ -0,0 +1,456 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from __future__ import annotations
+
+import httpx
+
+from .items import (
+ ItemsResource,
+ AsyncItemsResource,
+ ItemsResourceWithRawResponse,
+ AsyncItemsResourceWithRawResponse,
+ ItemsResourceWithStreamingResponse,
+ AsyncItemsResourceWithStreamingResponse,
+)
+from ...types import vault_list_params, vault_upsert_params
+from ..._types import Body, Omit, Query, Headers, NoneType, NotGiven, omit, not_given
+from ..._utils import path_template, maybe_transform, async_maybe_transform
+from ..._compat import cached_property
+from ..._resource import SyncAPIResource, AsyncAPIResource
+from ..._response import (
+ to_raw_response_wrapper,
+ to_streamed_response_wrapper,
+ async_to_raw_response_wrapper,
+ async_to_streamed_response_wrapper,
+)
+from ...pagination import SyncOffsetPagination, AsyncOffsetPagination
+from ...types.vault import Vault
+from ..._base_client import AsyncPaginator, make_request_options
+
+__all__ = ["VaultsResource", "AsyncVaultsResource"]
+
+
+class VaultsResource(SyncAPIResource):
+ @cached_property
+ def items(self) -> ItemsResource:
+ return ItemsResource(self._client)
+
+ @cached_property
+ def with_raw_response(self) -> VaultsResourceWithRawResponse:
+ """
+ This property can be used as a prefix for any HTTP method call to return
+ the raw response object instead of the parsed content.
+
+ For more information, see https://www.github.com/kernel/kernel-python-sdk#accessing-raw-response-data-eg-headers
+ """
+ return VaultsResourceWithRawResponse(self)
+
+ @cached_property
+ def with_streaming_response(self) -> VaultsResourceWithStreamingResponse:
+ """
+ An alternative to `.with_raw_response` that doesn't eagerly read the response body.
+
+ For more information, see https://www.github.com/kernel/kernel-python-sdk#with_streaming_response
+ """
+ return VaultsResourceWithStreamingResponse(self)
+
+ def retrieve(
+ self,
+ id_or_name: str,
+ *,
+ # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
+ # The extra values given here take precedence over values defined on the client or passed to this method.
+ extra_headers: Headers | None = None,
+ extra_query: Query | None = None,
+ extra_body: Body | None = None,
+ timeout: float | httpx.Timeout | None | NotGiven = not_given,
+ ) -> Vault:
+ """
+ Get a vault
+
+ Args:
+ extra_headers: Send extra headers
+
+ extra_query: Add additional query parameters to the request
+
+ extra_body: Add additional JSON properties to the request
+
+ timeout: Override the client-level default timeout for this request, in seconds
+ """
+ if not id_or_name:
+ raise ValueError(f"Expected a non-empty value for `id_or_name` but received {id_or_name!r}")
+ return self._get(
+ path_template("/vaults/{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
+ ),
+ cast_to=Vault,
+ )
+
+ def list(
+ self,
+ *,
+ limit: int | Omit = omit,
+ offset: int | Omit = omit,
+ # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
+ # The extra values given here take precedence over values defined on the client or passed to this method.
+ extra_headers: Headers | None = None,
+ extra_query: Query | None = None,
+ extra_body: Body | None = None,
+ timeout: float | httpx.Timeout | None | NotGiven = not_given,
+ ) -> SyncOffsetPagination[Vault]:
+ """
+ List vaults in the current project
+
+ Args:
+ extra_headers: Send extra headers
+
+ extra_query: Add additional query parameters to the request
+
+ extra_body: Add additional JSON properties to the request
+
+ timeout: Override the client-level default timeout for this request, in seconds
+ """
+ return self._get_api_list(
+ "/vaults",
+ page=SyncOffsetPagination[Vault],
+ options=make_request_options(
+ extra_headers=extra_headers,
+ extra_query=extra_query,
+ extra_body=extra_body,
+ timeout=timeout,
+ query=maybe_transform(
+ {
+ "limit": limit,
+ "offset": offset,
+ },
+ vault_list_params.VaultListParams,
+ ),
+ ),
+ model=Vault,
+ )
+
+ def delete(
+ self,
+ id_or_name: str,
+ *,
+ # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
+ # The extra values given here take precedence over values defined on the client or passed to this method.
+ extra_headers: Headers | None = None,
+ extra_query: Query | None = None,
+ extra_body: Body | None = None,
+ timeout: float | httpx.Timeout | None | NotGiven = not_given,
+ ) -> None:
+ """
+ Delete a vault and invalidate its items
+
+ Args:
+ extra_headers: Send extra headers
+
+ extra_query: Add additional query parameters to the request
+
+ extra_body: Add additional JSON properties to the request
+
+ timeout: Override the client-level default timeout for this request, in seconds
+ """
+ if not id_or_name:
+ raise ValueError(f"Expected a non-empty value for `id_or_name` but received {id_or_name!r}")
+ extra_headers = {"Accept": "*/*", **(extra_headers or {})}
+ return self._delete(
+ path_template("/vaults/{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
+ ),
+ cast_to=NoneType,
+ )
+
+ def upsert(
+ self,
+ *,
+ name: str,
+ # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
+ # The extra values given here take precedence over values defined on the client or passed to this method.
+ extra_headers: Headers | None = None,
+ extra_query: Query | None = None,
+ extra_body: Body | None = None,
+ timeout: float | httpx.Timeout | None | NotGiven = not_given,
+ ) -> Vault:
+ """
+ Create or retrieve a vault by immutable name
+
+ Args:
+ name: Immutable name used to create or retrieve the vault.
+
+ 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
+ """
+ return self._post(
+ "/vaults",
+ body=maybe_transform({"name": name}, vault_upsert_params.VaultUpsertParams),
+ options=make_request_options(
+ extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
+ ),
+ cast_to=Vault,
+ )
+
+
+class AsyncVaultsResource(AsyncAPIResource):
+ @cached_property
+ def items(self) -> AsyncItemsResource:
+ return AsyncItemsResource(self._client)
+
+ @cached_property
+ def with_raw_response(self) -> AsyncVaultsResourceWithRawResponse:
+ """
+ This property can be used as a prefix for any HTTP method call to return
+ the raw response object instead of the parsed content.
+
+ For more information, see https://www.github.com/kernel/kernel-python-sdk#accessing-raw-response-data-eg-headers
+ """
+ return AsyncVaultsResourceWithRawResponse(self)
+
+ @cached_property
+ def with_streaming_response(self) -> AsyncVaultsResourceWithStreamingResponse:
+ """
+ An alternative to `.with_raw_response` that doesn't eagerly read the response body.
+
+ For more information, see https://www.github.com/kernel/kernel-python-sdk#with_streaming_response
+ """
+ return AsyncVaultsResourceWithStreamingResponse(self)
+
+ async def retrieve(
+ self,
+ id_or_name: str,
+ *,
+ # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
+ # The extra values given here take precedence over values defined on the client or passed to this method.
+ extra_headers: Headers | None = None,
+ extra_query: Query | None = None,
+ extra_body: Body | None = None,
+ timeout: float | httpx.Timeout | None | NotGiven = not_given,
+ ) -> Vault:
+ """
+ Get a vault
+
+ Args:
+ extra_headers: Send extra headers
+
+ extra_query: Add additional query parameters to the request
+
+ extra_body: Add additional JSON properties to the request
+
+ timeout: Override the client-level default timeout for this request, in seconds
+ """
+ if not id_or_name:
+ raise ValueError(f"Expected a non-empty value for `id_or_name` but received {id_or_name!r}")
+ return await self._get(
+ path_template("/vaults/{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
+ ),
+ cast_to=Vault,
+ )
+
+ def list(
+ self,
+ *,
+ limit: int | Omit = omit,
+ offset: int | Omit = omit,
+ # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
+ # The extra values given here take precedence over values defined on the client or passed to this method.
+ extra_headers: Headers | None = None,
+ extra_query: Query | None = None,
+ extra_body: Body | None = None,
+ timeout: float | httpx.Timeout | None | NotGiven = not_given,
+ ) -> AsyncPaginator[Vault, AsyncOffsetPagination[Vault]]:
+ """
+ List vaults in the current project
+
+ Args:
+ extra_headers: Send extra headers
+
+ extra_query: Add additional query parameters to the request
+
+ extra_body: Add additional JSON properties to the request
+
+ timeout: Override the client-level default timeout for this request, in seconds
+ """
+ return self._get_api_list(
+ "/vaults",
+ page=AsyncOffsetPagination[Vault],
+ options=make_request_options(
+ extra_headers=extra_headers,
+ extra_query=extra_query,
+ extra_body=extra_body,
+ timeout=timeout,
+ query=maybe_transform(
+ {
+ "limit": limit,
+ "offset": offset,
+ },
+ vault_list_params.VaultListParams,
+ ),
+ ),
+ model=Vault,
+ )
+
+ async def delete(
+ self,
+ id_or_name: str,
+ *,
+ # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
+ # The extra values given here take precedence over values defined on the client or passed to this method.
+ extra_headers: Headers | None = None,
+ extra_query: Query | None = None,
+ extra_body: Body | None = None,
+ timeout: float | httpx.Timeout | None | NotGiven = not_given,
+ ) -> None:
+ """
+ Delete a vault and invalidate its items
+
+ Args:
+ extra_headers: Send extra headers
+
+ extra_query: Add additional query parameters to the request
+
+ extra_body: Add additional JSON properties to the request
+
+ timeout: Override the client-level default timeout for this request, in seconds
+ """
+ if not id_or_name:
+ raise ValueError(f"Expected a non-empty value for `id_or_name` but received {id_or_name!r}")
+ extra_headers = {"Accept": "*/*", **(extra_headers or {})}
+ return await self._delete(
+ path_template("/vaults/{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
+ ),
+ cast_to=NoneType,
+ )
+
+ async def upsert(
+ self,
+ *,
+ name: str,
+ # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
+ # The extra values given here take precedence over values defined on the client or passed to this method.
+ extra_headers: Headers | None = None,
+ extra_query: Query | None = None,
+ extra_body: Body | None = None,
+ timeout: float | httpx.Timeout | None | NotGiven = not_given,
+ ) -> Vault:
+ """
+ Create or retrieve a vault by immutable name
+
+ Args:
+ name: Immutable name used to create or retrieve the vault.
+
+ 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
+ """
+ return await self._post(
+ "/vaults",
+ body=await async_maybe_transform({"name": name}, vault_upsert_params.VaultUpsertParams),
+ options=make_request_options(
+ extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
+ ),
+ cast_to=Vault,
+ )
+
+
+class VaultsResourceWithRawResponse:
+ def __init__(self, vaults: VaultsResource) -> None:
+ self._vaults = vaults
+
+ self.retrieve = to_raw_response_wrapper(
+ vaults.retrieve,
+ )
+ self.list = to_raw_response_wrapper(
+ vaults.list,
+ )
+ self.delete = to_raw_response_wrapper(
+ vaults.delete,
+ )
+ self.upsert = to_raw_response_wrapper(
+ vaults.upsert,
+ )
+
+ @cached_property
+ def items(self) -> ItemsResourceWithRawResponse:
+ return ItemsResourceWithRawResponse(self._vaults.items)
+
+
+class AsyncVaultsResourceWithRawResponse:
+ def __init__(self, vaults: AsyncVaultsResource) -> None:
+ self._vaults = vaults
+
+ self.retrieve = async_to_raw_response_wrapper(
+ vaults.retrieve,
+ )
+ self.list = async_to_raw_response_wrapper(
+ vaults.list,
+ )
+ self.delete = async_to_raw_response_wrapper(
+ vaults.delete,
+ )
+ self.upsert = async_to_raw_response_wrapper(
+ vaults.upsert,
+ )
+
+ @cached_property
+ def items(self) -> AsyncItemsResourceWithRawResponse:
+ return AsyncItemsResourceWithRawResponse(self._vaults.items)
+
+
+class VaultsResourceWithStreamingResponse:
+ def __init__(self, vaults: VaultsResource) -> None:
+ self._vaults = vaults
+
+ self.retrieve = to_streamed_response_wrapper(
+ vaults.retrieve,
+ )
+ self.list = to_streamed_response_wrapper(
+ vaults.list,
+ )
+ self.delete = to_streamed_response_wrapper(
+ vaults.delete,
+ )
+ self.upsert = to_streamed_response_wrapper(
+ vaults.upsert,
+ )
+
+ @cached_property
+ def items(self) -> ItemsResourceWithStreamingResponse:
+ return ItemsResourceWithStreamingResponse(self._vaults.items)
+
+
+class AsyncVaultsResourceWithStreamingResponse:
+ def __init__(self, vaults: AsyncVaultsResource) -> None:
+ self._vaults = vaults
+
+ self.retrieve = async_to_streamed_response_wrapper(
+ vaults.retrieve,
+ )
+ self.list = async_to_streamed_response_wrapper(
+ vaults.list,
+ )
+ self.delete = async_to_streamed_response_wrapper(
+ vaults.delete,
+ )
+ self.upsert = async_to_streamed_response_wrapper(
+ vaults.upsert,
+ )
+
+ @cached_property
+ def items(self) -> AsyncItemsResourceWithStreamingResponse:
+ return AsyncItemsResourceWithStreamingResponse(self._vaults.items)
diff --git a/src/kernel/types/__init__.py b/src/kernel/types/__init__.py
index fffffd45..0310cfbe 100644
--- a/src/kernel/types/__init__.py
+++ b/src/kernel/types/__init__.py
@@ -6,6 +6,7 @@
from .. import _compat
from .tags import Tags as Tags
from .proxy import Proxy as Proxy
+from .vault import Vault as Vault
from .shared import (
LogEvent as LogEvent,
AppAction as AppAction,
@@ -35,11 +36,13 @@
from .audit_log_entry import AuditLogEntry as AuditLogEntry
from .created_api_key import CreatedAPIKey as CreatedAPIKey
from .lookup_response import LookupResponse as LookupResponse
+from .vault_reference import VaultReference as VaultReference
from .analysis_summary import AnalysisSummary as AnalysisSummary
from .browser_pool_ref import BrowserPoolRef as BrowserPoolRef
from .app_list_response import AppListResponse as AppListResponse
from .no_recommendation import NoRecommendation as NoRecommendation
from .proxy_list_params import ProxyListParams as ProxyListParams
+from .vault_list_params import VaultListParams as VaultListParams
from .browser_proxy_mode import BrowserProxyMode as BrowserProxyMode
from .proxy_check_params import ProxyCheckParams as ProxyCheckParams
from .api_key_list_params import APIKeyListParams as APIKeyListParams
@@ -51,6 +54,7 @@
from .proxy_create_params import ProxyCreateParams as ProxyCreateParams
from .proxy_list_response import ProxyListResponse as ProxyListResponse
from .proxy_update_params import ProxyUpdateParams as ProxyUpdateParams
+from .vault_upsert_params import VaultUpsertParams as VaultUpsertParams
from .browser_proxy_config import BrowserProxyConfig as BrowserProxyConfig
from .proxy_check_response import ProxyCheckResponse as ProxyCheckResponse
from .api_key_create_params import APIKeyCreateParams as APIKeyCreateParams
@@ -69,6 +73,7 @@
from .proxy_create_response import ProxyCreateResponse as ProxyCreateResponse
from .proxy_update_response import ProxyUpdateResponse as ProxyUpdateResponse
from .recommendation_result import RecommendationResult as RecommendationResult
+from .vault_reference_param import VaultReferenceParam as VaultReferenceParam
from .browser_memory_request import BrowserMemoryRequest as BrowserMemoryRequest
from .browser_network_config import BrowserNetworkConfig as BrowserNetworkConfig
from .credential_list_params import CredentialListParams as CredentialListParams
diff --git a/src/kernel/types/browser_create_params.py b/src/kernel/types/browser_create_params.py
index da890722..ab486672 100644
--- a/src/kernel/types/browser_create_params.py
+++ b/src/kernel/types/browser_create_params.py
@@ -6,6 +6,7 @@
from typing_extensions import Literal, TypedDict
from .tags_param import TagsParam
+from .vault_reference_param import VaultReferenceParam
from .browser_memory_request import BrowserMemoryRequest
from .browser_proxy_config_param import BrowserProxyConfigParam
from .browser_network_config_param import BrowserNetworkConfigParam
@@ -149,6 +150,12 @@ class BrowserCreateParams(TypedDict, total=False):
see is +/- 5 seconds around the specified value.
"""
+ vaults: Iterable[VaultReferenceParam]
+ """Project-scoped vaults to link to the browser session.
+
+ Links are immutable after creation.
+ """
+
viewport: BrowserViewport
"""
Initial browser window size in pixels with optional refresh rate. If omitted,
diff --git a/src/kernel/types/browser_create_response.py b/src/kernel/types/browser_create_response.py
index e5f63b6c..678bcbcb 100644
--- a/src/kernel/types/browser_create_response.py
+++ b/src/kernel/types/browser_create_response.py
@@ -1,6 +1,6 @@
# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
-from typing import Dict, Optional
+from typing import Dict, List, Optional
from datetime import datetime
from typing_extensions import Literal
@@ -10,6 +10,7 @@
from .browser_proxy import BrowserProxy
from .browser_usage import BrowserUsage
from .browser_memory import BrowserMemory
+from .vault_reference import VaultReference
from .browser_pool_ref import BrowserPoolRef
from .browser_network_config import BrowserNetworkConfig
from .shared.browser_viewport import BrowserViewport
@@ -125,6 +126,15 @@ class BrowserCreateResponse(BaseModel):
usage: Optional[BrowserUsage] = None
"""Session usage metrics."""
+ usage_status: Optional[Literal["pending", "ready"]] = None
+ """Whether final usage billing is still pending or complete.
+
+ Only present for deleted sessions.
+ """
+
+ vaults: Optional[List[VaultReference]] = None
+ """Vaults linked when the browser session was created."""
+
viewport: Optional[BrowserViewport] = None
"""
Initial browser window size in pixels with optional refresh rate. If omitted,
diff --git a/src/kernel/types/browser_list_response.py b/src/kernel/types/browser_list_response.py
index da294a61..aa281a39 100644
--- a/src/kernel/types/browser_list_response.py
+++ b/src/kernel/types/browser_list_response.py
@@ -1,6 +1,6 @@
# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
-from typing import Dict, Optional
+from typing import Dict, List, Optional
from datetime import datetime
from typing_extensions import Literal
@@ -10,6 +10,7 @@
from .browser_proxy import BrowserProxy
from .browser_usage import BrowserUsage
from .browser_memory import BrowserMemory
+from .vault_reference import VaultReference
from .browser_pool_ref import BrowserPoolRef
from .browser_network_config import BrowserNetworkConfig
from .shared.browser_viewport import BrowserViewport
@@ -125,6 +126,15 @@ class BrowserListResponse(BaseModel):
usage: Optional[BrowserUsage] = None
"""Session usage metrics."""
+ usage_status: Optional[Literal["pending", "ready"]] = None
+ """Whether final usage billing is still pending or complete.
+
+ Only present for deleted sessions.
+ """
+
+ vaults: Optional[List[VaultReference]] = None
+ """Vaults linked when the browser session was created."""
+
viewport: Optional[BrowserViewport] = None
"""
Initial browser window size in pixels with optional refresh rate. If omitted,
diff --git a/src/kernel/types/browser_pool_acquire_response.py b/src/kernel/types/browser_pool_acquire_response.py
index 755f5838..d472b5e0 100644
--- a/src/kernel/types/browser_pool_acquire_response.py
+++ b/src/kernel/types/browser_pool_acquire_response.py
@@ -1,6 +1,6 @@
# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
-from typing import Dict, Optional
+from typing import Dict, List, Optional
from datetime import datetime
from typing_extensions import Literal
@@ -10,6 +10,7 @@
from .browser_proxy import BrowserProxy
from .browser_usage import BrowserUsage
from .browser_memory import BrowserMemory
+from .vault_reference import VaultReference
from .browser_pool_ref import BrowserPoolRef
from .browser_network_config import BrowserNetworkConfig
from .shared.browser_viewport import BrowserViewport
@@ -125,6 +126,15 @@ class BrowserPoolAcquireResponse(BaseModel):
usage: Optional[BrowserUsage] = None
"""Session usage metrics."""
+ usage_status: Optional[Literal["pending", "ready"]] = None
+ """Whether final usage billing is still pending or complete.
+
+ Only present for deleted sessions.
+ """
+
+ vaults: Optional[List[VaultReference]] = None
+ """Vaults linked when the browser session was created."""
+
viewport: Optional[BrowserViewport] = None
"""
Initial browser window size in pixels with optional refresh rate. If omitted,
diff --git a/src/kernel/types/browser_retrieve_response.py b/src/kernel/types/browser_retrieve_response.py
index 332585ca..e6881ab1 100644
--- a/src/kernel/types/browser_retrieve_response.py
+++ b/src/kernel/types/browser_retrieve_response.py
@@ -1,6 +1,6 @@
# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
-from typing import Dict, Optional
+from typing import Dict, List, Optional
from datetime import datetime
from typing_extensions import Literal
@@ -10,6 +10,7 @@
from .browser_proxy import BrowserProxy
from .browser_usage import BrowserUsage
from .browser_memory import BrowserMemory
+from .vault_reference import VaultReference
from .browser_pool_ref import BrowserPoolRef
from .browser_network_config import BrowserNetworkConfig
from .shared.browser_viewport import BrowserViewport
@@ -125,6 +126,15 @@ class BrowserRetrieveResponse(BaseModel):
usage: Optional[BrowserUsage] = None
"""Session usage metrics."""
+ usage_status: Optional[Literal["pending", "ready"]] = None
+ """Whether final usage billing is still pending or complete.
+
+ Only present for deleted sessions.
+ """
+
+ vaults: Optional[List[VaultReference]] = None
+ """Vaults linked when the browser session was created."""
+
viewport: Optional[BrowserViewport] = None
"""
Initial browser window size in pixels with optional refresh rate. If omitted,
diff --git a/src/kernel/types/browser_update_response.py b/src/kernel/types/browser_update_response.py
index 7af24aee..65be1ba3 100644
--- a/src/kernel/types/browser_update_response.py
+++ b/src/kernel/types/browser_update_response.py
@@ -1,6 +1,6 @@
# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
-from typing import Dict, Optional
+from typing import Dict, List, Optional
from datetime import datetime
from typing_extensions import Literal
@@ -10,6 +10,7 @@
from .browser_proxy import BrowserProxy
from .browser_usage import BrowserUsage
from .browser_memory import BrowserMemory
+from .vault_reference import VaultReference
from .browser_pool_ref import BrowserPoolRef
from .browser_network_config import BrowserNetworkConfig
from .shared.browser_viewport import BrowserViewport
@@ -125,6 +126,15 @@ class BrowserUpdateResponse(BaseModel):
usage: Optional[BrowserUsage] = None
"""Session usage metrics."""
+ usage_status: Optional[Literal["pending", "ready"]] = None
+ """Whether final usage billing is still pending or complete.
+
+ Only present for deleted sessions.
+ """
+
+ vaults: Optional[List[VaultReference]] = None
+ """Vaults linked when the browser session was created."""
+
viewport: Optional[BrowserViewport] = None
"""
Initial browser window size in pixels with optional refresh rate. If omitted,
diff --git a/src/kernel/types/config_registry_list_params.py b/src/kernel/types/config_registry_list_params.py
index 5259e4b7..7277ef4b 100644
--- a/src/kernel/types/config_registry_list_params.py
+++ b/src/kernel/types/config_registry_list_params.py
@@ -13,9 +13,9 @@ class ConfigRegistryListParams(TypedDict, total=False):
offset: int
search: str
- """Case-insensitive domain search.
-
- Full URLs are reduced to their registrable domain.
+ """
+ Case-insensitive substring search over normalized targets, including domain,
+ subdomain, and path.
"""
sort_by: Literal["target", "analysis_status", "recommended_config", "last_requested_at", "success_rate"]
diff --git a/src/kernel/types/invocation_list_browsers_response.py b/src/kernel/types/invocation_list_browsers_response.py
index e31194b8..bb9d423d 100644
--- a/src/kernel/types/invocation_list_browsers_response.py
+++ b/src/kernel/types/invocation_list_browsers_response.py
@@ -10,6 +10,7 @@
from .browser_proxy import BrowserProxy
from .browser_usage import BrowserUsage
from .browser_memory import BrowserMemory
+from .vault_reference import VaultReference
from .browser_pool_ref import BrowserPoolRef
from .browser_network_config import BrowserNetworkConfig
from .shared.browser_viewport import BrowserViewport
@@ -125,6 +126,15 @@ class Browser(BaseModel):
usage: Optional[BrowserUsage] = None
"""Session usage metrics."""
+ usage_status: Optional[Literal["pending", "ready"]] = None
+ """Whether final usage billing is still pending or complete.
+
+ Only present for deleted sessions.
+ """
+
+ vaults: Optional[List[VaultReference]] = None
+ """Vaults linked when the browser session was created."""
+
viewport: Optional[BrowserViewport] = None
"""
Initial browser window size in pixels with optional refresh rate. If omitted,
diff --git a/src/kernel/types/recommendation_summary.py b/src/kernel/types/recommendation_summary.py
index df83ae35..278ba9ef 100644
--- a/src/kernel/types/recommendation_summary.py
+++ b/src/kernel/types/recommendation_summary.py
@@ -12,16 +12,22 @@
class RecommendationSummary(BaseModel):
analysis_id: str
- """ID of the most recently requested analysis for this domain."""
+ """ID of the most recently requested analysis for this exact target."""
analysis_status: Literal["running", "completed", "failed", "canceled"]
- """Lifecycle status of the most recently requested analysis for this domain."""
+ """Lifecycle status of the most recently requested analysis for this exact target."""
last_requested_at: datetime
- """Most recent time the selected project requested an analysis for this domain."""
+ """
+ Most recent time the selected project requested an analysis for this exact
+ target.
+ """
recommendation: Optional[Recommendation] = None
- """Current domain-level recommendation. Null when no eligible knowledge exists."""
+ """Recommendation produced by the latest analysis.
+
+ Null when that analysis did not produce one.
+ """
recommended_config_label: Optional[str] = None
"""Display label for the recommended browser configuration."""
@@ -29,8 +35,11 @@ class RecommendationSummary(BaseModel):
success_rate: Optional[float] = None
"""Success rate for the recommended configuration.
- Null when no eligible knowledge exists.
+ Null when the latest analysis did not produce one.
"""
target: str
- """Registrable domain previously analyzed by the selected project."""
+ """
+ Normalized exact target previously analyzed by the selected project, including
+ scheme, host, port, and path.
+ """
diff --git a/src/kernel/types/vault.py b/src/kernel/types/vault.py
new file mode 100644
index 00000000..210edfbe
--- /dev/null
+++ b/src/kernel/types/vault.py
@@ -0,0 +1,18 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from datetime import datetime
+
+from .._models import BaseModel
+
+__all__ = ["Vault"]
+
+
+class Vault(BaseModel):
+ id: str
+
+ created_at: datetime
+
+ name: str
+ """Immutable name assigned when the vault is created."""
+
+ updated_at: datetime
diff --git a/src/kernel/types/vault_list_params.py b/src/kernel/types/vault_list_params.py
new file mode 100644
index 00000000..7530bc28
--- /dev/null
+++ b/src/kernel/types/vault_list_params.py
@@ -0,0 +1,13 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from __future__ import annotations
+
+from typing_extensions import TypedDict
+
+__all__ = ["VaultListParams"]
+
+
+class VaultListParams(TypedDict, total=False):
+ limit: int
+
+ offset: int
diff --git a/src/kernel/types/vault_reference.py b/src/kernel/types/vault_reference.py
new file mode 100644
index 00000000..f22e7de8
--- /dev/null
+++ b/src/kernel/types/vault_reference.py
@@ -0,0 +1,15 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from typing import Optional
+
+from .._models import BaseModel
+
+__all__ = ["VaultReference"]
+
+
+class VaultReference(BaseModel):
+ """Reference to a project-scoped vault. Provide exactly one of id or name."""
+
+ id: Optional[str] = None
+
+ name: Optional[str] = None
diff --git a/src/kernel/types/vault_reference_param.py b/src/kernel/types/vault_reference_param.py
new file mode 100644
index 00000000..3e8fe9ac
--- /dev/null
+++ b/src/kernel/types/vault_reference_param.py
@@ -0,0 +1,15 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from __future__ import annotations
+
+from typing_extensions import TypedDict
+
+__all__ = ["VaultReferenceParam"]
+
+
+class VaultReferenceParam(TypedDict, total=False):
+ """Reference to a project-scoped vault. Provide exactly one of id or name."""
+
+ id: str
+
+ name: str
diff --git a/src/kernel/types/vault_upsert_params.py b/src/kernel/types/vault_upsert_params.py
new file mode 100644
index 00000000..84be182b
--- /dev/null
+++ b/src/kernel/types/vault_upsert_params.py
@@ -0,0 +1,12 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from __future__ import annotations
+
+from typing_extensions import Required, TypedDict
+
+__all__ = ["VaultUpsertParams"]
+
+
+class VaultUpsertParams(TypedDict, total=False):
+ name: Required[str]
+ """Immutable name used to create or retrieve the vault."""
diff --git a/src/kernel/types/vaults/__init__.py b/src/kernel/types/vaults/__init__.py
new file mode 100644
index 00000000..7ee998eb
--- /dev/null
+++ b/src/kernel/types/vaults/__init__.py
@@ -0,0 +1,23 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from __future__ import annotations
+
+from .vault_item import VaultItem as VaultItem
+from .vault_item_event import VaultItemEvent as VaultItemEvent
+from .vault_item_action import VaultItemAction as VaultItemAction
+from .item_events_params import ItemEventsParams as ItemEventsParams
+from .item_list_response import ItemListResponse as ItemListResponse
+from .item_update_params import ItemUpdateParams as ItemUpdateParams
+from .item_upsert_params import ItemUpsertParams as ItemUpsertParams
+from .vault_card_aliases import VaultCardAliases as VaultCardAliases
+from .card_vault_item_spec import CardVaultItemSpec as CardVaultItemSpec
+from .item_events_response import ItemEventsResponse as ItemEventsResponse
+from .item_retrieve_params import ItemRetrieveParams as ItemRetrieveParams
+from .vault_payment_method import VaultPaymentMethod as VaultPaymentMethod
+from .card_vault_item_state import CardVaultItemState as CardVaultItemState
+from .wallet_vault_item_spec import WalletVaultItemSpec as WalletVaultItemSpec
+from .wallet_vault_item_state import WalletVaultItemState as WalletVaultItemState
+from .card_vault_item_spec_param import CardVaultItemSpecParam as CardVaultItemSpecParam
+from .wallet_vault_item_spec_param import WalletVaultItemSpecParam as WalletVaultItemSpecParam
+from .item_perform_operation_params import ItemPerformOperationParams as ItemPerformOperationParams
+from .agentcard_checkout_authorization import AgentcardCheckoutAuthorization as AgentcardCheckoutAuthorization
diff --git a/src/kernel/types/vaults/agentcard_checkout_authorization.py b/src/kernel/types/vaults/agentcard_checkout_authorization.py
new file mode 100644
index 00000000..2a0dc815
--- /dev/null
+++ b/src/kernel/types/vaults/agentcard_checkout_authorization.py
@@ -0,0 +1,66 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from typing import Optional
+from datetime import datetime
+from typing_extensions import Literal
+
+from ..._models import BaseModel
+
+__all__ = ["AgentcardCheckoutAuthorization"]
+
+
+class AgentcardCheckoutAuthorization(BaseModel):
+ """The in-flight or most recent checkout authorization.
+
+ Present while a checkout is pending approval and after it settles.
+ """
+
+ id: str
+
+ amount_cents: int
+
+ created_at: datetime
+
+ currency: str
+
+ merchant: str
+
+ psp: str
+
+ status: Literal["awaiting_approval", "approved", "declined", "expired"]
+
+ actual_cents: Optional[int] = None
+
+ amount: Optional[str] = None
+ """Display amount shown on the approval screen."""
+
+ amount_authority: Optional[Literal["display_only", "stripe_payment_intent"]] = None
+
+ amount_verified: Optional[bool] = None
+
+ approval_url: Optional[str] = None
+
+ browser_id: Optional[str] = None
+ """Browser session that submitted the checkout."""
+
+ charged_amount_cents: Optional[int] = None
+
+ charged_currency: Optional[str] = None
+
+ charged_kind: Optional[Literal["captured", "authorized", "none"]] = None
+
+ expected_cents: Optional[int] = None
+
+ expires_at: Optional[datetime] = None
+
+ psp_error_code: Optional[str] = None
+
+ reason: Optional[str] = None
+
+ replay_attempted: Optional[bool] = None
+
+ replay_delivered: Optional[bool] = None
+ """Whether the processor response was delivered to the browser."""
+
+ replay_status: Optional[int] = None
+ """HTTP status of the replayed processor response."""
diff --git a/src/kernel/types/vaults/card_vault_item_spec.py b/src/kernel/types/vaults/card_vault_item_spec.py
new file mode 100644
index 00000000..65eebebd
--- /dev/null
+++ b/src/kernel/types/vaults/card_vault_item_spec.py
@@ -0,0 +1,125 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from typing import Dict, List, Union, Optional
+from typing_extensions import Literal, Annotated, TypeAlias
+
+from ..._utils import PropertyInfo
+from ..._models import BaseModel
+
+__all__ = [
+ "CardVaultItemSpec",
+ "LinkCardVaultItemSpec",
+ "LinkCardVaultItemSpecLineItem",
+ "LinkCardVaultItemSpecLineItemTotal",
+ "LinkCardVaultItemSpecTotal",
+ "AgentCardCardVaultItemSpec",
+]
+
+
+class LinkCardVaultItemSpecLineItemTotal(BaseModel):
+ amount: int
+ """Total amount in minor currency units."""
+
+ display_text: str
+
+ type: str
+
+
+class LinkCardVaultItemSpecLineItem(BaseModel):
+ name: str
+
+ description: Optional[str] = None
+
+ image_url: Optional[str] = None
+
+ product_url: Optional[str] = None
+
+ quantity: Optional[int] = None
+
+ sku: Optional[str] = None
+
+ totals: Optional[List[LinkCardVaultItemSpecLineItemTotal]] = None
+
+ unit_amount: Optional[int] = None
+ """Unit amount in minor currency units."""
+
+ url: Optional[str] = None
+
+
+class LinkCardVaultItemSpecTotal(BaseModel):
+ amount: int
+ """Total amount in minor currency units."""
+
+ display_text: str
+
+ type: str
+
+
+class LinkCardVaultItemSpec(BaseModel):
+ amount: int
+ """Integer amount in minor currency units."""
+
+ context: str
+
+ currency: str
+
+ merchant_name: str
+
+ merchant_url: str
+
+ payment_method_id: str
+ """Payment-method ID returned by the referenced wallet's payment-method listing.
+
+ The provider decides whether the selected funding method can satisfy the card
+ request.
+ """
+
+ provider: Literal["link"]
+
+ test: bool
+ """
+ Whether Link should return test credentials instead of a live payment
+ credential.
+ """
+
+ wallet: str
+ """Wallet item key used to mint this card."""
+
+ expires_at: Optional[int] = None
+
+ line_items: Optional[List[LinkCardVaultItemSpecLineItem]] = None
+
+ metadata: Optional[Dict[str, str]] = None
+
+ totals: Optional[List[LinkCardVaultItemSpecTotal]] = None
+
+
+class AgentCardCardVaultItemSpec(BaseModel):
+ """AgentCard reusable card.
+
+ Each checkout creates an approval-gated authorization for spec.merchant / spec.amount. The card stays ready after each authorization.
+ """
+
+ amount: int
+ """Integer amount in minor currency units."""
+
+ currency: str
+
+ merchant: str
+ """Merchant name shown on the cardholder's approval screen."""
+
+ provider: Literal["agentcard"]
+
+ wallet: str
+ """Wallet item key used to authorize checkouts."""
+
+ card_id: Optional[str] = None
+ """AgentCard vaulted card to pay with.
+
+ Omitted, the cardholder picks on the approval screen.
+ """
+
+
+CardVaultItemSpec: TypeAlias = Annotated[
+ Union[LinkCardVaultItemSpec, AgentCardCardVaultItemSpec], PropertyInfo(discriminator="provider")
+]
diff --git a/src/kernel/types/vaults/card_vault_item_spec_param.py b/src/kernel/types/vaults/card_vault_item_spec_param.py
new file mode 100644
index 00000000..22b81433
--- /dev/null
+++ b/src/kernel/types/vaults/card_vault_item_spec_param.py
@@ -0,0 +1,122 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from __future__ import annotations
+
+from typing import Dict, Union, Iterable
+from typing_extensions import Literal, Required, TypeAlias, TypedDict
+
+__all__ = [
+ "CardVaultItemSpecParam",
+ "LinkCardVaultItemSpec",
+ "LinkCardVaultItemSpecLineItem",
+ "LinkCardVaultItemSpecLineItemTotal",
+ "LinkCardVaultItemSpecTotal",
+ "AgentCardCardVaultItemSpec",
+]
+
+
+class LinkCardVaultItemSpecLineItemTotal(TypedDict, total=False):
+ amount: Required[int]
+ """Total amount in minor currency units."""
+
+ display_text: Required[str]
+
+ type: Required[str]
+
+
+class LinkCardVaultItemSpecLineItem(TypedDict, total=False):
+ name: Required[str]
+
+ description: str
+
+ image_url: str
+
+ product_url: str
+
+ quantity: int
+
+ sku: str
+
+ totals: Iterable[LinkCardVaultItemSpecLineItemTotal]
+
+ unit_amount: int
+ """Unit amount in minor currency units."""
+
+ url: str
+
+
+class LinkCardVaultItemSpecTotal(TypedDict, total=False):
+ amount: Required[int]
+ """Total amount in minor currency units."""
+
+ display_text: Required[str]
+
+ type: Required[str]
+
+
+class LinkCardVaultItemSpec(TypedDict, total=False):
+ amount: Required[int]
+ """Integer amount in minor currency units."""
+
+ context: Required[str]
+
+ currency: Required[str]
+
+ merchant_name: Required[str]
+
+ merchant_url: Required[str]
+
+ payment_method_id: Required[str]
+ """Payment-method ID returned by the referenced wallet's payment-method listing.
+
+ The provider decides whether the selected funding method can satisfy the card
+ request.
+ """
+
+ provider: Required[Literal["link"]]
+
+ test: Required[bool]
+ """
+ Whether Link should return test credentials instead of a live payment
+ credential.
+ """
+
+ wallet: Required[str]
+ """Wallet item key used to mint this card."""
+
+ expires_at: int
+
+ line_items: Iterable[LinkCardVaultItemSpecLineItem]
+
+ metadata: Dict[str, str]
+
+ totals: Iterable[LinkCardVaultItemSpecTotal]
+
+
+class AgentCardCardVaultItemSpec(TypedDict, total=False):
+ """AgentCard reusable card.
+
+ Each checkout creates an approval-gated authorization for spec.merchant / spec.amount. The card stays ready after each authorization.
+ """
+
+ amount: Required[int]
+ """Integer amount in minor currency units."""
+
+ currency: Required[str]
+
+ merchant: Required[str]
+ """Merchant name shown on the cardholder's approval screen."""
+
+ provider: Required[Literal["agentcard"]]
+
+ wallet: Required[str]
+ """Wallet item key used to authorize checkouts."""
+
+ card_id: str
+ """AgentCard vaulted card to pay with.
+
+ Omitted, the cardholder picks on the approval screen.
+ """
+
+
+CardVaultItemSpecParam: TypeAlias = Union[LinkCardVaultItemSpec, AgentCardCardVaultItemSpec]
diff --git a/src/kernel/types/vaults/card_vault_item_state.py b/src/kernel/types/vaults/card_vault_item_state.py
new file mode 100644
index 00000000..4808d1ff
--- /dev/null
+++ b/src/kernel/types/vaults/card_vault_item_state.py
@@ -0,0 +1,86 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from typing import TYPE_CHECKING, Dict, List, Union, Optional
+from typing_extensions import Literal, Annotated, TypeAlias
+
+from pydantic import Field as FieldInfo
+
+from ..._utils import PropertyInfo
+from ..._models import BaseModel
+from .vault_card_aliases import VaultCardAliases
+from .agentcard_checkout_authorization import AgentcardCheckoutAuthorization
+
+__all__ = ["CardVaultItemState", "LinkCardState", "LinkCardStateMasks", "AgentCardCardState", "AgentCardCardStateMasks"]
+
+
+class LinkCardStateMasks(BaseModel):
+ brand: Optional[str] = None
+
+ last4: Optional[str] = None
+
+ if TYPE_CHECKING:
+ # Some versions of Pydantic <2.8.0 have a bug and don’t allow assigning a
+ # value to this field, so for compatibility we avoid doing it at runtime.
+ __pydantic_extra__: Dict[str, str] = FieldInfo(init=False) # pyright: ignore[reportIncompatibleVariableOverride]
+
+ # Stub to indicate that arbitrary properties are accepted.
+ # To access properties that are not valid identifiers you can use `getattr`, e.g.
+ # `getattr(obj, '$type')`
+ def __getattr__(self, attr: str) -> str: ...
+ else:
+ __pydantic_extra__: Dict[str, str]
+
+
+class LinkCardState(BaseModel):
+ provider: Literal["link"]
+
+ status: Literal["requested", "pending_authorization", "ready", "consumed", "expired", "declined"]
+
+ aliases: Optional[VaultCardAliases] = None
+
+ domains: Optional[List[str]] = None
+
+ masks: Optional[LinkCardStateMasks] = None
+
+ status_reason: Optional[str] = None
+
+
+class AgentCardCardStateMasks(BaseModel):
+ brand: Optional[str] = None
+
+ last4: Optional[str] = None
+
+ if TYPE_CHECKING:
+ # Some versions of Pydantic <2.8.0 have a bug and don’t allow assigning a
+ # value to this field, so for compatibility we avoid doing it at runtime.
+ __pydantic_extra__: Dict[str, str] = FieldInfo(init=False) # pyright: ignore[reportIncompatibleVariableOverride]
+
+ # Stub to indicate that arbitrary properties are accepted.
+ # To access properties that are not valid identifiers you can use `getattr`, e.g.
+ # `getattr(obj, '$type')`
+ def __getattr__(self, attr: str) -> str: ...
+ else:
+ __pydantic_extra__: Dict[str, str]
+
+
+class AgentCardCardState(BaseModel):
+ provider: Literal["agentcard"]
+
+ status: Literal["requested", "ready", "pending_approval", "degraded"]
+
+ aliases: Optional[VaultCardAliases] = None
+
+ authorization: Optional[AgentcardCheckoutAuthorization] = None
+ """The in-flight or most recent checkout authorization.
+
+ Present while a checkout is pending approval and after it settles.
+ """
+
+ masks: Optional[AgentCardCardStateMasks] = None
+
+ status_reason: Optional[str] = None
+
+
+CardVaultItemState: TypeAlias = Annotated[
+ Union[LinkCardState, AgentCardCardState], PropertyInfo(discriminator="provider")
+]
diff --git a/src/kernel/types/vaults/item_events_params.py b/src/kernel/types/vaults/item_events_params.py
new file mode 100644
index 00000000..cee33414
--- /dev/null
+++ b/src/kernel/types/vaults/item_events_params.py
@@ -0,0 +1,17 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from __future__ import annotations
+
+from typing_extensions import Required, TypedDict
+
+__all__ = ["ItemEventsParams"]
+
+
+class ItemEventsParams(TypedDict, total=False):
+ id_or_name: Required[str]
+
+ after: str
+ """Return events after this event ID."""
+
+ wait: int
+ """Long-poll for new events for up to this many seconds."""
diff --git a/src/kernel/types/vaults/item_events_response.py b/src/kernel/types/vaults/item_events_response.py
new file mode 100644
index 00000000..fa1959fe
--- /dev/null
+++ b/src/kernel/types/vaults/item_events_response.py
@@ -0,0 +1,10 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from typing import List
+from typing_extensions import TypeAlias
+
+from .vault_item_event import VaultItemEvent
+
+__all__ = ["ItemEventsResponse"]
+
+ItemEventsResponse: TypeAlias = List[VaultItemEvent]
diff --git a/src/kernel/types/vaults/item_list_response.py b/src/kernel/types/vaults/item_list_response.py
new file mode 100644
index 00000000..9bc1fb58
--- /dev/null
+++ b/src/kernel/types/vaults/item_list_response.py
@@ -0,0 +1,10 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from typing import List
+from typing_extensions import TypeAlias
+
+from .vault_item import VaultItem
+
+__all__ = ["ItemListResponse"]
+
+ItemListResponse: TypeAlias = List[VaultItem]
diff --git a/src/kernel/types/vaults/item_perform_operation_params.py b/src/kernel/types/vaults/item_perform_operation_params.py
new file mode 100644
index 00000000..9d62f380
--- /dev/null
+++ b/src/kernel/types/vaults/item_perform_operation_params.py
@@ -0,0 +1,13 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from __future__ import annotations
+
+from typing_extensions import Literal, Required, TypedDict
+
+__all__ = ["ItemPerformOperationParams"]
+
+
+class ItemPerformOperationParams(TypedDict, total=False):
+ id_or_name: Required[str]
+
+ type: Required[Literal["authorize"]]
diff --git a/src/kernel/types/vaults/item_retrieve_params.py b/src/kernel/types/vaults/item_retrieve_params.py
new file mode 100644
index 00000000..976881b2
--- /dev/null
+++ b/src/kernel/types/vaults/item_retrieve_params.py
@@ -0,0 +1,21 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from __future__ import annotations
+
+from typing import List
+from typing_extensions import Literal, Required, TypedDict
+
+__all__ = ["ItemRetrieveParams"]
+
+
+class ItemRetrieveParams(TypedDict, total=False):
+ id_or_name: Required[str]
+
+ expand: List[Literal["payment_methods"]]
+ """Live fields advertised by `available_expansions` to include in `expanded`."""
+
+ wait: int
+ """
+ Hold for up to this many seconds while the item is pending authorization or
+ approval.
+ """
diff --git a/src/kernel/types/vaults/item_update_params.py b/src/kernel/types/vaults/item_update_params.py
new file mode 100644
index 00000000..38604305
--- /dev/null
+++ b/src/kernel/types/vaults/item_update_params.py
@@ -0,0 +1,20 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from __future__ import annotations
+
+from typing_extensions import Required, TypedDict
+
+from .card_vault_item_spec_param import CardVaultItemSpecParam
+
+__all__ = ["ItemUpdateParams"]
+
+
+class ItemUpdateParams(TypedDict, total=False):
+ id_or_name: Required[str]
+
+ spec: Required[CardVaultItemSpecParam]
+ """AgentCard reusable card.
+
+ Each checkout creates an approval-gated authorization for spec.merchant /
+ spec.amount. The card stays ready after each authorization.
+ """
diff --git a/src/kernel/types/vaults/item_upsert_params.py b/src/kernel/types/vaults/item_upsert_params.py
new file mode 100644
index 00000000..5f1a4b0e
--- /dev/null
+++ b/src/kernel/types/vaults/item_upsert_params.py
@@ -0,0 +1,41 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from __future__ import annotations
+
+from typing import Union
+from typing_extensions import Literal, Required, TypeAlias, TypedDict
+
+from .card_vault_item_spec_param import CardVaultItemSpecParam
+from .wallet_vault_item_spec_param import WalletVaultItemSpecParam
+
+__all__ = ["ItemUpsertParams", "WalletVaultItemRequest", "CardVaultItemRequest"]
+
+
+class WalletVaultItemRequest(TypedDict, total=False):
+ id_or_name: Required[str]
+
+ spec: Required[WalletVaultItemSpecParam]
+ """AgentCard wallet.
+
+ Mode (sandbox vs live) is fixed by the deployment's AgentCard credential; there
+ is no per-item test flag. user_id may only reference a user already enrolled by
+ a wallet in this organization.
+ """
+
+ type: Required[Literal["wallet"]]
+
+
+class CardVaultItemRequest(TypedDict, total=False):
+ id_or_name: Required[str]
+
+ spec: Required[CardVaultItemSpecParam]
+ """AgentCard reusable card.
+
+ Each checkout creates an approval-gated authorization for spec.merchant /
+ spec.amount. The card stays ready after each authorization.
+ """
+
+ type: Required[Literal["card"]]
+
+
+ItemUpsertParams: TypeAlias = Union[WalletVaultItemRequest, CardVaultItemRequest]
diff --git a/src/kernel/types/vaults/vault_card_aliases.py b/src/kernel/types/vaults/vault_card_aliases.py
new file mode 100644
index 00000000..149de39f
--- /dev/null
+++ b/src/kernel/types/vaults/vault_card_aliases.py
@@ -0,0 +1,15 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from ..._models import BaseModel
+
+__all__ = ["VaultCardAliases"]
+
+
+class VaultCardAliases(BaseModel):
+ cvc: str
+
+ exp_month: str
+
+ exp_year: str
+
+ number: str
diff --git a/src/kernel/types/vaults/vault_item.py b/src/kernel/types/vaults/vault_item.py
new file mode 100644
index 00000000..8418f0bc
--- /dev/null
+++ b/src/kernel/types/vaults/vault_item.py
@@ -0,0 +1,140 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from typing import List, Union, Optional
+from datetime import datetime
+from typing_extensions import Literal, Annotated, TypeAlias
+
+from ..._utils import PropertyInfo
+from ..._models import BaseModel
+from .vault_item_action import VaultItemAction
+from .card_vault_item_spec import CardVaultItemSpec
+from .vault_payment_method import VaultPaymentMethod
+from .card_vault_item_state import CardVaultItemState
+from .wallet_vault_item_spec import WalletVaultItemSpec
+from .wallet_vault_item_state import WalletVaultItemState
+
+__all__ = [
+ "VaultItem",
+ "WalletVaultItem",
+ "WalletVaultItemAvailableExpansion",
+ "WalletVaultItemAvailableOperation",
+ "WalletVaultItemExpanded",
+ "CardVaultItem",
+ "CardVaultItemAvailableExpansion",
+ "CardVaultItemAvailableOperation",
+]
+
+
+class WalletVaultItemAvailableExpansion(BaseModel):
+ """
+ Live data that can currently be requested by passing its type to the item GET expand parameter.
+ """
+
+ description: str
+
+ type: Literal["payment_methods"]
+
+
+class WalletVaultItemAvailableOperation(BaseModel):
+ """An operation that is currently valid for this item.
+
+ Read the description before invoking it through the item operations endpoint.
+ """
+
+ description: str
+
+ type: Literal["authorize"]
+
+
+class WalletVaultItemExpanded(BaseModel):
+ """Live, non-persisted data requested through the item GET expand parameter."""
+
+ payment_methods: Optional[List[VaultPaymentMethod]] = None
+
+
+class WalletVaultItem(BaseModel):
+ id: str
+
+ available_expansions: List[WalletVaultItemAvailableExpansion]
+
+ available_operations: List[WalletVaultItemAvailableOperation]
+
+ created_at: datetime
+
+ key: str
+ """Immutable item key assigned when the item is created."""
+
+ spec: WalletVaultItemSpec
+ """AgentCard wallet.
+
+ Mode (sandbox vs live) is fixed by the deployment's AgentCard credential; there
+ is no per-item test flag. user_id may only reference a user already enrolled by
+ a wallet in this organization.
+ """
+
+ state: WalletVaultItemState
+
+ type: Literal["wallet"]
+
+ updated_at: datetime
+
+ action: Optional[VaultItemAction] = None
+
+ expanded: Optional[WalletVaultItemExpanded] = None
+ """Live, non-persisted data requested through the item GET expand parameter."""
+
+ expires_at: Optional[datetime] = None
+
+
+class CardVaultItemAvailableExpansion(BaseModel):
+ """
+ Live data that can currently be requested by passing its type to the item GET expand parameter.
+ """
+
+ description: str
+
+ type: Literal["payment_methods"]
+
+
+class CardVaultItemAvailableOperation(BaseModel):
+ """An operation that is currently valid for this item.
+
+ Read the description before invoking it through the item operations endpoint.
+ """
+
+ description: str
+
+ type: Literal["authorize"]
+
+
+class CardVaultItem(BaseModel):
+ id: str
+
+ available_expansions: List[CardVaultItemAvailableExpansion]
+
+ available_operations: List[CardVaultItemAvailableOperation]
+
+ created_at: datetime
+
+ key: str
+ """Immutable item key assigned when the item is created."""
+
+ spec: CardVaultItemSpec
+ """AgentCard reusable card.
+
+ Each checkout creates an approval-gated authorization for spec.merchant /
+ spec.amount. The card stays ready after each authorization.
+ """
+
+ state: CardVaultItemState
+
+ type: Literal["card"]
+
+ updated_at: datetime
+
+ action: Optional[VaultItemAction] = None
+
+ expires_at: Optional[datetime] = None
+
+
+VaultItem: TypeAlias = Annotated[Union[WalletVaultItem, CardVaultItem], PropertyInfo(discriminator="type")]
diff --git a/src/kernel/types/vaults/vault_item_action.py b/src/kernel/types/vaults/vault_item_action.py
new file mode 100644
index 00000000..9054447e
--- /dev/null
+++ b/src/kernel/types/vaults/vault_item_action.py
@@ -0,0 +1,66 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from typing import Union
+from typing_extensions import Literal, Annotated, TypeAlias
+
+from ..._utils import PropertyInfo
+from ..._models import BaseModel
+
+__all__ = [
+ "VaultItemAction",
+ "LinkOAuthAction",
+ "SpendApprovalAction",
+ "PushApprovalAction",
+ "CollectAction",
+ "MfaAction",
+ "EmbeddedCeremonyAction",
+ "CardEnrollmentAction",
+]
+
+
+class LinkOAuthAction(BaseModel):
+ name: Literal["link_oauth"]
+
+ url: str
+
+
+class SpendApprovalAction(BaseModel):
+ name: Literal["spend_approval"]
+
+ url: str
+
+
+class PushApprovalAction(BaseModel):
+ name: Literal["push_approval"]
+
+
+class CollectAction(BaseModel):
+ name: Literal["collect"]
+
+
+class MfaAction(BaseModel):
+ name: Literal["mfa"]
+
+
+class EmbeddedCeremonyAction(BaseModel):
+ name: Literal["embedded_ceremony"]
+
+
+class CardEnrollmentAction(BaseModel):
+ name: Literal["card_enrollment"]
+
+ url: str
+
+
+VaultItemAction: TypeAlias = Annotated[
+ Union[
+ LinkOAuthAction,
+ SpendApprovalAction,
+ PushApprovalAction,
+ CollectAction,
+ MfaAction,
+ EmbeddedCeremonyAction,
+ CardEnrollmentAction,
+ ],
+ PropertyInfo(discriminator="name"),
+]
diff --git a/src/kernel/types/vaults/vault_item_event.py b/src/kernel/types/vaults/vault_item_event.py
new file mode 100644
index 00000000..2d4098be
--- /dev/null
+++ b/src/kernel/types/vaults/vault_item_event.py
@@ -0,0 +1,21 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from typing import Dict, Optional
+from datetime import datetime
+
+from ..._models import BaseModel
+
+__all__ = ["VaultItemEvent"]
+
+
+class VaultItemEvent(BaseModel):
+ id: str
+
+ created_at: datetime
+
+ name: str
+
+ browser_id: Optional[str] = None
+ """Browser session associated with the event, when applicable."""
+
+ data: Optional[Dict[str, object]] = None
diff --git a/src/kernel/types/vaults/vault_payment_method.py b/src/kernel/types/vaults/vault_payment_method.py
new file mode 100644
index 00000000..27f87ad1
--- /dev/null
+++ b/src/kernel/types/vaults/vault_payment_method.py
@@ -0,0 +1,51 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from typing import List, Optional
+
+from ..._models import BaseModel
+
+__all__ = ["VaultPaymentMethod", "Capabilities", "CapabilitiesSingleUseCard", "Display"]
+
+
+class CapabilitiesSingleUseCard(BaseModel):
+ eligible: bool
+
+ reasons: List[str]
+
+
+class Capabilities(BaseModel):
+ """Provider-reported advisory capabilities.
+
+ A missing capability is unknown, not ineligible; only eligible=false is an explicit negative signal.
+ """
+
+ single_use_card: Optional[CapabilitiesSingleUseCard] = None
+
+
+class Display(BaseModel):
+ brand: Optional[str] = None
+
+ label: Optional[str] = None
+
+ last4: Optional[str] = None
+
+
+class VaultPaymentMethod(BaseModel):
+ id: str
+
+ capabilities: Capabilities
+ """Provider-reported advisory capabilities.
+
+ A missing capability is unknown, not ineligible; only eligible=false is an
+ explicit negative signal.
+ """
+
+ display: Display
+
+ is_default: bool
+
+ provider: str
+ """Provider that issued this payment-method ID."""
+
+ type: str
+ """Provider-neutral payment-method type normalized to lowercase."""
diff --git a/src/kernel/types/vaults/wallet_vault_item_spec.py b/src/kernel/types/vaults/wallet_vault_item_spec.py
new file mode 100644
index 00000000..03176670
--- /dev/null
+++ b/src/kernel/types/vaults/wallet_vault_item_spec.py
@@ -0,0 +1,47 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from typing import Union, Optional
+from typing_extensions import Literal, Annotated, TypeAlias
+
+from ..._utils import PropertyInfo
+from ..._models import BaseModel
+
+__all__ = [
+ "WalletVaultItemSpec",
+ "LinkWalletVaultItemSpec",
+ "LinkWalletVaultItemSpecAuthorization",
+ "LinkWalletVaultItemSpecAuthorizationClient",
+ "AgentCardWalletVaultItemSpec",
+]
+
+
+class LinkWalletVaultItemSpecAuthorizationClient(BaseModel):
+ type: Literal["kernel_managed"]
+
+
+class LinkWalletVaultItemSpecAuthorization(BaseModel):
+ client: LinkWalletVaultItemSpecAuthorizationClient
+
+ method: Literal["oauth"]
+
+
+class LinkWalletVaultItemSpec(BaseModel):
+ authorization: LinkWalletVaultItemSpecAuthorization
+
+ provider: Literal["link"]
+
+
+class AgentCardWalletVaultItemSpec(BaseModel):
+ """AgentCard wallet.
+
+ Mode (sandbox vs live) is fixed by the deployment's AgentCard credential; there is no per-item test flag. user_id may only reference a user already enrolled by a wallet in this organization.
+ """
+
+ provider: Literal["agentcard"]
+
+ user_id: Optional[str] = None
+
+
+WalletVaultItemSpec: TypeAlias = Annotated[
+ Union[LinkWalletVaultItemSpec, AgentCardWalletVaultItemSpec], PropertyInfo(discriminator="provider")
+]
diff --git a/src/kernel/types/vaults/wallet_vault_item_spec_param.py b/src/kernel/types/vaults/wallet_vault_item_spec_param.py
new file mode 100644
index 00000000..dc30e054
--- /dev/null
+++ b/src/kernel/types/vaults/wallet_vault_item_spec_param.py
@@ -0,0 +1,44 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from __future__ import annotations
+
+from typing import Union
+from typing_extensions import Literal, Required, TypeAlias, TypedDict
+
+__all__ = [
+ "WalletVaultItemSpecParam",
+ "LinkWalletVaultItemSpec",
+ "LinkWalletVaultItemSpecAuthorization",
+ "LinkWalletVaultItemSpecAuthorizationClient",
+ "AgentCardWalletVaultItemSpec",
+]
+
+
+class LinkWalletVaultItemSpecAuthorizationClient(TypedDict, total=False):
+ type: Required[Literal["kernel_managed"]]
+
+
+class LinkWalletVaultItemSpecAuthorization(TypedDict, total=False):
+ client: Required[LinkWalletVaultItemSpecAuthorizationClient]
+
+ method: Required[Literal["oauth"]]
+
+
+class LinkWalletVaultItemSpec(TypedDict, total=False):
+ authorization: Required[LinkWalletVaultItemSpecAuthorization]
+
+ provider: Required[Literal["link"]]
+
+
+class AgentCardWalletVaultItemSpec(TypedDict, total=False):
+ """AgentCard wallet.
+
+ Mode (sandbox vs live) is fixed by the deployment's AgentCard credential; there is no per-item test flag. user_id may only reference a user already enrolled by a wallet in this organization.
+ """
+
+ provider: Required[Literal["agentcard"]]
+
+ user_id: str
+
+
+WalletVaultItemSpecParam: TypeAlias = Union[LinkWalletVaultItemSpec, AgentCardWalletVaultItemSpec]
diff --git a/src/kernel/types/vaults/wallet_vault_item_state.py b/src/kernel/types/vaults/wallet_vault_item_state.py
new file mode 100644
index 00000000..9b76f419
--- /dev/null
+++ b/src/kernel/types/vaults/wallet_vault_item_state.py
@@ -0,0 +1,33 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from typing import Union, Optional
+from typing_extensions import Literal, Annotated, TypeAlias
+
+from ..._utils import PropertyInfo
+from ..._models import BaseModel
+
+__all__ = ["WalletVaultItemState", "LinkWalletState", "AgentCardWalletState"]
+
+
+class LinkWalletState(BaseModel):
+ provider: Literal["link"]
+
+ status: Literal["pending_authorization", "connected", "declined", "reconnect_required", "degraded"]
+
+ status_reason: Optional[str] = None
+
+
+class AgentCardWalletState(BaseModel):
+ provider: Literal["agentcard"]
+
+ status: Literal["pending_authorization", "connected", "degraded"]
+
+ status_reason: Optional[str] = None
+
+ user_id: Optional[str] = None
+ """AgentCard user id linked to this wallet. Present once connected."""
+
+
+WalletVaultItemState: TypeAlias = Annotated[
+ Union[LinkWalletState, AgentCardWalletState], PropertyInfo(discriminator="provider")
+]
diff --git a/tests/api_resources/test_browsers.py b/tests/api_resources/test_browsers.py
index 3d0a3eb6..406b0ce8 100644
--- a/tests/api_resources/test_browsers.py
+++ b/tests/api_resources/test_browsers.py
@@ -94,6 +94,12 @@ def test_method_create_with_all_params(self, client: Kernel) -> None:
},
},
timeout_seconds=10,
+ vaults=[
+ {
+ "id": "id",
+ "name": "x",
+ }
+ ],
viewport={
"height": 800,
"width": 1280,
@@ -561,6 +567,12 @@ async def test_method_create_with_all_params(self, async_client: AsyncKernel) ->
},
},
timeout_seconds=10,
+ vaults=[
+ {
+ "id": "id",
+ "name": "x",
+ }
+ ],
viewport={
"height": 800,
"width": 1280,
diff --git a/tests/api_resources/test_vaults.py b/tests/api_resources/test_vaults.py
new file mode 100644
index 00000000..0836a7f8
--- /dev/null
+++ b/tests/api_resources/test_vaults.py
@@ -0,0 +1,335 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from __future__ import annotations
+
+import os
+from typing import Any, cast
+
+import pytest
+
+from kernel import Kernel, AsyncKernel
+from tests.utils import assert_matches_type
+from kernel.types import Vault
+from kernel.pagination import SyncOffsetPagination, AsyncOffsetPagination
+
+base_url = os.environ.get("TEST_API_BASE_URL", "http://127.0.0.1:4010")
+
+
+class TestVaults:
+ parametrize = pytest.mark.parametrize("client", [False, True], indirect=True, ids=["loose", "strict"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ def test_method_retrieve(self, client: Kernel) -> None:
+ vault = client.vaults.retrieve(
+ "id_or_name",
+ )
+ assert_matches_type(Vault, vault, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ def test_raw_response_retrieve(self, client: Kernel) -> None:
+ response = client.vaults.with_raw_response.retrieve(
+ "id_or_name",
+ )
+
+ assert response.is_closed is True
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+ vault = response.parse()
+ assert_matches_type(Vault, vault, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ def test_streaming_response_retrieve(self, client: Kernel) -> None:
+ with client.vaults.with_streaming_response.retrieve(
+ "id_or_name",
+ ) as response:
+ assert not response.is_closed
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+
+ vault = response.parse()
+ assert_matches_type(Vault, vault, path=["response"])
+
+ assert cast(Any, response.is_closed) is True
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ def test_path_params_retrieve(self, client: Kernel) -> None:
+ with pytest.raises(ValueError, match=r"Expected a non-empty value for `id_or_name` but received ''"):
+ client.vaults.with_raw_response.retrieve(
+ "",
+ )
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ def test_method_list(self, client: Kernel) -> None:
+ vault = client.vaults.list()
+ assert_matches_type(SyncOffsetPagination[Vault], vault, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ def test_method_list_with_all_params(self, client: Kernel) -> None:
+ vault = client.vaults.list(
+ limit=1,
+ offset=0,
+ )
+ assert_matches_type(SyncOffsetPagination[Vault], vault, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ def test_raw_response_list(self, client: Kernel) -> None:
+ response = client.vaults.with_raw_response.list()
+
+ assert response.is_closed is True
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+ vault = response.parse()
+ assert_matches_type(SyncOffsetPagination[Vault], vault, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ def test_streaming_response_list(self, client: Kernel) -> None:
+ with client.vaults.with_streaming_response.list() as response:
+ assert not response.is_closed
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+
+ vault = response.parse()
+ assert_matches_type(SyncOffsetPagination[Vault], vault, path=["response"])
+
+ assert cast(Any, response.is_closed) is True
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ def test_method_delete(self, client: Kernel) -> None:
+ vault = client.vaults.delete(
+ "id_or_name",
+ )
+ assert vault is None
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ def test_raw_response_delete(self, client: Kernel) -> None:
+ response = client.vaults.with_raw_response.delete(
+ "id_or_name",
+ )
+
+ assert response.is_closed is True
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+ vault = response.parse()
+ assert vault is None
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ def test_streaming_response_delete(self, client: Kernel) -> None:
+ with client.vaults.with_streaming_response.delete(
+ "id_or_name",
+ ) as response:
+ assert not response.is_closed
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+
+ vault = response.parse()
+ assert vault is None
+
+ assert cast(Any, response.is_closed) is True
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ def test_path_params_delete(self, client: Kernel) -> None:
+ with pytest.raises(ValueError, match=r"Expected a non-empty value for `id_or_name` but received ''"):
+ client.vaults.with_raw_response.delete(
+ "",
+ )
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ def test_method_upsert(self, client: Kernel) -> None:
+ vault = client.vaults.upsert(
+ name="name",
+ )
+ assert_matches_type(Vault, vault, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ def test_raw_response_upsert(self, client: Kernel) -> None:
+ response = client.vaults.with_raw_response.upsert(
+ name="name",
+ )
+
+ assert response.is_closed is True
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+ vault = response.parse()
+ assert_matches_type(Vault, vault, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ def test_streaming_response_upsert(self, client: Kernel) -> None:
+ with client.vaults.with_streaming_response.upsert(
+ name="name",
+ ) as response:
+ assert not response.is_closed
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+
+ vault = response.parse()
+ assert_matches_type(Vault, vault, path=["response"])
+
+ assert cast(Any, response.is_closed) is True
+
+
+class TestAsyncVaults:
+ parametrize = pytest.mark.parametrize(
+ "async_client", [False, True, {"http_client": "aiohttp"}], indirect=True, ids=["loose", "strict", "aiohttp"]
+ )
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ async def test_method_retrieve(self, async_client: AsyncKernel) -> None:
+ vault = await async_client.vaults.retrieve(
+ "id_or_name",
+ )
+ assert_matches_type(Vault, vault, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ async def test_raw_response_retrieve(self, async_client: AsyncKernel) -> None:
+ response = await async_client.vaults.with_raw_response.retrieve(
+ "id_or_name",
+ )
+
+ assert response.is_closed is True
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+ vault = await response.parse()
+ assert_matches_type(Vault, vault, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ async def test_streaming_response_retrieve(self, async_client: AsyncKernel) -> None:
+ async with async_client.vaults.with_streaming_response.retrieve(
+ "id_or_name",
+ ) as response:
+ assert not response.is_closed
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+
+ vault = await response.parse()
+ assert_matches_type(Vault, vault, 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_retrieve(self, async_client: AsyncKernel) -> None:
+ with pytest.raises(ValueError, match=r"Expected a non-empty value for `id_or_name` but received ''"):
+ await async_client.vaults.with_raw_response.retrieve(
+ "",
+ )
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ async def test_method_list(self, async_client: AsyncKernel) -> None:
+ vault = await async_client.vaults.list()
+ assert_matches_type(AsyncOffsetPagination[Vault], vault, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ async def test_method_list_with_all_params(self, async_client: AsyncKernel) -> None:
+ vault = await async_client.vaults.list(
+ limit=1,
+ offset=0,
+ )
+ assert_matches_type(AsyncOffsetPagination[Vault], vault, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ async def test_raw_response_list(self, async_client: AsyncKernel) -> None:
+ response = await async_client.vaults.with_raw_response.list()
+
+ assert response.is_closed is True
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+ vault = await response.parse()
+ assert_matches_type(AsyncOffsetPagination[Vault], vault, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ async def test_streaming_response_list(self, async_client: AsyncKernel) -> None:
+ async with async_client.vaults.with_streaming_response.list() as response:
+ assert not response.is_closed
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+
+ vault = await response.parse()
+ assert_matches_type(AsyncOffsetPagination[Vault], vault, path=["response"])
+
+ assert cast(Any, response.is_closed) is True
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ async def test_method_delete(self, async_client: AsyncKernel) -> None:
+ vault = await async_client.vaults.delete(
+ "id_or_name",
+ )
+ assert vault is None
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ async def test_raw_response_delete(self, async_client: AsyncKernel) -> None:
+ response = await async_client.vaults.with_raw_response.delete(
+ "id_or_name",
+ )
+
+ assert response.is_closed is True
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+ vault = await response.parse()
+ assert vault is None
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ async def test_streaming_response_delete(self, async_client: AsyncKernel) -> None:
+ async with async_client.vaults.with_streaming_response.delete(
+ "id_or_name",
+ ) as response:
+ assert not response.is_closed
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+
+ vault = await response.parse()
+ assert vault is None
+
+ assert cast(Any, response.is_closed) is True
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ async def test_path_params_delete(self, async_client: AsyncKernel) -> None:
+ with pytest.raises(ValueError, match=r"Expected a non-empty value for `id_or_name` but received ''"):
+ await async_client.vaults.with_raw_response.delete(
+ "",
+ )
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ async def test_method_upsert(self, async_client: AsyncKernel) -> None:
+ vault = await async_client.vaults.upsert(
+ name="name",
+ )
+ assert_matches_type(Vault, vault, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ async def test_raw_response_upsert(self, async_client: AsyncKernel) -> None:
+ response = await async_client.vaults.with_raw_response.upsert(
+ name="name",
+ )
+
+ assert response.is_closed is True
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+ vault = await response.parse()
+ assert_matches_type(Vault, vault, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ async def test_streaming_response_upsert(self, async_client: AsyncKernel) -> None:
+ async with async_client.vaults.with_streaming_response.upsert(
+ name="name",
+ ) as response:
+ assert not response.is_closed
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+
+ vault = await response.parse()
+ assert_matches_type(Vault, vault, path=["response"])
+
+ assert cast(Any, response.is_closed) is True
diff --git a/tests/api_resources/vaults/__init__.py b/tests/api_resources/vaults/__init__.py
new file mode 100644
index 00000000..fd8019a9
--- /dev/null
+++ b/tests/api_resources/vaults/__init__.py
@@ -0,0 +1 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
diff --git a/tests/api_resources/vaults/test_items.py b/tests/api_resources/vaults/test_items.py
new file mode 100644
index 00000000..9d0f9ec6
--- /dev/null
+++ b/tests/api_resources/vaults/test_items.py
@@ -0,0 +1,1432 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from __future__ import annotations
+
+import os
+from typing import Any, cast
+
+import pytest
+
+from kernel import Kernel, AsyncKernel
+from tests.utils import assert_matches_type
+from kernel.types.vaults import (
+ VaultItem,
+ ItemListResponse,
+ ItemEventsResponse,
+)
+
+base_url = os.environ.get("TEST_API_BASE_URL", "http://127.0.0.1:4010")
+
+
+class TestItems:
+ parametrize = pytest.mark.parametrize("client", [False, True], indirect=True, ids=["loose", "strict"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ def test_method_retrieve(self, client: Kernel) -> None:
+ item = client.vaults.items.retrieve(
+ key="x",
+ id_or_name="id_or_name",
+ )
+ assert_matches_type(VaultItem, item, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ def test_method_retrieve_with_all_params(self, client: Kernel) -> None:
+ item = client.vaults.items.retrieve(
+ key="x",
+ id_or_name="id_or_name",
+ expand=["payment_methods"],
+ wait=0,
+ )
+ assert_matches_type(VaultItem, item, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ def test_raw_response_retrieve(self, client: Kernel) -> None:
+ response = client.vaults.items.with_raw_response.retrieve(
+ key="x",
+ id_or_name="id_or_name",
+ )
+
+ assert response.is_closed is True
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+ item = response.parse()
+ assert_matches_type(VaultItem, item, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ def test_streaming_response_retrieve(self, client: Kernel) -> None:
+ with client.vaults.items.with_streaming_response.retrieve(
+ key="x",
+ id_or_name="id_or_name",
+ ) as response:
+ assert not response.is_closed
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+
+ item = response.parse()
+ assert_matches_type(VaultItem, item, path=["response"])
+
+ assert cast(Any, response.is_closed) is True
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ def test_path_params_retrieve(self, client: Kernel) -> None:
+ with pytest.raises(ValueError, match=r"Expected a non-empty value for `id_or_name` but received ''"):
+ client.vaults.items.with_raw_response.retrieve(
+ key="x",
+ id_or_name="",
+ )
+
+ with pytest.raises(ValueError, match=r"Expected a non-empty value for `key` but received ''"):
+ client.vaults.items.with_raw_response.retrieve(
+ key="",
+ id_or_name="id_or_name",
+ )
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ def test_method_update(self, client: Kernel) -> None:
+ item = client.vaults.items.update(
+ key="x",
+ id_or_name="id_or_name",
+ spec={
+ "amount": 1,
+ "context": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
+ "currency": "bFx",
+ "merchant_name": "x",
+ "merchant_url": "https://example.com",
+ "payment_method_id": "x",
+ "provider": "link",
+ "test": True,
+ "wallet": "wallet",
+ },
+ )
+ assert_matches_type(VaultItem, item, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ def test_method_update_with_all_params(self, client: Kernel) -> None:
+ item = client.vaults.items.update(
+ key="x",
+ id_or_name="id_or_name",
+ spec={
+ "amount": 1,
+ "context": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
+ "currency": "bFx",
+ "merchant_name": "x",
+ "merchant_url": "https://example.com",
+ "payment_method_id": "x",
+ "provider": "link",
+ "test": True,
+ "wallet": "wallet",
+ "expires_at": 0,
+ "line_items": [
+ {
+ "name": "name",
+ "description": "description",
+ "image_url": "image_url",
+ "product_url": "product_url",
+ "quantity": 1,
+ "sku": "sku",
+ "totals": [
+ {
+ "amount": 0,
+ "display_text": "display_text",
+ "type": "type",
+ }
+ ],
+ "unit_amount": 0,
+ "url": "url",
+ }
+ ],
+ "metadata": {"foo": "string"},
+ "totals": [
+ {
+ "amount": 0,
+ "display_text": "display_text",
+ "type": "type",
+ }
+ ],
+ },
+ )
+ assert_matches_type(VaultItem, item, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ def test_raw_response_update(self, client: Kernel) -> None:
+ response = client.vaults.items.with_raw_response.update(
+ key="x",
+ id_or_name="id_or_name",
+ spec={
+ "amount": 1,
+ "context": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
+ "currency": "bFx",
+ "merchant_name": "x",
+ "merchant_url": "https://example.com",
+ "payment_method_id": "x",
+ "provider": "link",
+ "test": True,
+ "wallet": "wallet",
+ },
+ )
+
+ assert response.is_closed is True
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+ item = response.parse()
+ assert_matches_type(VaultItem, item, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ def test_streaming_response_update(self, client: Kernel) -> None:
+ with client.vaults.items.with_streaming_response.update(
+ key="x",
+ id_or_name="id_or_name",
+ spec={
+ "amount": 1,
+ "context": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
+ "currency": "bFx",
+ "merchant_name": "x",
+ "merchant_url": "https://example.com",
+ "payment_method_id": "x",
+ "provider": "link",
+ "test": True,
+ "wallet": "wallet",
+ },
+ ) as response:
+ assert not response.is_closed
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+
+ item = response.parse()
+ assert_matches_type(VaultItem, item, 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_or_name` but received ''"):
+ client.vaults.items.with_raw_response.update(
+ key="x",
+ id_or_name="",
+ spec={
+ "amount": 1,
+ "context": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
+ "currency": "bFx",
+ "merchant_name": "x",
+ "merchant_url": "https://example.com",
+ "payment_method_id": "x",
+ "provider": "link",
+ "test": True,
+ "wallet": "wallet",
+ },
+ )
+
+ with pytest.raises(ValueError, match=r"Expected a non-empty value for `key` but received ''"):
+ client.vaults.items.with_raw_response.update(
+ key="",
+ id_or_name="id_or_name",
+ spec={
+ "amount": 1,
+ "context": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
+ "currency": "bFx",
+ "merchant_name": "x",
+ "merchant_url": "https://example.com",
+ "payment_method_id": "x",
+ "provider": "link",
+ "test": True,
+ "wallet": "wallet",
+ },
+ )
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ def test_method_list(self, client: Kernel) -> None:
+ item = client.vaults.items.list(
+ "id_or_name",
+ )
+ assert_matches_type(ItemListResponse, item, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ def test_raw_response_list(self, client: Kernel) -> None:
+ response = client.vaults.items.with_raw_response.list(
+ "id_or_name",
+ )
+
+ assert response.is_closed is True
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+ item = response.parse()
+ assert_matches_type(ItemListResponse, item, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ def test_streaming_response_list(self, client: Kernel) -> None:
+ with client.vaults.items.with_streaming_response.list(
+ "id_or_name",
+ ) as response:
+ assert not response.is_closed
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+
+ item = response.parse()
+ assert_matches_type(ItemListResponse, item, path=["response"])
+
+ assert cast(Any, response.is_closed) is True
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ def test_path_params_list(self, client: Kernel) -> None:
+ with pytest.raises(ValueError, match=r"Expected a non-empty value for `id_or_name` but received ''"):
+ client.vaults.items.with_raw_response.list(
+ "",
+ )
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ def test_method_delete(self, client: Kernel) -> None:
+ item = client.vaults.items.delete(
+ key="x",
+ id_or_name="id_or_name",
+ )
+ assert item is None
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ def test_raw_response_delete(self, client: Kernel) -> None:
+ response = client.vaults.items.with_raw_response.delete(
+ key="x",
+ id_or_name="id_or_name",
+ )
+
+ assert response.is_closed is True
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+ item = response.parse()
+ assert item is None
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ def test_streaming_response_delete(self, client: Kernel) -> None:
+ with client.vaults.items.with_streaming_response.delete(
+ key="x",
+ id_or_name="id_or_name",
+ ) as response:
+ assert not response.is_closed
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+
+ item = response.parse()
+ assert item is None
+
+ assert cast(Any, response.is_closed) is True
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ def test_path_params_delete(self, client: Kernel) -> None:
+ with pytest.raises(ValueError, match=r"Expected a non-empty value for `id_or_name` but received ''"):
+ client.vaults.items.with_raw_response.delete(
+ key="x",
+ id_or_name="",
+ )
+
+ with pytest.raises(ValueError, match=r"Expected a non-empty value for `key` but received ''"):
+ client.vaults.items.with_raw_response.delete(
+ key="",
+ id_or_name="id_or_name",
+ )
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ def test_method_events(self, client: Kernel) -> None:
+ item = client.vaults.items.events(
+ key="key",
+ id_or_name="id_or_name",
+ )
+ assert_matches_type(ItemEventsResponse, item, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ def test_method_events_with_all_params(self, client: Kernel) -> None:
+ item = client.vaults.items.events(
+ key="key",
+ id_or_name="id_or_name",
+ after="after",
+ wait=0,
+ )
+ assert_matches_type(ItemEventsResponse, item, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ def test_raw_response_events(self, client: Kernel) -> None:
+ response = client.vaults.items.with_raw_response.events(
+ key="key",
+ id_or_name="id_or_name",
+ )
+
+ assert response.is_closed is True
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+ item = response.parse()
+ assert_matches_type(ItemEventsResponse, item, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ def test_streaming_response_events(self, client: Kernel) -> None:
+ with client.vaults.items.with_streaming_response.events(
+ key="key",
+ id_or_name="id_or_name",
+ ) as response:
+ assert not response.is_closed
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+
+ item = response.parse()
+ assert_matches_type(ItemEventsResponse, item, path=["response"])
+
+ assert cast(Any, response.is_closed) is True
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ def test_path_params_events(self, client: Kernel) -> None:
+ with pytest.raises(ValueError, match=r"Expected a non-empty value for `id_or_name` but received ''"):
+ client.vaults.items.with_raw_response.events(
+ key="key",
+ id_or_name="",
+ )
+
+ with pytest.raises(ValueError, match=r"Expected a non-empty value for `key` but received ''"):
+ client.vaults.items.with_raw_response.events(
+ key="",
+ id_or_name="id_or_name",
+ )
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ def test_method_perform_operation(self, client: Kernel) -> None:
+ item = client.vaults.items.perform_operation(
+ key="key",
+ id_or_name="id_or_name",
+ type="authorize",
+ )
+ assert_matches_type(VaultItem, item, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ def test_raw_response_perform_operation(self, client: Kernel) -> None:
+ response = client.vaults.items.with_raw_response.perform_operation(
+ key="key",
+ id_or_name="id_or_name",
+ type="authorize",
+ )
+
+ assert response.is_closed is True
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+ item = response.parse()
+ assert_matches_type(VaultItem, item, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ def test_streaming_response_perform_operation(self, client: Kernel) -> None:
+ with client.vaults.items.with_streaming_response.perform_operation(
+ key="key",
+ id_or_name="id_or_name",
+ type="authorize",
+ ) as response:
+ assert not response.is_closed
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+
+ item = response.parse()
+ assert_matches_type(VaultItem, item, path=["response"])
+
+ assert cast(Any, response.is_closed) is True
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ def test_path_params_perform_operation(self, client: Kernel) -> None:
+ with pytest.raises(ValueError, match=r"Expected a non-empty value for `id_or_name` but received ''"):
+ client.vaults.items.with_raw_response.perform_operation(
+ key="key",
+ id_or_name="",
+ type="authorize",
+ )
+
+ with pytest.raises(ValueError, match=r"Expected a non-empty value for `key` but received ''"):
+ client.vaults.items.with_raw_response.perform_operation(
+ key="",
+ id_or_name="id_or_name",
+ type="authorize",
+ )
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ def test_method_upsert_overload_1(self, client: Kernel) -> None:
+ item = client.vaults.items.upsert(
+ key="x",
+ id_or_name="id_or_name",
+ spec={
+ "authorization": {
+ "client": {"type": "kernel_managed"},
+ "method": "oauth",
+ },
+ "provider": "link",
+ },
+ type="wallet",
+ )
+ assert_matches_type(VaultItem, item, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ def test_method_upsert_with_all_params_overload_1(self, client: Kernel) -> None:
+ item = client.vaults.items.upsert(
+ key="x",
+ id_or_name="id_or_name",
+ spec={
+ "authorization": {
+ "client": {"type": "kernel_managed"},
+ "method": "oauth",
+ },
+ "provider": "link",
+ },
+ type="wallet",
+ )
+ assert_matches_type(VaultItem, item, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ def test_raw_response_upsert_overload_1(self, client: Kernel) -> None:
+ response = client.vaults.items.with_raw_response.upsert(
+ key="x",
+ id_or_name="id_or_name",
+ spec={
+ "authorization": {
+ "client": {"type": "kernel_managed"},
+ "method": "oauth",
+ },
+ "provider": "link",
+ },
+ type="wallet",
+ )
+
+ assert response.is_closed is True
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+ item = response.parse()
+ assert_matches_type(VaultItem, item, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ def test_streaming_response_upsert_overload_1(self, client: Kernel) -> None:
+ with client.vaults.items.with_streaming_response.upsert(
+ key="x",
+ id_or_name="id_or_name",
+ spec={
+ "authorization": {
+ "client": {"type": "kernel_managed"},
+ "method": "oauth",
+ },
+ "provider": "link",
+ },
+ type="wallet",
+ ) as response:
+ assert not response.is_closed
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+
+ item = response.parse()
+ assert_matches_type(VaultItem, item, path=["response"])
+
+ assert cast(Any, response.is_closed) is True
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ def test_path_params_upsert_overload_1(self, client: Kernel) -> None:
+ with pytest.raises(ValueError, match=r"Expected a non-empty value for `id_or_name` but received ''"):
+ client.vaults.items.with_raw_response.upsert(
+ key="x",
+ id_or_name="",
+ spec={
+ "authorization": {
+ "client": {"type": "kernel_managed"},
+ "method": "oauth",
+ },
+ "provider": "link",
+ },
+ type="wallet",
+ )
+
+ with pytest.raises(ValueError, match=r"Expected a non-empty value for `key` but received ''"):
+ client.vaults.items.with_raw_response.upsert(
+ key="",
+ id_or_name="id_or_name",
+ spec={
+ "authorization": {
+ "client": {"type": "kernel_managed"},
+ "method": "oauth",
+ },
+ "provider": "link",
+ },
+ type="wallet",
+ )
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ def test_method_upsert_overload_2(self, client: Kernel) -> None:
+ item = client.vaults.items.upsert(
+ key="x",
+ id_or_name="id_or_name",
+ spec={
+ "amount": 1,
+ "context": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
+ "currency": "bFx",
+ "merchant_name": "x",
+ "merchant_url": "https://example.com",
+ "payment_method_id": "x",
+ "provider": "link",
+ "test": True,
+ "wallet": "wallet",
+ },
+ type="card",
+ )
+ assert_matches_type(VaultItem, item, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ def test_method_upsert_with_all_params_overload_2(self, client: Kernel) -> None:
+ item = client.vaults.items.upsert(
+ key="x",
+ id_or_name="id_or_name",
+ spec={
+ "amount": 1,
+ "context": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
+ "currency": "bFx",
+ "merchant_name": "x",
+ "merchant_url": "https://example.com",
+ "payment_method_id": "x",
+ "provider": "link",
+ "test": True,
+ "wallet": "wallet",
+ "expires_at": 0,
+ "line_items": [
+ {
+ "name": "name",
+ "description": "description",
+ "image_url": "image_url",
+ "product_url": "product_url",
+ "quantity": 1,
+ "sku": "sku",
+ "totals": [
+ {
+ "amount": 0,
+ "display_text": "display_text",
+ "type": "type",
+ }
+ ],
+ "unit_amount": 0,
+ "url": "url",
+ }
+ ],
+ "metadata": {"foo": "string"},
+ "totals": [
+ {
+ "amount": 0,
+ "display_text": "display_text",
+ "type": "type",
+ }
+ ],
+ },
+ type="card",
+ )
+ assert_matches_type(VaultItem, item, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ def test_raw_response_upsert_overload_2(self, client: Kernel) -> None:
+ response = client.vaults.items.with_raw_response.upsert(
+ key="x",
+ id_or_name="id_or_name",
+ spec={
+ "amount": 1,
+ "context": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
+ "currency": "bFx",
+ "merchant_name": "x",
+ "merchant_url": "https://example.com",
+ "payment_method_id": "x",
+ "provider": "link",
+ "test": True,
+ "wallet": "wallet",
+ },
+ type="card",
+ )
+
+ assert response.is_closed is True
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+ item = response.parse()
+ assert_matches_type(VaultItem, item, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ def test_streaming_response_upsert_overload_2(self, client: Kernel) -> None:
+ with client.vaults.items.with_streaming_response.upsert(
+ key="x",
+ id_or_name="id_or_name",
+ spec={
+ "amount": 1,
+ "context": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
+ "currency": "bFx",
+ "merchant_name": "x",
+ "merchant_url": "https://example.com",
+ "payment_method_id": "x",
+ "provider": "link",
+ "test": True,
+ "wallet": "wallet",
+ },
+ type="card",
+ ) as response:
+ assert not response.is_closed
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+
+ item = response.parse()
+ assert_matches_type(VaultItem, item, path=["response"])
+
+ assert cast(Any, response.is_closed) is True
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ def test_path_params_upsert_overload_2(self, client: Kernel) -> None:
+ with pytest.raises(ValueError, match=r"Expected a non-empty value for `id_or_name` but received ''"):
+ client.vaults.items.with_raw_response.upsert(
+ key="x",
+ id_or_name="",
+ spec={
+ "amount": 1,
+ "context": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
+ "currency": "bFx",
+ "merchant_name": "x",
+ "merchant_url": "https://example.com",
+ "payment_method_id": "x",
+ "provider": "link",
+ "test": True,
+ "wallet": "wallet",
+ },
+ type="card",
+ )
+
+ with pytest.raises(ValueError, match=r"Expected a non-empty value for `key` but received ''"):
+ client.vaults.items.with_raw_response.upsert(
+ key="",
+ id_or_name="id_or_name",
+ spec={
+ "amount": 1,
+ "context": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
+ "currency": "bFx",
+ "merchant_name": "x",
+ "merchant_url": "https://example.com",
+ "payment_method_id": "x",
+ "provider": "link",
+ "test": True,
+ "wallet": "wallet",
+ },
+ type="card",
+ )
+
+
+class TestAsyncItems:
+ parametrize = pytest.mark.parametrize(
+ "async_client", [False, True, {"http_client": "aiohttp"}], indirect=True, ids=["loose", "strict", "aiohttp"]
+ )
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ async def test_method_retrieve(self, async_client: AsyncKernel) -> None:
+ item = await async_client.vaults.items.retrieve(
+ key="x",
+ id_or_name="id_or_name",
+ )
+ assert_matches_type(VaultItem, item, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ async def test_method_retrieve_with_all_params(self, async_client: AsyncKernel) -> None:
+ item = await async_client.vaults.items.retrieve(
+ key="x",
+ id_or_name="id_or_name",
+ expand=["payment_methods"],
+ wait=0,
+ )
+ assert_matches_type(VaultItem, item, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ async def test_raw_response_retrieve(self, async_client: AsyncKernel) -> None:
+ response = await async_client.vaults.items.with_raw_response.retrieve(
+ key="x",
+ id_or_name="id_or_name",
+ )
+
+ assert response.is_closed is True
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+ item = await response.parse()
+ assert_matches_type(VaultItem, item, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ async def test_streaming_response_retrieve(self, async_client: AsyncKernel) -> None:
+ async with async_client.vaults.items.with_streaming_response.retrieve(
+ key="x",
+ id_or_name="id_or_name",
+ ) as response:
+ assert not response.is_closed
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+
+ item = await response.parse()
+ assert_matches_type(VaultItem, item, 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_retrieve(self, async_client: AsyncKernel) -> None:
+ with pytest.raises(ValueError, match=r"Expected a non-empty value for `id_or_name` but received ''"):
+ await async_client.vaults.items.with_raw_response.retrieve(
+ key="x",
+ id_or_name="",
+ )
+
+ with pytest.raises(ValueError, match=r"Expected a non-empty value for `key` but received ''"):
+ await async_client.vaults.items.with_raw_response.retrieve(
+ key="",
+ id_or_name="id_or_name",
+ )
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ async def test_method_update(self, async_client: AsyncKernel) -> None:
+ item = await async_client.vaults.items.update(
+ key="x",
+ id_or_name="id_or_name",
+ spec={
+ "amount": 1,
+ "context": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
+ "currency": "bFx",
+ "merchant_name": "x",
+ "merchant_url": "https://example.com",
+ "payment_method_id": "x",
+ "provider": "link",
+ "test": True,
+ "wallet": "wallet",
+ },
+ )
+ assert_matches_type(VaultItem, item, 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:
+ item = await async_client.vaults.items.update(
+ key="x",
+ id_or_name="id_or_name",
+ spec={
+ "amount": 1,
+ "context": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
+ "currency": "bFx",
+ "merchant_name": "x",
+ "merchant_url": "https://example.com",
+ "payment_method_id": "x",
+ "provider": "link",
+ "test": True,
+ "wallet": "wallet",
+ "expires_at": 0,
+ "line_items": [
+ {
+ "name": "name",
+ "description": "description",
+ "image_url": "image_url",
+ "product_url": "product_url",
+ "quantity": 1,
+ "sku": "sku",
+ "totals": [
+ {
+ "amount": 0,
+ "display_text": "display_text",
+ "type": "type",
+ }
+ ],
+ "unit_amount": 0,
+ "url": "url",
+ }
+ ],
+ "metadata": {"foo": "string"},
+ "totals": [
+ {
+ "amount": 0,
+ "display_text": "display_text",
+ "type": "type",
+ }
+ ],
+ },
+ )
+ assert_matches_type(VaultItem, item, 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.vaults.items.with_raw_response.update(
+ key="x",
+ id_or_name="id_or_name",
+ spec={
+ "amount": 1,
+ "context": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
+ "currency": "bFx",
+ "merchant_name": "x",
+ "merchant_url": "https://example.com",
+ "payment_method_id": "x",
+ "provider": "link",
+ "test": True,
+ "wallet": "wallet",
+ },
+ )
+
+ assert response.is_closed is True
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+ item = await response.parse()
+ assert_matches_type(VaultItem, item, 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.vaults.items.with_streaming_response.update(
+ key="x",
+ id_or_name="id_or_name",
+ spec={
+ "amount": 1,
+ "context": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
+ "currency": "bFx",
+ "merchant_name": "x",
+ "merchant_url": "https://example.com",
+ "payment_method_id": "x",
+ "provider": "link",
+ "test": True,
+ "wallet": "wallet",
+ },
+ ) as response:
+ assert not response.is_closed
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+
+ item = await response.parse()
+ assert_matches_type(VaultItem, item, 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_or_name` but received ''"):
+ await async_client.vaults.items.with_raw_response.update(
+ key="x",
+ id_or_name="",
+ spec={
+ "amount": 1,
+ "context": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
+ "currency": "bFx",
+ "merchant_name": "x",
+ "merchant_url": "https://example.com",
+ "payment_method_id": "x",
+ "provider": "link",
+ "test": True,
+ "wallet": "wallet",
+ },
+ )
+
+ with pytest.raises(ValueError, match=r"Expected a non-empty value for `key` but received ''"):
+ await async_client.vaults.items.with_raw_response.update(
+ key="",
+ id_or_name="id_or_name",
+ spec={
+ "amount": 1,
+ "context": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
+ "currency": "bFx",
+ "merchant_name": "x",
+ "merchant_url": "https://example.com",
+ "payment_method_id": "x",
+ "provider": "link",
+ "test": True,
+ "wallet": "wallet",
+ },
+ )
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ async def test_method_list(self, async_client: AsyncKernel) -> None:
+ item = await async_client.vaults.items.list(
+ "id_or_name",
+ )
+ assert_matches_type(ItemListResponse, item, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ async def test_raw_response_list(self, async_client: AsyncKernel) -> None:
+ response = await async_client.vaults.items.with_raw_response.list(
+ "id_or_name",
+ )
+
+ assert response.is_closed is True
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+ item = await response.parse()
+ assert_matches_type(ItemListResponse, item, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ async def test_streaming_response_list(self, async_client: AsyncKernel) -> None:
+ async with async_client.vaults.items.with_streaming_response.list(
+ "id_or_name",
+ ) as response:
+ assert not response.is_closed
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+
+ item = await response.parse()
+ assert_matches_type(ItemListResponse, item, 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_list(self, async_client: AsyncKernel) -> None:
+ with pytest.raises(ValueError, match=r"Expected a non-empty value for `id_or_name` but received ''"):
+ await async_client.vaults.items.with_raw_response.list(
+ "",
+ )
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ async def test_method_delete(self, async_client: AsyncKernel) -> None:
+ item = await async_client.vaults.items.delete(
+ key="x",
+ id_or_name="id_or_name",
+ )
+ assert item is None
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ async def test_raw_response_delete(self, async_client: AsyncKernel) -> None:
+ response = await async_client.vaults.items.with_raw_response.delete(
+ key="x",
+ id_or_name="id_or_name",
+ )
+
+ assert response.is_closed is True
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+ item = await response.parse()
+ assert item is None
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ async def test_streaming_response_delete(self, async_client: AsyncKernel) -> None:
+ async with async_client.vaults.items.with_streaming_response.delete(
+ key="x",
+ id_or_name="id_or_name",
+ ) as response:
+ assert not response.is_closed
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+
+ item = await response.parse()
+ assert item is None
+
+ assert cast(Any, response.is_closed) is True
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ async def test_path_params_delete(self, async_client: AsyncKernel) -> None:
+ with pytest.raises(ValueError, match=r"Expected a non-empty value for `id_or_name` but received ''"):
+ await async_client.vaults.items.with_raw_response.delete(
+ key="x",
+ id_or_name="",
+ )
+
+ with pytest.raises(ValueError, match=r"Expected a non-empty value for `key` but received ''"):
+ await async_client.vaults.items.with_raw_response.delete(
+ key="",
+ id_or_name="id_or_name",
+ )
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ async def test_method_events(self, async_client: AsyncKernel) -> None:
+ item = await async_client.vaults.items.events(
+ key="key",
+ id_or_name="id_or_name",
+ )
+ assert_matches_type(ItemEventsResponse, item, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ async def test_method_events_with_all_params(self, async_client: AsyncKernel) -> None:
+ item = await async_client.vaults.items.events(
+ key="key",
+ id_or_name="id_or_name",
+ after="after",
+ wait=0,
+ )
+ assert_matches_type(ItemEventsResponse, item, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ async def test_raw_response_events(self, async_client: AsyncKernel) -> None:
+ response = await async_client.vaults.items.with_raw_response.events(
+ key="key",
+ id_or_name="id_or_name",
+ )
+
+ assert response.is_closed is True
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+ item = await response.parse()
+ assert_matches_type(ItemEventsResponse, item, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ async def test_streaming_response_events(self, async_client: AsyncKernel) -> None:
+ async with async_client.vaults.items.with_streaming_response.events(
+ key="key",
+ id_or_name="id_or_name",
+ ) as response:
+ assert not response.is_closed
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+
+ item = await response.parse()
+ assert_matches_type(ItemEventsResponse, item, path=["response"])
+
+ assert cast(Any, response.is_closed) is True
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ async def test_path_params_events(self, async_client: AsyncKernel) -> None:
+ with pytest.raises(ValueError, match=r"Expected a non-empty value for `id_or_name` but received ''"):
+ await async_client.vaults.items.with_raw_response.events(
+ key="key",
+ id_or_name="",
+ )
+
+ with pytest.raises(ValueError, match=r"Expected a non-empty value for `key` but received ''"):
+ await async_client.vaults.items.with_raw_response.events(
+ key="",
+ id_or_name="id_or_name",
+ )
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ async def test_method_perform_operation(self, async_client: AsyncKernel) -> None:
+ item = await async_client.vaults.items.perform_operation(
+ key="key",
+ id_or_name="id_or_name",
+ type="authorize",
+ )
+ assert_matches_type(VaultItem, item, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ async def test_raw_response_perform_operation(self, async_client: AsyncKernel) -> None:
+ response = await async_client.vaults.items.with_raw_response.perform_operation(
+ key="key",
+ id_or_name="id_or_name",
+ type="authorize",
+ )
+
+ assert response.is_closed is True
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+ item = await response.parse()
+ assert_matches_type(VaultItem, item, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ async def test_streaming_response_perform_operation(self, async_client: AsyncKernel) -> None:
+ async with async_client.vaults.items.with_streaming_response.perform_operation(
+ key="key",
+ id_or_name="id_or_name",
+ type="authorize",
+ ) as response:
+ assert not response.is_closed
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+
+ item = await response.parse()
+ assert_matches_type(VaultItem, item, 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_perform_operation(self, async_client: AsyncKernel) -> None:
+ with pytest.raises(ValueError, match=r"Expected a non-empty value for `id_or_name` but received ''"):
+ await async_client.vaults.items.with_raw_response.perform_operation(
+ key="key",
+ id_or_name="",
+ type="authorize",
+ )
+
+ with pytest.raises(ValueError, match=r"Expected a non-empty value for `key` but received ''"):
+ await async_client.vaults.items.with_raw_response.perform_operation(
+ key="",
+ id_or_name="id_or_name",
+ type="authorize",
+ )
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ async def test_method_upsert_overload_1(self, async_client: AsyncKernel) -> None:
+ item = await async_client.vaults.items.upsert(
+ key="x",
+ id_or_name="id_or_name",
+ spec={
+ "authorization": {
+ "client": {"type": "kernel_managed"},
+ "method": "oauth",
+ },
+ "provider": "link",
+ },
+ type="wallet",
+ )
+ assert_matches_type(VaultItem, item, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ async def test_method_upsert_with_all_params_overload_1(self, async_client: AsyncKernel) -> None:
+ item = await async_client.vaults.items.upsert(
+ key="x",
+ id_or_name="id_or_name",
+ spec={
+ "authorization": {
+ "client": {"type": "kernel_managed"},
+ "method": "oauth",
+ },
+ "provider": "link",
+ },
+ type="wallet",
+ )
+ assert_matches_type(VaultItem, item, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ async def test_raw_response_upsert_overload_1(self, async_client: AsyncKernel) -> None:
+ response = await async_client.vaults.items.with_raw_response.upsert(
+ key="x",
+ id_or_name="id_or_name",
+ spec={
+ "authorization": {
+ "client": {"type": "kernel_managed"},
+ "method": "oauth",
+ },
+ "provider": "link",
+ },
+ type="wallet",
+ )
+
+ assert response.is_closed is True
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+ item = await response.parse()
+ assert_matches_type(VaultItem, item, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ async def test_streaming_response_upsert_overload_1(self, async_client: AsyncKernel) -> None:
+ async with async_client.vaults.items.with_streaming_response.upsert(
+ key="x",
+ id_or_name="id_or_name",
+ spec={
+ "authorization": {
+ "client": {"type": "kernel_managed"},
+ "method": "oauth",
+ },
+ "provider": "link",
+ },
+ type="wallet",
+ ) as response:
+ assert not response.is_closed
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+
+ item = await response.parse()
+ assert_matches_type(VaultItem, item, 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_upsert_overload_1(self, async_client: AsyncKernel) -> None:
+ with pytest.raises(ValueError, match=r"Expected a non-empty value for `id_or_name` but received ''"):
+ await async_client.vaults.items.with_raw_response.upsert(
+ key="x",
+ id_or_name="",
+ spec={
+ "authorization": {
+ "client": {"type": "kernel_managed"},
+ "method": "oauth",
+ },
+ "provider": "link",
+ },
+ type="wallet",
+ )
+
+ with pytest.raises(ValueError, match=r"Expected a non-empty value for `key` but received ''"):
+ await async_client.vaults.items.with_raw_response.upsert(
+ key="",
+ id_or_name="id_or_name",
+ spec={
+ "authorization": {
+ "client": {"type": "kernel_managed"},
+ "method": "oauth",
+ },
+ "provider": "link",
+ },
+ type="wallet",
+ )
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ async def test_method_upsert_overload_2(self, async_client: AsyncKernel) -> None:
+ item = await async_client.vaults.items.upsert(
+ key="x",
+ id_or_name="id_or_name",
+ spec={
+ "amount": 1,
+ "context": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
+ "currency": "bFx",
+ "merchant_name": "x",
+ "merchant_url": "https://example.com",
+ "payment_method_id": "x",
+ "provider": "link",
+ "test": True,
+ "wallet": "wallet",
+ },
+ type="card",
+ )
+ assert_matches_type(VaultItem, item, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ async def test_method_upsert_with_all_params_overload_2(self, async_client: AsyncKernel) -> None:
+ item = await async_client.vaults.items.upsert(
+ key="x",
+ id_or_name="id_or_name",
+ spec={
+ "amount": 1,
+ "context": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
+ "currency": "bFx",
+ "merchant_name": "x",
+ "merchant_url": "https://example.com",
+ "payment_method_id": "x",
+ "provider": "link",
+ "test": True,
+ "wallet": "wallet",
+ "expires_at": 0,
+ "line_items": [
+ {
+ "name": "name",
+ "description": "description",
+ "image_url": "image_url",
+ "product_url": "product_url",
+ "quantity": 1,
+ "sku": "sku",
+ "totals": [
+ {
+ "amount": 0,
+ "display_text": "display_text",
+ "type": "type",
+ }
+ ],
+ "unit_amount": 0,
+ "url": "url",
+ }
+ ],
+ "metadata": {"foo": "string"},
+ "totals": [
+ {
+ "amount": 0,
+ "display_text": "display_text",
+ "type": "type",
+ }
+ ],
+ },
+ type="card",
+ )
+ assert_matches_type(VaultItem, item, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ async def test_raw_response_upsert_overload_2(self, async_client: AsyncKernel) -> None:
+ response = await async_client.vaults.items.with_raw_response.upsert(
+ key="x",
+ id_or_name="id_or_name",
+ spec={
+ "amount": 1,
+ "context": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
+ "currency": "bFx",
+ "merchant_name": "x",
+ "merchant_url": "https://example.com",
+ "payment_method_id": "x",
+ "provider": "link",
+ "test": True,
+ "wallet": "wallet",
+ },
+ type="card",
+ )
+
+ assert response.is_closed is True
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+ item = await response.parse()
+ assert_matches_type(VaultItem, item, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ async def test_streaming_response_upsert_overload_2(self, async_client: AsyncKernel) -> None:
+ async with async_client.vaults.items.with_streaming_response.upsert(
+ key="x",
+ id_or_name="id_or_name",
+ spec={
+ "amount": 1,
+ "context": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
+ "currency": "bFx",
+ "merchant_name": "x",
+ "merchant_url": "https://example.com",
+ "payment_method_id": "x",
+ "provider": "link",
+ "test": True,
+ "wallet": "wallet",
+ },
+ type="card",
+ ) as response:
+ assert not response.is_closed
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+
+ item = await response.parse()
+ assert_matches_type(VaultItem, item, 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_upsert_overload_2(self, async_client: AsyncKernel) -> None:
+ with pytest.raises(ValueError, match=r"Expected a non-empty value for `id_or_name` but received ''"):
+ await async_client.vaults.items.with_raw_response.upsert(
+ key="x",
+ id_or_name="",
+ spec={
+ "amount": 1,
+ "context": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
+ "currency": "bFx",
+ "merchant_name": "x",
+ "merchant_url": "https://example.com",
+ "payment_method_id": "x",
+ "provider": "link",
+ "test": True,
+ "wallet": "wallet",
+ },
+ type="card",
+ )
+
+ with pytest.raises(ValueError, match=r"Expected a non-empty value for `key` but received ''"):
+ await async_client.vaults.items.with_raw_response.upsert(
+ key="",
+ id_or_name="id_or_name",
+ spec={
+ "amount": 1,
+ "context": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
+ "currency": "bFx",
+ "merchant_name": "x",
+ "merchant_url": "https://example.com",
+ "payment_method_id": "x",
+ "provider": "link",
+ "test": True,
+ "wallet": "wallet",
+ },
+ type="card",
+ )
From 57668d6628b80da948a0e036e4827db86c92cad0 Mon Sep 17 00:00:00 2001
From: "kernel-internal[bot]"
<260533166+kernel-internal[bot]@users.noreply.github.com>
Date: Fri, 4 Sep 2026 18:23:16 +0000
Subject: [PATCH 231/366] release: 0.99.0
---
.release-please-manifest.json | 2 +-
CHANGELOG.md | 8 ++++++++
pyproject.toml | 2 +-
src/kernel/_version.py | 2 +-
4 files changed, 11 insertions(+), 3 deletions(-)
diff --git a/.release-please-manifest.json b/.release-please-manifest.json
index a8124647..a579a434 100644
--- a/.release-please-manifest.json
+++ b/.release-please-manifest.json
@@ -1,3 +1,3 @@
{
- ".": "0.98.0"
+ ".": "0.99.0"
}
\ No newline at end of file
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 992c5f68..2c0c8bf7 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,13 @@
# Changelog
+## [0.99.0](https://github.com/kernel/kernel-python-sdk/compare/v0.98.0...v0.99.0) (2026-09-04)
+
+
+### Features
+
+* Expose OTLP destination delivery health ([9ab1d58](https://github.com/kernel/kernel-python-sdk/commit/9ab1d583d7bb4dabba5a640883dda9f2ac248f18))
+* Repair Vault SDK custom-code seals ([645ce00](https://github.com/kernel/kernel-python-sdk/commit/645ce00d68172a323d11b7e4dd4f3997cc287fe0))
+
## [0.98.0](https://github.com/kernel/kernel-python-sdk/compare/v0.97.0...v0.98.0) (2026-09-02)
diff --git a/pyproject.toml b/pyproject.toml
index fbe3ab05..dc940a7b 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -1,6 +1,6 @@
[project]
name = "kernel"
-version = "0.98.0"
+version = "0.99.0"
description = "The official Python library for the kernel API"
dynamic = ["readme"]
license = "Apache-2.0"
diff --git a/src/kernel/_version.py b/src/kernel/_version.py
index 4adaa91f..18073be8 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.98.0" # x-release-please-version
+__version__ = "0.99.0" # x-release-please-version
From 043d2521509d4e44a4b3e43694db3a77bd6fe546 Mon Sep 17 00:00:00 2001
From: "kernel-internal[bot]"
<260533166+kernel-internal[bot]@users.noreply.github.com>
Date: Fri, 4 Sep 2026 19:31:16 +0000
Subject: [PATCH 232/366] feat: Fix vault provider errors and remove test card
mode
Stainless-Generated-From: 784d7148c308af595e9b55dc6af92f4d371a3361
---
src/kernel/resources/vaults/items.py | 20 ++++++++--------
.../types/vaults/card_vault_item_spec.py | 12 ++++------
.../vaults/card_vault_item_spec_param.py | 12 ++++------
src/kernel/types/vaults/item_update_params.py | 6 +----
src/kernel/types/vaults/item_upsert_params.py | 6 +----
src/kernel/types/vaults/vault_item.py | 6 +----
tests/api_resources/vaults/test_items.py | 24 -------------------
7 files changed, 21 insertions(+), 65 deletions(-)
diff --git a/src/kernel/resources/vaults/items.py b/src/kernel/resources/vaults/items.py
index 17bef3c6..b87d67cb 100644
--- a/src/kernel/resources/vaults/items.py
+++ b/src/kernel/resources/vaults/items.py
@@ -131,8 +131,7 @@ def update(
Update a card specification before or between authorizations
Args:
- spec: AgentCard reusable card. Each checkout creates an approval-gated authorization
- for spec.merchant / spec.amount. The card stays ready after each authorization.
+ spec: Live payment card. Test-mode card creation is not supported.
extra_headers: Send extra headers
@@ -296,7 +295,9 @@ def perform_operation(
"""
Retrieve the item first and invoke only an operation listed in
`available_operations`, following its natural-language description. Operations
- may call an external provider and can return the item's updated state.
+ may call an external provider and can return the item's updated state. If the
+ provider rate limits spend-request creation, returns HTTP 429 with code
+ `spend_request_rate_limited`; stop and back off before retrying.
Args:
extra_headers: Send extra headers
@@ -375,8 +376,7 @@ def upsert(
Create or retrieve an identical vault item by immutable key
Args:
- spec: AgentCard reusable card. Each checkout creates an approval-gated authorization
- for spec.merchant / spec.amount. The card stays ready after each authorization.
+ spec: Live payment card. Test-mode card creation is not supported.
extra_headers: Send extra headers
@@ -523,8 +523,7 @@ async def update(
Update a card specification before or between authorizations
Args:
- spec: AgentCard reusable card. Each checkout creates an approval-gated authorization
- for spec.merchant / spec.amount. The card stays ready after each authorization.
+ spec: Live payment card. Test-mode card creation is not supported.
extra_headers: Send extra headers
@@ -688,7 +687,9 @@ async def perform_operation(
"""
Retrieve the item first and invoke only an operation listed in
`available_operations`, following its natural-language description. Operations
- may call an external provider and can return the item's updated state.
+ may call an external provider and can return the item's updated state. If the
+ provider rate limits spend-request creation, returns HTTP 429 with code
+ `spend_request_rate_limited`; stop and back off before retrying.
Args:
extra_headers: Send extra headers
@@ -769,8 +770,7 @@ async def upsert(
Create or retrieve an identical vault item by immutable key
Args:
- spec: AgentCard reusable card. Each checkout creates an approval-gated authorization
- for spec.merchant / spec.amount. The card stays ready after each authorization.
+ spec: Live payment card. Test-mode card creation is not supported.
extra_headers: Send extra headers
diff --git a/src/kernel/types/vaults/card_vault_item_spec.py b/src/kernel/types/vaults/card_vault_item_spec.py
index 65eebebd..d66a5453 100644
--- a/src/kernel/types/vaults/card_vault_item_spec.py
+++ b/src/kernel/types/vaults/card_vault_item_spec.py
@@ -56,6 +56,8 @@ class LinkCardVaultItemSpecTotal(BaseModel):
class LinkCardVaultItemSpec(BaseModel):
+ """Live payment card. Test-mode card creation is not supported."""
+
amount: int
"""Integer amount in minor currency units."""
@@ -76,12 +78,6 @@ class LinkCardVaultItemSpec(BaseModel):
provider: Literal["link"]
- test: bool
- """
- Whether Link should return test credentials instead of a live payment
- credential.
- """
-
wallet: str
"""Wallet item key used to mint this card."""
@@ -95,9 +91,9 @@ class LinkCardVaultItemSpec(BaseModel):
class AgentCardCardVaultItemSpec(BaseModel):
- """AgentCard reusable card.
+ """AgentCard reusable live payment card.
- Each checkout creates an approval-gated authorization for spec.merchant / spec.amount. The card stays ready after each authorization.
+ Test-mode card creation is not supported. Each checkout creates an approval-gated authorization for spec.merchant / spec.amount. The card stays ready after each authorization.
"""
amount: int
diff --git a/src/kernel/types/vaults/card_vault_item_spec_param.py b/src/kernel/types/vaults/card_vault_item_spec_param.py
index 22b81433..3cced6ea 100644
--- a/src/kernel/types/vaults/card_vault_item_spec_param.py
+++ b/src/kernel/types/vaults/card_vault_item_spec_param.py
@@ -55,6 +55,8 @@ class LinkCardVaultItemSpecTotal(TypedDict, total=False):
class LinkCardVaultItemSpec(TypedDict, total=False):
+ """Live payment card. Test-mode card creation is not supported."""
+
amount: Required[int]
"""Integer amount in minor currency units."""
@@ -75,12 +77,6 @@ class LinkCardVaultItemSpec(TypedDict, total=False):
provider: Required[Literal["link"]]
- test: Required[bool]
- """
- Whether Link should return test credentials instead of a live payment
- credential.
- """
-
wallet: Required[str]
"""Wallet item key used to mint this card."""
@@ -94,9 +90,9 @@ class LinkCardVaultItemSpec(TypedDict, total=False):
class AgentCardCardVaultItemSpec(TypedDict, total=False):
- """AgentCard reusable card.
+ """AgentCard reusable live payment card.
- Each checkout creates an approval-gated authorization for spec.merchant / spec.amount. The card stays ready after each authorization.
+ Test-mode card creation is not supported. Each checkout creates an approval-gated authorization for spec.merchant / spec.amount. The card stays ready after each authorization.
"""
amount: Required[int]
diff --git a/src/kernel/types/vaults/item_update_params.py b/src/kernel/types/vaults/item_update_params.py
index 38604305..1ea45428 100644
--- a/src/kernel/types/vaults/item_update_params.py
+++ b/src/kernel/types/vaults/item_update_params.py
@@ -13,8 +13,4 @@ class ItemUpdateParams(TypedDict, total=False):
id_or_name: Required[str]
spec: Required[CardVaultItemSpecParam]
- """AgentCard reusable card.
-
- Each checkout creates an approval-gated authorization for spec.merchant /
- spec.amount. The card stays ready after each authorization.
- """
+ """Live payment card. Test-mode card creation is not supported."""
diff --git a/src/kernel/types/vaults/item_upsert_params.py b/src/kernel/types/vaults/item_upsert_params.py
index 5f1a4b0e..c3fe00e8 100644
--- a/src/kernel/types/vaults/item_upsert_params.py
+++ b/src/kernel/types/vaults/item_upsert_params.py
@@ -29,11 +29,7 @@ class CardVaultItemRequest(TypedDict, total=False):
id_or_name: Required[str]
spec: Required[CardVaultItemSpecParam]
- """AgentCard reusable card.
-
- Each checkout creates an approval-gated authorization for spec.merchant /
- spec.amount. The card stays ready after each authorization.
- """
+ """Live payment card. Test-mode card creation is not supported."""
type: Required[Literal["card"]]
diff --git a/src/kernel/types/vaults/vault_item.py b/src/kernel/types/vaults/vault_item.py
index 8418f0bc..7f4c9b81 100644
--- a/src/kernel/types/vaults/vault_item.py
+++ b/src/kernel/types/vaults/vault_item.py
@@ -120,11 +120,7 @@ class CardVaultItem(BaseModel):
"""Immutable item key assigned when the item is created."""
spec: CardVaultItemSpec
- """AgentCard reusable card.
-
- Each checkout creates an approval-gated authorization for spec.merchant /
- spec.amount. The card stays ready after each authorization.
- """
+ """Live payment card. Test-mode card creation is not supported."""
state: CardVaultItemState
diff --git a/tests/api_resources/vaults/test_items.py b/tests/api_resources/vaults/test_items.py
index 9d0f9ec6..51759529 100644
--- a/tests/api_resources/vaults/test_items.py
+++ b/tests/api_resources/vaults/test_items.py
@@ -98,7 +98,6 @@ def test_method_update(self, client: Kernel) -> None:
"merchant_url": "https://example.com",
"payment_method_id": "x",
"provider": "link",
- "test": True,
"wallet": "wallet",
},
)
@@ -118,7 +117,6 @@ def test_method_update_with_all_params(self, client: Kernel) -> None:
"merchant_url": "https://example.com",
"payment_method_id": "x",
"provider": "link",
- "test": True,
"wallet": "wallet",
"expires_at": 0,
"line_items": [
@@ -166,7 +164,6 @@ def test_raw_response_update(self, client: Kernel) -> None:
"merchant_url": "https://example.com",
"payment_method_id": "x",
"provider": "link",
- "test": True,
"wallet": "wallet",
},
)
@@ -190,7 +187,6 @@ def test_streaming_response_update(self, client: Kernel) -> None:
"merchant_url": "https://example.com",
"payment_method_id": "x",
"provider": "link",
- "test": True,
"wallet": "wallet",
},
) as response:
@@ -217,7 +213,6 @@ def test_path_params_update(self, client: Kernel) -> None:
"merchant_url": "https://example.com",
"payment_method_id": "x",
"provider": "link",
- "test": True,
"wallet": "wallet",
},
)
@@ -234,7 +229,6 @@ def test_path_params_update(self, client: Kernel) -> None:
"merchant_url": "https://example.com",
"payment_method_id": "x",
"provider": "link",
- "test": True,
"wallet": "wallet",
},
)
@@ -576,7 +570,6 @@ def test_method_upsert_overload_2(self, client: Kernel) -> None:
"merchant_url": "https://example.com",
"payment_method_id": "x",
"provider": "link",
- "test": True,
"wallet": "wallet",
},
type="card",
@@ -597,7 +590,6 @@ def test_method_upsert_with_all_params_overload_2(self, client: Kernel) -> None:
"merchant_url": "https://example.com",
"payment_method_id": "x",
"provider": "link",
- "test": True,
"wallet": "wallet",
"expires_at": 0,
"line_items": [
@@ -646,7 +638,6 @@ def test_raw_response_upsert_overload_2(self, client: Kernel) -> None:
"merchant_url": "https://example.com",
"payment_method_id": "x",
"provider": "link",
- "test": True,
"wallet": "wallet",
},
type="card",
@@ -671,7 +662,6 @@ def test_streaming_response_upsert_overload_2(self, client: Kernel) -> None:
"merchant_url": "https://example.com",
"payment_method_id": "x",
"provider": "link",
- "test": True,
"wallet": "wallet",
},
type="card",
@@ -699,7 +689,6 @@ def test_path_params_upsert_overload_2(self, client: Kernel) -> None:
"merchant_url": "https://example.com",
"payment_method_id": "x",
"provider": "link",
- "test": True,
"wallet": "wallet",
},
type="card",
@@ -717,7 +706,6 @@ def test_path_params_upsert_overload_2(self, client: Kernel) -> None:
"merchant_url": "https://example.com",
"payment_method_id": "x",
"provider": "link",
- "test": True,
"wallet": "wallet",
},
type="card",
@@ -806,7 +794,6 @@ async def test_method_update(self, async_client: AsyncKernel) -> None:
"merchant_url": "https://example.com",
"payment_method_id": "x",
"provider": "link",
- "test": True,
"wallet": "wallet",
},
)
@@ -826,7 +813,6 @@ async def test_method_update_with_all_params(self, async_client: AsyncKernel) ->
"merchant_url": "https://example.com",
"payment_method_id": "x",
"provider": "link",
- "test": True,
"wallet": "wallet",
"expires_at": 0,
"line_items": [
@@ -874,7 +860,6 @@ async def test_raw_response_update(self, async_client: AsyncKernel) -> None:
"merchant_url": "https://example.com",
"payment_method_id": "x",
"provider": "link",
- "test": True,
"wallet": "wallet",
},
)
@@ -898,7 +883,6 @@ async def test_streaming_response_update(self, async_client: AsyncKernel) -> Non
"merchant_url": "https://example.com",
"payment_method_id": "x",
"provider": "link",
- "test": True,
"wallet": "wallet",
},
) as response:
@@ -925,7 +909,6 @@ async def test_path_params_update(self, async_client: AsyncKernel) -> None:
"merchant_url": "https://example.com",
"payment_method_id": "x",
"provider": "link",
- "test": True,
"wallet": "wallet",
},
)
@@ -942,7 +925,6 @@ async def test_path_params_update(self, async_client: AsyncKernel) -> None:
"merchant_url": "https://example.com",
"payment_method_id": "x",
"provider": "link",
- "test": True,
"wallet": "wallet",
},
)
@@ -1284,7 +1266,6 @@ async def test_method_upsert_overload_2(self, async_client: AsyncKernel) -> None
"merchant_url": "https://example.com",
"payment_method_id": "x",
"provider": "link",
- "test": True,
"wallet": "wallet",
},
type="card",
@@ -1305,7 +1286,6 @@ async def test_method_upsert_with_all_params_overload_2(self, async_client: Asyn
"merchant_url": "https://example.com",
"payment_method_id": "x",
"provider": "link",
- "test": True,
"wallet": "wallet",
"expires_at": 0,
"line_items": [
@@ -1354,7 +1334,6 @@ async def test_raw_response_upsert_overload_2(self, async_client: AsyncKernel) -
"merchant_url": "https://example.com",
"payment_method_id": "x",
"provider": "link",
- "test": True,
"wallet": "wallet",
},
type="card",
@@ -1379,7 +1358,6 @@ async def test_streaming_response_upsert_overload_2(self, async_client: AsyncKer
"merchant_url": "https://example.com",
"payment_method_id": "x",
"provider": "link",
- "test": True,
"wallet": "wallet",
},
type="card",
@@ -1407,7 +1385,6 @@ async def test_path_params_upsert_overload_2(self, async_client: AsyncKernel) ->
"merchant_url": "https://example.com",
"payment_method_id": "x",
"provider": "link",
- "test": True,
"wallet": "wallet",
},
type="card",
@@ -1425,7 +1402,6 @@ async def test_path_params_upsert_overload_2(self, async_client: AsyncKernel) ->
"merchant_url": "https://example.com",
"payment_method_id": "x",
"provider": "link",
- "test": True,
"wallet": "wallet",
},
type="card",
From b08e9ea764f7a815d614846eab0ed50c81c68b65 Mon Sep 17 00:00:00 2001
From: "kernel-internal[bot]"
<260533166+kernel-internal[bot]@users.noreply.github.com>
Date: Fri, 4 Sep 2026 19:35:50 +0000
Subject: [PATCH 233/366] release: 0.100.0
---
.release-please-manifest.json | 2 +-
CHANGELOG.md | 7 +++++++
pyproject.toml | 2 +-
src/kernel/_version.py | 2 +-
4 files changed, 10 insertions(+), 3 deletions(-)
diff --git a/.release-please-manifest.json b/.release-please-manifest.json
index a579a434..97d8dbba 100644
--- a/.release-please-manifest.json
+++ b/.release-please-manifest.json
@@ -1,3 +1,3 @@
{
- ".": "0.99.0"
+ ".": "0.100.0"
}
\ No newline at end of file
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 2c0c8bf7..7a14e35a 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,12 @@
# Changelog
+## [0.100.0](https://github.com/kernel/kernel-python-sdk/compare/v0.99.0...v0.100.0) (2026-09-04)
+
+
+### Features
+
+* Fix vault provider errors and remove test card mode ([043d252](https://github.com/kernel/kernel-python-sdk/commit/043d2521509d4e44a4b3e43694db3a77bd6fe546))
+
## [0.99.0](https://github.com/kernel/kernel-python-sdk/compare/v0.98.0...v0.99.0) (2026-09-04)
diff --git a/pyproject.toml b/pyproject.toml
index dc940a7b..84718a85 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -1,6 +1,6 @@
[project]
name = "kernel"
-version = "0.99.0"
+version = "0.100.0"
description = "The official Python library for the kernel API"
dynamic = ["readme"]
license = "Apache-2.0"
diff --git a/src/kernel/_version.py b/src/kernel/_version.py
index 18073be8..2fba39cd 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.99.0" # x-release-please-version
+__version__ = "0.100.0" # x-release-please-version
From 1c3b388f1a4597f65ed3926a8ec2646f5e0fc994 Mon Sep 17 00:00:00 2001
From: "kernel-internal[bot]"
<260533166+kernel-internal[bot]@users.noreply.github.com>
Date: Sat, 5 Sep 2026 18:39:11 +0000
Subject: [PATCH 234/366] feat: Expose vault access in organization
entitlements
Stainless-Generated-From: 0553bf6048bcef35a58d4c3536f16301fa5ed99a
---
.../types/organization/org_entitlements.py | 16 ++++++++++++++++
1 file changed, 16 insertions(+)
diff --git a/src/kernel/types/organization/org_entitlements.py b/src/kernel/types/organization/org_entitlements.py
index c940cbef..ed13dcc8 100644
--- a/src/kernel/types/organization/org_entitlements.py
+++ b/src/kernel/types/organization/org_entitlements.py
@@ -21,6 +21,7 @@
"FeaturesManagedProxies",
"FeaturesProfiles",
"FeaturesProxyBypassHosts",
+ "FeaturesVaults",
"Limits",
"Plan",
]
@@ -114,6 +115,15 @@ class FeaturesProxyBypassHosts(BaseModel):
"""Whether the organization is entitled to use this feature."""
+class FeaturesVaults(BaseModel):
+ """
+ Whether the organization can access vaults, using the same access check as vault API routes.
+ """
+
+ enabled: bool
+ """Whether the organization is entitled to use this feature."""
+
+
class Features(BaseModel):
browser_extensions: FeaturesBrowserExtensions
@@ -139,6 +149,12 @@ class Features(BaseModel):
proxy_bypass_hosts: FeaturesProxyBypassHosts
+ vaults: FeaturesVaults
+ """
+ Whether the organization can access vaults, using the same access check as vault
+ API routes.
+ """
+
class Limits(BaseModel):
default_max_concurrent_invocations_per_app: int
From b0d5628acbe0a9d9d006a3349ba1338fdc3d4402 Mon Sep 17 00:00:00 2001
From: "kernel-internal[bot]"
<260533166+kernel-internal[bot]@users.noreply.github.com>
Date: Sat, 5 Sep 2026 20:21:54 +0000
Subject: [PATCH 235/366] feat: Limit free organizations to three vaults
Stainless-Generated-From: ba7e1af409ddd0c708f01417cc714eb89c3739f0
---
src/kernel/resources/organization/limits.py | 4 ++--
src/kernel/resources/vaults/vaults.py | 8 ++++++--
src/kernel/types/organization/org_entitlements.py | 6 ++++++
src/kernel/types/organization/org_limits.py | 9 +++++++++
4 files changed, 23 insertions(+), 4 deletions(-)
diff --git a/src/kernel/resources/organization/limits.py b/src/kernel/resources/organization/limits.py
index d1bb6ecb..9dcd9dbb 100644
--- a/src/kernel/resources/organization/limits.py
+++ b/src/kernel/resources/organization/limits.py
@@ -55,7 +55,7 @@ def retrieve(
extra_body: Body | None = None,
timeout: float | httpx.Timeout | None | NotGiven = not_given,
) -> OrgLimits:
- """Get the organization's effective limits and managed auth usage."""
+ """Get the organization's effective limits and managed auth and vault usage."""
return self._get(
"/org/limits",
options=make_request_options(
@@ -138,7 +138,7 @@ async def retrieve(
extra_body: Body | None = None,
timeout: float | httpx.Timeout | None | NotGiven = not_given,
) -> OrgLimits:
- """Get the organization's effective limits and managed auth usage."""
+ """Get the organization's effective limits and managed auth and vault usage."""
return await self._get(
"/org/limits",
options=make_request_options(
diff --git a/src/kernel/resources/vaults/vaults.py b/src/kernel/resources/vaults/vaults.py
index ec33277c..f88ac5f1 100644
--- a/src/kernel/resources/vaults/vaults.py
+++ b/src/kernel/resources/vaults/vaults.py
@@ -176,7 +176,9 @@ def upsert(
timeout: float | httpx.Timeout | None | NotGiven = not_given,
) -> Vault:
"""
- Create or retrieve a vault by immutable name
+ Free organizations can store up to 3 non-deleted vaults across all projects.
+ Paid plans and active trials have no vault cap. Retrieving an existing vault by
+ name succeeds even at the limit.
Args:
name: Immutable name used to create or retrieve the vault.
@@ -345,7 +347,9 @@ async def upsert(
timeout: float | httpx.Timeout | None | NotGiven = not_given,
) -> Vault:
"""
- Create or retrieve a vault by immutable name
+ Free organizations can store up to 3 non-deleted vaults across all projects.
+ Paid plans and active trials have no vault cap. Retrieving an existing vault by
+ name succeeds even at the limit.
Args:
name: Immutable name used to create or retrieve the vault.
diff --git a/src/kernel/types/organization/org_entitlements.py b/src/kernel/types/organization/org_entitlements.py
index ed13dcc8..614ce2fa 100644
--- a/src/kernel/types/organization/org_entitlements.py
+++ b/src/kernel/types/organization/org_entitlements.py
@@ -172,6 +172,12 @@ class Limits(BaseModel):
max_concurrent_invocations: int
"""Effective organization-wide concurrent app invocation ceiling."""
+ max_vaults: Optional[int] = None
+ """Maximum non-deleted vaults allowed org-wide across all projects.
+
+ Null means unlimited. The vaults feature flag still controls access.
+ """
+
class Plan(BaseModel):
id: Literal["FREE", "HOBBYIST", "START_UP", "ENTERPRISE"]
diff --git a/src/kernel/types/organization/org_limits.py b/src/kernel/types/organization/org_limits.py
index bdc3cb4a..4e24a5f0 100644
--- a/src/kernel/types/organization/org_limits.py
+++ b/src/kernel/types/organization/org_limits.py
@@ -22,6 +22,12 @@ class OrgLimits(BaseModel):
projects.
"""
+ max_vaults: Optional[int] = None
+ """Maximum non-deleted vaults allowed org-wide across all projects.
+
+ Null means unlimited.
+ """
+
min_health_check_interval_seconds: int
"""
Smallest health_check_interval the organization's plan accepts on a managed auth
@@ -29,6 +35,9 @@ class OrgLimits(BaseModel):
stored below the floor are grandfathered until edited.
"""
+ vaults_used: int
+ """Current non-deleted vault count across all projects in the organization."""
+
default_project_max_concurrent_sessions: Optional[int] = None
"""
Default maximum concurrent browsers applied to every project that has no
From 733f5234bf6d42c82e4f1587b27f35806d7f2ed1 Mon Sep 17 00:00:00 2001
From: tnsardesai <18272584+tnsardesai@users.noreply.github.com>
Date: Tue, 8 Sep 2026 17:44:53 +0000
Subject: [PATCH 236/366] Prevent replaying streams after auth read errors
---
src/kernel/_base_client.py | 7 +-
src/kernel/_client.py | 9 +++
src/kernel/lib/browser_routing/routing.py | 16 ++++
tests/test_browser_routing.py | 96 +++++++++++++++++++++++
4 files changed, 126 insertions(+), 2 deletions(-)
diff --git a/src/kernel/_base_client.py b/src/kernel/_base_client.py
index 2599dc41..f9598c80 100644
--- a/src/kernel/_base_client.py
+++ b/src/kernel/_base_client.py
@@ -772,6 +772,9 @@ def _calculate_retry_timeout(
timeout = sleep_seconds * jitter
return timeout if timeout >= 0 else 0
+ def _should_retry_on_connection_error(self, _request: httpx.Request) -> bool:
+ return True
+
def _should_retry(self, response: httpx.Response) -> bool:
# Note: this is not a standard header
should_retry_header = response.headers.get("x-should-retry")
@@ -1026,7 +1029,7 @@ def request(
except Exception as err:
log.debug("Encountered Exception", exc_info=True)
- if remaining_retries > 0:
+ if remaining_retries > 0 and self._should_retry_on_connection_error(request):
self._sleep_for_retry(
retries_taken=retries_taken,
max_retries=max_retries,
@@ -1610,7 +1613,7 @@ async def request(
except Exception as err:
log.debug("Encountered Exception", exc_info=True)
- if remaining_retries > 0:
+ if remaining_retries > 0 and self._should_retry_on_connection_error(request):
await self._sleep_for_retry(
retries_taken=retries_taken,
max_retries=max_retries,
diff --git a/src/kernel/_client.py b/src/kernel/_client.py
index 36f0141c..098f8b10 100644
--- a/src/kernel/_client.py
+++ b/src/kernel/_client.py
@@ -45,6 +45,7 @@
should_retry_stale_direct_vm_auth,
install_stale_direct_vm_auth_eviction,
maybe_evict_browser_route_from_response,
+ should_retry_direct_vm_connection_error,
install_async_stale_direct_vm_auth_eviction,
maybe_populate_browser_route_cache_from_response,
)
@@ -375,6 +376,10 @@ def _prepare_options(self, options: Any) -> Any:
def _prepare_request(self, request: httpx.Request) -> None:
strip_direct_vm_auth(request, cache=self.browser_route_cache)
+ @override
+ def _should_retry_on_connection_error(self, request: httpx.Request) -> bool:
+ return should_retry_direct_vm_connection_error(request)
+
@override
def _should_retry(self, response: httpx.Response) -> bool:
if is_stale_direct_vm_auth_response(response):
@@ -767,6 +772,10 @@ async def _prepare_options(self, options: Any) -> Any:
async def _prepare_request(self, request: httpx.Request) -> None:
strip_direct_vm_auth(request, cache=self.browser_route_cache)
+ @override
+ def _should_retry_on_connection_error(self, request: httpx.Request) -> bool:
+ return should_retry_direct_vm_connection_error(request)
+
@override
def _should_retry(self, response: httpx.Response) -> bool:
if is_stale_direct_vm_auth_response(response):
diff --git a/src/kernel/lib/browser_routing/routing.py b/src/kernel/lib/browser_routing/routing.py
index df5f6567..cbf5719c 100644
--- a/src/kernel/lib/browser_routing/routing.py
+++ b/src/kernel/lib/browser_routing/routing.py
@@ -33,6 +33,7 @@ class BrowserRoutingConfig:
_EVICTION_HOOK_CACHE_ATTR = "_kernel_browser_route_cache"
+_STALE_DIRECT_VM_AUTH_REQUEST_EXTENSION = "kernel_stale_direct_vm_auth"
_BROWSER_ROUTE_CACHEABLE_PATH = re.compile(r"^/(?:v\d+/)?browsers(?:/[^/]+)?/?$")
@@ -220,6 +221,7 @@ def install_stale_direct_vm_auth_eviction(client: httpx.Client, *, cache: Browse
def evict(response: httpx.Response) -> None:
if is_stale_direct_vm_auth_response(response):
+ response.request.extensions[_STALE_DIRECT_VM_AUTH_REQUEST_EXTENSION] = True
maybe_evict_browser_route_from_response(response, cache=cache)
setattr(evict, _EVICTION_HOOK_CACHE_ATTR, cache)
@@ -234,6 +236,7 @@ def install_async_stale_direct_vm_auth_eviction(client: httpx.AsyncClient, *, ca
async def evict(response: httpx.Response) -> None:
if is_stale_direct_vm_auth_response(response):
+ response.request.extensions[_STALE_DIRECT_VM_AUTH_REQUEST_EXTENSION] = True
maybe_evict_browser_route_from_response(response, cache=cache)
setattr(evict, _EVICTION_HOOK_CACHE_ATTR, cache)
@@ -246,6 +249,19 @@ def _has_eviction_hook(hooks: list[Any], cache: BrowserRouteCache) -> bool:
return any(getattr(hook, _EVICTION_HOOK_CACHE_ATTR, None) is cache for hook in hooks)
+def should_retry_direct_vm_connection_error(request: httpx.Request) -> bool:
+ """Prevent generic retries from replaying a stream after stale VM auth.
+
+ httpx may raise while eagerly reading the error response body, before the
+ SDK can pass the response to `_should_retry`. The response hook records the
+ known stale-auth status on the request so this connection-error path can
+ apply the same body replay check.
+ """
+ if not request.extensions.get(_STALE_DIRECT_VM_AUTH_REQUEST_EXTENSION):
+ return True
+ return direct_vm_request_body_is_replayable(request)
+
+
def should_retry_stale_direct_vm_auth(response: httpx.Response) -> bool:
"""Whether a stale direct-to-VM auth failure can be retried on the control plane.
diff --git a/tests/test_browser_routing.py b/tests/test_browser_routing.py
index 2ab6ad2a..f4de29b2 100644
--- a/tests/test_browser_routing.py
+++ b/tests/test_browser_routing.py
@@ -50,6 +50,10 @@ def _skip_retry_sleep(_self: object, **_kwargs: object) -> None:
return None
+async def _skip_async_retry_sleep(_self: object, **_kwargs: object) -> None:
+ return None
+
+
class _UnseekableFile(io.RawIOBase):
"""A file-like upload body that cannot be rewound, e.g. a pipe.
@@ -1347,6 +1351,98 @@ async def handle_request(request: httpx.Request) -> httpx.Response:
assert requests[1].headers.get("Authorization") == f"Bearer {api_key}"
+def test_stale_direct_vm_auth_body_read_failure_does_not_retry_unreplayable_write(
+ monkeypatch: pytest.MonkeyPatch,
+) -> None:
+ monkeypatch.delenv("KERNEL_BROWSER_ROUTING_SUBRESOURCES", raising=False)
+ monkeypatch.setattr("kernel._base_client.SyncAPIClient._sleep_for_retry", _skip_retry_sleep)
+ requests: list[tuple[httpx.URL, bytes]] = []
+
+ def handle_request(request: httpx.Request) -> httpx.Response:
+ body = b"".join(request.stream)
+ requests.append((request.url, body))
+ if "browser-session.test" in str(request.url):
+ return httpx.Response(401, stream=_FailingSyncStream(), headers={"content-type": "text/plain"})
+ return httpx.Response(201)
+
+ class Transport(httpx.BaseTransport):
+ @override
+ def handle_request(self, request: httpx.Request) -> httpx.Response:
+ return handle_request(request)
+
+ http_client = httpx.Client(transport=Transport())
+ with Kernel(
+ base_url=base_url,
+ api_key=api_key,
+ http_client=http_client,
+ _strict_response_validation=True,
+ ) as client:
+ _cache_browser(client)
+ with pytest.raises(APIConnectionError):
+ client.browsers.fs.write_file("sess-1", _UnseekableFile(b"payload"), path="/tmp/x")
+
+ assert requests == [
+ (
+ httpx.URL("http://browser-session.test/browser/kernel/fs/write_file?path=%2Ftmp%2Fx&jwt=token-abc"),
+ b"payload",
+ )
+ ]
+ assert client.browser_route_cache.get("sess-1") is None
+
+ client.browsers.fs.write_file("sess-1", b"next", path="/tmp/x")
+
+ assert requests[1] == (httpx.URL(f"{base_url}/browsers/sess-1/fs/write_file?path=%2Ftmp%2Fx"), b"next")
+
+
+@pytest.mark.asyncio
+async def test_async_stale_direct_vm_auth_body_read_failure_does_not_retry_unreplayable_write(
+ monkeypatch: pytest.MonkeyPatch,
+) -> None:
+ monkeypatch.delenv("KERNEL_BROWSER_ROUTING_SUBRESOURCES", raising=False)
+ monkeypatch.setattr("kernel._base_client.AsyncAPIClient._sleep_for_retry", _skip_async_retry_sleep)
+ requests: list[tuple[httpx.URL, bytes]] = []
+
+ async def handle_request(request: httpx.Request) -> httpx.Response:
+ body = b"".join([chunk async for chunk in request.stream])
+ requests.append((request.url, body))
+ if "browser-session.test" in str(request.url):
+ return httpx.Response(403, stream=_FailingAsyncStream(), headers={"content-type": "text/plain"})
+ return httpx.Response(201)
+
+ async def payload() -> AsyncIterator[bytes]:
+ yield b"payload"
+
+ class Transport(httpx.AsyncBaseTransport):
+ @override
+ async def handle_async_request(self, request: httpx.Request) -> httpx.Response:
+ return await handle_request(request)
+
+ http_client = httpx.AsyncClient(transport=Transport())
+ async with AsyncKernel(
+ base_url=base_url,
+ api_key=api_key,
+ http_client=http_client,
+ _strict_response_validation=True,
+ ) as client:
+ route = browser_route_from_browser(_fake_browser())
+ assert route is not None
+ client.browser_route_cache.set(route)
+ with pytest.raises(APIConnectionError):
+ await client.browsers.fs.write_file("sess-1", cast(Any, payload()), path="/tmp/x")
+
+ assert requests == [
+ (
+ httpx.URL("http://browser-session.test/browser/kernel/fs/write_file?path=%2Ftmp%2Fx&jwt=token-abc"),
+ b"payload",
+ )
+ ]
+ assert client.browser_route_cache.get("sess-1") is None
+
+ await client.browsers.fs.write_file("sess-1", b"next", path="/tmp/x")
+
+ assert requests[1] == (httpx.URL(f"{base_url}/browsers/sess-1/fs/write_file?path=%2Ftmp%2Fx"), b"next")
+
+
def test_copied_client_registers_one_route_eviction_hook() -> None:
with Kernel(base_url=base_url, api_key=api_key, _strict_response_validation=True) as client:
copied = client.copy(api_key="sk-456")
From f33e9ca75b8c4789feb25f23e49416a0598443a7 Mon Sep 17 00:00:00 2001
From: tnsardesai <18272584+tnsardesai@users.noreply.github.com>
Date: Tue, 8 Sep 2026 17:49:06 +0000
Subject: [PATCH 237/366] Fix routing test stream types
---
tests/test_browser_routing.py | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/tests/test_browser_routing.py b/tests/test_browser_routing.py
index f4de29b2..601e7de3 100644
--- a/tests/test_browser_routing.py
+++ b/tests/test_browser_routing.py
@@ -1359,7 +1359,7 @@ def test_stale_direct_vm_auth_body_read_failure_does_not_retry_unreplayable_writ
requests: list[tuple[httpx.URL, bytes]] = []
def handle_request(request: httpx.Request) -> httpx.Response:
- body = b"".join(request.stream)
+ body = b"".join(cast(Iterator[bytes], request.stream))
requests.append((request.url, body))
if "browser-session.test" in str(request.url):
return httpx.Response(401, stream=_FailingSyncStream(), headers={"content-type": "text/plain"})
@@ -1403,7 +1403,7 @@ async def test_async_stale_direct_vm_auth_body_read_failure_does_not_retry_unrep
requests: list[tuple[httpx.URL, bytes]] = []
async def handle_request(request: httpx.Request) -> httpx.Response:
- body = b"".join([chunk async for chunk in request.stream])
+ body = b"".join([chunk async for chunk in cast(AsyncIterator[bytes], request.stream)])
requests.append((request.url, body))
if "browser-session.test" in str(request.url):
return httpx.Response(403, stream=_FailingAsyncStream(), headers={"content-type": "text/plain"})
From 5d8ec7fd326ff0595a7aee6d1b5cf50e94ca85ee Mon Sep 17 00:00:00 2001
From: tnsardesai <18272584+tnsardesai@users.noreply.github.com>
Date: Tue, 8 Sep 2026 22:19:56 +0000
Subject: [PATCH 238/366] Preserve stream replayability before request hooks
---
src/kernel/_base_client.py | 4 +--
src/kernel/_client.py | 6 ++--
src/kernel/lib/browser_routing/routing.py | 16 +++++++--
tests/test_browser_routing.py | 41 ++++++++++++++++++-----
4 files changed, 52 insertions(+), 15 deletions(-)
diff --git a/src/kernel/_base_client.py b/src/kernel/_base_client.py
index f9598c80..a73c619b 100644
--- a/src/kernel/_base_client.py
+++ b/src/kernel/_base_client.py
@@ -1015,7 +1015,7 @@ def request(
except httpx.TimeoutException as err:
log.debug("Encountered httpx.TimeoutException", exc_info=True)
- if remaining_retries > 0:
+ if remaining_retries > 0 and self._should_retry_on_connection_error(request):
self._sleep_for_retry(
retries_taken=retries_taken,
max_retries=max_retries,
@@ -1599,7 +1599,7 @@ async def request(
except httpx.TimeoutException as err:
log.debug("Encountered httpx.TimeoutException", exc_info=True)
- if remaining_retries > 0:
+ if remaining_retries > 0 and self._should_retry_on_connection_error(request):
await self._sleep_for_retry(
retries_taken=retries_taken,
max_retries=max_retries,
diff --git a/src/kernel/_client.py b/src/kernel/_client.py
index 098f8b10..4cac60a9 100644
--- a/src/kernel/_client.py
+++ b/src/kernel/_client.py
@@ -38,7 +38,7 @@
from .lib.browser_routing.routing import (
BrowserRouteCache,
BrowserRoutingConfig,
- strip_direct_vm_auth,
+ prepare_direct_vm_request,
rewrite_direct_vm_options,
browser_routing_config_from_env,
is_stale_direct_vm_auth_response,
@@ -374,7 +374,7 @@ def _prepare_options(self, options: Any) -> Any:
@override
def _prepare_request(self, request: httpx.Request) -> None:
- strip_direct_vm_auth(request, cache=self.browser_route_cache)
+ prepare_direct_vm_request(request, cache=self.browser_route_cache)
@override
def _should_retry_on_connection_error(self, request: httpx.Request) -> bool:
@@ -770,7 +770,7 @@ async def _prepare_options(self, options: Any) -> Any:
@override
async def _prepare_request(self, request: httpx.Request) -> None:
- strip_direct_vm_auth(request, cache=self.browser_route_cache)
+ prepare_direct_vm_request(request, cache=self.browser_route_cache)
@override
def _should_retry_on_connection_error(self, request: httpx.Request) -> bool:
diff --git a/src/kernel/lib/browser_routing/routing.py b/src/kernel/lib/browser_routing/routing.py
index cbf5719c..f2085c26 100644
--- a/src/kernel/lib/browser_routing/routing.py
+++ b/src/kernel/lib/browser_routing/routing.py
@@ -34,6 +34,7 @@ class BrowserRoutingConfig:
_EVICTION_HOOK_CACHE_ATTR = "_kernel_browser_route_cache"
_STALE_DIRECT_VM_AUTH_REQUEST_EXTENSION = "kernel_stale_direct_vm_auth"
+_DIRECT_VM_BODY_REPLAYABLE_REQUEST_EXTENSION = "kernel_direct_vm_body_replayable"
_BROWSER_ROUTE_CACHEABLE_PATH = re.compile(r"^/(?:v\d+/)?browsers(?:/[^/]+)?/?$")
@@ -276,12 +277,18 @@ def should_retry_stale_direct_vm_auth(response: httpx.Response) -> bool:
def direct_vm_request_body_is_replayable(request: httpx.Request) -> bool:
+ replayable = request.extensions.get(_DIRECT_VM_BODY_REPLAYABLE_REQUEST_EXTENSION)
+ if isinstance(replayable, bool):
+ return replayable
+ return _classify_direct_vm_request_body_replayability(request)
+
+
+def _classify_direct_vm_request_body_replayability(request: httpx.Request) -> bool:
try:
_ = request.content
except httpx.RequestNotRead:
pass
else:
- # httpx already buffered the body, so rebuilding it yields the same bytes.
return True
# httpx encodes multipart bodies as a stream of fields it re-renders per attempt.
@@ -410,10 +417,15 @@ def rewrite_direct_vm_options(
return rewritten
-def strip_direct_vm_auth(request: httpx.Request, *, cache: BrowserRouteCache) -> None:
+def prepare_direct_vm_request(request: httpx.Request, *, cache: BrowserRouteCache) -> None:
raw = str(request.url)
for route in cache.values():
if raw.startswith(route.base_url.rstrip("/") + "/"):
+ # Request hooks and custom auth can buffer this request while consuming
+ # the original body that the SDK would use to build a retry.
+ request.extensions[_DIRECT_VM_BODY_REPLAYABLE_REQUEST_EXTENSION] = (
+ _classify_direct_vm_request_body_replayability(request)
+ )
request.headers.pop("Authorization", None)
return
diff --git a/tests/test_browser_routing.py b/tests/test_browser_routing.py
index 601e7de3..36706a9f 100644
--- a/tests/test_browser_routing.py
+++ b/tests/test_browser_routing.py
@@ -14,6 +14,7 @@
from kernel import (
Kernel,
AsyncKernel,
+ APITimeoutError,
APIConnectionError,
AuthenticationError,
InternalServerError,
@@ -1269,15 +1270,21 @@ def test_indexed_multipart_body_flattens_only_given_values() -> None:
class _FailingSyncStream(httpx.SyncByteStream):
"""A response body that fails while it is being read."""
+ def __init__(self, error_type: type[httpx.TransportError] = httpx.ReadError) -> None:
+ self.error_type = error_type
+
@override
def __iter__(self) -> Iterator[bytes]:
- raise httpx.ReadError("connection reset while reading the error body")
+ raise self.error_type("connection failed while reading the error body")
class _FailingAsyncStream(httpx.AsyncByteStream):
+ def __init__(self, error_type: type[httpx.TransportError] = httpx.ReadError) -> None:
+ self.error_type = error_type
+
@override
async def __aiter__(self) -> AsyncIterator[bytes]:
- raise httpx.ReadError("connection reset while reading the error body")
+ raise self.error_type("connection failed while reading the error body")
yield b"" # pragma: no cover - unreachable, keeps this an async generator
@@ -1351,8 +1358,14 @@ async def handle_request(request: httpx.Request) -> httpx.Response:
assert requests[1].headers.get("Authorization") == f"Bearer {api_key}"
+@pytest.mark.parametrize(
+ ("error_type", "expected_error"),
+ [(httpx.ReadError, APIConnectionError), (httpx.ReadTimeout, APITimeoutError)],
+)
def test_stale_direct_vm_auth_body_read_failure_does_not_retry_unreplayable_write(
monkeypatch: pytest.MonkeyPatch,
+ error_type: type[httpx.TransportError],
+ expected_error: type[Exception],
) -> None:
monkeypatch.delenv("KERNEL_BROWSER_ROUTING_SUBRESOURCES", raising=False)
monkeypatch.setattr("kernel._base_client.SyncAPIClient._sleep_for_retry", _skip_retry_sleep)
@@ -1362,15 +1375,18 @@ def handle_request(request: httpx.Request) -> httpx.Response:
body = b"".join(cast(Iterator[bytes], request.stream))
requests.append((request.url, body))
if "browser-session.test" in str(request.url):
- return httpx.Response(401, stream=_FailingSyncStream(), headers={"content-type": "text/plain"})
+ return httpx.Response(401, stream=_FailingSyncStream(error_type), headers={"content-type": "text/plain"})
return httpx.Response(201)
+ def read_request(request: httpx.Request) -> None:
+ request.read()
+
class Transport(httpx.BaseTransport):
@override
def handle_request(self, request: httpx.Request) -> httpx.Response:
return handle_request(request)
- http_client = httpx.Client(transport=Transport())
+ http_client = httpx.Client(transport=Transport(), event_hooks={"request": [read_request]})
with Kernel(
base_url=base_url,
api_key=api_key,
@@ -1378,7 +1394,7 @@ def handle_request(self, request: httpx.Request) -> httpx.Response:
_strict_response_validation=True,
) as client:
_cache_browser(client)
- with pytest.raises(APIConnectionError):
+ with pytest.raises(expected_error):
client.browsers.fs.write_file("sess-1", _UnseekableFile(b"payload"), path="/tmp/x")
assert requests == [
@@ -1395,8 +1411,14 @@ def handle_request(self, request: httpx.Request) -> httpx.Response:
@pytest.mark.asyncio
+@pytest.mark.parametrize(
+ ("error_type", "expected_error"),
+ [(httpx.ReadError, APIConnectionError), (httpx.ReadTimeout, APITimeoutError)],
+)
async def test_async_stale_direct_vm_auth_body_read_failure_does_not_retry_unreplayable_write(
monkeypatch: pytest.MonkeyPatch,
+ error_type: type[httpx.TransportError],
+ expected_error: type[Exception],
) -> None:
monkeypatch.delenv("KERNEL_BROWSER_ROUTING_SUBRESOURCES", raising=False)
monkeypatch.setattr("kernel._base_client.AsyncAPIClient._sleep_for_retry", _skip_async_retry_sleep)
@@ -1406,9 +1428,12 @@ async def handle_request(request: httpx.Request) -> httpx.Response:
body = b"".join([chunk async for chunk in cast(AsyncIterator[bytes], request.stream)])
requests.append((request.url, body))
if "browser-session.test" in str(request.url):
- return httpx.Response(403, stream=_FailingAsyncStream(), headers={"content-type": "text/plain"})
+ return httpx.Response(403, stream=_FailingAsyncStream(error_type), headers={"content-type": "text/plain"})
return httpx.Response(201)
+ async def read_request(request: httpx.Request) -> None:
+ await request.aread()
+
async def payload() -> AsyncIterator[bytes]:
yield b"payload"
@@ -1417,7 +1442,7 @@ class Transport(httpx.AsyncBaseTransport):
async def handle_async_request(self, request: httpx.Request) -> httpx.Response:
return await handle_request(request)
- http_client = httpx.AsyncClient(transport=Transport())
+ http_client = httpx.AsyncClient(transport=Transport(), event_hooks={"request": [read_request]})
async with AsyncKernel(
base_url=base_url,
api_key=api_key,
@@ -1427,7 +1452,7 @@ async def handle_async_request(self, request: httpx.Request) -> httpx.Response:
route = browser_route_from_browser(_fake_browser())
assert route is not None
client.browser_route_cache.set(route)
- with pytest.raises(APIConnectionError):
+ with pytest.raises(expected_error):
await client.browsers.fs.write_file("sess-1", cast(Any, payload()), path="/tmp/x")
assert requests == [
From 92acf13bdca32d8fc4845b1c992ce89c442ebfdd Mon Sep 17 00:00:00 2001
From: tnsardesai <18272584+tnsardesai@users.noreply.github.com>
Date: Tue, 8 Sep 2026 23:35:16 +0000
Subject: [PATCH 239/366] Prevent retries of unreplayable VM bodies
---
src/kernel/_client.py | 5 ++
src/kernel/lib/browser_routing/routing.py | 14 +--
tests/test_browser_routing.py | 100 ++++++++++++++++++++++
3 files changed, 112 insertions(+), 7 deletions(-)
diff --git a/src/kernel/_client.py b/src/kernel/_client.py
index 4cac60a9..a47a1c60 100644
--- a/src/kernel/_client.py
+++ b/src/kernel/_client.py
@@ -47,6 +47,7 @@
maybe_evict_browser_route_from_response,
should_retry_direct_vm_connection_error,
install_async_stale_direct_vm_auth_eviction,
+ direct_vm_request_body_is_known_unreplayable,
maybe_populate_browser_route_cache_from_response,
)
@@ -382,6 +383,8 @@ def _should_retry_on_connection_error(self, request: httpx.Request) -> bool:
@override
def _should_retry(self, response: httpx.Response) -> bool:
+ if direct_vm_request_body_is_known_unreplayable(response.request):
+ return False
if is_stale_direct_vm_auth_response(response):
# The route was already evicted by the response hook; retry only when
# the body can be rebuilt, otherwise the caller sees the original auth
@@ -778,6 +781,8 @@ def _should_retry_on_connection_error(self, request: httpx.Request) -> bool:
@override
def _should_retry(self, response: httpx.Response) -> bool:
+ if direct_vm_request_body_is_known_unreplayable(response.request):
+ return False
if is_stale_direct_vm_auth_response(response):
# The route was already evicted by the response hook; retry only when
# the body can be rebuilt, otherwise the caller sees the original auth
diff --git a/src/kernel/lib/browser_routing/routing.py b/src/kernel/lib/browser_routing/routing.py
index f2085c26..068aef47 100644
--- a/src/kernel/lib/browser_routing/routing.py
+++ b/src/kernel/lib/browser_routing/routing.py
@@ -251,13 +251,9 @@ def _has_eviction_hook(hooks: list[Any], cache: BrowserRouteCache) -> bool:
def should_retry_direct_vm_connection_error(request: httpx.Request) -> bool:
- """Prevent generic retries from replaying a stream after stale VM auth.
-
- httpx may raise while eagerly reading the error response body, before the
- SDK can pass the response to `_should_retry`. The response hook records the
- known stale-auth status on the request so this connection-error path can
- apply the same body replay check.
- """
+ """Prevent connection retries from replaying an unreplayable VM request body."""
+ if direct_vm_request_body_is_known_unreplayable(request):
+ return False
if not request.extensions.get(_STALE_DIRECT_VM_AUTH_REQUEST_EXTENSION):
return True
return direct_vm_request_body_is_replayable(request)
@@ -276,6 +272,10 @@ def should_retry_stale_direct_vm_auth(response: httpx.Response) -> bool:
return direct_vm_request_body_is_replayable(response.request)
+def direct_vm_request_body_is_known_unreplayable(request: httpx.Request) -> bool:
+ return request.extensions.get(_DIRECT_VM_BODY_REPLAYABLE_REQUEST_EXTENSION) is False
+
+
def direct_vm_request_body_is_replayable(request: httpx.Request) -> bool:
replayable = request.extensions.get(_DIRECT_VM_BODY_REPLAYABLE_REQUEST_EXTENSION)
if isinstance(replayable, bool):
diff --git a/tests/test_browser_routing.py b/tests/test_browser_routing.py
index 36706a9f..df2b74bd 100644
--- a/tests/test_browser_routing.py
+++ b/tests/test_browser_routing.py
@@ -1468,6 +1468,106 @@ async def handle_async_request(self, request: httpx.Request) -> httpx.Response:
assert requests[1] == (httpx.URL(f"{base_url}/browsers/sess-1/fs/write_file?path=%2Ftmp%2Fx"), b"next")
+@pytest.mark.parametrize(
+ ("failure_type", "expected_error"),
+ [
+ (httpx.ReadError, APIConnectionError),
+ (httpx.ReadTimeout, APITimeoutError),
+ (None, InternalServerError),
+ ],
+)
+def test_direct_vm_failure_does_not_retry_unreplayable_write(
+ monkeypatch: pytest.MonkeyPatch,
+ failure_type: type[httpx.TransportError] | None,
+ expected_error: type[Exception],
+) -> None:
+ monkeypatch.delenv("KERNEL_BROWSER_ROUTING_SUBRESOURCES", raising=False)
+ monkeypatch.setattr("kernel._base_client.SyncAPIClient._sleep_for_retry", _skip_retry_sleep)
+ requests: list[tuple[httpx.URL, bytes]] = []
+
+ class Transport(httpx.BaseTransport):
+ @override
+ def handle_request(self, request: httpx.Request) -> httpx.Response:
+ body = b"".join(cast(Iterator[bytes], request.stream))
+ requests.append((request.url, body))
+ if failure_type is not None:
+ raise failure_type("connection failed after sending the request", request=request)
+ return httpx.Response(500, json={"error": "boom"})
+
+ http_client = httpx.Client(transport=Transport())
+ with Kernel(
+ base_url=base_url,
+ api_key=api_key,
+ http_client=http_client,
+ _strict_response_validation=True,
+ ) as client:
+ _cache_browser(client)
+ with pytest.raises(expected_error):
+ client.browsers.fs.write_file("sess-1", _UnseekableFile(b"payload"), path="/tmp/x")
+
+ assert client.browser_route_cache.get("sess-1") is not None
+
+ assert requests == [
+ (
+ httpx.URL("http://browser-session.test/browser/kernel/fs/write_file?path=%2Ftmp%2Fx&jwt=token-abc"),
+ b"payload",
+ )
+ ]
+
+
+@pytest.mark.asyncio
+@pytest.mark.parametrize(
+ ("failure_type", "expected_error"),
+ [
+ (httpx.ReadError, APIConnectionError),
+ (httpx.ReadTimeout, APITimeoutError),
+ (None, InternalServerError),
+ ],
+)
+async def test_async_direct_vm_failure_does_not_retry_unreplayable_write(
+ monkeypatch: pytest.MonkeyPatch,
+ failure_type: type[httpx.TransportError] | None,
+ expected_error: type[Exception],
+) -> None:
+ monkeypatch.delenv("KERNEL_BROWSER_ROUTING_SUBRESOURCES", raising=False)
+ monkeypatch.setattr("kernel._base_client.AsyncAPIClient._sleep_for_retry", _skip_async_retry_sleep)
+ requests: list[tuple[httpx.URL, bytes]] = []
+
+ async def payload() -> AsyncIterator[bytes]:
+ yield b"payload"
+
+ class Transport(httpx.AsyncBaseTransport):
+ @override
+ async def handle_async_request(self, request: httpx.Request) -> httpx.Response:
+ body = b"".join([chunk async for chunk in cast(AsyncIterator[bytes], request.stream)])
+ requests.append((request.url, body))
+ if failure_type is not None:
+ raise failure_type("connection failed after sending the request", request=request)
+ return httpx.Response(500, json={"error": "boom"})
+
+ http_client = httpx.AsyncClient(transport=Transport())
+ async with AsyncKernel(
+ base_url=base_url,
+ api_key=api_key,
+ http_client=http_client,
+ _strict_response_validation=True,
+ ) as client:
+ route = browser_route_from_browser(_fake_browser())
+ assert route is not None
+ client.browser_route_cache.set(route)
+ with pytest.raises(expected_error):
+ await client.browsers.fs.write_file("sess-1", cast(Any, payload()), path="/tmp/x")
+
+ assert client.browser_route_cache.get("sess-1") is not None
+
+ assert requests == [
+ (
+ httpx.URL("http://browser-session.test/browser/kernel/fs/write_file?path=%2Ftmp%2Fx&jwt=token-abc"),
+ b"payload",
+ )
+ ]
+
+
def test_copied_client_registers_one_route_eviction_hook() -> None:
with Kernel(base_url=base_url, api_key=api_key, _strict_response_validation=True) as client:
copied = client.copy(api_key="sk-456")
From 1a3bbc3766d55f6e29c086c8a2528875ba915b26 Mon Sep 17 00:00:00 2001
From: tnsardesai <18272584+tnsardesai@users.noreply.github.com>
Date: Wed, 9 Sep 2026 00:55:56 +0000
Subject: [PATCH 240/366] Harden direct VM request handling
---
src/kernel/_client.py | 8 +-
src/kernel/lib/browser_routing/routing.py | 92 ++++++--
src/kernel/resources/browsers/fs/fs.py | 4 +-
tests/test_browser_routing.py | 273 +++++++++++++++++++++-
4 files changed, 349 insertions(+), 28 deletions(-)
diff --git a/src/kernel/_client.py b/src/kernel/_client.py
index a47a1c60..ff6d3164 100644
--- a/src/kernel/_client.py
+++ b/src/kernel/_client.py
@@ -41,9 +41,11 @@
prepare_direct_vm_request,
rewrite_direct_vm_options,
browser_routing_config_from_env,
+ install_direct_vm_auth_stripping,
is_stale_direct_vm_auth_response,
should_retry_stale_direct_vm_auth,
install_stale_direct_vm_auth_eviction,
+ install_async_direct_vm_auth_stripping,
maybe_evict_browser_route_from_response,
should_retry_direct_vm_connection_error,
install_async_stale_direct_vm_auth_eviction,
@@ -210,6 +212,7 @@ def __init__(
)
self.browser_route_cache = _browser_route_cache or BrowserRouteCache()
self._browser_routing = browser_routing_config_from_env()
+ install_direct_vm_auth_stripping(self._client)
install_stale_direct_vm_auth_eviction(self._client, cache=self.browser_route_cache)
@cached_property
@@ -375,7 +378,7 @@ def _prepare_options(self, options: Any) -> Any:
@override
def _prepare_request(self, request: httpx.Request) -> None:
- prepare_direct_vm_request(request, cache=self.browser_route_cache)
+ prepare_direct_vm_request(request)
@override
def _should_retry_on_connection_error(self, request: httpx.Request) -> bool:
@@ -608,6 +611,7 @@ def __init__(
)
self.browser_route_cache = _browser_route_cache or BrowserRouteCache()
self._browser_routing = browser_routing_config_from_env()
+ install_async_direct_vm_auth_stripping(self._client)
install_async_stale_direct_vm_auth_eviction(self._client, cache=self.browser_route_cache)
@cached_property
@@ -773,7 +777,7 @@ async def _prepare_options(self, options: Any) -> Any:
@override
async def _prepare_request(self, request: httpx.Request) -> None:
- prepare_direct_vm_request(request, cache=self.browser_route_cache)
+ prepare_direct_vm_request(request)
@override
def _should_retry_on_connection_error(self, request: httpx.Request) -> bool:
diff --git a/src/kernel/lib/browser_routing/routing.py b/src/kernel/lib/browser_routing/routing.py
index 068aef47..8e152c02 100644
--- a/src/kernel/lib/browser_routing/routing.py
+++ b/src/kernel/lib/browser_routing/routing.py
@@ -15,6 +15,7 @@
cdp_ws_url_from_browser_like,
session_id_from_browser_like,
)
+from ..._utils import is_given
from ..._compat import model_copy
from ..._models import FinalRequestOptions
from ..._constants import RAW_RESPONSE_HEADER
@@ -33,6 +34,8 @@ class BrowserRoutingConfig:
_EVICTION_HOOK_CACHE_ATTR = "_kernel_browser_route_cache"
+_DIRECT_VM_AUTH_HOOK_ATTR = "_kernel_direct_vm_auth_hook"
+_DIRECT_VM_REQUEST_MARKER_HEADER = "x-kernel-direct-vm-request"
_STALE_DIRECT_VM_AUTH_REQUEST_EXTENSION = "kernel_stale_direct_vm_auth"
_DIRECT_VM_BODY_REPLAYABLE_REQUEST_EXTENSION = "kernel_direct_vm_body_replayable"
@@ -211,22 +214,24 @@ def install_stale_direct_vm_auth_eviction(client: httpx.Client, *, cache: Browse
whose body read fails — the read error surfaces from `send()` instead and the
dead route would stay cached, wedging every later call for that session. A
response event hook runs after the status is known and before any body is
- read, which keeps eviction independent of the body. For a caller-supplied
+ read, which keeps eviction independent of the body. It also rejects redirects
+ before httpx can replay an unreplayable direct request body. For a caller-supplied
`http_client`, the hook is installed into that client's `event_hooks` and
- prepended so an existing hook cannot pre-empt eviction by reading a failing
- body or raising.
+ prepended so an existing hook cannot pre-empt these safeguards by reading a
+ failing body or raising.
"""
hooks = client.event_hooks.setdefault("response", [])
if _has_eviction_hook(hooks, cache):
return
- def evict(response: httpx.Response) -> None:
+ def handle_response(response: httpx.Response) -> None:
+ _reject_unreplayable_direct_vm_redirect(response, cache=cache)
if is_stale_direct_vm_auth_response(response):
response.request.extensions[_STALE_DIRECT_VM_AUTH_REQUEST_EXTENSION] = True
maybe_evict_browser_route_from_response(response, cache=cache)
- setattr(evict, _EVICTION_HOOK_CACHE_ATTR, cache)
- hooks.insert(0, evict)
+ setattr(handle_response, _EVICTION_HOOK_CACHE_ATTR, cache)
+ hooks.insert(0, handle_response)
def install_async_stale_direct_vm_auth_eviction(client: httpx.AsyncClient, *, cache: BrowserRouteCache) -> None:
@@ -235,13 +240,42 @@ def install_async_stale_direct_vm_auth_eviction(client: httpx.AsyncClient, *, ca
if _has_eviction_hook(hooks, cache):
return
- async def evict(response: httpx.Response) -> None:
+ async def handle_response(response: httpx.Response) -> None:
+ _reject_unreplayable_direct_vm_redirect(response, cache=cache)
if is_stale_direct_vm_auth_response(response):
response.request.extensions[_STALE_DIRECT_VM_AUTH_REQUEST_EXTENSION] = True
maybe_evict_browser_route_from_response(response, cache=cache)
- setattr(evict, _EVICTION_HOOK_CACHE_ATTR, cache)
- hooks.insert(0, evict)
+ setattr(handle_response, _EVICTION_HOOK_CACHE_ATTR, cache)
+ hooks.insert(0, handle_response)
+
+
+def install_direct_vm_auth_stripping(client: httpx.Client) -> None:
+ """Remove Authorization after httpx auth and existing request hooks run."""
+ hooks = client.event_hooks.setdefault("request", [])
+ if any(getattr(hook, _DIRECT_VM_AUTH_HOOK_ATTR, False) for hook in hooks):
+ return
+
+ def strip_auth(request: httpx.Request) -> None:
+ if _is_direct_vm_request(request):
+ request.headers.pop("Authorization", None)
+
+ setattr(strip_auth, _DIRECT_VM_AUTH_HOOK_ATTR, True)
+ hooks.append(strip_auth)
+
+
+def install_async_direct_vm_auth_stripping(client: httpx.AsyncClient) -> None:
+ """Async counterpart of `install_direct_vm_auth_stripping`."""
+ hooks = client.event_hooks.setdefault("request", [])
+ if any(getattr(hook, _DIRECT_VM_AUTH_HOOK_ATTR, False) for hook in hooks):
+ return
+
+ async def strip_auth(request: httpx.Request) -> None:
+ if _is_direct_vm_request(request):
+ request.headers.pop("Authorization", None)
+
+ setattr(strip_auth, _DIRECT_VM_AUTH_HOOK_ATTR, True)
+ hooks.append(strip_auth)
def _has_eviction_hook(hooks: list[Any], cache: BrowserRouteCache) -> bool:
@@ -250,6 +284,17 @@ def _has_eviction_hook(hooks: list[Any], cache: BrowserRouteCache) -> bool:
return any(getattr(hook, _EVICTION_HOOK_CACHE_ATTR, None) is cache for hook in hooks)
+def _reject_unreplayable_direct_vm_redirect(response: httpx.Response, *, cache: BrowserRouteCache) -> None:
+ if not response.has_redirect_location or not direct_vm_request_body_is_known_unreplayable(response.request):
+ return
+
+ jwt = str(response.request.url.params.get("jwt") or "").strip()
+ session_id = _session_id_from_direct_vm_response(response, cache=cache)
+ if session_id and jwt:
+ cache.delete_if_jwt(session_id, jwt)
+ raise httpx.RequestError("Cannot safely redirect an unreplayable direct VM request", request=response.request)
+
+
def should_retry_direct_vm_connection_error(request: httpx.Request) -> bool:
"""Prevent connection retries from replaying an unreplayable VM request body."""
if direct_vm_request_body_is_known_unreplayable(request):
@@ -414,20 +459,27 @@ def rewrite_direct_vm_options(
params.update(options.params)
params["jwt"] = route.jwt
rewritten.params = params or options.params
+
+ headers = dict(options.headers) if is_given(options.headers) else {}
+ headers[_DIRECT_VM_REQUEST_MARKER_HEADER] = "true"
+ rewritten.headers = headers
return rewritten
-def prepare_direct_vm_request(request: httpx.Request, *, cache: BrowserRouteCache) -> None:
- raw = str(request.url)
- for route in cache.values():
- if raw.startswith(route.base_url.rstrip("/") + "/"):
- # Request hooks and custom auth can buffer this request while consuming
- # the original body that the SDK would use to build a retry.
- request.extensions[_DIRECT_VM_BODY_REPLAYABLE_REQUEST_EXTENSION] = (
- _classify_direct_vm_request_body_replayability(request)
- )
- request.headers.pop("Authorization", None)
- return
+def prepare_direct_vm_request(request: httpx.Request) -> None:
+ if request.headers.pop(_DIRECT_VM_REQUEST_MARKER_HEADER, None) is None:
+ return
+
+ # Request hooks and custom auth can buffer this request while consuming
+ # the original body that the SDK would use to build a retry.
+ request.extensions[_DIRECT_VM_BODY_REPLAYABLE_REQUEST_EXTENSION] = _classify_direct_vm_request_body_replayability(
+ request
+ )
+ request.headers.pop("Authorization", None)
+
+
+def _is_direct_vm_request(request: httpx.Request) -> bool:
+ return isinstance(request.extensions.get(_DIRECT_VM_BODY_REPLAYABLE_REQUEST_EXTENSION), bool)
def match_direct_vm_path(path: str) -> tuple[str, str, str] | None:
diff --git a/src/kernel/resources/browsers/fs/fs.py b/src/kernel/resources/browsers/fs/fs.py
index ab6f572d..7496287d 100644
--- a/src/kernel/resources/browsers/fs/fs.py
+++ b/src/kernel/resources/browsers/fs/fs.py
@@ -510,7 +510,7 @@ def upload(
if not id_or_name:
raise ValueError(f"Expected a non-empty value for `id_or_name` but received {id_or_name!r}")
extra_headers = {"Accept": "*/*", **(extra_headers or {})}
- body = deepcopy_with_paths({"files": files}, [["files", "", "file"]])
+ body = deepcopy_with_paths({"files": list(files)}, [["files", "", "file"]])
# The remote filesystem pairs each file part with the sibling fields of the
# same array entry, so both halves of the form use indexed names
# (`files[0][file]`, `files[0][dest_path]`).
@@ -1078,7 +1078,7 @@ async def upload(
if not id_or_name:
raise ValueError(f"Expected a non-empty value for `id_or_name` but received {id_or_name!r}")
extra_headers = {"Accept": "*/*", **(extra_headers or {})}
- body = deepcopy_with_paths({"files": files}, [["files", "", "file"]])
+ body = deepcopy_with_paths({"files": list(files)}, [["files", "", "file"]])
# The remote filesystem pairs each file part with the sibling fields of the
# same array entry, so both halves of the form use indexed names
# (`files[0][file]`, `files[0][dest_path]`).
diff --git a/tests/test_browser_routing.py b/tests/test_browser_routing.py
index df2b74bd..f3e40678 100644
--- a/tests/test_browser_routing.py
+++ b/tests/test_browser_routing.py
@@ -3,7 +3,7 @@
import io
import os
import asyncio
-from typing import Any, Iterator, AsyncIterator, cast
+from typing import Any, Iterable, Iterator, AsyncIterator, cast
from pathlib import Path
from typing_extensions import override
@@ -27,6 +27,7 @@
browser_route_from_browser,
browser_routing_config_from_env,
)
+from kernel.types.browsers.f_upload_params import File as UploadFile
base_url = os.environ.get("TEST_API_BASE_URL", "http://127.0.0.1:4010")
api_key = "sk-123"
@@ -91,6 +92,19 @@ def seek(self, _offset: int, _whence: int = 0) -> int:
raise io.UnsupportedOperation("not seekable")
+class _UnreplayableAsyncBody:
+ def __init__(self, content: bytes) -> None:
+ self._content = content
+
+ def __aiter__(self) -> AsyncIterator[bytes]:
+ return self._iterate()
+
+ async def _iterate(self) -> AsyncIterator[bytes]:
+ if self._content:
+ content, self._content = self._content, b""
+ yield content
+
+
class _NoSeekableAttrFile(io.RawIOBase):
"""A file-like upload body whose `seek()` raises and reports no seekability."""
@@ -796,6 +810,65 @@ def test_fs_upload_routes_indexed_multipart_to_vm(monkeypatch: pytest.MonkeyPatc
assert b"files[][" not in body
+@pytest.mark.parametrize("container", ["tuple", "generator"])
+def test_fs_upload_accepts_iterable_files(monkeypatch: pytest.MonkeyPatch, container: str) -> None:
+ monkeypatch.delenv("KERNEL_BROWSER_ROUTING_SUBRESOURCES", raising=False)
+ requests: list[httpx.Request] = []
+
+ def handle_request(request: httpx.Request) -> httpx.Response:
+ requests.append(request)
+ return httpx.Response(201)
+
+ entries: tuple[UploadFile, ...] = ({"dest_path": "/tmp/one", "file": b"abc"},)
+ files: Iterable[UploadFile] = entries if container == "tuple" else iter(entries)
+ http_client = httpx.Client(transport=httpx.MockTransport(handle_request))
+ with Kernel(
+ base_url=base_url,
+ api_key=api_key,
+ http_client=http_client,
+ _strict_response_validation=True,
+ ) as client:
+ _cache_browser(client)
+ client.browsers.fs.upload("sess-1", files=files)
+
+ body = requests[0].content
+ assert b'name="files[0][dest_path]"' in body
+ assert b'name="files[0][file]"; filename="upload"' in body
+ assert b"\r\nabc\r\n" in body
+ assert b"\r\nb'abc'\r\n" not in body
+
+
+@pytest.mark.asyncio
+@pytest.mark.parametrize("container", ["tuple", "generator"])
+async def test_async_fs_upload_accepts_iterable_files(monkeypatch: pytest.MonkeyPatch, container: str) -> None:
+ monkeypatch.delenv("KERNEL_BROWSER_ROUTING_SUBRESOURCES", raising=False)
+ requests: list[httpx.Request] = []
+
+ async def handle_request(request: httpx.Request) -> httpx.Response:
+ requests.append(request)
+ return httpx.Response(201)
+
+ entries: tuple[UploadFile, ...] = ({"dest_path": "/tmp/one", "file": b"abc"},)
+ files: Iterable[UploadFile] = entries if container == "tuple" else iter(entries)
+ http_client = httpx.AsyncClient(transport=httpx.MockTransport(handle_request))
+ async with AsyncKernel(
+ base_url=base_url,
+ api_key=api_key,
+ http_client=http_client,
+ _strict_response_validation=True,
+ ) as client:
+ route = browser_route_from_browser(_fake_browser())
+ assert route is not None
+ client.browser_route_cache.set(route)
+ await client.browsers.fs.upload("sess-1", files=files)
+
+ body = requests[0].content
+ assert b'name="files[0][dest_path]"' in body
+ assert b'name="files[0][file]"; filename="upload"' in body
+ assert b"\r\nabc\r\n" in body
+ assert b"\r\nb'abc'\r\n" not in body
+
+
@respx.mock
def test_fs_watch_events_stream_routes_to_vm(monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.delenv("KERNEL_BROWSER_ROUTING_SUBRESOURCES", raising=False)
@@ -1568,12 +1641,204 @@ async def handle_async_request(self, request: httpx.Request) -> httpx.Response:
]
-def test_copied_client_registers_one_route_eviction_hook() -> None:
+@pytest.mark.parametrize("status_code", [307, 308])
+def test_direct_vm_redirect_does_not_replay_unreplayable_write(
+ monkeypatch: pytest.MonkeyPatch,
+ status_code: int,
+) -> None:
+ monkeypatch.delenv("KERNEL_BROWSER_ROUTING_SUBRESOURCES", raising=False)
+ monkeypatch.setattr("kernel._base_client.SyncAPIClient._sleep_for_retry", _skip_retry_sleep)
+ requests: list[tuple[httpx.URL, bytes]] = []
+
+ class Transport(httpx.BaseTransport):
+ @override
+ def handle_request(self, request: httpx.Request) -> httpx.Response:
+ body = b"".join(cast(Iterator[bytes], request.stream))
+ requests.append((request.url, body))
+ return httpx.Response(
+ status_code,
+ headers={"location": "http://other-vm.test/browser/kernel/fs/write_file"},
+ )
+
+ http_client = httpx.Client(transport=Transport(), follow_redirects=True)
+ with Kernel(
+ base_url=base_url,
+ api_key=api_key,
+ http_client=http_client,
+ _strict_response_validation=True,
+ ) as client:
+ _cache_browser(client)
+ with pytest.raises(APIConnectionError):
+ client.browsers.fs.write_file("sess-1", _UnseekableFile(b"payload"), path="/tmp/x")
+ assert client.browser_route_cache.get("sess-1") is None
+
+ assert requests == [
+ (
+ httpx.URL("http://browser-session.test/browser/kernel/fs/write_file?path=%2Ftmp%2Fx&jwt=token-abc"),
+ b"payload",
+ )
+ ]
+
+
+@pytest.mark.asyncio
+@pytest.mark.parametrize("status_code", [307, 308])
+async def test_async_direct_vm_redirect_does_not_replay_unreplayable_write(
+ monkeypatch: pytest.MonkeyPatch,
+ status_code: int,
+) -> None:
+ monkeypatch.delenv("KERNEL_BROWSER_ROUTING_SUBRESOURCES", raising=False)
+ monkeypatch.setattr("kernel._base_client.AsyncAPIClient._sleep_for_retry", _skip_async_retry_sleep)
+ requests: list[tuple[httpx.URL, bytes]] = []
+
+ class Transport(httpx.AsyncBaseTransport):
+ @override
+ async def handle_async_request(self, request: httpx.Request) -> httpx.Response:
+ body = b"".join([chunk async for chunk in cast(AsyncIterator[bytes], request.stream)])
+ requests.append((request.url, body))
+ return httpx.Response(
+ status_code,
+ headers={"location": "http://other-vm.test/browser/kernel/fs/write_file"},
+ )
+
+ http_client = httpx.AsyncClient(transport=Transport(), follow_redirects=True)
+ async with AsyncKernel(
+ base_url=base_url,
+ api_key=api_key,
+ http_client=http_client,
+ _strict_response_validation=True,
+ ) as client:
+ route = browser_route_from_browser(_fake_browser())
+ assert route is not None
+ client.browser_route_cache.set(route)
+ with pytest.raises(APIConnectionError):
+ await client.browsers.fs.write_file(
+ "sess-1",
+ cast(Any, _UnreplayableAsyncBody(b"payload")),
+ path="/tmp/x",
+ )
+ assert client.browser_route_cache.get("sess-1") is None
+
+ assert requests == [
+ (
+ httpx.URL("http://browser-session.test/browser/kernel/fs/write_file?path=%2Ftmp%2Fx&jwt=token-abc"),
+ b"payload",
+ )
+ ]
+
+
+def test_direct_vm_auth_is_removed_after_httpx_and_request_hooks(monkeypatch: pytest.MonkeyPatch) -> None:
+ monkeypatch.delenv("KERNEL_BROWSER_ROUTING_SUBRESOURCES", raising=False)
+ requests: list[httpx.Request] = []
+
+ def add_auth(request: httpx.Request) -> None:
+ request.headers["Authorization"] = "Bearer from-hook"
+
+ def handle_request(request: httpx.Request) -> httpx.Response:
+ requests.append(request)
+ return httpx.Response(200, json=[])
+
+ http_client = httpx.Client(
+ transport=httpx.MockTransport(handle_request),
+ auth=("user", "password"),
+ event_hooks={"request": [add_auth]},
+ )
+ with Kernel(
+ base_url=base_url,
+ api_key=api_key,
+ http_client=http_client,
+ _strict_response_validation=True,
+ ) as client:
+ _cache_browser(client)
+ client.browsers.fs.list_files("sess-1", path="/tmp")
+ client.browser_route_cache.delete("sess-1")
+ client.browsers.fs.list_files("sess-1", path="/tmp")
+
+ assert requests[0].headers.get("Authorization") is None
+ assert requests[0].headers.get("x-kernel-direct-vm-request") is None
+ assert requests[1].headers.get("Authorization") == "Bearer from-hook"
+
+
+@pytest.mark.asyncio
+async def test_async_direct_vm_auth_is_removed_after_httpx_and_request_hooks(
+ monkeypatch: pytest.MonkeyPatch,
+) -> None:
+ monkeypatch.delenv("KERNEL_BROWSER_ROUTING_SUBRESOURCES", raising=False)
+ requests: list[httpx.Request] = []
+
+ async def add_auth(request: httpx.Request) -> None:
+ request.headers["Authorization"] = "Bearer from-hook"
+
+ async def handle_request(request: httpx.Request) -> httpx.Response:
+ requests.append(request)
+ return httpx.Response(200, json=[])
+
+ http_client = httpx.AsyncClient(
+ transport=httpx.MockTransport(handle_request),
+ auth=("user", "password"),
+ event_hooks={"request": [add_auth]},
+ )
+ async with AsyncKernel(
+ base_url=base_url,
+ api_key=api_key,
+ http_client=http_client,
+ _strict_response_validation=True,
+ ) as client:
+ route = browser_route_from_browser(_fake_browser())
+ assert route is not None
+ client.browser_route_cache.set(route)
+ await client.browsers.fs.list_files("sess-1", path="/tmp")
+ client.browser_route_cache.delete("sess-1")
+ await client.browsers.fs.list_files("sess-1", path="/tmp")
+
+ assert requests[0].headers.get("Authorization") is None
+ assert requests[0].headers.get("x-kernel-direct-vm-request") is None
+ assert requests[1].headers.get("Authorization") == "Bearer from-hook"
+
+
+def test_direct_vm_preparation_survives_route_eviction(monkeypatch: pytest.MonkeyPatch) -> None:
+ from kernel.lib.browser_routing.routing import direct_vm_request_body_is_known_unreplayable
+
+ monkeypatch.delenv("KERNEL_BROWSER_ROUTING_SUBRESOURCES", raising=False)
+ requests: list[tuple[httpx.Request, bytes]] = []
+
+ class EvictingKernel(Kernel):
+ @override
+ def _prepare_request(self, request: httpx.Request) -> None:
+ self.browser_route_cache.delete("sess-1")
+ super()._prepare_request(request)
+
+ def handle_request(request: httpx.Request) -> httpx.Response:
+ body = b"".join(cast(Iterator[bytes], request.stream))
+ requests.append((request, body))
+ return httpx.Response(201)
+
+ http_client = httpx.Client(transport=httpx.MockTransport(handle_request))
+ with EvictingKernel(
+ base_url=base_url,
+ api_key=api_key,
+ http_client=http_client,
+ _strict_response_validation=True,
+ ) as client:
+ _cache_browser(client)
+ client.browsers.fs.write_file("sess-1", _UnseekableFile(b"payload"), path="/tmp/x")
+
+ request, body = requests[0]
+ assert request.url.host == "browser-session.test"
+ assert request.url.params.get("jwt") == "token-abc"
+ assert request.headers.get("Authorization") is None
+ assert request.headers.get("x-kernel-direct-vm-request") is None
+ assert direct_vm_request_body_is_known_unreplayable(request)
+ assert body == b"payload"
+
+
+def test_copied_client_registers_one_direct_vm_hook_per_phase() -> None:
with Kernel(base_url=base_url, api_key=api_key, _strict_response_validation=True) as client:
copied = client.copy(api_key="sk-456")
assert copied.browser_route_cache is client.browser_route_cache
- hooks = client._client.event_hooks["response"] # pyright: ignore[reportPrivateUsage]
- assert len(hooks) == 1
+ response_hooks = client._client.event_hooks["response"] # pyright: ignore[reportPrivateUsage]
+ request_hooks = client._client.event_hooks["request"] # pyright: ignore[reportPrivateUsage]
+ assert len(response_hooks) == 1
+ assert len(request_hooks) == 1
def test_route_eviction_hook_runs_before_caller_response_hooks(
From 3037090cd665d38b321b2d3cd40a9cfc7580f88c Mon Sep 17 00:00:00 2001
From: tnsardesai <18272584+tnsardesai@users.noreply.github.com>
Date: Wed, 9 Sep 2026 16:47:27 +0000
Subject: [PATCH 241/366] Evict the originating browser route cache
---
src/kernel/_client.py | 4 +-
src/kernel/lib/browser_routing/routing.py | 19 +++-
tests/test_browser_routing.py | 107 ++++++++++++++++++++++
3 files changed, 123 insertions(+), 7 deletions(-)
diff --git a/src/kernel/_client.py b/src/kernel/_client.py
index ff6d3164..4ddcd26c 100644
--- a/src/kernel/_client.py
+++ b/src/kernel/_client.py
@@ -378,7 +378,7 @@ def _prepare_options(self, options: Any) -> Any:
@override
def _prepare_request(self, request: httpx.Request) -> None:
- prepare_direct_vm_request(request)
+ prepare_direct_vm_request(request, cache=self.browser_route_cache)
@override
def _should_retry_on_connection_error(self, request: httpx.Request) -> bool:
@@ -777,7 +777,7 @@ async def _prepare_options(self, options: Any) -> Any:
@override
async def _prepare_request(self, request: httpx.Request) -> None:
- prepare_direct_vm_request(request)
+ prepare_direct_vm_request(request, cache=self.browser_route_cache)
@override
def _should_retry_on_connection_error(self, request: httpx.Request) -> bool:
diff --git a/src/kernel/lib/browser_routing/routing.py b/src/kernel/lib/browser_routing/routing.py
index 8e152c02..e90dee37 100644
--- a/src/kernel/lib/browser_routing/routing.py
+++ b/src/kernel/lib/browser_routing/routing.py
@@ -38,6 +38,7 @@ class BrowserRoutingConfig:
_DIRECT_VM_REQUEST_MARKER_HEADER = "x-kernel-direct-vm-request"
_STALE_DIRECT_VM_AUTH_REQUEST_EXTENSION = "kernel_stale_direct_vm_auth"
_DIRECT_VM_BODY_REPLAYABLE_REQUEST_EXTENSION = "kernel_direct_vm_body_replayable"
+_DIRECT_VM_ROUTE_CACHE_REQUEST_EXTENSION = "kernel_direct_vm_route_cache"
_BROWSER_ROUTE_CACHEABLE_PATH = re.compile(r"^/(?:v\d+/)?browsers(?:/[^/]+)?/?$")
@@ -225,10 +226,11 @@ def install_stale_direct_vm_auth_eviction(client: httpx.Client, *, cache: Browse
return
def handle_response(response: httpx.Response) -> None:
- _reject_unreplayable_direct_vm_redirect(response, cache=cache)
+ request_cache = _direct_vm_route_cache(response.request) or cache
+ _reject_unreplayable_direct_vm_redirect(response, cache=request_cache)
if is_stale_direct_vm_auth_response(response):
response.request.extensions[_STALE_DIRECT_VM_AUTH_REQUEST_EXTENSION] = True
- maybe_evict_browser_route_from_response(response, cache=cache)
+ maybe_evict_browser_route_from_response(response, cache=request_cache)
setattr(handle_response, _EVICTION_HOOK_CACHE_ATTR, cache)
hooks.insert(0, handle_response)
@@ -241,10 +243,11 @@ def install_async_stale_direct_vm_auth_eviction(client: httpx.AsyncClient, *, ca
return
async def handle_response(response: httpx.Response) -> None:
- _reject_unreplayable_direct_vm_redirect(response, cache=cache)
+ request_cache = _direct_vm_route_cache(response.request) or cache
+ _reject_unreplayable_direct_vm_redirect(response, cache=request_cache)
if is_stale_direct_vm_auth_response(response):
response.request.extensions[_STALE_DIRECT_VM_AUTH_REQUEST_EXTENSION] = True
- maybe_evict_browser_route_from_response(response, cache=cache)
+ maybe_evict_browser_route_from_response(response, cache=request_cache)
setattr(handle_response, _EVICTION_HOOK_CACHE_ATTR, cache)
hooks.insert(0, handle_response)
@@ -466,7 +469,7 @@ def rewrite_direct_vm_options(
return rewritten
-def prepare_direct_vm_request(request: httpx.Request) -> None:
+def prepare_direct_vm_request(request: httpx.Request, *, cache: BrowserRouteCache) -> None:
if request.headers.pop(_DIRECT_VM_REQUEST_MARKER_HEADER, None) is None:
return
@@ -475,9 +478,15 @@ def prepare_direct_vm_request(request: httpx.Request) -> None:
request.extensions[_DIRECT_VM_BODY_REPLAYABLE_REQUEST_EXTENSION] = _classify_direct_vm_request_body_replayability(
request
)
+ request.extensions[_DIRECT_VM_ROUTE_CACHE_REQUEST_EXTENSION] = cache
request.headers.pop("Authorization", None)
+def _direct_vm_route_cache(request: httpx.Request) -> BrowserRouteCache | None:
+ cache = request.extensions.get(_DIRECT_VM_ROUTE_CACHE_REQUEST_EXTENSION)
+ return cache if isinstance(cache, BrowserRouteCache) else None
+
+
def _is_direct_vm_request(request: httpx.Request) -> bool:
return isinstance(request.extensions.get(_DIRECT_VM_BODY_REPLAYABLE_REQUEST_EXTENSION), bool)
diff --git a/tests/test_browser_routing.py b/tests/test_browser_routing.py
index f3e40678..584b8491 100644
--- a/tests/test_browser_routing.py
+++ b/tests/test_browser_routing.py
@@ -1726,6 +1726,113 @@ async def handle_async_request(self, request: httpx.Request) -> httpx.Response:
]
+@pytest.mark.parametrize("origin_first", [True, False])
+def test_direct_vm_redirect_evicts_originating_cache_when_http_client_is_shared(
+ monkeypatch: pytest.MonkeyPatch,
+ origin_first: bool,
+) -> None:
+ monkeypatch.delenv("KERNEL_BROWSER_ROUTING_SUBRESOURCES", raising=False)
+ requests: list[tuple[httpx.URL, bytes]] = []
+
+ class Transport(httpx.BaseTransport):
+ @override
+ def handle_request(self, request: httpx.Request) -> httpx.Response:
+ body = b"".join(cast(Iterator[bytes], request.stream))
+ requests.append((request.url, body))
+ return httpx.Response(
+ 307,
+ headers={"location": "http://other-vm.test/browser/kernel/fs/write_file"},
+ )
+
+ http_client = httpx.Client(transport=Transport(), follow_redirects=True)
+
+ def make_client() -> Kernel:
+ return Kernel(
+ base_url=base_url,
+ api_key=api_key,
+ http_client=http_client,
+ _strict_response_validation=True,
+ )
+
+ if origin_first:
+ client, other_client = make_client(), make_client()
+ else:
+ other_client, client = make_client(), make_client()
+
+ try:
+ _cache_browser(client)
+ with pytest.raises(APIConnectionError):
+ client.browsers.fs.write_file("sess-1", _UnseekableFile(b"payload"), path="/tmp/x")
+ assert client.browser_route_cache.get("sess-1") is None
+ assert other_client.browser_route_cache.get("sess-1") is None
+ finally:
+ http_client.close()
+
+ assert requests == [
+ (
+ httpx.URL("http://browser-session.test/browser/kernel/fs/write_file?path=%2Ftmp%2Fx&jwt=token-abc"),
+ b"payload",
+ )
+ ]
+
+
+@pytest.mark.asyncio
+@pytest.mark.parametrize("origin_first", [True, False])
+async def test_async_direct_vm_redirect_evicts_originating_cache_when_http_client_is_shared(
+ monkeypatch: pytest.MonkeyPatch,
+ origin_first: bool,
+) -> None:
+ monkeypatch.delenv("KERNEL_BROWSER_ROUTING_SUBRESOURCES", raising=False)
+ requests: list[tuple[httpx.URL, bytes]] = []
+
+ class Transport(httpx.AsyncBaseTransport):
+ @override
+ async def handle_async_request(self, request: httpx.Request) -> httpx.Response:
+ body = b"".join([chunk async for chunk in cast(AsyncIterator[bytes], request.stream)])
+ requests.append((request.url, body))
+ return httpx.Response(
+ 308,
+ headers={"location": "http://other-vm.test/browser/kernel/fs/write_file"},
+ )
+
+ http_client = httpx.AsyncClient(transport=Transport(), follow_redirects=True)
+
+ def make_client() -> AsyncKernel:
+ return AsyncKernel(
+ base_url=base_url,
+ api_key=api_key,
+ http_client=http_client,
+ _strict_response_validation=True,
+ )
+
+ if origin_first:
+ client, other_client = make_client(), make_client()
+ else:
+ other_client, client = make_client(), make_client()
+
+ try:
+ route = browser_route_from_browser(_fake_browser())
+ assert route is not None
+ client.browser_route_cache.set(route)
+ with pytest.raises(APIConnectionError):
+ await client.browsers.fs.write_file(
+ "sess-1",
+ cast(Any, _UnreplayableAsyncBody(b"payload")),
+ path="/tmp/x",
+ )
+ assert client.browser_route_cache.get("sess-1") is None
+ assert other_client.browser_route_cache.get("sess-1") is None
+ finally:
+ await http_client.aclose()
+
+ assert requests == [
+ (
+ httpx.URL("http://browser-session.test/browser/kernel/fs/write_file?path=%2Ftmp%2Fx&jwt=token-abc"),
+ b"payload",
+ )
+ ]
+
+
def test_direct_vm_auth_is_removed_after_httpx_and_request_hooks(monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.delenv("KERNEL_BROWSER_ROUTING_SUBRESOURCES", raising=False)
requests: list[httpx.Request] = []
From ba94ef3eed41b365440fb30abd8c579e7ac85e7a Mon Sep 17 00:00:00 2001
From: tnsardesai <18272584+tnsardesai@users.noreply.github.com>
Date: Wed, 9 Sep 2026 16:59:40 +0000
Subject: [PATCH 242/366] Preserve direct request ownership
---
src/kernel/_client.py | 4 +--
src/kernel/lib/browser_routing/raw_http.py | 6 ++--
src/kernel/lib/browser_routing/routing.py | 34 ++++++++++++----------
tests/test_browser_routing.py | 30 +++++++++++++++----
4 files changed, 48 insertions(+), 26 deletions(-)
diff --git a/src/kernel/_client.py b/src/kernel/_client.py
index 4ddcd26c..aed72055 100644
--- a/src/kernel/_client.py
+++ b/src/kernel/_client.py
@@ -213,7 +213,7 @@ def __init__(
self.browser_route_cache = _browser_route_cache or BrowserRouteCache()
self._browser_routing = browser_routing_config_from_env()
install_direct_vm_auth_stripping(self._client)
- install_stale_direct_vm_auth_eviction(self._client, cache=self.browser_route_cache)
+ install_stale_direct_vm_auth_eviction(self._client)
@cached_property
def deployments(self) -> DeploymentsResource:
@@ -612,7 +612,7 @@ def __init__(
self.browser_route_cache = _browser_route_cache or BrowserRouteCache()
self._browser_routing = browser_routing_config_from_env()
install_async_direct_vm_auth_stripping(self._client)
- install_async_stale_direct_vm_auth_eviction(self._client, cache=self.browser_route_cache)
+ install_async_stale_direct_vm_auth_eviction(self._client)
@cached_property
def deployments(self) -> AsyncDeploymentsResource:
diff --git a/src/kernel/lib/browser_routing/raw_http.py b/src/kernel/lib/browser_routing/raw_http.py
index 5e644e39..b6da0da6 100644
--- a/src/kernel/lib/browser_routing/raw_http.py
+++ b/src/kernel/lib/browser_routing/raw_http.py
@@ -7,7 +7,7 @@
import httpx
from .util import sanitize_curl_raw_params
-from .routing import BrowserRoute
+from .routing import BrowserRoute, mark_direct_vm_headers
from ..._types import Body, Timeout, NotGiven, not_given
from ..._models import FinalRequestOptions
@@ -33,7 +33,7 @@ def request_via_browser_route(
method=method.upper(),
url=route.base_url.rstrip("/") + "/curl/raw",
params=query,
- headers=headers or {},
+ headers=mark_direct_vm_headers(headers),
content=_normalize_binary_content(content),
json_data=json,
timeout=_normalize_timeout(timeout),
@@ -93,7 +93,7 @@ async def async_request_via_browser_route(
method=method.upper(),
url=route.base_url.rstrip("/") + "/curl/raw",
params=query,
- headers=headers or {},
+ headers=mark_direct_vm_headers(headers),
content=_normalize_binary_content(content),
json_data=json,
timeout=_normalize_timeout(timeout),
diff --git a/src/kernel/lib/browser_routing/routing.py b/src/kernel/lib/browser_routing/routing.py
index e90dee37..803a759b 100644
--- a/src/kernel/lib/browser_routing/routing.py
+++ b/src/kernel/lib/browser_routing/routing.py
@@ -33,7 +33,7 @@ class BrowserRoutingConfig:
subresources: tuple[str, ...] = field(default_factory=tuple)
-_EVICTION_HOOK_CACHE_ATTR = "_kernel_browser_route_cache"
+_DIRECT_VM_EVICTION_HOOK_ATTR = "_kernel_direct_vm_eviction_hook"
_DIRECT_VM_AUTH_HOOK_ATTR = "_kernel_direct_vm_auth_hook"
_DIRECT_VM_REQUEST_MARKER_HEADER = "x-kernel-direct-vm-request"
_STALE_DIRECT_VM_AUTH_REQUEST_EXTENSION = "kernel_stale_direct_vm_auth"
@@ -207,7 +207,7 @@ def is_stale_direct_vm_auth_response(response: httpx.Response) -> bool:
return bool(response.request.url.params.get("jwt"))
-def install_stale_direct_vm_auth_eviction(client: httpx.Client, *, cache: BrowserRouteCache) -> None:
+def install_stale_direct_vm_auth_eviction(client: httpx.Client) -> None:
"""Evict stale direct-to-VM routes as soon as the response status is known.
httpx reads the body of a non-streamed response inside `send()`, so a caller
@@ -222,34 +222,38 @@ def install_stale_direct_vm_auth_eviction(client: httpx.Client, *, cache: Browse
failing body or raising.
"""
hooks = client.event_hooks.setdefault("response", [])
- if _has_eviction_hook(hooks, cache):
+ if any(getattr(hook, _DIRECT_VM_EVICTION_HOOK_ATTR, False) for hook in hooks):
return
def handle_response(response: httpx.Response) -> None:
- request_cache = _direct_vm_route_cache(response.request) or cache
+ request_cache = _direct_vm_route_cache(response.request)
+ if request_cache is None:
+ return
_reject_unreplayable_direct_vm_redirect(response, cache=request_cache)
if is_stale_direct_vm_auth_response(response):
response.request.extensions[_STALE_DIRECT_VM_AUTH_REQUEST_EXTENSION] = True
maybe_evict_browser_route_from_response(response, cache=request_cache)
- setattr(handle_response, _EVICTION_HOOK_CACHE_ATTR, cache)
+ setattr(handle_response, _DIRECT_VM_EVICTION_HOOK_ATTR, True)
hooks.insert(0, handle_response)
-def install_async_stale_direct_vm_auth_eviction(client: httpx.AsyncClient, *, cache: BrowserRouteCache) -> None:
+def install_async_stale_direct_vm_auth_eviction(client: httpx.AsyncClient) -> None:
"""Async counterpart of `install_stale_direct_vm_auth_eviction`."""
hooks = client.event_hooks.setdefault("response", [])
- if _has_eviction_hook(hooks, cache):
+ if any(getattr(hook, _DIRECT_VM_EVICTION_HOOK_ATTR, False) for hook in hooks):
return
async def handle_response(response: httpx.Response) -> None:
- request_cache = _direct_vm_route_cache(response.request) or cache
+ request_cache = _direct_vm_route_cache(response.request)
+ if request_cache is None:
+ return
_reject_unreplayable_direct_vm_redirect(response, cache=request_cache)
if is_stale_direct_vm_auth_response(response):
response.request.extensions[_STALE_DIRECT_VM_AUTH_REQUEST_EXTENSION] = True
maybe_evict_browser_route_from_response(response, cache=request_cache)
- setattr(handle_response, _EVICTION_HOOK_CACHE_ATTR, cache)
+ setattr(handle_response, _DIRECT_VM_EVICTION_HOOK_ATTR, True)
hooks.insert(0, handle_response)
@@ -281,12 +285,6 @@ async def strip_auth(request: httpx.Request) -> None:
hooks.append(strip_auth)
-def _has_eviction_hook(hooks: list[Any], cache: BrowserRouteCache) -> bool:
- # A copied client shares both the httpx client and the route cache, so the
- # hook is registered once per cache instead of once per client.
- return any(getattr(hook, _EVICTION_HOOK_CACHE_ATTR, None) is cache for hook in hooks)
-
-
def _reject_unreplayable_direct_vm_redirect(response: httpx.Response, *, cache: BrowserRouteCache) -> None:
if not response.has_redirect_location or not direct_vm_request_body_is_known_unreplayable(response.request):
return
@@ -469,6 +467,12 @@ def rewrite_direct_vm_options(
return rewritten
+def mark_direct_vm_headers(headers: Mapping[str, str] | None) -> dict[str, str]:
+ marked = dict(headers or {})
+ marked[_DIRECT_VM_REQUEST_MARKER_HEADER] = "true"
+ return marked
+
+
def prepare_direct_vm_request(request: httpx.Request, *, cache: BrowserRouteCache) -> None:
if request.headers.pop(_DIRECT_VM_REQUEST_MARKER_HEADER, None) is None:
return
diff --git a/tests/test_browser_routing.py b/tests/test_browser_routing.py
index 584b8491..88e2edf4 100644
--- a/tests/test_browser_routing.py
+++ b/tests/test_browser_routing.py
@@ -190,6 +190,8 @@ def test_browser_request_uses_curl_raw() -> None:
request = cast(httpx.Request, cast(Any, route.calls[0]).request)
assert "curl/raw" in str(request.url)
assert request.url.params.get("jwt") == "token-abc"
+ assert request.headers.get("Authorization") is None
+ assert request.headers.get("x-kernel-direct-vm-request") is None
@respx.mock
@@ -344,6 +346,8 @@ async def test_async_raw_browser_create_warms_route_cache() -> None:
assert routed.content == b"ok"
request = cast(httpx.Request, cast(Any, routed_request.calls[0]).request)
assert request.url.params.get("jwt") == "token-abc"
+ assert request.headers.get("Authorization") is None
+ assert request.headers.get("x-kernel-direct-vm-request") is None
@respx.mock
@@ -1727,9 +1731,15 @@ async def handle_async_request(self, request: httpx.Request) -> httpx.Response:
@pytest.mark.parametrize("origin_first", [True, False])
-def test_direct_vm_redirect_evicts_originating_cache_when_http_client_is_shared(
+@pytest.mark.parametrize(
+ ("status_code", "expected_error"),
+ [(307, APIConnectionError), (401, AuthenticationError)],
+)
+def test_direct_vm_failure_evicts_originating_cache_when_http_client_is_shared(
monkeypatch: pytest.MonkeyPatch,
origin_first: bool,
+ status_code: int,
+ expected_error: type[Exception],
) -> None:
monkeypatch.delenv("KERNEL_BROWSER_ROUTING_SUBRESOURCES", raising=False)
requests: list[tuple[httpx.URL, bytes]] = []
@@ -1740,7 +1750,7 @@ def handle_request(self, request: httpx.Request) -> httpx.Response:
body = b"".join(cast(Iterator[bytes], request.stream))
requests.append((request.url, body))
return httpx.Response(
- 307,
+ status_code,
headers={"location": "http://other-vm.test/browser/kernel/fs/write_file"},
)
@@ -1760,8 +1770,9 @@ def make_client() -> Kernel:
other_client, client = make_client(), make_client()
try:
+ assert len(http_client.event_hooks["response"]) == 1
_cache_browser(client)
- with pytest.raises(APIConnectionError):
+ with pytest.raises(expected_error):
client.browsers.fs.write_file("sess-1", _UnseekableFile(b"payload"), path="/tmp/x")
assert client.browser_route_cache.get("sess-1") is None
assert other_client.browser_route_cache.get("sess-1") is None
@@ -1778,9 +1789,15 @@ def make_client() -> Kernel:
@pytest.mark.asyncio
@pytest.mark.parametrize("origin_first", [True, False])
-async def test_async_direct_vm_redirect_evicts_originating_cache_when_http_client_is_shared(
+@pytest.mark.parametrize(
+ ("status_code", "expected_error"),
+ [(308, APIConnectionError), (403, PermissionDeniedError)],
+)
+async def test_async_direct_vm_failure_evicts_originating_cache_when_http_client_is_shared(
monkeypatch: pytest.MonkeyPatch,
origin_first: bool,
+ status_code: int,
+ expected_error: type[Exception],
) -> None:
monkeypatch.delenv("KERNEL_BROWSER_ROUTING_SUBRESOURCES", raising=False)
requests: list[tuple[httpx.URL, bytes]] = []
@@ -1791,7 +1808,7 @@ async def handle_async_request(self, request: httpx.Request) -> httpx.Response:
body = b"".join([chunk async for chunk in cast(AsyncIterator[bytes], request.stream)])
requests.append((request.url, body))
return httpx.Response(
- 308,
+ status_code,
headers={"location": "http://other-vm.test/browser/kernel/fs/write_file"},
)
@@ -1811,10 +1828,11 @@ def make_client() -> AsyncKernel:
other_client, client = make_client(), make_client()
try:
+ assert len(http_client.event_hooks["response"]) == 1
route = browser_route_from_browser(_fake_browser())
assert route is not None
client.browser_route_cache.set(route)
- with pytest.raises(APIConnectionError):
+ with pytest.raises(expected_error):
await client.browsers.fs.write_file(
"sess-1",
cast(Any, _UnreplayableAsyncBody(b"payload")),
From e0d9d715689585224cde848845ebaed90c404f64 Mon Sep 17 00:00:00 2001
From: "kernel-internal[bot]"
<260533166+kernel-internal[bot]@users.noreply.github.com>
Date: Wed, 9 Sep 2026 17:00:40 +0000
Subject: [PATCH 243/366] feat: Add config analysis lifecycle guarantees
Stainless-Generated-From: 3a35e0b87b6364867c5a45f32039f9448f234afd
---
src/kernel/types/analysis.py | 7 +++++--
src/kernel/types/recommendation_summary.py | 2 +-
2 files changed, 6 insertions(+), 3 deletions(-)
diff --git a/src/kernel/types/analysis.py b/src/kernel/types/analysis.py
index 9bce3019..34184ae4 100644
--- a/src/kernel/types/analysis.py
+++ b/src/kernel/types/analysis.py
@@ -17,8 +17,11 @@ class Analysis(BaseModel):
created_at: datetime
"""Time the analysis was created."""
+ expires_at: datetime
+ """Deadline after which a still-running analysis becomes expired."""
+
failure: Optional[ErrorModel] = None
- """Present for failed or canceled analyses.
+ """Present for failed, canceled, or expired analyses.
Messages contain safe retry guidance rather than internal workflow errors.
"""
@@ -26,5 +29,5 @@ class Analysis(BaseModel):
finished_at: Optional[datetime] = None
"""Time the analysis reached a terminal status. Null while it is running."""
- status: Literal["running", "completed", "failed", "canceled"]
+ status: Literal["running", "completed", "failed", "canceled", "expired"]
"""Lifecycle status of a background analysis."""
diff --git a/src/kernel/types/recommendation_summary.py b/src/kernel/types/recommendation_summary.py
index 278ba9ef..5716ae60 100644
--- a/src/kernel/types/recommendation_summary.py
+++ b/src/kernel/types/recommendation_summary.py
@@ -14,7 +14,7 @@ class RecommendationSummary(BaseModel):
analysis_id: str
"""ID of the most recently requested analysis for this exact target."""
- analysis_status: Literal["running", "completed", "failed", "canceled"]
+ analysis_status: Literal["running", "completed", "failed", "canceled", "expired"]
"""Lifecycle status of the most recently requested analysis for this exact target."""
last_requested_at: datetime
From 94c0e236a61db6767c2f63d78498af7fb7cfc991 Mon Sep 17 00:00:00 2001
From: tnsardesai <18272584+tnsardesai@users.noreply.github.com>
Date: Wed, 9 Sep 2026 17:12:41 +0000
Subject: [PATCH 244/366] Evict stale routes from browser streams
---
src/kernel/lib/browser_routing/raw_http.py | 4 +-
src/kernel/lib/browser_routing/routing.py | 8 ++-
tests/test_browser_routing.py | 59 ++++++++++++++++++++++
3 files changed, 69 insertions(+), 2 deletions(-)
diff --git a/src/kernel/lib/browser_routing/raw_http.py b/src/kernel/lib/browser_routing/raw_http.py
index b6da0da6..3fded8ca 100644
--- a/src/kernel/lib/browser_routing/raw_http.py
+++ b/src/kernel/lib/browser_routing/raw_http.py
@@ -7,7 +7,7 @@
import httpx
from .util import sanitize_curl_raw_params
-from .routing import BrowserRoute, mark_direct_vm_headers
+from .routing import BrowserRoute, mark_direct_vm_headers, direct_vm_request_extensions
from ..._types import Body, Timeout, NotGiven, not_given
from ..._models import FinalRequestOptions
@@ -70,6 +70,7 @@ def stream_via_browser_route(
headers=request_headers,
content=_normalize_binary_content(content),
timeout=_normalize_timeout(effective_timeout),
+ extensions=direct_vm_request_extensions(cache=parent.browser_route_cache),
) as response:
yield response
@@ -130,6 +131,7 @@ async def async_stream_via_browser_route(
headers=request_headers,
content=_normalize_binary_content(content),
timeout=_normalize_timeout(effective_timeout),
+ extensions=direct_vm_request_extensions(cache=parent.browser_route_cache),
) as response:
yield response
diff --git a/src/kernel/lib/browser_routing/routing.py b/src/kernel/lib/browser_routing/routing.py
index 803a759b..61f786cb 100644
--- a/src/kernel/lib/browser_routing/routing.py
+++ b/src/kernel/lib/browser_routing/routing.py
@@ -491,8 +491,14 @@ def _direct_vm_route_cache(request: httpx.Request) -> BrowserRouteCache | None:
return cache if isinstance(cache, BrowserRouteCache) else None
+def direct_vm_request_extensions(*, cache: BrowserRouteCache) -> dict[str, object]:
+ return {_DIRECT_VM_ROUTE_CACHE_REQUEST_EXTENSION: cache}
+
+
def _is_direct_vm_request(request: httpx.Request) -> bool:
- return isinstance(request.extensions.get(_DIRECT_VM_BODY_REPLAYABLE_REQUEST_EXTENSION), bool)
+ return _direct_vm_route_cache(request) is not None or isinstance(
+ request.extensions.get(_DIRECT_VM_BODY_REPLAYABLE_REQUEST_EXTENSION), bool
+ )
def match_direct_vm_path(path: str) -> tuple[str, str, str] | None:
diff --git a/tests/test_browser_routing.py b/tests/test_browser_routing.py
index 88e2edf4..1908c688 100644
--- a/tests/test_browser_routing.py
+++ b/tests/test_browser_routing.py
@@ -194,6 +194,65 @@ def test_browser_request_uses_curl_raw() -> None:
assert request.headers.get("x-kernel-direct-vm-request") is None
+def test_browser_stream_evicts_stale_route() -> None:
+ requests: list[httpx.Request] = []
+
+ def handle_request(request: httpx.Request) -> httpx.Response:
+ requests.append(request)
+ if request.url.host == "browser-session.test":
+ return httpx.Response(401)
+ return httpx.Response(201)
+
+ http_client = httpx.Client(transport=httpx.MockTransport(handle_request))
+ with Kernel(
+ base_url=base_url,
+ api_key=api_key,
+ http_client=http_client,
+ _strict_response_validation=True,
+ ) as client:
+ _cache_browser(client)
+ with client.browsers.stream("sess-1", "POST", "https://example.com", content=[b"payload"]) as response:
+ assert response.status_code == 401
+ assert client.browser_route_cache.get("sess-1") is None
+ client.browsers.fs.write_file("sess-1", b"next", path="/tmp/x")
+
+ assert requests[0].url.host == "browser-session.test"
+ assert requests[0].headers.get("Authorization") is None
+ assert requests[1].url == httpx.URL(f"{base_url}/browsers/sess-1/fs/write_file?path=%2Ftmp%2Fx")
+ assert requests[1].headers.get("Authorization") == f"Bearer {api_key}"
+
+
+@pytest.mark.asyncio
+async def test_async_browser_stream_evicts_stale_route() -> None:
+ requests: list[httpx.Request] = []
+
+ async def handle_request(request: httpx.Request) -> httpx.Response:
+ requests.append(request)
+ if request.url.host == "browser-session.test":
+ return httpx.Response(403)
+ return httpx.Response(201)
+
+ http_client = httpx.AsyncClient(transport=httpx.MockTransport(handle_request))
+ async with AsyncKernel(
+ base_url=base_url,
+ api_key=api_key,
+ http_client=http_client,
+ _strict_response_validation=True,
+ ) as client:
+ route = browser_route_from_browser(_fake_browser())
+ assert route is not None
+ client.browser_route_cache.set(route)
+ async with client.browsers.stream("sess-1", "POST", "https://example.com", content=b"payload") as response:
+ assert response.status_code == 403
+ assert client.browser_route_cache.get("sess-1") is None
+ await client.browsers.fs.write_file("sess-1", b"next", path="/tmp/x")
+
+ assert requests[0].url.host == "browser-session.test"
+ assert requests[0].headers.get("Authorization") is None
+ assert requests[1].url == httpx.URL(f"{base_url}/browsers/sess-1/fs/write_file?path=%2Ftmp%2Fx")
+ assert requests[1].headers.get("Authorization") == f"Bearer {api_key}"
+
+
@respx.mock
def test_telemetry_stream_routes_directly_to_vm(monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.setenv("KERNEL_BROWSER_ROUTING_SUBRESOURCES", "telemetry/stream")
From 78adb8a4ae929028497e30b0a5988050eee7c1f4 Mon Sep 17 00:00:00 2001
From: "kernel-internal[bot]"
<260533166+kernel-internal[bot]@users.noreply.github.com>
Date: Wed, 9 Sep 2026 23:53:58 +0000
Subject: [PATCH 245/366] feat: Support international ISP proxy countries
Stainless-Generated-From: 3cb3f808b95f723b62071d2e57647368fd68baae
---
src/kernel/types/proxy.py | 5 ++++-
src/kernel/types/proxy_check_response.py | 5 ++++-
src/kernel/types/proxy_create_params.py | 5 ++++-
src/kernel/types/proxy_create_response.py | 5 ++++-
src/kernel/types/proxy_list_response.py | 5 ++++-
src/kernel/types/proxy_retrieve_response.py | 5 ++++-
src/kernel/types/proxy_update_response.py | 5 ++++-
7 files changed, 28 insertions(+), 7 deletions(-)
diff --git a/src/kernel/types/proxy.py b/src/kernel/types/proxy.py
index d5238ad7..53ff0612 100644
--- a/src/kernel/types/proxy.py
+++ b/src/kernel/types/proxy.py
@@ -37,7 +37,10 @@ class ConfigRegistryManagedProxyCreateConfigIspProxyConfig(BaseModel):
"""Configuration for an ISP proxy."""
country: Optional[str] = None
- """ISO 3166 country code. Defaults to US if not provided."""
+ """ISO 3166 country code.
+
+ Supported countries are US, GB, FR, DE, and SG. Defaults to US if not provided.
+ """
class ConfigRegistryManagedProxyCreateConfigResidentialProxyConfig(BaseModel):
diff --git a/src/kernel/types/proxy_check_response.py b/src/kernel/types/proxy_check_response.py
index 49833d66..3f3a0467 100644
--- a/src/kernel/types/proxy_check_response.py
+++ b/src/kernel/types/proxy_check_response.py
@@ -28,7 +28,10 @@ class ConfigIspProxyConfig(BaseModel):
"""Configuration for an ISP proxy."""
country: Optional[str] = None
- """ISO 3166 country code. Defaults to US if not provided."""
+ """ISO 3166 country code.
+
+ Supported countries are US, GB, FR, DE, and SG. Defaults to US if not provided.
+ """
class ConfigResidentialProxyConfig(BaseModel):
diff --git a/src/kernel/types/proxy_create_params.py b/src/kernel/types/proxy_create_params.py
index 7069d9b1..ed7f413b 100644
--- a/src/kernel/types/proxy_create_params.py
+++ b/src/kernel/types/proxy_create_params.py
@@ -50,7 +50,10 @@ class ConfigIspProxyConfig(TypedDict, total=False):
"""Configuration for an ISP proxy."""
country: str
- """ISO 3166 country code. Defaults to US if not provided."""
+ """ISO 3166 country code.
+
+ Supported countries are US, GB, FR, DE, and SG. Defaults to US if not provided.
+ """
class ConfigResidentialProxyConfig(TypedDict, total=False):
diff --git a/src/kernel/types/proxy_create_response.py b/src/kernel/types/proxy_create_response.py
index eeddcc3a..187168a3 100644
--- a/src/kernel/types/proxy_create_response.py
+++ b/src/kernel/types/proxy_create_response.py
@@ -28,7 +28,10 @@ class ConfigIspProxyConfig(BaseModel):
"""Configuration for an ISP proxy."""
country: Optional[str] = None
- """ISO 3166 country code. Defaults to US if not provided."""
+ """ISO 3166 country code.
+
+ Supported countries are US, GB, FR, DE, and SG. Defaults to US if not provided.
+ """
class ConfigResidentialProxyConfig(BaseModel):
diff --git a/src/kernel/types/proxy_list_response.py b/src/kernel/types/proxy_list_response.py
index 878ad7f2..1cd6ca18 100644
--- a/src/kernel/types/proxy_list_response.py
+++ b/src/kernel/types/proxy_list_response.py
@@ -28,7 +28,10 @@ class ConfigIspProxyConfig(BaseModel):
"""Configuration for an ISP proxy."""
country: Optional[str] = None
- """ISO 3166 country code. Defaults to US if not provided."""
+ """ISO 3166 country code.
+
+ Supported countries are US, GB, FR, DE, and SG. Defaults to US if not provided.
+ """
class ConfigResidentialProxyConfig(BaseModel):
diff --git a/src/kernel/types/proxy_retrieve_response.py b/src/kernel/types/proxy_retrieve_response.py
index f2eb1487..349ee91b 100644
--- a/src/kernel/types/proxy_retrieve_response.py
+++ b/src/kernel/types/proxy_retrieve_response.py
@@ -28,7 +28,10 @@ class ConfigIspProxyConfig(BaseModel):
"""Configuration for an ISP proxy."""
country: Optional[str] = None
- """ISO 3166 country code. Defaults to US if not provided."""
+ """ISO 3166 country code.
+
+ Supported countries are US, GB, FR, DE, and SG. Defaults to US if not provided.
+ """
class ConfigResidentialProxyConfig(BaseModel):
diff --git a/src/kernel/types/proxy_update_response.py b/src/kernel/types/proxy_update_response.py
index bae95bba..7dcd14aa 100644
--- a/src/kernel/types/proxy_update_response.py
+++ b/src/kernel/types/proxy_update_response.py
@@ -28,7 +28,10 @@ class ConfigIspProxyConfig(BaseModel):
"""Configuration for an ISP proxy."""
country: Optional[str] = None
- """ISO 3166 country code. Defaults to US if not provided."""
+ """ISO 3166 country code.
+
+ Supported countries are US, GB, FR, DE, and SG. Defaults to US if not provided.
+ """
class ConfigResidentialProxyConfig(BaseModel):
From bb00cdaabdeb569ed1bf46b538d15de8768b9686 Mon Sep 17 00:00:00 2001
From: "kernel-internal[bot]"
<260533166+kernel-internal[bot]@users.noreply.github.com>
Date: Thu, 10 Sep 2026 18:08:29 +0000
Subject: [PATCH 246/366] feat: Clarify proxy country defaults
Stainless-Generated-From: 155baadc5eebaddc769c20d1bf6d1ae1806fc8d1
---
src/kernel/types/proxy.py | 10 ++++++++--
src/kernel/types/proxy_check_response.py | 10 ++++++++--
src/kernel/types/proxy_create_params.py | 10 ++++++++--
src/kernel/types/proxy_create_response.py | 10 ++++++++--
src/kernel/types/proxy_list_response.py | 10 ++++++++--
src/kernel/types/proxy_retrieve_response.py | 10 ++++++++--
src/kernel/types/proxy_update_response.py | 10 ++++++++--
7 files changed, 56 insertions(+), 14 deletions(-)
diff --git a/src/kernel/types/proxy.py b/src/kernel/types/proxy.py
index 53ff0612..9c945ce6 100644
--- a/src/kernel/types/proxy.py
+++ b/src/kernel/types/proxy.py
@@ -56,7 +56,10 @@ class ConfigRegistryManagedProxyCreateConfigResidentialProxyConfig(BaseModel):
"""
country: Optional[str] = None
- """ISO 3166 country code."""
+ """ISO 3166 country code.
+
+ If omitted, the proxy uses the global pool without country targeting.
+ """
os: Optional[Literal["windows", "macos", "android"]] = None
"""Operating system of the residential device."""
@@ -75,7 +78,10 @@ class ConfigRegistryManagedProxyCreateConfigMobileProxyConfig(BaseModel):
"""Provider city alias. Mobile carrier routing can make observed geo vary."""
country: Optional[str] = None
- """ISO 3166 country code"""
+ """ISO 3166 country code.
+
+ If omitted, the proxy uses the global pool without country targeting.
+ """
state: Optional[str] = None
"""US-only state code. Mobile carrier routing can make observed geo vary."""
diff --git a/src/kernel/types/proxy_check_response.py b/src/kernel/types/proxy_check_response.py
index 3f3a0467..d2821093 100644
--- a/src/kernel/types/proxy_check_response.py
+++ b/src/kernel/types/proxy_check_response.py
@@ -47,7 +47,10 @@ class ConfigResidentialProxyConfig(BaseModel):
"""
country: Optional[str] = None
- """ISO 3166 country code."""
+ """ISO 3166 country code.
+
+ If omitted, the proxy uses the global pool without country targeting.
+ """
os: Optional[Literal["windows", "macos", "android"]] = None
"""Operating system of the residential device."""
@@ -66,7 +69,10 @@ class ConfigMobileProxyConfig(BaseModel):
"""Provider city alias. Mobile carrier routing can make observed geo vary."""
country: Optional[str] = None
- """ISO 3166 country code"""
+ """ISO 3166 country code.
+
+ If omitted, the proxy uses the global pool without country targeting.
+ """
state: Optional[str] = None
"""US-only state code. Mobile carrier routing can make observed geo vary."""
diff --git a/src/kernel/types/proxy_create_params.py b/src/kernel/types/proxy_create_params.py
index ed7f413b..3e5a7935 100644
--- a/src/kernel/types/proxy_create_params.py
+++ b/src/kernel/types/proxy_create_params.py
@@ -69,7 +69,10 @@ class ConfigResidentialProxyConfig(TypedDict, total=False):
"""
country: str
- """ISO 3166 country code."""
+ """ISO 3166 country code.
+
+ If omitted, the proxy uses the global pool without country targeting.
+ """
os: Literal["windows", "macos", "android"]
"""Operating system of the residential device."""
@@ -88,7 +91,10 @@ class ConfigMobileProxyConfig(TypedDict, total=False):
"""Provider city alias. Mobile carrier routing can make observed geo vary."""
country: str
- """ISO 3166 country code"""
+ """ISO 3166 country code.
+
+ If omitted, the proxy uses the global pool without country targeting.
+ """
state: str
"""US-only state code. Mobile carrier routing can make observed geo vary."""
diff --git a/src/kernel/types/proxy_create_response.py b/src/kernel/types/proxy_create_response.py
index 187168a3..7f0b1d7c 100644
--- a/src/kernel/types/proxy_create_response.py
+++ b/src/kernel/types/proxy_create_response.py
@@ -47,7 +47,10 @@ class ConfigResidentialProxyConfig(BaseModel):
"""
country: Optional[str] = None
- """ISO 3166 country code."""
+ """ISO 3166 country code.
+
+ If omitted, the proxy uses the global pool without country targeting.
+ """
os: Optional[Literal["windows", "macos", "android"]] = None
"""Operating system of the residential device."""
@@ -66,7 +69,10 @@ class ConfigMobileProxyConfig(BaseModel):
"""Provider city alias. Mobile carrier routing can make observed geo vary."""
country: Optional[str] = None
- """ISO 3166 country code"""
+ """ISO 3166 country code.
+
+ If omitted, the proxy uses the global pool without country targeting.
+ """
state: Optional[str] = None
"""US-only state code. Mobile carrier routing can make observed geo vary."""
diff --git a/src/kernel/types/proxy_list_response.py b/src/kernel/types/proxy_list_response.py
index 1cd6ca18..eea62ec8 100644
--- a/src/kernel/types/proxy_list_response.py
+++ b/src/kernel/types/proxy_list_response.py
@@ -47,7 +47,10 @@ class ConfigResidentialProxyConfig(BaseModel):
"""
country: Optional[str] = None
- """ISO 3166 country code."""
+ """ISO 3166 country code.
+
+ If omitted, the proxy uses the global pool without country targeting.
+ """
os: Optional[Literal["windows", "macos", "android"]] = None
"""Operating system of the residential device."""
@@ -66,7 +69,10 @@ class ConfigMobileProxyConfig(BaseModel):
"""Provider city alias. Mobile carrier routing can make observed geo vary."""
country: Optional[str] = None
- """ISO 3166 country code"""
+ """ISO 3166 country code.
+
+ If omitted, the proxy uses the global pool without country targeting.
+ """
state: Optional[str] = None
"""US-only state code. Mobile carrier routing can make observed geo vary."""
diff --git a/src/kernel/types/proxy_retrieve_response.py b/src/kernel/types/proxy_retrieve_response.py
index 349ee91b..26a72906 100644
--- a/src/kernel/types/proxy_retrieve_response.py
+++ b/src/kernel/types/proxy_retrieve_response.py
@@ -47,7 +47,10 @@ class ConfigResidentialProxyConfig(BaseModel):
"""
country: Optional[str] = None
- """ISO 3166 country code."""
+ """ISO 3166 country code.
+
+ If omitted, the proxy uses the global pool without country targeting.
+ """
os: Optional[Literal["windows", "macos", "android"]] = None
"""Operating system of the residential device."""
@@ -66,7 +69,10 @@ class ConfigMobileProxyConfig(BaseModel):
"""Provider city alias. Mobile carrier routing can make observed geo vary."""
country: Optional[str] = None
- """ISO 3166 country code"""
+ """ISO 3166 country code.
+
+ If omitted, the proxy uses the global pool without country targeting.
+ """
state: Optional[str] = None
"""US-only state code. Mobile carrier routing can make observed geo vary."""
diff --git a/src/kernel/types/proxy_update_response.py b/src/kernel/types/proxy_update_response.py
index 7dcd14aa..85b8d5ab 100644
--- a/src/kernel/types/proxy_update_response.py
+++ b/src/kernel/types/proxy_update_response.py
@@ -47,7 +47,10 @@ class ConfigResidentialProxyConfig(BaseModel):
"""
country: Optional[str] = None
- """ISO 3166 country code."""
+ """ISO 3166 country code.
+
+ If omitted, the proxy uses the global pool without country targeting.
+ """
os: Optional[Literal["windows", "macos", "android"]] = None
"""Operating system of the residential device."""
@@ -66,7 +69,10 @@ class ConfigMobileProxyConfig(BaseModel):
"""Provider city alias. Mobile carrier routing can make observed geo vary."""
country: Optional[str] = None
- """ISO 3166 country code"""
+ """ISO 3166 country code.
+
+ If omitted, the proxy uses the global pool without country targeting.
+ """
state: Optional[str] = None
"""US-only state code. Mobile carrier routing can make observed geo vary."""
From 7162cbbed49eb4d0a8d431bbb84680e48bdce8a2 Mon Sep 17 00:00:00 2001
From: "kernel-internal[bot]"
<260533166+kernel-internal[bot]@users.noreply.github.com>
Date: Thu, 10 Sep 2026 18:23:51 +0000
Subject: [PATCH 247/366] feat: Populate safe failure reasons on invocation
responses
Stainless-Generated-From: fcdc7d2aeb8e076e96239ca22da8698fa3315d71
---
src/kernel/types/invocation_create_response.py | 14 +++++++++++---
src/kernel/types/invocation_list_response.py | 15 ++++++++++++---
src/kernel/types/invocation_retrieve_response.py | 15 ++++++++++++---
src/kernel/types/invocation_state_event.py | 15 ++++++++++++---
src/kernel/types/invocation_update_response.py | 15 ++++++++++++---
5 files changed, 59 insertions(+), 15 deletions(-)
diff --git a/src/kernel/types/invocation_create_response.py b/src/kernel/types/invocation_create_response.py
index 21fbcf33..3e775279 100644
--- a/src/kernel/types/invocation_create_response.py
+++ b/src/kernel/types/invocation_create_response.py
@@ -19,10 +19,18 @@ class InvocationCreateResponse(BaseModel):
"""Status of the invocation"""
output: Optional[str] = None
- """The return value of the action that was invoked, rendered as a JSON string.
+ """The action result or detailed failure output.
- This could be: string, number, boolean, array, object, or null.
+ Often a JSON-encoded value, but failures may contain plain text. May contain
+ sensitive application data.
"""
status_reason: Optional[str] = None
- """Status reason"""
+ """
+ A nonempty, customer-safe summary of the recorded failure output, always present
+ when status is failed and omitted otherwise. Recognized messages receive a
+ specific summary; other failures receive a generic summary. Message matching
+ does not establish whether the failure originated in the platform or action
+ code. Does not include raw action output or internal error details.
+ Human-readable text, not a stable identifier for retry logic.
+ """
diff --git a/src/kernel/types/invocation_list_response.py b/src/kernel/types/invocation_list_response.py
index e635b4d8..f55dff9c 100644
--- a/src/kernel/types/invocation_list_response.py
+++ b/src/kernel/types/invocation_list_response.py
@@ -35,9 +35,10 @@ class InvocationListResponse(BaseModel):
"""
output: Optional[str] = None
- """Output produced by the action, rendered as a JSON string.
+ """The action result or detailed failure output.
- This could be: string, number, boolean, array, object, or null.
+ Often a JSON-encoded value, but failures may contain plain text. May contain
+ sensitive application data.
"""
payload: Optional[str] = None
@@ -47,4 +48,12 @@ class InvocationListResponse(BaseModel):
"""
status_reason: Optional[str] = None
- """Status reason"""
+ """
+ A nonempty, customer-safe summary of the recorded failure output, always present
+ when status is failed and omitted otherwise, including in the first failed
+ invocation_state event. Recognized messages receive a specific summary; other
+ failures receive a generic summary. Message matching does not establish whether
+ the failure originated in the platform or action code. Does not include raw
+ action output or internal error details. Available for historical invocations as
+ well. Human-readable text, not a stable identifier for retry logic.
+ """
diff --git a/src/kernel/types/invocation_retrieve_response.py b/src/kernel/types/invocation_retrieve_response.py
index 580424eb..e16c02de 100644
--- a/src/kernel/types/invocation_retrieve_response.py
+++ b/src/kernel/types/invocation_retrieve_response.py
@@ -35,9 +35,10 @@ class InvocationRetrieveResponse(BaseModel):
"""
output: Optional[str] = None
- """Output produced by the action, rendered as a JSON string.
+ """The action result or detailed failure output.
- This could be: string, number, boolean, array, object, or null.
+ Often a JSON-encoded value, but failures may contain plain text. May contain
+ sensitive application data.
"""
payload: Optional[str] = None
@@ -47,4 +48,12 @@ class InvocationRetrieveResponse(BaseModel):
"""
status_reason: Optional[str] = None
- """Status reason"""
+ """
+ A nonempty, customer-safe summary of the recorded failure output, always present
+ when status is failed and omitted otherwise, including in the first failed
+ invocation_state event. Recognized messages receive a specific summary; other
+ failures receive a generic summary. Message matching does not establish whether
+ the failure originated in the platform or action code. Does not include raw
+ action output or internal error details. Available for historical invocations as
+ well. Human-readable text, not a stable identifier for retry logic.
+ """
diff --git a/src/kernel/types/invocation_state_event.py b/src/kernel/types/invocation_state_event.py
index f32bf8e9..00abae7d 100644
--- a/src/kernel/types/invocation_state_event.py
+++ b/src/kernel/types/invocation_state_event.py
@@ -35,9 +35,10 @@ class Invocation(BaseModel):
"""
output: Optional[str] = None
- """Output produced by the action, rendered as a JSON string.
+ """The action result or detailed failure output.
- This could be: string, number, boolean, array, object, or null.
+ Often a JSON-encoded value, but failures may contain plain text. May contain
+ sensitive application data.
"""
payload: Optional[str] = None
@@ -47,7 +48,15 @@ class Invocation(BaseModel):
"""
status_reason: Optional[str] = None
- """Status reason"""
+ """
+ A nonempty, customer-safe summary of the recorded failure output, always present
+ when status is failed and omitted otherwise, including in the first failed
+ invocation_state event. Recognized messages receive a specific summary; other
+ failures receive a generic summary. Message matching does not establish whether
+ the failure originated in the platform or action code. Does not include raw
+ action output or internal error details. Available for historical invocations as
+ well. Human-readable text, not a stable identifier for retry logic.
+ """
class InvocationStateEvent(BaseModel):
diff --git a/src/kernel/types/invocation_update_response.py b/src/kernel/types/invocation_update_response.py
index 3bcc8bc0..92dfe5ed 100644
--- a/src/kernel/types/invocation_update_response.py
+++ b/src/kernel/types/invocation_update_response.py
@@ -35,9 +35,10 @@ class InvocationUpdateResponse(BaseModel):
"""
output: Optional[str] = None
- """Output produced by the action, rendered as a JSON string.
+ """The action result or detailed failure output.
- This could be: string, number, boolean, array, object, or null.
+ Often a JSON-encoded value, but failures may contain plain text. May contain
+ sensitive application data.
"""
payload: Optional[str] = None
@@ -47,4 +48,12 @@ class InvocationUpdateResponse(BaseModel):
"""
status_reason: Optional[str] = None
- """Status reason"""
+ """
+ A nonempty, customer-safe summary of the recorded failure output, always present
+ when status is failed and omitted otherwise, including in the first failed
+ invocation_state event. Recognized messages receive a specific summary; other
+ failures receive a generic summary. Message matching does not establish whether
+ the failure originated in the platform or action code. Does not include raw
+ action output or internal error details. Available for historical invocations as
+ well. Human-readable text, not a stable identifier for retry logic.
+ """
From b00a6942ff9c831f624e87c12923ee109f2d6cf7 Mon Sep 17 00:00:00 2001
From: "kernel-internal[bot]"
<260533166+kernel-internal[bot]@users.noreply.github.com>
Date: Thu, 10 Sep 2026 19:51:49 +0000
Subject: [PATCH 248/366] feat: Brand origin TLS timeouts and classify provider
failures
Stainless-Generated-From: 82064b62da0e2b55a59a8d321bfd5cb7aebcc733
---
src/kernel/types/browsers/browser_proxy_error_event.py | 7 +++++--
1 file changed, 5 insertions(+), 2 deletions(-)
diff --git a/src/kernel/types/browsers/browser_proxy_error_event.py b/src/kernel/types/browsers/browser_proxy_error_event.py
index 6f52d3bb..d8c9dd93 100644
--- a/src/kernel/types/browsers/browser_proxy_error_event.py
+++ b/src/kernel/types/browsers/browser_proxy_error_event.py
@@ -20,6 +20,8 @@ class Data(BrowserEventContext):
"destination_blocked",
"provider_blacklisted",
"provider_unreachable",
+ "provider_rejected",
+ "origin_tls_timeout",
"proxy_unavailable",
"upstream_timeout",
"upstream_dns_failure",
@@ -29,8 +31,9 @@ class Data(BrowserEventContext):
Proxy-layer error code: the X-Kernel-Proxy-Error response header value from a
branded 5xx error page served by the metro egress host-proxy. Values mirror what
the proxy emits: destination_blocked, provider_blacklisted,
- provider_unreachable, proxy_unavailable, upstream_timeout, upstream_dns_failure,
- upstream_connect_failed. Unknown header values are dropped.
+ provider_unreachable, provider_rejected, origin_tls_timeout, proxy_unavailable,
+ upstream_timeout, upstream_dns_failure, upstream_connect_failed. Unknown header
+ values are dropped.
"""
request_id: str
From 938e40b2a9f4e37ef8221787debbb948e019eeeb Mon Sep 17 00:00:00 2001
From: "kernel-internal[bot]"
<260533166+kernel-internal[bot]@users.noreply.github.com>
Date: Thu, 10 Sep 2026 20:18:30 +0000
Subject: [PATCH 249/366] feat: Return vendor guidance with config registry
recommendations
Stainless-Generated-From: fc242d1d87ecb547ccce40df21209f679a994ff1
---
src/kernel/types/config_registry_response.py | 8 ++++++++
src/kernel/types/lookup_response.py | 8 ++++++++
2 files changed, 16 insertions(+)
diff --git a/src/kernel/types/config_registry_response.py b/src/kernel/types/config_registry_response.py
index 9995173f..eead2467 100644
--- a/src/kernel/types/config_registry_response.py
+++ b/src/kernel/types/config_registry_response.py
@@ -21,3 +21,11 @@ class ConfigRegistryResponse(BaseModel):
"""A recommendation or a structured no-recommendation result."""
target: Target
+
+ guidance: Optional[str] = None
+ """Short advisory markdown to facilitate navigating this target.
+
+ Returned even when no configuration reached the target, since knowing what
+ prevented success is useful without a configuration. Not verified against this
+ target. Null when nothing applicable was observed or no notes exist.
+ """
diff --git a/src/kernel/types/lookup_response.py b/src/kernel/types/lookup_response.py
index cf0f5c16..b961e9c7 100644
--- a/src/kernel/types/lookup_response.py
+++ b/src/kernel/types/lookup_response.py
@@ -13,3 +13,11 @@ class LookupResponse(BaseModel):
recommendation: Optional[Recommendation] = None
target: Target
+
+ guidance: Optional[str] = None
+ """Short advisory markdown to facilitate navigating this target.
+
+ Returned even when no configuration reached the target, since knowing what
+ prevented success is useful without a configuration. Not verified against this
+ target. Null when nothing applicable was observed or no notes exist.
+ """
From adacc1321a9c1cc1624e5adf6c2521b0f09851f4 Mon Sep 17 00:00:00 2001
From: "kernel-internal[bot]"
<260533166+kernel-internal[bot]@users.noreply.github.com>
Date: Thu, 10 Sep 2026 21:30:43 +0000
Subject: [PATCH 250/366] feat: Revert vendor guidance recommendations
Stainless-Generated-From: c7bbf68cb905fcc69c495e1d6c2b6e14016acfc1
---
src/kernel/types/config_registry_response.py | 8 --------
src/kernel/types/lookup_response.py | 8 --------
2 files changed, 16 deletions(-)
diff --git a/src/kernel/types/config_registry_response.py b/src/kernel/types/config_registry_response.py
index eead2467..9995173f 100644
--- a/src/kernel/types/config_registry_response.py
+++ b/src/kernel/types/config_registry_response.py
@@ -21,11 +21,3 @@ class ConfigRegistryResponse(BaseModel):
"""A recommendation or a structured no-recommendation result."""
target: Target
-
- guidance: Optional[str] = None
- """Short advisory markdown to facilitate navigating this target.
-
- Returned even when no configuration reached the target, since knowing what
- prevented success is useful without a configuration. Not verified against this
- target. Null when nothing applicable was observed or no notes exist.
- """
diff --git a/src/kernel/types/lookup_response.py b/src/kernel/types/lookup_response.py
index b961e9c7..cf0f5c16 100644
--- a/src/kernel/types/lookup_response.py
+++ b/src/kernel/types/lookup_response.py
@@ -13,11 +13,3 @@ class LookupResponse(BaseModel):
recommendation: Optional[Recommendation] = None
target: Target
-
- guidance: Optional[str] = None
- """Short advisory markdown to facilitate navigating this target.
-
- Returned even when no configuration reached the target, since knowing what
- prevented success is useful without a configuration. Not verified against this
- target. Null when nothing applicable was observed or no notes exist.
- """
From 478bc09cc5c8f6b2850628d02b88d699cc35328f Mon Sep 17 00:00:00 2001
From: "kernel-internal[bot]"
<260533166+kernel-internal[bot]@users.noreply.github.com>
Date: Fri, 11 Sep 2026 12:56:30 +0000
Subject: [PATCH 251/366] feat: Integrate configurable vault providers across
API and checkout
Stainless-Generated-From: 1545965ff3ec7017df37c083d816134ae7af0bee
---
.stats.yml | 2 +-
api.md | 16 +
src/kernel/_client.py | 38 +
src/kernel/resources/__init__.py | 14 +
.../resources/vault_provider_configs.py | 721 ++++++++++++++++++
src/kernel/resources/vaults/items.py | 97 ++-
src/kernel/resources/vaults/vaults.py | 14 +-
src/kernel/types/__init__.py | 4 +
src/kernel/types/vault_provider_config.py | 58 ++
.../vault_provider_config_create_params.py | 47 ++
.../vault_provider_config_list_params.py | 13 +
.../vault_provider_config_update_params.py | 28 +
src/kernel/types/vaults/__init__.py | 1 -
.../types/vaults/card_vault_item_state.py | 20 +-
src/kernel/types/vaults/item_upsert_params.py | 163 +++-
src/kernel/types/vaults/vault_item.py | 9 +-
.../types/vaults/wallet_vault_item_spec.py | 56 +-
.../vaults/wallet_vault_item_spec_param.py | 44 --
.../test_vault_provider_configs.py | 569 ++++++++++++++
tests/api_resources/test_vaults.py | 12 +-
tests/api_resources/vaults/test_items.py | 168 ++--
21 files changed, 1916 insertions(+), 178 deletions(-)
create mode 100644 src/kernel/resources/vault_provider_configs.py
create mode 100644 src/kernel/types/vault_provider_config.py
create mode 100644 src/kernel/types/vault_provider_config_create_params.py
create mode 100644 src/kernel/types/vault_provider_config_list_params.py
create mode 100644 src/kernel/types/vault_provider_config_update_params.py
delete mode 100644 src/kernel/types/vaults/wallet_vault_item_spec_param.py
create mode 100644 tests/api_resources/test_vault_provider_configs.py
diff --git a/.stats.yml b/.stats.yml
index 5fe5772a..a0ee50ed 100644
--- a/.stats.yml
+++ b/.stats.yml
@@ -1 +1 @@
-configured_endpoints: 158
+configured_endpoints: 163
diff --git a/api.md b/api.md
index 797c5d9c..63957ab1 100644
--- a/api.md
+++ b/api.md
@@ -487,6 +487,22 @@ Methods:
- client.browser_pools.flush(id_or_name) -> None
- client.browser_pools.release(id_or_name, \*\*params) -> None
+# VaultProviderConfigs
+
+Types:
+
+```python
+from kernel.types import VaultProviderConfig
+```
+
+Methods:
+
+- client.vault_provider_configs.create(\*\*params) -> VaultProviderConfig
+- client.vault_provider_configs.retrieve(id_or_name) -> VaultProviderConfig
+- client.vault_provider_configs.update(id_or_name, \*\*params) -> VaultProviderConfig
+- client.vault_provider_configs.list(\*\*params) -> SyncOffsetPagination[VaultProviderConfig]
+- client.vault_provider_configs.delete(id_or_name) -> None
+
# Vaults
Types:
diff --git a/src/kernel/_client.py b/src/kernel/_client.py
index acdb7357..fd2e4dae 100644
--- a/src/kernel/_client.py
+++ b/src/kernel/_client.py
@@ -66,6 +66,7 @@
browser_pools,
config_registry,
credential_providers,
+ vault_provider_configs,
)
from .resources.apps import AppsResource, AsyncAppsResource
from .resources.proxies import ProxiesResource, AsyncProxiesResource
@@ -83,6 +84,7 @@
from .resources.telemetry.telemetry import TelemetryResource, AsyncTelemetryResource
from .resources.credential_providers import CredentialProvidersResource, AsyncCredentialProvidersResource
from .resources.audit_logs.audit_logs import AuditLogsResource, AsyncAuditLogsResource
+ from .resources.vault_provider_configs import VaultProviderConfigsResource, AsyncVaultProviderConfigsResource
from .resources.organization.organization import OrganizationResource, AsyncOrganizationResource
from .resources.config_registry.config_registry import ConfigRegistryResource, AsyncConfigRegistryResource
@@ -281,6 +283,12 @@ def browser_pools(self) -> BrowserPoolsResource:
return BrowserPoolsResource(self)
+ @cached_property
+ def vault_provider_configs(self) -> VaultProviderConfigsResource:
+ from .resources.vault_provider_configs import VaultProviderConfigsResource
+
+ return VaultProviderConfigsResource(self)
+
@cached_property
def vaults(self) -> VaultsResource:
from .resources.vaults import VaultsResource
@@ -670,6 +678,12 @@ def browser_pools(self) -> AsyncBrowserPoolsResource:
return AsyncBrowserPoolsResource(self)
+ @cached_property
+ def vault_provider_configs(self) -> AsyncVaultProviderConfigsResource:
+ from .resources.vault_provider_configs import AsyncVaultProviderConfigsResource
+
+ return AsyncVaultProviderConfigsResource(self)
+
@cached_property
def vaults(self) -> AsyncVaultsResource:
from .resources.vaults import AsyncVaultsResource
@@ -963,6 +977,12 @@ def browser_pools(self) -> browser_pools.BrowserPoolsResourceWithRawResponse:
return BrowserPoolsResourceWithRawResponse(self._client.browser_pools)
+ @cached_property
+ def vault_provider_configs(self) -> vault_provider_configs.VaultProviderConfigsResourceWithRawResponse:
+ from .resources.vault_provider_configs import VaultProviderConfigsResourceWithRawResponse
+
+ return VaultProviderConfigsResourceWithRawResponse(self._client.vault_provider_configs)
+
@cached_property
def vaults(self) -> vaults.VaultsResourceWithRawResponse:
from .resources.vaults import VaultsResourceWithRawResponse
@@ -1096,6 +1116,12 @@ def browser_pools(self) -> browser_pools.AsyncBrowserPoolsResourceWithRawRespons
return AsyncBrowserPoolsResourceWithRawResponse(self._client.browser_pools)
+ @cached_property
+ def vault_provider_configs(self) -> vault_provider_configs.AsyncVaultProviderConfigsResourceWithRawResponse:
+ from .resources.vault_provider_configs import AsyncVaultProviderConfigsResourceWithRawResponse
+
+ return AsyncVaultProviderConfigsResourceWithRawResponse(self._client.vault_provider_configs)
+
@cached_property
def vaults(self) -> vaults.AsyncVaultsResourceWithRawResponse:
from .resources.vaults import AsyncVaultsResourceWithRawResponse
@@ -1229,6 +1255,12 @@ def browser_pools(self) -> browser_pools.BrowserPoolsResourceWithStreamingRespon
return BrowserPoolsResourceWithStreamingResponse(self._client.browser_pools)
+ @cached_property
+ def vault_provider_configs(self) -> vault_provider_configs.VaultProviderConfigsResourceWithStreamingResponse:
+ from .resources.vault_provider_configs import VaultProviderConfigsResourceWithStreamingResponse
+
+ return VaultProviderConfigsResourceWithStreamingResponse(self._client.vault_provider_configs)
+
@cached_property
def vaults(self) -> vaults.VaultsResourceWithStreamingResponse:
from .resources.vaults import VaultsResourceWithStreamingResponse
@@ -1362,6 +1394,12 @@ def browser_pools(self) -> browser_pools.AsyncBrowserPoolsResourceWithStreamingR
return AsyncBrowserPoolsResourceWithStreamingResponse(self._client.browser_pools)
+ @cached_property
+ def vault_provider_configs(self) -> vault_provider_configs.AsyncVaultProviderConfigsResourceWithStreamingResponse:
+ from .resources.vault_provider_configs import AsyncVaultProviderConfigsResourceWithStreamingResponse
+
+ return AsyncVaultProviderConfigsResourceWithStreamingResponse(self._client.vault_provider_configs)
+
@cached_property
def vaults(self) -> vaults.AsyncVaultsResourceWithStreamingResponse:
from .resources.vaults import AsyncVaultsResourceWithStreamingResponse
diff --git a/src/kernel/resources/__init__.py b/src/kernel/resources/__init__.py
index 5bc0849c..21426930 100644
--- a/src/kernel/resources/__init__.py
+++ b/src/kernel/resources/__init__.py
@@ -144,6 +144,14 @@
CredentialProvidersResourceWithStreamingResponse,
AsyncCredentialProvidersResourceWithStreamingResponse,
)
+from .vault_provider_configs import (
+ VaultProviderConfigsResource,
+ AsyncVaultProviderConfigsResource,
+ VaultProviderConfigsResourceWithRawResponse,
+ AsyncVaultProviderConfigsResourceWithRawResponse,
+ VaultProviderConfigsResourceWithStreamingResponse,
+ AsyncVaultProviderConfigsResourceWithStreamingResponse,
+)
__all__ = [
"DeploymentsResource",
@@ -212,6 +220,12 @@
"AsyncBrowserPoolsResourceWithRawResponse",
"BrowserPoolsResourceWithStreamingResponse",
"AsyncBrowserPoolsResourceWithStreamingResponse",
+ "VaultProviderConfigsResource",
+ "AsyncVaultProviderConfigsResource",
+ "VaultProviderConfigsResourceWithRawResponse",
+ "AsyncVaultProviderConfigsResourceWithRawResponse",
+ "VaultProviderConfigsResourceWithStreamingResponse",
+ "AsyncVaultProviderConfigsResourceWithStreamingResponse",
"VaultsResource",
"AsyncVaultsResource",
"VaultsResourceWithRawResponse",
diff --git a/src/kernel/resources/vault_provider_configs.py b/src/kernel/resources/vault_provider_configs.py
new file mode 100644
index 00000000..69f29d3d
--- /dev/null
+++ b/src/kernel/resources/vault_provider_configs.py
@@ -0,0 +1,721 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from __future__ import annotations
+
+from typing import Any, cast
+from typing_extensions import Literal, overload
+
+import httpx
+
+from ..types import (
+ vault_provider_config_list_params,
+ vault_provider_config_create_params,
+ vault_provider_config_update_params,
+)
+from .._types import Body, Omit, Query, Headers, NoneType, NotGiven, omit, not_given
+from .._utils import path_template, required_args, maybe_transform, async_maybe_transform
+from .._compat import cached_property
+from .._resource import SyncAPIResource, AsyncAPIResource
+from .._response import (
+ to_raw_response_wrapper,
+ to_streamed_response_wrapper,
+ async_to_raw_response_wrapper,
+ async_to_streamed_response_wrapper,
+)
+from ..pagination import SyncOffsetPagination, AsyncOffsetPagination
+from .._base_client import AsyncPaginator, make_request_options
+from ..types.vault_provider_config import VaultProviderConfig
+
+__all__ = ["VaultProviderConfigsResource", "AsyncVaultProviderConfigsResource"]
+
+
+class VaultProviderConfigsResource(SyncAPIResource):
+ @cached_property
+ def with_raw_response(self) -> VaultProviderConfigsResourceWithRawResponse:
+ """
+ This property can be used as a prefix for any HTTP method call to return
+ the raw response object instead of the parsed content.
+
+ For more information, see https://www.github.com/kernel/kernel-python-sdk#accessing-raw-response-data-eg-headers
+ """
+ return VaultProviderConfigsResourceWithRawResponse(self)
+
+ @cached_property
+ def with_streaming_response(self) -> VaultProviderConfigsResourceWithStreamingResponse:
+ """
+ An alternative to `.with_raw_response` that doesn't eagerly read the response body.
+
+ For more information, see https://www.github.com/kernel/kernel-python-sdk#with_streaming_response
+ """
+ return VaultProviderConfigsResourceWithStreamingResponse(self)
+
+ @overload
+ def create(
+ self,
+ *,
+ credentials: vault_provider_config_create_params.VaultLinkProviderConfigRequestCredentials,
+ name: str,
+ provider: Literal["link"],
+ # 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,
+ ) -> VaultProviderConfig:
+ """Register a configuration shared across the organization's projects.
+
+ Names are
+ unique within the organization; duplicate names return 409 without replacing
+ credentials. A configuration serves many wallets. Secret credentials are never
+ returned. Requires an organization-scoped credential or dashboard
+ authentication; project-scoped credentials receive 403.
+
+ Args:
+ name: Unique within the organization.
+
+ 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
+ """
+ ...
+
+ @overload
+ def create(
+ self,
+ *,
+ credentials: vault_provider_config_create_params.VaultAgentCardProviderConfigRequestCredentials,
+ name: str,
+ provider: Literal["agentcard"],
+ # 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,
+ ) -> VaultProviderConfig:
+ """Register a configuration shared across the organization's projects.
+
+ Names are
+ unique within the organization; duplicate names return 409 without replacing
+ credentials. A configuration serves many wallets. Secret credentials are never
+ returned. Requires an organization-scoped credential or dashboard
+ authentication; project-scoped credentials receive 403.
+
+ Args:
+ name: Unique within the organization.
+
+ 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
+ """
+ ...
+
+ @required_args(["credentials", "name", "provider"])
+ def create(
+ self,
+ *,
+ credentials: vault_provider_config_create_params.VaultLinkProviderConfigRequestCredentials
+ | vault_provider_config_create_params.VaultAgentCardProviderConfigRequestCredentials,
+ name: str,
+ provider: Literal["link"] | Literal["agentcard"],
+ # 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,
+ ) -> VaultProviderConfig:
+ return cast(
+ VaultProviderConfig,
+ self._post(
+ "/vault-provider-configs",
+ body=maybe_transform(
+ {
+ "credentials": credentials,
+ "name": name,
+ "provider": provider,
+ },
+ vault_provider_config_create_params.VaultProviderConfigCreateParams,
+ ),
+ options=make_request_options(
+ extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
+ ),
+ cast_to=cast(
+ Any, VaultProviderConfig
+ ), # Union types cannot be passed in as arguments in the type system
+ ),
+ )
+
+ def retrieve(
+ self,
+ id_or_name: str,
+ *,
+ # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
+ # The extra values given here take precedence over values defined on the client or passed to this method.
+ extra_headers: Headers | None = None,
+ extra_query: Query | None = None,
+ extra_body: Body | None = None,
+ timeout: float | httpx.Timeout | None | NotGiven = not_given,
+ ) -> VaultProviderConfig:
+ """Look up a configuration by ID or name.
+
+ Returns 404 when it does not exist in the
+ organization.
+
+ Args:
+ extra_headers: Send extra headers
+
+ extra_query: Add additional query parameters to the request
+
+ extra_body: Add additional JSON properties to the request
+
+ timeout: Override the client-level default timeout for this request, in seconds
+ """
+ if not id_or_name:
+ raise ValueError(f"Expected a non-empty value for `id_or_name` but received {id_or_name!r}")
+ return cast(
+ VaultProviderConfig,
+ self._get(
+ path_template("/vault-provider-configs/{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
+ ),
+ cast_to=cast(
+ Any, VaultProviderConfig
+ ), # Union types cannot be passed in as arguments in the type system
+ ),
+ )
+
+ def update(
+ self,
+ id_or_name: str,
+ *,
+ credentials: vault_provider_config_update_params.Credentials | Omit = omit,
+ name: str | Omit = omit,
+ # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
+ # The extra values given here take precedence over values defined on the client or passed to this method.
+ extra_headers: Headers | None = None,
+ extra_query: Query | None = None,
+ extra_body: Body | None = None,
+ timeout: float | httpx.Timeout | None | NotGiven = not_given,
+ ) -> VaultProviderConfig:
+ """Update the supplied fields; omitted fields remain unchanged.
+
+ Names must remain
+ unique within the organization. Requires an organization-scoped credential or
+ dashboard authentication; project-scoped credentials receive 403.
+
+ Args:
+ credentials: Fields to update. Omitted credentials are left unchanged. A rejected update
+ leaves existing credentials unchanged.
+
+ name: Unique within the organization.
+
+ extra_headers: Send extra headers
+
+ extra_query: Add additional query parameters to the request
+
+ extra_body: Add additional JSON properties to the request
+
+ timeout: Override the client-level default timeout for this request, in seconds
+ """
+ if not id_or_name:
+ raise ValueError(f"Expected a non-empty value for `id_or_name` but received {id_or_name!r}")
+ return cast(
+ VaultProviderConfig,
+ self._patch(
+ path_template("/vault-provider-configs/{id_or_name}", id_or_name=id_or_name),
+ body=maybe_transform(
+ {
+ "credentials": credentials,
+ "name": name,
+ },
+ vault_provider_config_update_params.VaultProviderConfigUpdateParams,
+ ),
+ options=make_request_options(
+ extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
+ ),
+ cast_to=cast(
+ Any, VaultProviderConfig
+ ), # Union types cannot be passed in as arguments in the type system
+ ),
+ )
+
+ def list(
+ self,
+ *,
+ limit: int | Omit = omit,
+ offset: int | Omit = omit,
+ # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
+ # The extra values given here take precedence over values defined on the client or passed to this method.
+ extra_headers: Headers | None = None,
+ extra_query: Query | None = None,
+ extra_body: Body | None = None,
+ timeout: float | httpx.Timeout | None | NotGiven = not_given,
+ ) -> SyncOffsetPagination[VaultProviderConfig]:
+ """
+ Secret credentials are never returned.
+
+ Args:
+ extra_headers: Send extra headers
+
+ extra_query: Add additional query parameters to the request
+
+ extra_body: Add additional JSON properties to the request
+
+ timeout: Override the client-level default timeout for this request, in seconds
+ """
+ return self._get_api_list(
+ "/vault-provider-configs",
+ page=SyncOffsetPagination[VaultProviderConfig],
+ options=make_request_options(
+ extra_headers=extra_headers,
+ extra_query=extra_query,
+ extra_body=extra_body,
+ timeout=timeout,
+ query=maybe_transform(
+ {
+ "limit": limit,
+ "offset": offset,
+ },
+ vault_provider_config_list_params.VaultProviderConfigListParams,
+ ),
+ ),
+ model=cast(Any, VaultProviderConfig), # Union types cannot be passed in as arguments in the type system
+ )
+
+ def delete(
+ self,
+ id_or_name: str,
+ *,
+ # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
+ # The extra values given here take precedence over values defined on the client or passed to this method.
+ extra_headers: Headers | None = None,
+ extra_query: Query | None = None,
+ extra_body: Body | None = None,
+ timeout: float | httpx.Timeout | None | NotGiven = not_given,
+ ) -> None:
+ """Delete a configuration in the organization.
+
+ Returns 409 while any non-deleted
+ vault item references the configuration, regardless of connection status. Does
+ not delete the external OAuth client or revoke unrelated grants. Requires an
+ organization-scoped credential or dashboard authentication; project-scoped
+ credentials receive 403.
+
+ Args:
+ extra_headers: Send extra headers
+
+ extra_query: Add additional query parameters to the request
+
+ extra_body: Add additional JSON properties to the request
+
+ timeout: Override the client-level default timeout for this request, in seconds
+ """
+ if not id_or_name:
+ raise ValueError(f"Expected a non-empty value for `id_or_name` but received {id_or_name!r}")
+ extra_headers = {"Accept": "*/*", **(extra_headers or {})}
+ return self._delete(
+ path_template("/vault-provider-configs/{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
+ ),
+ cast_to=NoneType,
+ )
+
+
+class AsyncVaultProviderConfigsResource(AsyncAPIResource):
+ @cached_property
+ def with_raw_response(self) -> AsyncVaultProviderConfigsResourceWithRawResponse:
+ """
+ This property can be used as a prefix for any HTTP method call to return
+ the raw response object instead of the parsed content.
+
+ For more information, see https://www.github.com/kernel/kernel-python-sdk#accessing-raw-response-data-eg-headers
+ """
+ return AsyncVaultProviderConfigsResourceWithRawResponse(self)
+
+ @cached_property
+ def with_streaming_response(self) -> AsyncVaultProviderConfigsResourceWithStreamingResponse:
+ """
+ An alternative to `.with_raw_response` that doesn't eagerly read the response body.
+
+ For more information, see https://www.github.com/kernel/kernel-python-sdk#with_streaming_response
+ """
+ return AsyncVaultProviderConfigsResourceWithStreamingResponse(self)
+
+ @overload
+ async def create(
+ self,
+ *,
+ credentials: vault_provider_config_create_params.VaultLinkProviderConfigRequestCredentials,
+ name: str,
+ provider: Literal["link"],
+ # 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,
+ ) -> VaultProviderConfig:
+ """Register a configuration shared across the organization's projects.
+
+ Names are
+ unique within the organization; duplicate names return 409 without replacing
+ credentials. A configuration serves many wallets. Secret credentials are never
+ returned. Requires an organization-scoped credential or dashboard
+ authentication; project-scoped credentials receive 403.
+
+ Args:
+ name: Unique within the organization.
+
+ 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
+ """
+ ...
+
+ @overload
+ async def create(
+ self,
+ *,
+ credentials: vault_provider_config_create_params.VaultAgentCardProviderConfigRequestCredentials,
+ name: str,
+ provider: Literal["agentcard"],
+ # 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,
+ ) -> VaultProviderConfig:
+ """Register a configuration shared across the organization's projects.
+
+ Names are
+ unique within the organization; duplicate names return 409 without replacing
+ credentials. A configuration serves many wallets. Secret credentials are never
+ returned. Requires an organization-scoped credential or dashboard
+ authentication; project-scoped credentials receive 403.
+
+ Args:
+ name: Unique within the organization.
+
+ 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
+ """
+ ...
+
+ @required_args(["credentials", "name", "provider"])
+ async def create(
+ self,
+ *,
+ credentials: vault_provider_config_create_params.VaultLinkProviderConfigRequestCredentials
+ | vault_provider_config_create_params.VaultAgentCardProviderConfigRequestCredentials,
+ name: str,
+ provider: Literal["link"] | Literal["agentcard"],
+ # 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,
+ ) -> VaultProviderConfig:
+ return cast(
+ VaultProviderConfig,
+ await self._post(
+ "/vault-provider-configs",
+ body=await async_maybe_transform(
+ {
+ "credentials": credentials,
+ "name": name,
+ "provider": provider,
+ },
+ vault_provider_config_create_params.VaultProviderConfigCreateParams,
+ ),
+ options=make_request_options(
+ extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
+ ),
+ cast_to=cast(
+ Any, VaultProviderConfig
+ ), # Union types cannot be passed in as arguments in the type system
+ ),
+ )
+
+ async def retrieve(
+ self,
+ id_or_name: str,
+ *,
+ # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
+ # The extra values given here take precedence over values defined on the client or passed to this method.
+ extra_headers: Headers | None = None,
+ extra_query: Query | None = None,
+ extra_body: Body | None = None,
+ timeout: float | httpx.Timeout | None | NotGiven = not_given,
+ ) -> VaultProviderConfig:
+ """Look up a configuration by ID or name.
+
+ Returns 404 when it does not exist in the
+ organization.
+
+ Args:
+ extra_headers: Send extra headers
+
+ extra_query: Add additional query parameters to the request
+
+ extra_body: Add additional JSON properties to the request
+
+ timeout: Override the client-level default timeout for this request, in seconds
+ """
+ if not id_or_name:
+ raise ValueError(f"Expected a non-empty value for `id_or_name` but received {id_or_name!r}")
+ return cast(
+ VaultProviderConfig,
+ await self._get(
+ path_template("/vault-provider-configs/{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
+ ),
+ cast_to=cast(
+ Any, VaultProviderConfig
+ ), # Union types cannot be passed in as arguments in the type system
+ ),
+ )
+
+ async def update(
+ self,
+ id_or_name: str,
+ *,
+ credentials: vault_provider_config_update_params.Credentials | Omit = omit,
+ name: str | Omit = omit,
+ # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
+ # The extra values given here take precedence over values defined on the client or passed to this method.
+ extra_headers: Headers | None = None,
+ extra_query: Query | None = None,
+ extra_body: Body | None = None,
+ timeout: float | httpx.Timeout | None | NotGiven = not_given,
+ ) -> VaultProviderConfig:
+ """Update the supplied fields; omitted fields remain unchanged.
+
+ Names must remain
+ unique within the organization. Requires an organization-scoped credential or
+ dashboard authentication; project-scoped credentials receive 403.
+
+ Args:
+ credentials: Fields to update. Omitted credentials are left unchanged. A rejected update
+ leaves existing credentials unchanged.
+
+ name: Unique within the organization.
+
+ extra_headers: Send extra headers
+
+ extra_query: Add additional query parameters to the request
+
+ extra_body: Add additional JSON properties to the request
+
+ timeout: Override the client-level default timeout for this request, in seconds
+ """
+ if not id_or_name:
+ raise ValueError(f"Expected a non-empty value for `id_or_name` but received {id_or_name!r}")
+ return cast(
+ VaultProviderConfig,
+ await self._patch(
+ path_template("/vault-provider-configs/{id_or_name}", id_or_name=id_or_name),
+ body=await async_maybe_transform(
+ {
+ "credentials": credentials,
+ "name": name,
+ },
+ vault_provider_config_update_params.VaultProviderConfigUpdateParams,
+ ),
+ options=make_request_options(
+ extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
+ ),
+ cast_to=cast(
+ Any, VaultProviderConfig
+ ), # Union types cannot be passed in as arguments in the type system
+ ),
+ )
+
+ def list(
+ self,
+ *,
+ limit: int | Omit = omit,
+ offset: int | Omit = omit,
+ # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
+ # The extra values given here take precedence over values defined on the client or passed to this method.
+ extra_headers: Headers | None = None,
+ extra_query: Query | None = None,
+ extra_body: Body | None = None,
+ timeout: float | httpx.Timeout | None | NotGiven = not_given,
+ ) -> AsyncPaginator[VaultProviderConfig, AsyncOffsetPagination[VaultProviderConfig]]:
+ """
+ Secret credentials are never returned.
+
+ Args:
+ extra_headers: Send extra headers
+
+ extra_query: Add additional query parameters to the request
+
+ extra_body: Add additional JSON properties to the request
+
+ timeout: Override the client-level default timeout for this request, in seconds
+ """
+ return self._get_api_list(
+ "/vault-provider-configs",
+ page=AsyncOffsetPagination[VaultProviderConfig],
+ options=make_request_options(
+ extra_headers=extra_headers,
+ extra_query=extra_query,
+ extra_body=extra_body,
+ timeout=timeout,
+ query=maybe_transform(
+ {
+ "limit": limit,
+ "offset": offset,
+ },
+ vault_provider_config_list_params.VaultProviderConfigListParams,
+ ),
+ ),
+ model=cast(Any, VaultProviderConfig), # Union types cannot be passed in as arguments in the type system
+ )
+
+ async def delete(
+ self,
+ id_or_name: str,
+ *,
+ # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
+ # The extra values given here take precedence over values defined on the client or passed to this method.
+ extra_headers: Headers | None = None,
+ extra_query: Query | None = None,
+ extra_body: Body | None = None,
+ timeout: float | httpx.Timeout | None | NotGiven = not_given,
+ ) -> None:
+ """Delete a configuration in the organization.
+
+ Returns 409 while any non-deleted
+ vault item references the configuration, regardless of connection status. Does
+ not delete the external OAuth client or revoke unrelated grants. Requires an
+ organization-scoped credential or dashboard authentication; project-scoped
+ credentials receive 403.
+
+ Args:
+ extra_headers: Send extra headers
+
+ extra_query: Add additional query parameters to the request
+
+ extra_body: Add additional JSON properties to the request
+
+ timeout: Override the client-level default timeout for this request, in seconds
+ """
+ if not id_or_name:
+ raise ValueError(f"Expected a non-empty value for `id_or_name` but received {id_or_name!r}")
+ extra_headers = {"Accept": "*/*", **(extra_headers or {})}
+ return await self._delete(
+ path_template("/vault-provider-configs/{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
+ ),
+ cast_to=NoneType,
+ )
+
+
+class VaultProviderConfigsResourceWithRawResponse:
+ def __init__(self, vault_provider_configs: VaultProviderConfigsResource) -> None:
+ self._vault_provider_configs = vault_provider_configs
+
+ self.create = to_raw_response_wrapper(
+ vault_provider_configs.create,
+ )
+ self.retrieve = to_raw_response_wrapper(
+ vault_provider_configs.retrieve,
+ )
+ self.update = to_raw_response_wrapper(
+ vault_provider_configs.update,
+ )
+ self.list = to_raw_response_wrapper(
+ vault_provider_configs.list,
+ )
+ self.delete = to_raw_response_wrapper(
+ vault_provider_configs.delete,
+ )
+
+
+class AsyncVaultProviderConfigsResourceWithRawResponse:
+ def __init__(self, vault_provider_configs: AsyncVaultProviderConfigsResource) -> None:
+ self._vault_provider_configs = vault_provider_configs
+
+ self.create = async_to_raw_response_wrapper(
+ vault_provider_configs.create,
+ )
+ self.retrieve = async_to_raw_response_wrapper(
+ vault_provider_configs.retrieve,
+ )
+ self.update = async_to_raw_response_wrapper(
+ vault_provider_configs.update,
+ )
+ self.list = async_to_raw_response_wrapper(
+ vault_provider_configs.list,
+ )
+ self.delete = async_to_raw_response_wrapper(
+ vault_provider_configs.delete,
+ )
+
+
+class VaultProviderConfigsResourceWithStreamingResponse:
+ def __init__(self, vault_provider_configs: VaultProviderConfigsResource) -> None:
+ self._vault_provider_configs = vault_provider_configs
+
+ self.create = to_streamed_response_wrapper(
+ vault_provider_configs.create,
+ )
+ self.retrieve = to_streamed_response_wrapper(
+ vault_provider_configs.retrieve,
+ )
+ self.update = to_streamed_response_wrapper(
+ vault_provider_configs.update,
+ )
+ self.list = to_streamed_response_wrapper(
+ vault_provider_configs.list,
+ )
+ self.delete = to_streamed_response_wrapper(
+ vault_provider_configs.delete,
+ )
+
+
+class AsyncVaultProviderConfigsResourceWithStreamingResponse:
+ def __init__(self, vault_provider_configs: AsyncVaultProviderConfigsResource) -> None:
+ self._vault_provider_configs = vault_provider_configs
+
+ self.create = async_to_streamed_response_wrapper(
+ vault_provider_configs.create,
+ )
+ self.retrieve = async_to_streamed_response_wrapper(
+ vault_provider_configs.retrieve,
+ )
+ self.update = async_to_streamed_response_wrapper(
+ vault_provider_configs.update,
+ )
+ self.list = async_to_streamed_response_wrapper(
+ vault_provider_configs.list,
+ )
+ self.delete = async_to_streamed_response_wrapper(
+ vault_provider_configs.delete,
+ )
diff --git a/src/kernel/resources/vaults/items.py b/src/kernel/resources/vaults/items.py
index b87d67cb..dd790910 100644
--- a/src/kernel/resources/vaults/items.py
+++ b/src/kernel/resources/vaults/items.py
@@ -29,7 +29,6 @@
from ...types.vaults.item_list_response import ItemListResponse
from ...types.vaults.item_events_response import ItemEventsResponse
from ...types.vaults.card_vault_item_spec_param import CardVaultItemSpecParam
-from ...types.vaults.wallet_vault_item_spec_param import WalletVaultItemSpecParam
__all__ = ["ItemsResource", "AsyncItemsResource"]
@@ -127,8 +126,15 @@ def update(
extra_body: Body | None = None,
timeout: float | httpx.Timeout | None | NotGiven = not_given,
) -> VaultItem:
- """
- Update a card specification before or between authorizations
+ """Requested cards accept a replacement specification.
+
+ Pending issuance requests
+ may update provider-supported fields on their existing request, subject to
+ atomic provider approval checks; omitted optional fields remain unchanged and
+ explicit empty lists clear them. Wallet/provider binding and unsupported fields
+ cannot change after authorization starts. An uncertain update enters
+ recovery_required and must not be retried. Checkout cards may be edited between
+ authorizations.
Args:
spec: Live payment card. Test-mode card creation is not supported.
@@ -203,7 +209,9 @@ def delete(
timeout: float | httpx.Timeout | None | NotGiven = not_given,
) -> None:
"""
- Delete a vault item and invalidate its secret value
+ Unresolved payment operations block deletion, including operations on child
+ cards of a wallet. Reconcile the original attempt with the provider or support
+ first; deleting or recreating an item is not proof that a payment did not occur.
Args:
extra_headers: Send extra headers
@@ -295,8 +303,10 @@ def perform_operation(
"""
Retrieve the item first and invoke only an operation listed in
`available_operations`, following its natural-language description. Operations
- may call an external provider and can return the item's updated state. If the
- provider rate limits spend-request creation, returns HTTP 429 with code
+ may call an external provider and return updated state. Link cards advertise
+ authorize. AgentCard cards are created with PUT and request approval when their
+ aliases are used at checkout; they do not expose this operation. If
+ spend-request creation is rate limited, returns HTTP 429 with code
`spend_request_rate_limited`; stop and back off before retrying.
Args:
@@ -330,7 +340,7 @@ def upsert(
key: str,
*,
id_or_name: str,
- spec: WalletVaultItemSpecParam,
+ spec: item_upsert_params.WalletVaultItemRequestSpec,
type: Literal["wallet"],
# 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.
@@ -340,12 +350,20 @@ def upsert(
timeout: float | httpx.Timeout | None | NotGiven = not_given,
) -> VaultItem:
"""
- Create or retrieve an identical vault item by immutable key
+ Create an item under a key unique within its vault, or retrieve the existing
+ item when its specification matches. An identical card PUT returns the existing
+ card in any lifecycle state without polling the provider, reauthorizing,
+ replacing aliases, or resetting recovery. Conflicting specifications return 409.
+ Provider-specific authorization requirements and retry behavior are described in
+ the item's request schema.
Args:
- spec: AgentCard wallet. Mode (sandbox vs live) is fixed by the deployment's AgentCard
- credential; there is no per-item test flag. user_id may only reference a user
- already enrolled by a wallet in this organization.
+ spec: AgentCard wallet. Omit provider_config to use Kernel-managed credentials, or
+ select a customer-owned configuration. Mode (sandbox vs live) is determined by
+ the selected credential; there is no per-item test flag. Without user_id,
+ creation returns a hosted enrollment action and Kernel polls until the user
+ connects. user_id may only reference a user already enrolled by a wallet in this
+ organization under the same configuration.
extra_headers: Send extra headers
@@ -373,7 +391,12 @@ def upsert(
timeout: float | httpx.Timeout | None | NotGiven = not_given,
) -> VaultItem:
"""
- Create or retrieve an identical vault item by immutable key
+ Create an item under a key unique within its vault, or retrieve the existing
+ item when its specification matches. An identical card PUT returns the existing
+ card in any lifecycle state without polling the provider, reauthorizing,
+ replacing aliases, or resetting recovery. Conflicting specifications return 409.
+ Provider-specific authorization requirements and retry behavior are described in
+ the item's request schema.
Args:
spec: Live payment card. Test-mode card creation is not supported.
@@ -394,7 +417,7 @@ def upsert(
key: str,
*,
id_or_name: str,
- spec: WalletVaultItemSpecParam | CardVaultItemSpecParam,
+ spec: item_upsert_params.WalletVaultItemRequestSpec | CardVaultItemSpecParam,
type: Literal["wallet"] | Literal["card"],
# 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.
@@ -519,8 +542,15 @@ async def update(
extra_body: Body | None = None,
timeout: float | httpx.Timeout | None | NotGiven = not_given,
) -> VaultItem:
- """
- Update a card specification before or between authorizations
+ """Requested cards accept a replacement specification.
+
+ Pending issuance requests
+ may update provider-supported fields on their existing request, subject to
+ atomic provider approval checks; omitted optional fields remain unchanged and
+ explicit empty lists clear them. Wallet/provider binding and unsupported fields
+ cannot change after authorization starts. An uncertain update enters
+ recovery_required and must not be retried. Checkout cards may be edited between
+ authorizations.
Args:
spec: Live payment card. Test-mode card creation is not supported.
@@ -595,7 +625,9 @@ async def delete(
timeout: float | httpx.Timeout | None | NotGiven = not_given,
) -> None:
"""
- Delete a vault item and invalidate its secret value
+ Unresolved payment operations block deletion, including operations on child
+ cards of a wallet. Reconcile the original attempt with the provider or support
+ first; deleting or recreating an item is not proof that a payment did not occur.
Args:
extra_headers: Send extra headers
@@ -687,8 +719,10 @@ async def perform_operation(
"""
Retrieve the item first and invoke only an operation listed in
`available_operations`, following its natural-language description. Operations
- may call an external provider and can return the item's updated state. If the
- provider rate limits spend-request creation, returns HTTP 429 with code
+ may call an external provider and return updated state. Link cards advertise
+ authorize. AgentCard cards are created with PUT and request approval when their
+ aliases are used at checkout; they do not expose this operation. If
+ spend-request creation is rate limited, returns HTTP 429 with code
`spend_request_rate_limited`; stop and back off before retrying.
Args:
@@ -724,7 +758,7 @@ async def upsert(
key: str,
*,
id_or_name: str,
- spec: WalletVaultItemSpecParam,
+ spec: item_upsert_params.WalletVaultItemRequestSpec,
type: Literal["wallet"],
# 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.
@@ -734,12 +768,20 @@ async def upsert(
timeout: float | httpx.Timeout | None | NotGiven = not_given,
) -> VaultItem:
"""
- Create or retrieve an identical vault item by immutable key
+ Create an item under a key unique within its vault, or retrieve the existing
+ item when its specification matches. An identical card PUT returns the existing
+ card in any lifecycle state without polling the provider, reauthorizing,
+ replacing aliases, or resetting recovery. Conflicting specifications return 409.
+ Provider-specific authorization requirements and retry behavior are described in
+ the item's request schema.
Args:
- spec: AgentCard wallet. Mode (sandbox vs live) is fixed by the deployment's AgentCard
- credential; there is no per-item test flag. user_id may only reference a user
- already enrolled by a wallet in this organization.
+ spec: AgentCard wallet. Omit provider_config to use Kernel-managed credentials, or
+ select a customer-owned configuration. Mode (sandbox vs live) is determined by
+ the selected credential; there is no per-item test flag. Without user_id,
+ creation returns a hosted enrollment action and Kernel polls until the user
+ connects. user_id may only reference a user already enrolled by a wallet in this
+ organization under the same configuration.
extra_headers: Send extra headers
@@ -767,7 +809,12 @@ async def upsert(
timeout: float | httpx.Timeout | None | NotGiven = not_given,
) -> VaultItem:
"""
- Create or retrieve an identical vault item by immutable key
+ Create an item under a key unique within its vault, or retrieve the existing
+ item when its specification matches. An identical card PUT returns the existing
+ card in any lifecycle state without polling the provider, reauthorizing,
+ replacing aliases, or resetting recovery. Conflicting specifications return 409.
+ Provider-specific authorization requirements and retry behavior are described in
+ the item's request schema.
Args:
spec: Live payment card. Test-mode card creation is not supported.
@@ -788,7 +835,7 @@ async def upsert(
key: str,
*,
id_or_name: str,
- spec: WalletVaultItemSpecParam | CardVaultItemSpecParam,
+ spec: item_upsert_params.WalletVaultItemRequestSpec | CardVaultItemSpecParam,
type: Literal["wallet"] | Literal["card"],
# 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.
diff --git a/src/kernel/resources/vaults/vaults.py b/src/kernel/resources/vaults/vaults.py
index f88ac5f1..16619462 100644
--- a/src/kernel/resources/vaults/vaults.py
+++ b/src/kernel/resources/vaults/vaults.py
@@ -141,8 +141,11 @@ def delete(
extra_body: Body | None = None,
timeout: float | httpx.Timeout | None | NotGiven = not_given,
) -> None:
- """
- Delete a vault and invalidate its items
+ """Unresolved payment operations block deletion.
+
+ Reconcile the original attempt
+ with the provider or support first; deleting or recreating an item is not proof
+ that a payment did not occur.
Args:
extra_headers: Send extra headers
@@ -312,8 +315,11 @@ async def delete(
extra_body: Body | None = None,
timeout: float | httpx.Timeout | None | NotGiven = not_given,
) -> None:
- """
- Delete a vault and invalidate its items
+ """Unresolved payment operations block deletion.
+
+ Reconcile the original attempt
+ with the provider or support first; deleting or recreating an item is not proof
+ that a payment did not occur.
Args:
extra_headers: Send extra headers
diff --git a/src/kernel/types/__init__.py b/src/kernel/types/__init__.py
index 0310cfbe..70e9581a 100644
--- a/src/kernel/types/__init__.py
+++ b/src/kernel/types/__init__.py
@@ -73,6 +73,7 @@
from .proxy_create_response import ProxyCreateResponse as ProxyCreateResponse
from .proxy_update_response import ProxyUpdateResponse as ProxyUpdateResponse
from .recommendation_result import RecommendationResult as RecommendationResult
+from .vault_provider_config import VaultProviderConfig as VaultProviderConfig
from .vault_reference_param import VaultReferenceParam as VaultReferenceParam
from .browser_memory_request import BrowserMemoryRequest as BrowserMemoryRequest
from .browser_network_config import BrowserNetworkConfig as BrowserNetworkConfig
@@ -131,6 +132,9 @@
from .credential_provider_create_params import CredentialProviderCreateParams as CredentialProviderCreateParams
from .credential_provider_update_params import CredentialProviderUpdateParams as CredentialProviderUpdateParams
from .invocation_list_browsers_response import InvocationListBrowsersResponse as InvocationListBrowsersResponse
+from .vault_provider_config_list_params import VaultProviderConfigListParams as VaultProviderConfigListParams
+from .vault_provider_config_create_params import VaultProviderConfigCreateParams as VaultProviderConfigCreateParams
+from .vault_provider_config_update_params import VaultProviderConfigUpdateParams as VaultProviderConfigUpdateParams
from .credential_provider_list_items_response import (
CredentialProviderListItemsResponse as CredentialProviderListItemsResponse,
)
diff --git a/src/kernel/types/vault_provider_config.py b/src/kernel/types/vault_provider_config.py
new file mode 100644
index 00000000..7ab9a049
--- /dev/null
+++ b/src/kernel/types/vault_provider_config.py
@@ -0,0 +1,58 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from typing import Union
+from datetime import datetime
+from typing_extensions import Literal, Annotated, TypeAlias
+
+from .._utils import PropertyInfo
+from .._models import BaseModel
+
+__all__ = ["VaultProviderConfig", "VaultLinkProviderConfig", "VaultAgentCardProviderConfig"]
+
+
+class VaultLinkProviderConfig(BaseModel):
+ """Response schema for a Link configuration, without secret credentials.
+
+ Kernel generates the ID and timestamps. Configuration creation uses VaultLinkProviderConfigRequest.
+ """
+
+ id: str
+
+ client_id: str
+ """OAuth client identity; immutable. Secret credentials are never returned."""
+
+ created_at: datetime
+
+ name: str
+ """Unique within the organization."""
+
+ provider: Literal["link"]
+
+ updated_at: datetime
+
+
+class VaultAgentCardProviderConfig(BaseModel):
+ """Response schema for an AgentCard configuration, without secret credentials.
+
+ Kernel generates the ID and timestamps and introspects test_mode from the credentials. Configuration creation uses VaultAgentCardProviderConfigRequest.
+ """
+
+ id: str
+
+ client_id: str
+
+ created_at: datetime
+
+ name: str
+
+ provider: Literal["agentcard"]
+
+ test_mode: bool
+ """Introspected mode of the selected credential; true means sandbox objects."""
+
+ updated_at: datetime
+
+
+VaultProviderConfig: TypeAlias = Annotated[
+ Union[VaultLinkProviderConfig, VaultAgentCardProviderConfig], PropertyInfo(discriminator="provider")
+]
diff --git a/src/kernel/types/vault_provider_config_create_params.py b/src/kernel/types/vault_provider_config_create_params.py
new file mode 100644
index 00000000..185f06a6
--- /dev/null
+++ b/src/kernel/types/vault_provider_config_create_params.py
@@ -0,0 +1,47 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from __future__ import annotations
+
+from typing import Union
+from typing_extensions import Literal, Required, TypeAlias, TypedDict
+
+__all__ = [
+ "VaultProviderConfigCreateParams",
+ "VaultLinkProviderConfigRequest",
+ "VaultLinkProviderConfigRequestCredentials",
+ "VaultAgentCardProviderConfigRequest",
+ "VaultAgentCardProviderConfigRequestCredentials",
+]
+
+
+class VaultLinkProviderConfigRequest(TypedDict, total=False):
+ credentials: Required[VaultLinkProviderConfigRequestCredentials]
+
+ name: Required[str]
+ """Unique within the organization."""
+
+ provider: Required[Literal["link"]]
+
+
+class VaultLinkProviderConfigRequestCredentials(TypedDict, total=False):
+ client_id: Required[str]
+
+ client_secret: Required[str]
+
+
+class VaultAgentCardProviderConfigRequest(TypedDict, total=False):
+ credentials: Required[VaultAgentCardProviderConfigRequestCredentials]
+
+ name: Required[str]
+ """Unique within the organization."""
+
+ provider: Required[Literal["agentcard"]]
+
+
+class VaultAgentCardProviderConfigRequestCredentials(TypedDict, total=False):
+ client_id: Required[str]
+
+ client_secret: Required[str]
+
+
+VaultProviderConfigCreateParams: TypeAlias = Union[VaultLinkProviderConfigRequest, VaultAgentCardProviderConfigRequest]
diff --git a/src/kernel/types/vault_provider_config_list_params.py b/src/kernel/types/vault_provider_config_list_params.py
new file mode 100644
index 00000000..27065f97
--- /dev/null
+++ b/src/kernel/types/vault_provider_config_list_params.py
@@ -0,0 +1,13 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from __future__ import annotations
+
+from typing_extensions import TypedDict
+
+__all__ = ["VaultProviderConfigListParams"]
+
+
+class VaultProviderConfigListParams(TypedDict, total=False):
+ limit: int
+
+ offset: int
diff --git a/src/kernel/types/vault_provider_config_update_params.py b/src/kernel/types/vault_provider_config_update_params.py
new file mode 100644
index 00000000..49d1be8a
--- /dev/null
+++ b/src/kernel/types/vault_provider_config_update_params.py
@@ -0,0 +1,28 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from __future__ import annotations
+
+from typing_extensions import TypedDict
+
+__all__ = ["VaultProviderConfigUpdateParams", "Credentials"]
+
+
+class VaultProviderConfigUpdateParams(TypedDict, total=False):
+ credentials: Credentials
+ """Fields to update.
+
+ Omitted credentials are left unchanged. A rejected update leaves existing
+ credentials unchanged.
+ """
+
+ name: str
+ """Unique within the organization."""
+
+
+class Credentials(TypedDict, total=False):
+ """Fields to update.
+
+ Omitted credentials are left unchanged. A rejected update leaves existing credentials unchanged.
+ """
+
+ client_secret: str
diff --git a/src/kernel/types/vaults/__init__.py b/src/kernel/types/vaults/__init__.py
index 7ee998eb..623e45d4 100644
--- a/src/kernel/types/vaults/__init__.py
+++ b/src/kernel/types/vaults/__init__.py
@@ -18,6 +18,5 @@
from .wallet_vault_item_spec import WalletVaultItemSpec as WalletVaultItemSpec
from .wallet_vault_item_state import WalletVaultItemState as WalletVaultItemState
from .card_vault_item_spec_param import CardVaultItemSpecParam as CardVaultItemSpecParam
-from .wallet_vault_item_spec_param import WalletVaultItemSpecParam as WalletVaultItemSpecParam
from .item_perform_operation_params import ItemPerformOperationParams as ItemPerformOperationParams
from .agentcard_checkout_authorization import AgentcardCheckoutAuthorization as AgentcardCheckoutAuthorization
diff --git a/src/kernel/types/vaults/card_vault_item_state.py b/src/kernel/types/vaults/card_vault_item_state.py
index 4808d1ff..7bb7e274 100644
--- a/src/kernel/types/vaults/card_vault_item_state.py
+++ b/src/kernel/types/vaults/card_vault_item_state.py
@@ -34,7 +34,17 @@ def __getattr__(self, attr: str) -> str: ...
class LinkCardState(BaseModel):
provider: Literal["link"]
- status: Literal["requested", "pending_authorization", "ready", "consumed", "expired", "declined"]
+ status: Literal[
+ "requested", "pending_authorization", "ready", "consumed", "expired", "declined", "recovery_required"
+ ]
+ """recovery_required means an original provider operation has an unresolved
+ outcome.
+
+ Do not retry, delete, or replace it. Known references may be observed safely,
+ but unknown creation without an ID and uncertain card-material retrieval require
+ manual reconciliation with the provider or support. There is no reset or
+ caller-asserted reconciliation operation.
+ """
aliases: Optional[VaultCardAliases] = None
@@ -66,7 +76,13 @@ def __getattr__(self, attr: str) -> str: ...
class AgentCardCardState(BaseModel):
provider: Literal["agentcard"]
- status: Literal["requested", "ready", "pending_approval", "degraded"]
+ status: Literal["requested", "ready", "pending_approval", "degraded", "recovery_required"]
+ """recovery_required means the original checkout outcome is unresolved.
+
+ Do not retry, delete, or replace it. Known authorization IDs may be reconciled
+ through provider observations; otherwise contact the provider or support for
+ manual reconciliation. It does not mean declined or expired.
+ """
aliases: Optional[VaultCardAliases] = None
diff --git a/src/kernel/types/vaults/item_upsert_params.py b/src/kernel/types/vaults/item_upsert_params.py
index c3fe00e8..2a62d726 100644
--- a/src/kernel/types/vaults/item_upsert_params.py
+++ b/src/kernel/types/vaults/item_upsert_params.py
@@ -6,25 +6,176 @@
from typing_extensions import Literal, Required, TypeAlias, TypedDict
from .card_vault_item_spec_param import CardVaultItemSpecParam
-from .wallet_vault_item_spec_param import WalletVaultItemSpecParam
-__all__ = ["ItemUpsertParams", "WalletVaultItemRequest", "CardVaultItemRequest"]
+__all__ = [
+ "ItemUpsertParams",
+ "WalletVaultItemRequest",
+ "WalletVaultItemRequestSpec",
+ "WalletVaultItemRequestSpecLinkWalletVaultItemRequestSpec",
+ "WalletVaultItemRequestSpecLinkWalletVaultItemRequestSpecAuthorization",
+ "WalletVaultItemRequestSpecLinkWalletVaultItemRequestSpecAuthorizationKernelManagedLinkAuthorizationInput",
+ "WalletVaultItemRequestSpecLinkWalletVaultItemRequestSpecAuthorizationKernelManagedLinkAuthorizationInputClient",
+ "WalletVaultItemRequestSpecLinkWalletVaultItemRequestSpecAuthorizationImportedLinkAuthorizationInput",
+ "WalletVaultItemRequestSpecLinkWalletVaultItemRequestSpecAuthorizationImportedLinkAuthorizationInputClient",
+ "WalletVaultItemRequestSpecLinkWalletVaultItemRequestSpecAuthorizationImportedLinkAuthorizationInputClientProviderConfig",
+ "WalletVaultItemRequestSpecLinkWalletVaultItemRequestSpecAuthorizationImportedLinkAuthorizationInputTokens",
+ "WalletVaultItemRequestSpecAgentCardWalletVaultItemSpec",
+ "WalletVaultItemRequestSpecAgentCardWalletVaultItemSpecProviderConfig",
+ "CardVaultItemRequest",
+]
class WalletVaultItemRequest(TypedDict, total=False):
id_or_name: Required[str]
- spec: Required[WalletVaultItemSpecParam]
+ spec: Required[WalletVaultItemRequestSpec]
"""AgentCard wallet.
- Mode (sandbox vs live) is fixed by the deployment's AgentCard credential; there
- is no per-item test flag. user_id may only reference a user already enrolled by
- a wallet in this organization.
+ Omit provider_config to use Kernel-managed credentials, or select a
+ customer-owned configuration. Mode (sandbox vs live) is determined by the
+ selected credential; there is no per-item test flag. Without user_id, creation
+ returns a hosted enrollment action and Kernel polls until the user connects.
+ user_id may only reference a user already enrolled by a wallet in this
+ organization under the same configuration.
"""
type: Required[Literal["wallet"]]
+class WalletVaultItemRequestSpecLinkWalletVaultItemRequestSpecAuthorizationKernelManagedLinkAuthorizationInputClient(
+ TypedDict, total=False
+):
+ type: Required[Literal["kernel_managed"]]
+
+
+class WalletVaultItemRequestSpecLinkWalletVaultItemRequestSpecAuthorizationKernelManagedLinkAuthorizationInput(
+ TypedDict, total=False
+):
+ """Kernel starts and completes the user's Link authorization flow."""
+
+ client: Required[
+ WalletVaultItemRequestSpecLinkWalletVaultItemRequestSpecAuthorizationKernelManagedLinkAuthorizationInputClient
+ ]
+
+ method: Required[Literal["oauth"]]
+
+
+class WalletVaultItemRequestSpecLinkWalletVaultItemRequestSpecAuthorizationImportedLinkAuthorizationInputClientProviderConfig(
+ TypedDict, total=False
+):
+ """Select a provider config by ID or name.
+
+ Responses return the ID. Renaming a config does not change existing wallet bindings; a wallet cannot switch to a different config after creation.
+ """
+
+ id: str
+
+ name: str
+
+
+class WalletVaultItemRequestSpecLinkWalletVaultItemRequestSpecAuthorizationImportedLinkAuthorizationInputClient(
+ TypedDict, total=False
+):
+ provider_config: Required[
+ WalletVaultItemRequestSpecLinkWalletVaultItemRequestSpecAuthorizationImportedLinkAuthorizationInputClientProviderConfig
+ ]
+ """Select a provider config by ID or name.
+
+ Responses return the ID. Renaming a config does not change existing wallet
+ bindings; a wallet cannot switch to a different config after creation.
+ """
+
+ type: Required[Literal["customer_managed"]]
+
+
+class WalletVaultItemRequestSpecLinkWalletVaultItemRequestSpecAuthorizationImportedLinkAuthorizationInputTokens(
+ TypedDict, total=False
+):
+ """Send the token pair from your backend.
+
+ Both tokens must be from the same Link grant under the referenced client. Supply a currently valid access token. Kernel refreshes when needed after import and uses the expiry returned by Link for subsequent tokens. Tokens are never returned in wallet responses, events, or logs.
+ """
+
+ access_token: Required[str]
+
+ refresh_token: Required[str]
+
+
+class WalletVaultItemRequestSpecLinkWalletVaultItemRequestSpecAuthorizationImportedLinkAuthorizationInput(
+ TypedDict, total=False
+):
+ """The customer's backend completes Link OAuth and supplies the resulting tokens.
+
+ For a new wallet, Kernel verifies the access token can access Link payment methods without consuming or rotating the refresh token. Valid access creates a wallet with state.status=connected. An expired, invalid, revoked, or insufficiently scoped access token returns 400 and no wallet is created. Refresh expired tokens in your backend before importing them. A failed import does not modify existing wallets.
+ After successful import, Kernel owns subsequent refresh-token rotation; the customer must stop refreshing this grant. Import does not verify the refresh token: if it or the configured client credentials are rejected during a later refresh, the imported wallet becomes degraded. An unknown refresh outcome also leaves it degraded; Kernel does not retry a refresh token that may already have been consumed. There is no in-place reauthorization operation for an imported wallet.
+ If this imported wallet's credentials become unusable, obtain a fresh Link OAuth grant in your backend and create a wallet under a NEW wallet key. Use the new wallet for NEW cards and payments, not to retry an old payment whose outcome is uncertain. This does not replace the old grant, rebind existing cards, or resolve their payment outcomes. Retain the old wallet and its cards while reconciling any uncertain payments with the provider or support. Do not repeat an uncertain payment on the new wallet, and do not treat deletion as evidence that it did not execute. Deletion of the old wallet can remain blocked by unresolved child cards.
+ Repeating a create for the same item key and non-secret spec returns the existing wallet without replacing tokens, even if they have rotated or the wallet needs reconnection. ID and name references resolving to the same config are equivalent. A different config or non-secret spec returns 409. This create operation does not replace an existing grant.
+ """
+
+ client: Required[
+ WalletVaultItemRequestSpecLinkWalletVaultItemRequestSpecAuthorizationImportedLinkAuthorizationInputClient
+ ]
+
+ method: Required[Literal["oauth"]]
+
+ tokens: Required[
+ WalletVaultItemRequestSpecLinkWalletVaultItemRequestSpecAuthorizationImportedLinkAuthorizationInputTokens
+ ]
+ """Send the token pair from your backend.
+
+ Both tokens must be from the same Link grant under the referenced client. Supply
+ a currently valid access token. Kernel refreshes when needed after import and
+ uses the expiry returned by Link for subsequent tokens. Tokens are never
+ returned in wallet responses, events, or logs.
+ """
+
+
+WalletVaultItemRequestSpecLinkWalletVaultItemRequestSpecAuthorization: TypeAlias = Union[
+ WalletVaultItemRequestSpecLinkWalletVaultItemRequestSpecAuthorizationKernelManagedLinkAuthorizationInput,
+ WalletVaultItemRequestSpecLinkWalletVaultItemRequestSpecAuthorizationImportedLinkAuthorizationInput,
+]
+
+
+class WalletVaultItemRequestSpecLinkWalletVaultItemRequestSpec(TypedDict, total=False):
+ authorization: Required[WalletVaultItemRequestSpecLinkWalletVaultItemRequestSpecAuthorization]
+ """Kernel starts and completes the user's Link authorization flow."""
+
+ provider: Required[Literal["link"]]
+
+
+class WalletVaultItemRequestSpecAgentCardWalletVaultItemSpecProviderConfig(TypedDict, total=False):
+ """Select an AgentCard configuration.
+
+ The wallet's configuration cannot be changed after creation.
+ """
+
+ id: str
+
+ name: str
+
+
+class WalletVaultItemRequestSpecAgentCardWalletVaultItemSpec(TypedDict, total=False):
+ """AgentCard wallet.
+
+ Omit provider_config to use Kernel-managed credentials, or select a customer-owned configuration. Mode (sandbox vs live) is determined by the selected credential; there is no per-item test flag. Without user_id, creation returns a hosted enrollment action and Kernel polls until the user connects. user_id may only reference a user already enrolled by a wallet in this organization under the same configuration.
+ """
+
+ provider: Required[Literal["agentcard"]]
+
+ provider_config: WalletVaultItemRequestSpecAgentCardWalletVaultItemSpecProviderConfig
+ """Select an AgentCard configuration.
+
+ The wallet's configuration cannot be changed after creation.
+ """
+
+ user_id: str
+
+
+WalletVaultItemRequestSpec: TypeAlias = Union[
+ WalletVaultItemRequestSpecLinkWalletVaultItemRequestSpec, WalletVaultItemRequestSpecAgentCardWalletVaultItemSpec
+]
+
+
class CardVaultItemRequest(TypedDict, total=False):
id_or_name: Required[str]
diff --git a/src/kernel/types/vaults/vault_item.py b/src/kernel/types/vaults/vault_item.py
index 7f4c9b81..2cc7ab26 100644
--- a/src/kernel/types/vaults/vault_item.py
+++ b/src/kernel/types/vaults/vault_item.py
@@ -67,9 +67,12 @@ class WalletVaultItem(BaseModel):
spec: WalletVaultItemSpec
"""AgentCard wallet.
- Mode (sandbox vs live) is fixed by the deployment's AgentCard credential; there
- is no per-item test flag. user_id may only reference a user already enrolled by
- a wallet in this organization.
+ Omit provider_config to use Kernel-managed credentials, or select a
+ customer-owned configuration. Mode (sandbox vs live) is determined by the
+ selected credential; there is no per-item test flag. Without user_id, creation
+ returns a hosted enrollment action and Kernel polls until the user connects.
+ user_id may only reference a user already enrolled by a wallet in this
+ organization under the same configuration.
"""
state: WalletVaultItemState
diff --git a/src/kernel/types/vaults/wallet_vault_item_spec.py b/src/kernel/types/vaults/wallet_vault_item_spec.py
index 03176670..2c17933d 100644
--- a/src/kernel/types/vaults/wallet_vault_item_spec.py
+++ b/src/kernel/types/vaults/wallet_vault_item_spec.py
@@ -11,14 +11,49 @@
"LinkWalletVaultItemSpec",
"LinkWalletVaultItemSpecAuthorization",
"LinkWalletVaultItemSpecAuthorizationClient",
+ "LinkWalletVaultItemSpecAuthorizationClientKernelManagedOAuthClient",
+ "LinkWalletVaultItemSpecAuthorizationClientCustomerManagedOAuthClient",
+ "LinkWalletVaultItemSpecAuthorizationClientCustomerManagedOAuthClientProviderConfig",
"AgentCardWalletVaultItemSpec",
+ "AgentCardWalletVaultItemSpecProviderConfig",
]
-class LinkWalletVaultItemSpecAuthorizationClient(BaseModel):
+class LinkWalletVaultItemSpecAuthorizationClientKernelManagedOAuthClient(BaseModel):
type: Literal["kernel_managed"]
+class LinkWalletVaultItemSpecAuthorizationClientCustomerManagedOAuthClientProviderConfig(BaseModel):
+ """Select a provider config by ID or name.
+
+ Responses return the ID. Renaming a config does not change existing wallet bindings; a wallet cannot switch to a different config after creation.
+ """
+
+ id: Optional[str] = None
+
+ name: Optional[str] = None
+
+
+class LinkWalletVaultItemSpecAuthorizationClientCustomerManagedOAuthClient(BaseModel):
+ provider_config: LinkWalletVaultItemSpecAuthorizationClientCustomerManagedOAuthClientProviderConfig
+ """Select a provider config by ID or name.
+
+ Responses return the ID. Renaming a config does not change existing wallet
+ bindings; a wallet cannot switch to a different config after creation.
+ """
+
+ type: Literal["customer_managed"]
+
+
+LinkWalletVaultItemSpecAuthorizationClient: TypeAlias = Annotated[
+ Union[
+ LinkWalletVaultItemSpecAuthorizationClientKernelManagedOAuthClient,
+ LinkWalletVaultItemSpecAuthorizationClientCustomerManagedOAuthClient,
+ ],
+ PropertyInfo(discriminator="type"),
+]
+
+
class LinkWalletVaultItemSpecAuthorization(BaseModel):
client: LinkWalletVaultItemSpecAuthorizationClient
@@ -31,14 +66,31 @@ class LinkWalletVaultItemSpec(BaseModel):
provider: Literal["link"]
+class AgentCardWalletVaultItemSpecProviderConfig(BaseModel):
+ """Select an AgentCard configuration.
+
+ The wallet's configuration cannot be changed after creation.
+ """
+
+ id: Optional[str] = None
+
+ name: Optional[str] = None
+
+
class AgentCardWalletVaultItemSpec(BaseModel):
"""AgentCard wallet.
- Mode (sandbox vs live) is fixed by the deployment's AgentCard credential; there is no per-item test flag. user_id may only reference a user already enrolled by a wallet in this organization.
+ Omit provider_config to use Kernel-managed credentials, or select a customer-owned configuration. Mode (sandbox vs live) is determined by the selected credential; there is no per-item test flag. Without user_id, creation returns a hosted enrollment action and Kernel polls until the user connects. user_id may only reference a user already enrolled by a wallet in this organization under the same configuration.
"""
provider: Literal["agentcard"]
+ provider_config: Optional[AgentCardWalletVaultItemSpecProviderConfig] = None
+ """Select an AgentCard configuration.
+
+ The wallet's configuration cannot be changed after creation.
+ """
+
user_id: Optional[str] = None
diff --git a/src/kernel/types/vaults/wallet_vault_item_spec_param.py b/src/kernel/types/vaults/wallet_vault_item_spec_param.py
deleted file mode 100644
index dc30e054..00000000
--- a/src/kernel/types/vaults/wallet_vault_item_spec_param.py
+++ /dev/null
@@ -1,44 +0,0 @@
-# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
-
-from __future__ import annotations
-
-from typing import Union
-from typing_extensions import Literal, Required, TypeAlias, TypedDict
-
-__all__ = [
- "WalletVaultItemSpecParam",
- "LinkWalletVaultItemSpec",
- "LinkWalletVaultItemSpecAuthorization",
- "LinkWalletVaultItemSpecAuthorizationClient",
- "AgentCardWalletVaultItemSpec",
-]
-
-
-class LinkWalletVaultItemSpecAuthorizationClient(TypedDict, total=False):
- type: Required[Literal["kernel_managed"]]
-
-
-class LinkWalletVaultItemSpecAuthorization(TypedDict, total=False):
- client: Required[LinkWalletVaultItemSpecAuthorizationClient]
-
- method: Required[Literal["oauth"]]
-
-
-class LinkWalletVaultItemSpec(TypedDict, total=False):
- authorization: Required[LinkWalletVaultItemSpecAuthorization]
-
- provider: Required[Literal["link"]]
-
-
-class AgentCardWalletVaultItemSpec(TypedDict, total=False):
- """AgentCard wallet.
-
- Mode (sandbox vs live) is fixed by the deployment's AgentCard credential; there is no per-item test flag. user_id may only reference a user already enrolled by a wallet in this organization.
- """
-
- provider: Required[Literal["agentcard"]]
-
- user_id: str
-
-
-WalletVaultItemSpecParam: TypeAlias = Union[LinkWalletVaultItemSpec, AgentCardWalletVaultItemSpec]
diff --git a/tests/api_resources/test_vault_provider_configs.py b/tests/api_resources/test_vault_provider_configs.py
new file mode 100644
index 00000000..7ab21cd5
--- /dev/null
+++ b/tests/api_resources/test_vault_provider_configs.py
@@ -0,0 +1,569 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from __future__ import annotations
+
+import os
+from typing import Any, cast
+
+import pytest
+
+from kernel import Kernel, AsyncKernel
+from tests.utils import assert_matches_type
+from kernel.types import (
+ VaultProviderConfig,
+)
+from kernel.pagination import SyncOffsetPagination, AsyncOffsetPagination
+
+base_url = os.environ.get("TEST_API_BASE_URL", "http://127.0.0.1:4010")
+
+
+class TestVaultProviderConfigs:
+ parametrize = pytest.mark.parametrize("client", [False, True], indirect=True, ids=["loose", "strict"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ def test_method_create_overload_1(self, client: Kernel) -> None:
+ vault_provider_config = client.vault_provider_configs.create(
+ credentials={
+ "client_id": "x",
+ "client_secret": "x",
+ },
+ name="name",
+ provider="link",
+ )
+ assert_matches_type(VaultProviderConfig, vault_provider_config, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ def test_raw_response_create_overload_1(self, client: Kernel) -> None:
+ response = client.vault_provider_configs.with_raw_response.create(
+ credentials={
+ "client_id": "x",
+ "client_secret": "x",
+ },
+ name="name",
+ provider="link",
+ )
+
+ assert response.is_closed is True
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+ vault_provider_config = response.parse()
+ assert_matches_type(VaultProviderConfig, vault_provider_config, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ def test_streaming_response_create_overload_1(self, client: Kernel) -> None:
+ with client.vault_provider_configs.with_streaming_response.create(
+ credentials={
+ "client_id": "x",
+ "client_secret": "x",
+ },
+ name="name",
+ provider="link",
+ ) as response:
+ assert not response.is_closed
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+
+ vault_provider_config = response.parse()
+ assert_matches_type(VaultProviderConfig, vault_provider_config, path=["response"])
+
+ assert cast(Any, response.is_closed) is True
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ def test_method_create_overload_2(self, client: Kernel) -> None:
+ vault_provider_config = client.vault_provider_configs.create(
+ credentials={
+ "client_id": "x",
+ "client_secret": "x",
+ },
+ name="name",
+ provider="agentcard",
+ )
+ assert_matches_type(VaultProviderConfig, vault_provider_config, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ def test_raw_response_create_overload_2(self, client: Kernel) -> None:
+ response = client.vault_provider_configs.with_raw_response.create(
+ credentials={
+ "client_id": "x",
+ "client_secret": "x",
+ },
+ name="name",
+ provider="agentcard",
+ )
+
+ assert response.is_closed is True
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+ vault_provider_config = response.parse()
+ assert_matches_type(VaultProviderConfig, vault_provider_config, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ def test_streaming_response_create_overload_2(self, client: Kernel) -> None:
+ with client.vault_provider_configs.with_streaming_response.create(
+ credentials={
+ "client_id": "x",
+ "client_secret": "x",
+ },
+ name="name",
+ provider="agentcard",
+ ) as response:
+ assert not response.is_closed
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+
+ vault_provider_config = response.parse()
+ assert_matches_type(VaultProviderConfig, vault_provider_config, path=["response"])
+
+ assert cast(Any, response.is_closed) is True
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ def test_method_retrieve(self, client: Kernel) -> None:
+ vault_provider_config = client.vault_provider_configs.retrieve(
+ "id_or_name",
+ )
+ assert_matches_type(VaultProviderConfig, vault_provider_config, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ def test_raw_response_retrieve(self, client: Kernel) -> None:
+ response = client.vault_provider_configs.with_raw_response.retrieve(
+ "id_or_name",
+ )
+
+ assert response.is_closed is True
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+ vault_provider_config = response.parse()
+ assert_matches_type(VaultProviderConfig, vault_provider_config, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ def test_streaming_response_retrieve(self, client: Kernel) -> None:
+ with client.vault_provider_configs.with_streaming_response.retrieve(
+ "id_or_name",
+ ) as response:
+ assert not response.is_closed
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+
+ vault_provider_config = response.parse()
+ assert_matches_type(VaultProviderConfig, vault_provider_config, path=["response"])
+
+ assert cast(Any, response.is_closed) is True
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ def test_path_params_retrieve(self, client: Kernel) -> None:
+ with pytest.raises(ValueError, match=r"Expected a non-empty value for `id_or_name` but received ''"):
+ client.vault_provider_configs.with_raw_response.retrieve(
+ "",
+ )
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ def test_method_update(self, client: Kernel) -> None:
+ vault_provider_config = client.vault_provider_configs.update(
+ id_or_name="id_or_name",
+ )
+ assert_matches_type(VaultProviderConfig, vault_provider_config, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ def test_method_update_with_all_params(self, client: Kernel) -> None:
+ vault_provider_config = client.vault_provider_configs.update(
+ id_or_name="id_or_name",
+ credentials={"client_secret": "x"},
+ name="renamed-link-client",
+ )
+ assert_matches_type(VaultProviderConfig, vault_provider_config, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ def test_raw_response_update(self, client: Kernel) -> None:
+ response = client.vault_provider_configs.with_raw_response.update(
+ id_or_name="id_or_name",
+ )
+
+ assert response.is_closed is True
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+ vault_provider_config = response.parse()
+ assert_matches_type(VaultProviderConfig, vault_provider_config, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ def test_streaming_response_update(self, client: Kernel) -> None:
+ with client.vault_provider_configs.with_streaming_response.update(
+ id_or_name="id_or_name",
+ ) as response:
+ assert not response.is_closed
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+
+ vault_provider_config = response.parse()
+ assert_matches_type(VaultProviderConfig, vault_provider_config, 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_or_name` but received ''"):
+ client.vault_provider_configs.with_raw_response.update(
+ id_or_name="",
+ )
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ def test_method_list(self, client: Kernel) -> None:
+ vault_provider_config = client.vault_provider_configs.list()
+ assert_matches_type(SyncOffsetPagination[VaultProviderConfig], vault_provider_config, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ def test_method_list_with_all_params(self, client: Kernel) -> None:
+ vault_provider_config = client.vault_provider_configs.list(
+ limit=1,
+ offset=0,
+ )
+ assert_matches_type(SyncOffsetPagination[VaultProviderConfig], vault_provider_config, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ def test_raw_response_list(self, client: Kernel) -> None:
+ response = client.vault_provider_configs.with_raw_response.list()
+
+ assert response.is_closed is True
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+ vault_provider_config = response.parse()
+ assert_matches_type(SyncOffsetPagination[VaultProviderConfig], vault_provider_config, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ def test_streaming_response_list(self, client: Kernel) -> None:
+ with client.vault_provider_configs.with_streaming_response.list() as response:
+ assert not response.is_closed
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+
+ vault_provider_config = response.parse()
+ assert_matches_type(SyncOffsetPagination[VaultProviderConfig], vault_provider_config, path=["response"])
+
+ assert cast(Any, response.is_closed) is True
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ def test_method_delete(self, client: Kernel) -> None:
+ vault_provider_config = client.vault_provider_configs.delete(
+ "id_or_name",
+ )
+ assert vault_provider_config is None
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ def test_raw_response_delete(self, client: Kernel) -> None:
+ response = client.vault_provider_configs.with_raw_response.delete(
+ "id_or_name",
+ )
+
+ assert response.is_closed is True
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+ vault_provider_config = response.parse()
+ assert vault_provider_config is None
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ def test_streaming_response_delete(self, client: Kernel) -> None:
+ with client.vault_provider_configs.with_streaming_response.delete(
+ "id_or_name",
+ ) as response:
+ assert not response.is_closed
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+
+ vault_provider_config = response.parse()
+ assert vault_provider_config is None
+
+ assert cast(Any, response.is_closed) is True
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ def test_path_params_delete(self, client: Kernel) -> None:
+ with pytest.raises(ValueError, match=r"Expected a non-empty value for `id_or_name` but received ''"):
+ client.vault_provider_configs.with_raw_response.delete(
+ "",
+ )
+
+
+class TestAsyncVaultProviderConfigs:
+ parametrize = pytest.mark.parametrize(
+ "async_client", [False, True, {"http_client": "aiohttp"}], indirect=True, ids=["loose", "strict", "aiohttp"]
+ )
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ async def test_method_create_overload_1(self, async_client: AsyncKernel) -> None:
+ vault_provider_config = await async_client.vault_provider_configs.create(
+ credentials={
+ "client_id": "x",
+ "client_secret": "x",
+ },
+ name="name",
+ provider="link",
+ )
+ assert_matches_type(VaultProviderConfig, vault_provider_config, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ async def test_raw_response_create_overload_1(self, async_client: AsyncKernel) -> None:
+ response = await async_client.vault_provider_configs.with_raw_response.create(
+ credentials={
+ "client_id": "x",
+ "client_secret": "x",
+ },
+ name="name",
+ provider="link",
+ )
+
+ assert response.is_closed is True
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+ vault_provider_config = await response.parse()
+ assert_matches_type(VaultProviderConfig, vault_provider_config, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ async def test_streaming_response_create_overload_1(self, async_client: AsyncKernel) -> None:
+ async with async_client.vault_provider_configs.with_streaming_response.create(
+ credentials={
+ "client_id": "x",
+ "client_secret": "x",
+ },
+ name="name",
+ provider="link",
+ ) as response:
+ assert not response.is_closed
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+
+ vault_provider_config = await response.parse()
+ assert_matches_type(VaultProviderConfig, vault_provider_config, path=["response"])
+
+ assert cast(Any, response.is_closed) is True
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ async def test_method_create_overload_2(self, async_client: AsyncKernel) -> None:
+ vault_provider_config = await async_client.vault_provider_configs.create(
+ credentials={
+ "client_id": "x",
+ "client_secret": "x",
+ },
+ name="name",
+ provider="agentcard",
+ )
+ assert_matches_type(VaultProviderConfig, vault_provider_config, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ async def test_raw_response_create_overload_2(self, async_client: AsyncKernel) -> None:
+ response = await async_client.vault_provider_configs.with_raw_response.create(
+ credentials={
+ "client_id": "x",
+ "client_secret": "x",
+ },
+ name="name",
+ provider="agentcard",
+ )
+
+ assert response.is_closed is True
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+ vault_provider_config = await response.parse()
+ assert_matches_type(VaultProviderConfig, vault_provider_config, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ async def test_streaming_response_create_overload_2(self, async_client: AsyncKernel) -> None:
+ async with async_client.vault_provider_configs.with_streaming_response.create(
+ credentials={
+ "client_id": "x",
+ "client_secret": "x",
+ },
+ name="name",
+ provider="agentcard",
+ ) as response:
+ assert not response.is_closed
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+
+ vault_provider_config = await response.parse()
+ assert_matches_type(VaultProviderConfig, vault_provider_config, path=["response"])
+
+ assert cast(Any, response.is_closed) is True
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ async def test_method_retrieve(self, async_client: AsyncKernel) -> None:
+ vault_provider_config = await async_client.vault_provider_configs.retrieve(
+ "id_or_name",
+ )
+ assert_matches_type(VaultProviderConfig, vault_provider_config, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ async def test_raw_response_retrieve(self, async_client: AsyncKernel) -> None:
+ response = await async_client.vault_provider_configs.with_raw_response.retrieve(
+ "id_or_name",
+ )
+
+ assert response.is_closed is True
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+ vault_provider_config = await response.parse()
+ assert_matches_type(VaultProviderConfig, vault_provider_config, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ async def test_streaming_response_retrieve(self, async_client: AsyncKernel) -> None:
+ async with async_client.vault_provider_configs.with_streaming_response.retrieve(
+ "id_or_name",
+ ) as response:
+ assert not response.is_closed
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+
+ vault_provider_config = await response.parse()
+ assert_matches_type(VaultProviderConfig, vault_provider_config, 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_retrieve(self, async_client: AsyncKernel) -> None:
+ with pytest.raises(ValueError, match=r"Expected a non-empty value for `id_or_name` but received ''"):
+ await async_client.vault_provider_configs.with_raw_response.retrieve(
+ "",
+ )
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ async def test_method_update(self, async_client: AsyncKernel) -> None:
+ vault_provider_config = await async_client.vault_provider_configs.update(
+ id_or_name="id_or_name",
+ )
+ assert_matches_type(VaultProviderConfig, vault_provider_config, 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:
+ vault_provider_config = await async_client.vault_provider_configs.update(
+ id_or_name="id_or_name",
+ credentials={"client_secret": "x"},
+ name="renamed-link-client",
+ )
+ assert_matches_type(VaultProviderConfig, vault_provider_config, 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.vault_provider_configs.with_raw_response.update(
+ id_or_name="id_or_name",
+ )
+
+ assert response.is_closed is True
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+ vault_provider_config = await response.parse()
+ assert_matches_type(VaultProviderConfig, vault_provider_config, 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.vault_provider_configs.with_streaming_response.update(
+ id_or_name="id_or_name",
+ ) as response:
+ assert not response.is_closed
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+
+ vault_provider_config = await response.parse()
+ assert_matches_type(VaultProviderConfig, vault_provider_config, 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_or_name` but received ''"):
+ await async_client.vault_provider_configs.with_raw_response.update(
+ id_or_name="",
+ )
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ async def test_method_list(self, async_client: AsyncKernel) -> None:
+ vault_provider_config = await async_client.vault_provider_configs.list()
+ assert_matches_type(AsyncOffsetPagination[VaultProviderConfig], vault_provider_config, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ async def test_method_list_with_all_params(self, async_client: AsyncKernel) -> None:
+ vault_provider_config = await async_client.vault_provider_configs.list(
+ limit=1,
+ offset=0,
+ )
+ assert_matches_type(AsyncOffsetPagination[VaultProviderConfig], vault_provider_config, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ async def test_raw_response_list(self, async_client: AsyncKernel) -> None:
+ response = await async_client.vault_provider_configs.with_raw_response.list()
+
+ assert response.is_closed is True
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+ vault_provider_config = await response.parse()
+ assert_matches_type(AsyncOffsetPagination[VaultProviderConfig], vault_provider_config, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ async def test_streaming_response_list(self, async_client: AsyncKernel) -> None:
+ async with async_client.vault_provider_configs.with_streaming_response.list() as response:
+ assert not response.is_closed
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+
+ vault_provider_config = await response.parse()
+ assert_matches_type(AsyncOffsetPagination[VaultProviderConfig], vault_provider_config, path=["response"])
+
+ assert cast(Any, response.is_closed) is True
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ async def test_method_delete(self, async_client: AsyncKernel) -> None:
+ vault_provider_config = await async_client.vault_provider_configs.delete(
+ "id_or_name",
+ )
+ assert vault_provider_config is None
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ async def test_raw_response_delete(self, async_client: AsyncKernel) -> None:
+ response = await async_client.vault_provider_configs.with_raw_response.delete(
+ "id_or_name",
+ )
+
+ assert response.is_closed is True
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+ vault_provider_config = await response.parse()
+ assert vault_provider_config is None
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ async def test_streaming_response_delete(self, async_client: AsyncKernel) -> None:
+ async with async_client.vault_provider_configs.with_streaming_response.delete(
+ "id_or_name",
+ ) as response:
+ assert not response.is_closed
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+
+ vault_provider_config = await response.parse()
+ assert vault_provider_config is None
+
+ assert cast(Any, response.is_closed) is True
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ async def test_path_params_delete(self, async_client: AsyncKernel) -> None:
+ with pytest.raises(ValueError, match=r"Expected a non-empty value for `id_or_name` but received ''"):
+ await async_client.vault_provider_configs.with_raw_response.delete(
+ "",
+ )
diff --git a/tests/api_resources/test_vaults.py b/tests/api_resources/test_vaults.py
index 0836a7f8..f9b36c73 100644
--- a/tests/api_resources/test_vaults.py
+++ b/tests/api_resources/test_vaults.py
@@ -143,7 +143,7 @@ def test_path_params_delete(self, client: Kernel) -> None:
@parametrize
def test_method_upsert(self, client: Kernel) -> None:
vault = client.vaults.upsert(
- name="name",
+ name="checkout",
)
assert_matches_type(Vault, vault, path=["response"])
@@ -151,7 +151,7 @@ def test_method_upsert(self, client: Kernel) -> None:
@parametrize
def test_raw_response_upsert(self, client: Kernel) -> None:
response = client.vaults.with_raw_response.upsert(
- name="name",
+ name="checkout",
)
assert response.is_closed is True
@@ -163,7 +163,7 @@ def test_raw_response_upsert(self, client: Kernel) -> None:
@parametrize
def test_streaming_response_upsert(self, client: Kernel) -> None:
with client.vaults.with_streaming_response.upsert(
- name="name",
+ name="checkout",
) as response:
assert not response.is_closed
assert response.http_request.headers.get("X-Stainless-Lang") == "python"
@@ -304,7 +304,7 @@ async def test_path_params_delete(self, async_client: AsyncKernel) -> None:
@parametrize
async def test_method_upsert(self, async_client: AsyncKernel) -> None:
vault = await async_client.vaults.upsert(
- name="name",
+ name="checkout",
)
assert_matches_type(Vault, vault, path=["response"])
@@ -312,7 +312,7 @@ async def test_method_upsert(self, async_client: AsyncKernel) -> None:
@parametrize
async def test_raw_response_upsert(self, async_client: AsyncKernel) -> None:
response = await async_client.vaults.with_raw_response.upsert(
- name="name",
+ name="checkout",
)
assert response.is_closed is True
@@ -324,7 +324,7 @@ async def test_raw_response_upsert(self, async_client: AsyncKernel) -> None:
@parametrize
async def test_streaming_response_upsert(self, async_client: AsyncKernel) -> None:
async with async_client.vaults.with_streaming_response.upsert(
- name="name",
+ name="checkout",
) as response:
assert not response.is_closed
assert response.http_request.headers.get("X-Stainless-Lang") == "python"
diff --git a/tests/api_resources/vaults/test_items.py b/tests/api_resources/vaults/test_items.py
index 51759529..9747f919 100644
--- a/tests/api_resources/vaults/test_items.py
+++ b/tests/api_resources/vaults/test_items.py
@@ -91,14 +91,14 @@ def test_method_update(self, client: Kernel) -> None:
key="x",
id_or_name="id_or_name",
spec={
- "amount": 1,
- "context": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
- "currency": "bFx",
- "merchant_name": "x",
- "merchant_url": "https://example.com",
- "payment_method_id": "x",
+ "amount": 3000,
+ "context": "The order total changed to USD 30.00 including shipping and taxes for one notebook. Update this unapproved request rather than creating a second payment.",
+ "currency": "usd",
+ "merchant_name": "Example Store",
+ "merchant_url": "https://store.example.com",
+ "payment_method_id": "pm_example",
"provider": "link",
- "wallet": "wallet",
+ "wallet": "link-wallet",
},
)
assert_matches_type(VaultItem, item, path=["response"])
@@ -110,14 +110,14 @@ def test_method_update_with_all_params(self, client: Kernel) -> None:
key="x",
id_or_name="id_or_name",
spec={
- "amount": 1,
- "context": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
- "currency": "bFx",
- "merchant_name": "x",
- "merchant_url": "https://example.com",
- "payment_method_id": "x",
+ "amount": 3000,
+ "context": "The order total changed to USD 30.00 including shipping and taxes for one notebook. Update this unapproved request rather than creating a second payment.",
+ "currency": "usd",
+ "merchant_name": "Example Store",
+ "merchant_url": "https://store.example.com",
+ "payment_method_id": "pm_example",
"provider": "link",
- "wallet": "wallet",
+ "wallet": "link-wallet",
"expires_at": 0,
"line_items": [
{
@@ -157,14 +157,14 @@ def test_raw_response_update(self, client: Kernel) -> None:
key="x",
id_or_name="id_or_name",
spec={
- "amount": 1,
- "context": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
- "currency": "bFx",
- "merchant_name": "x",
- "merchant_url": "https://example.com",
- "payment_method_id": "x",
+ "amount": 3000,
+ "context": "The order total changed to USD 30.00 including shipping and taxes for one notebook. Update this unapproved request rather than creating a second payment.",
+ "currency": "usd",
+ "merchant_name": "Example Store",
+ "merchant_url": "https://store.example.com",
+ "payment_method_id": "pm_example",
"provider": "link",
- "wallet": "wallet",
+ "wallet": "link-wallet",
},
)
@@ -180,14 +180,14 @@ def test_streaming_response_update(self, client: Kernel) -> None:
key="x",
id_or_name="id_or_name",
spec={
- "amount": 1,
- "context": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
- "currency": "bFx",
- "merchant_name": "x",
- "merchant_url": "https://example.com",
- "payment_method_id": "x",
+ "amount": 3000,
+ "context": "The order total changed to USD 30.00 including shipping and taxes for one notebook. Update this unapproved request rather than creating a second payment.",
+ "currency": "usd",
+ "merchant_name": "Example Store",
+ "merchant_url": "https://store.example.com",
+ "payment_method_id": "pm_example",
"provider": "link",
- "wallet": "wallet",
+ "wallet": "link-wallet",
},
) as response:
assert not response.is_closed
@@ -206,14 +206,14 @@ def test_path_params_update(self, client: Kernel) -> None:
key="x",
id_or_name="",
spec={
- "amount": 1,
- "context": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
- "currency": "bFx",
- "merchant_name": "x",
- "merchant_url": "https://example.com",
- "payment_method_id": "x",
+ "amount": 3000,
+ "context": "The order total changed to USD 30.00 including shipping and taxes for one notebook. Update this unapproved request rather than creating a second payment.",
+ "currency": "usd",
+ "merchant_name": "Example Store",
+ "merchant_url": "https://store.example.com",
+ "payment_method_id": "pm_example",
"provider": "link",
- "wallet": "wallet",
+ "wallet": "link-wallet",
},
)
@@ -222,14 +222,14 @@ def test_path_params_update(self, client: Kernel) -> None:
key="",
id_or_name="id_or_name",
spec={
- "amount": 1,
- "context": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
- "currency": "bFx",
- "merchant_name": "x",
- "merchant_url": "https://example.com",
- "payment_method_id": "x",
+ "amount": 3000,
+ "context": "The order total changed to USD 30.00 including shipping and taxes for one notebook. Update this unapproved request rather than creating a second payment.",
+ "currency": "usd",
+ "merchant_name": "Example Store",
+ "merchant_url": "https://store.example.com",
+ "payment_method_id": "pm_example",
"provider": "link",
- "wallet": "wallet",
+ "wallet": "link-wallet",
},
)
@@ -787,14 +787,14 @@ async def test_method_update(self, async_client: AsyncKernel) -> None:
key="x",
id_or_name="id_or_name",
spec={
- "amount": 1,
- "context": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
- "currency": "bFx",
- "merchant_name": "x",
- "merchant_url": "https://example.com",
- "payment_method_id": "x",
+ "amount": 3000,
+ "context": "The order total changed to USD 30.00 including shipping and taxes for one notebook. Update this unapproved request rather than creating a second payment.",
+ "currency": "usd",
+ "merchant_name": "Example Store",
+ "merchant_url": "https://store.example.com",
+ "payment_method_id": "pm_example",
"provider": "link",
- "wallet": "wallet",
+ "wallet": "link-wallet",
},
)
assert_matches_type(VaultItem, item, path=["response"])
@@ -806,14 +806,14 @@ async def test_method_update_with_all_params(self, async_client: AsyncKernel) ->
key="x",
id_or_name="id_or_name",
spec={
- "amount": 1,
- "context": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
- "currency": "bFx",
- "merchant_name": "x",
- "merchant_url": "https://example.com",
- "payment_method_id": "x",
+ "amount": 3000,
+ "context": "The order total changed to USD 30.00 including shipping and taxes for one notebook. Update this unapproved request rather than creating a second payment.",
+ "currency": "usd",
+ "merchant_name": "Example Store",
+ "merchant_url": "https://store.example.com",
+ "payment_method_id": "pm_example",
"provider": "link",
- "wallet": "wallet",
+ "wallet": "link-wallet",
"expires_at": 0,
"line_items": [
{
@@ -853,14 +853,14 @@ async def test_raw_response_update(self, async_client: AsyncKernel) -> None:
key="x",
id_or_name="id_or_name",
spec={
- "amount": 1,
- "context": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
- "currency": "bFx",
- "merchant_name": "x",
- "merchant_url": "https://example.com",
- "payment_method_id": "x",
+ "amount": 3000,
+ "context": "The order total changed to USD 30.00 including shipping and taxes for one notebook. Update this unapproved request rather than creating a second payment.",
+ "currency": "usd",
+ "merchant_name": "Example Store",
+ "merchant_url": "https://store.example.com",
+ "payment_method_id": "pm_example",
"provider": "link",
- "wallet": "wallet",
+ "wallet": "link-wallet",
},
)
@@ -876,14 +876,14 @@ async def test_streaming_response_update(self, async_client: AsyncKernel) -> Non
key="x",
id_or_name="id_or_name",
spec={
- "amount": 1,
- "context": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
- "currency": "bFx",
- "merchant_name": "x",
- "merchant_url": "https://example.com",
- "payment_method_id": "x",
+ "amount": 3000,
+ "context": "The order total changed to USD 30.00 including shipping and taxes for one notebook. Update this unapproved request rather than creating a second payment.",
+ "currency": "usd",
+ "merchant_name": "Example Store",
+ "merchant_url": "https://store.example.com",
+ "payment_method_id": "pm_example",
"provider": "link",
- "wallet": "wallet",
+ "wallet": "link-wallet",
},
) as response:
assert not response.is_closed
@@ -902,14 +902,14 @@ async def test_path_params_update(self, async_client: AsyncKernel) -> None:
key="x",
id_or_name="",
spec={
- "amount": 1,
- "context": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
- "currency": "bFx",
- "merchant_name": "x",
- "merchant_url": "https://example.com",
- "payment_method_id": "x",
+ "amount": 3000,
+ "context": "The order total changed to USD 30.00 including shipping and taxes for one notebook. Update this unapproved request rather than creating a second payment.",
+ "currency": "usd",
+ "merchant_name": "Example Store",
+ "merchant_url": "https://store.example.com",
+ "payment_method_id": "pm_example",
"provider": "link",
- "wallet": "wallet",
+ "wallet": "link-wallet",
},
)
@@ -918,14 +918,14 @@ async def test_path_params_update(self, async_client: AsyncKernel) -> None:
key="",
id_or_name="id_or_name",
spec={
- "amount": 1,
- "context": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
- "currency": "bFx",
- "merchant_name": "x",
- "merchant_url": "https://example.com",
- "payment_method_id": "x",
+ "amount": 3000,
+ "context": "The order total changed to USD 30.00 including shipping and taxes for one notebook. Update this unapproved request rather than creating a second payment.",
+ "currency": "usd",
+ "merchant_name": "Example Store",
+ "merchant_url": "https://store.example.com",
+ "payment_method_id": "pm_example",
"provider": "link",
- "wallet": "wallet",
+ "wallet": "link-wallet",
},
)
From eadb334eb14d0adba4a53cb985866532ba03f64f Mon Sep 17 00:00:00 2001
From: "kernel-internal[bot]"
<260533166+kernel-internal[bot]@users.noreply.github.com>
Date: Fri, 11 Sep 2026 13:01:24 +0000
Subject: [PATCH 252/366] release: 0.101.0
---
.release-please-manifest.json | 2 +-
CHANGELOG.md | 16 ++++++++++++++++
pyproject.toml | 2 +-
src/kernel/_version.py | 2 +-
4 files changed, 19 insertions(+), 3 deletions(-)
diff --git a/.release-please-manifest.json b/.release-please-manifest.json
index 97d8dbba..aebbc8bc 100644
--- a/.release-please-manifest.json
+++ b/.release-please-manifest.json
@@ -1,3 +1,3 @@
{
- ".": "0.100.0"
+ ".": "0.101.0"
}
\ No newline at end of file
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 7a14e35a..77497eb7 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,21 @@
# Changelog
+## [0.101.0](https://github.com/kernel/kernel-python-sdk/compare/v0.100.0...v0.101.0) (2026-09-11)
+
+
+### Features
+
+* Add config analysis lifecycle guarantees ([e0d9d71](https://github.com/kernel/kernel-python-sdk/commit/e0d9d715689585224cde848845ebaed90c404f64))
+* Brand origin TLS timeouts and classify provider failures ([b00a694](https://github.com/kernel/kernel-python-sdk/commit/b00a6942ff9c831f624e87c12923ee109f2d6cf7))
+* Clarify proxy country defaults ([bb00cda](https://github.com/kernel/kernel-python-sdk/commit/bb00cdaabdeb569ed1bf46b538d15de8768b9686))
+* Expose vault access in organization entitlements ([1c3b388](https://github.com/kernel/kernel-python-sdk/commit/1c3b388f1a4597f65ed3926a8ec2646f5e0fc994))
+* Integrate configurable vault providers across API and checkout ([478bc09](https://github.com/kernel/kernel-python-sdk/commit/478bc09cc5c8f6b2850628d02b88d699cc35328f))
+* Limit free organizations to three vaults ([b0d5628](https://github.com/kernel/kernel-python-sdk/commit/b0d5628acbe0a9d9d006a3349ba1338fdc3d4402))
+* Populate safe failure reasons on invocation responses ([7162cbb](https://github.com/kernel/kernel-python-sdk/commit/7162cbbed49eb4d0a8d431bbb84680e48bdce8a2))
+* Return vendor guidance with config registry recommendations ([938e40b](https://github.com/kernel/kernel-python-sdk/commit/938e40b2a9f4e37ef8221787debbb948e019eeeb))
+* Revert vendor guidance recommendations ([adacc13](https://github.com/kernel/kernel-python-sdk/commit/adacc1321a9c1cc1624e5adf6c2521b0f09851f4))
+* Support international ISP proxy countries ([78adb8a](https://github.com/kernel/kernel-python-sdk/commit/78adb8a4ae929028497e30b0a5988050eee7c1f4))
+
## [0.100.0](https://github.com/kernel/kernel-python-sdk/compare/v0.99.0...v0.100.0) (2026-09-04)
diff --git a/pyproject.toml b/pyproject.toml
index 84718a85..ce24d358 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -1,6 +1,6 @@
[project]
name = "kernel"
-version = "0.100.0"
+version = "0.101.0"
description = "The official Python library for the kernel API"
dynamic = ["readme"]
license = "Apache-2.0"
diff --git a/src/kernel/_version.py b/src/kernel/_version.py
index 2fba39cd..058933d0 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.100.0" # x-release-please-version
+__version__ = "0.101.0" # x-release-please-version
From e7850fd5757934c731373c5cb09b4444a29d7e84 Mon Sep 17 00:00:00 2001
From: "kernel-internal[bot]"
<260533166+kernel-internal[bot]@users.noreply.github.com>
Date: Fri, 11 Sep 2026 14:49:18 +0000
Subject: [PATCH 253/366] feat: Document punctuation key sequences
Stainless-Generated-From: 16c95773a23ee13f801cdb8e6685186475f077a0
---
src/kernel/resources/browsers/computer.py | 8 ++++++--
src/kernel/types/browsers/computer_batch_params.py | 5 ++++-
src/kernel/types/browsers/computer_press_key_params.py | 5 ++++-
3 files changed, 14 insertions(+), 4 deletions(-)
diff --git a/src/kernel/resources/browsers/computer.py b/src/kernel/resources/browsers/computer.py
index dcb242c6..b40ac897 100644
--- a/src/kernel/resources/browsers/computer.py
+++ b/src/kernel/resources/browsers/computer.py
@@ -397,7 +397,9 @@ def press_key(
keys: List of key symbols to press. Each item should be a key symbol supported by
xdotool (see X11 keysym definitions). Examples include "Return", "Shift",
"Ctrl", "Alt", "F5". Items in this list could also be combinations, e.g.
- "Ctrl+t" or "Ctrl+Shift+Tab".
+ "Ctrl+t" or "Ctrl+Shift+Tab". Use X11 names for punctuation in combinations,
+ such as "Ctrl+minus" or "Ctrl+plus". A literal hyphen is also accepted as an
+ alias, so "Ctrl+-" is normalized to "Ctrl+minus".
duration: Duration to hold the keys down in milliseconds. If omitted or 0, keys are
tapped.
@@ -1002,7 +1004,9 @@ async def press_key(
keys: List of key symbols to press. Each item should be a key symbol supported by
xdotool (see X11 keysym definitions). Examples include "Return", "Shift",
"Ctrl", "Alt", "F5". Items in this list could also be combinations, e.g.
- "Ctrl+t" or "Ctrl+Shift+Tab".
+ "Ctrl+t" or "Ctrl+Shift+Tab". Use X11 names for punctuation in combinations,
+ such as "Ctrl+minus" or "Ctrl+plus". A literal hyphen is also accepted as an
+ alias, so "Ctrl+-" is normalized to "Ctrl+minus".
duration: Duration to hold the keys down in milliseconds. If omitted or 0, keys are
tapped.
diff --git a/src/kernel/types/browsers/computer_batch_params.py b/src/kernel/types/browsers/computer_batch_params.py
index 7fc6abb5..dbd91756 100644
--- a/src/kernel/types/browsers/computer_batch_params.py
+++ b/src/kernel/types/browsers/computer_batch_params.py
@@ -110,7 +110,10 @@ class ActionPressKey(TypedDict, total=False):
Each item should be a key symbol supported by xdotool (see X11 keysym
definitions). Examples include "Return", "Shift", "Ctrl", "Alt", "F5". Items in
- this list could also be combinations, e.g. "Ctrl+t" or "Ctrl+Shift+Tab".
+ this list could also be combinations, e.g. "Ctrl+t" or "Ctrl+Shift+Tab". Use X11
+ names for punctuation in combinations, such as "Ctrl+minus" or "Ctrl+plus". A
+ literal hyphen is also accepted as an alias, so "Ctrl+-" is normalized to
+ "Ctrl+minus".
"""
duration: int
diff --git a/src/kernel/types/browsers/computer_press_key_params.py b/src/kernel/types/browsers/computer_press_key_params.py
index ea2c9b45..8722e1b3 100644
--- a/src/kernel/types/browsers/computer_press_key_params.py
+++ b/src/kernel/types/browsers/computer_press_key_params.py
@@ -15,7 +15,10 @@ class ComputerPressKeyParams(TypedDict, total=False):
Each item should be a key symbol supported by xdotool (see X11 keysym
definitions). Examples include "Return", "Shift", "Ctrl", "Alt", "F5". Items in
- this list could also be combinations, e.g. "Ctrl+t" or "Ctrl+Shift+Tab".
+ this list could also be combinations, e.g. "Ctrl+t" or "Ctrl+Shift+Tab". Use X11
+ names for punctuation in combinations, such as "Ctrl+minus" or "Ctrl+plus". A
+ literal hyphen is also accepted as an alias, so "Ctrl+-" is normalized to
+ "Ctrl+minus".
"""
duration: int
From dfcc7ecbaa087a07d11a5b0eb4738b41b8fadd8b Mon Sep 17 00:00:00 2001
From: "kernel-internal[bot]"
<260533166+kernel-internal[bot]@users.noreply.github.com>
Date: Fri, 11 Sep 2026 18:07:01 +0000
Subject: [PATCH 254/366] feat: Reapply vendor guidance recommendations
Stainless-Generated-From: bac03320a3e21e2df40ca486c6fb34dd1054883a
---
src/kernel/types/config_registry_response.py | 8 ++++++++
src/kernel/types/lookup_response.py | 8 ++++++++
2 files changed, 16 insertions(+)
diff --git a/src/kernel/types/config_registry_response.py b/src/kernel/types/config_registry_response.py
index 9995173f..eead2467 100644
--- a/src/kernel/types/config_registry_response.py
+++ b/src/kernel/types/config_registry_response.py
@@ -21,3 +21,11 @@ class ConfigRegistryResponse(BaseModel):
"""A recommendation or a structured no-recommendation result."""
target: Target
+
+ guidance: Optional[str] = None
+ """Short advisory markdown to facilitate navigating this target.
+
+ Returned even when no configuration reached the target, since knowing what
+ prevented success is useful without a configuration. Not verified against this
+ target. Null when nothing applicable was observed or no notes exist.
+ """
diff --git a/src/kernel/types/lookup_response.py b/src/kernel/types/lookup_response.py
index cf0f5c16..b961e9c7 100644
--- a/src/kernel/types/lookup_response.py
+++ b/src/kernel/types/lookup_response.py
@@ -13,3 +13,11 @@ class LookupResponse(BaseModel):
recommendation: Optional[Recommendation] = None
target: Target
+
+ guidance: Optional[str] = None
+ """Short advisory markdown to facilitate navigating this target.
+
+ Returned even when no configuration reached the target, since knowing what
+ prevented success is useful without a configuration. Not verified against this
+ target. Null when nothing applicable was observed or no notes exist.
+ """
From 22a87318e6ac2f1318a295b9d22de5b2306e66b2 Mon Sep 17 00:00:00 2001
From: "kernel-internal[bot]"
<260533166+kernel-internal[bot]@users.noreply.github.com>
Date: Fri, 11 Sep 2026 18:48:43 +0000
Subject: [PATCH 255/366] feat: Allow replacing AgentCard cards after unknown
creates
Stainless-Generated-From: 67f35ca72de9ae5a9602554c8edc58fa688db472
---
src/kernel/resources/vaults/items.py | 16 ++++++++++------
src/kernel/types/vaults/card_vault_item_state.py | 8 +++++---
2 files changed, 15 insertions(+), 9 deletions(-)
diff --git a/src/kernel/resources/vaults/items.py b/src/kernel/resources/vaults/items.py
index dd790910..ac45401a 100644
--- a/src/kernel/resources/vaults/items.py
+++ b/src/kernel/resources/vaults/items.py
@@ -209,9 +209,11 @@ def delete(
timeout: float | httpx.Timeout | None | NotGiven = not_given,
) -> None:
"""
- Unresolved payment operations block deletion, including operations on child
- cards of a wallet. Reconcile the original attempt with the provider or support
- first; deleting or recreating an item is not proof that a payment did not occur.
+ Unresolved payment operations normally block deletion, including operations on
+ child cards of a wallet. An AgentCard checkout whose create response returned no
+ authorization ID may be explicitly abandoned by deleting that card directly;
+ deleting its wallet or vault remains blocked. Deleting or recreating an item is
+ not proof that a payment did not occur.
Args:
extra_headers: Send extra headers
@@ -625,9 +627,11 @@ async def delete(
timeout: float | httpx.Timeout | None | NotGiven = not_given,
) -> None:
"""
- Unresolved payment operations block deletion, including operations on child
- cards of a wallet. Reconcile the original attempt with the provider or support
- first; deleting or recreating an item is not proof that a payment did not occur.
+ Unresolved payment operations normally block deletion, including operations on
+ child cards of a wallet. An AgentCard checkout whose create response returned no
+ authorization ID may be explicitly abandoned by deleting that card directly;
+ deleting its wallet or vault remains blocked. Deleting or recreating an item is
+ not proof that a payment did not occur.
Args:
extra_headers: Send extra headers
diff --git a/src/kernel/types/vaults/card_vault_item_state.py b/src/kernel/types/vaults/card_vault_item_state.py
index 7bb7e274..c64f2ab7 100644
--- a/src/kernel/types/vaults/card_vault_item_state.py
+++ b/src/kernel/types/vaults/card_vault_item_state.py
@@ -79,9 +79,11 @@ class AgentCardCardState(BaseModel):
status: Literal["requested", "ready", "pending_approval", "degraded", "recovery_required"]
"""recovery_required means the original checkout outcome is unresolved.
- Do not retry, delete, or replace it. Known authorization IDs may be reconciled
- through provider observations; otherwise contact the provider or support for
- manual reconciliation. It does not mean declined or expired.
+ Automatic reuse is blocked. Known authorization IDs must be reconciled through
+ provider observations or support. When no authorization ID was returned, an
+ explicitly confirmed item deletion may abandon the unresolved attempt so the
+ caller can create a replacement; deletion does not prove that the original
+ attempt failed. It does not mean declined or expired.
"""
aliases: Optional[VaultCardAliases] = None
From c276ebc6fd266924894b300b13ff02f30d0c8527 Mon Sep 17 00:00:00 2001
From: "kernel-internal[bot]"
<260533166+kernel-internal[bot]@users.noreply.github.com>
Date: Fri, 11 Sep 2026 21:18:42 +0000
Subject: [PATCH 256/366] feat: Report proxy-restricted targets from config
registry lookup
Stainless-Generated-From: 8a678485a2950c5630b02de56a8c5b3aa4b0de98
---
src/kernel/types/lookup_response.py | 5 +++--
1 file changed, 3 insertions(+), 2 deletions(-)
diff --git a/src/kernel/types/lookup_response.py b/src/kernel/types/lookup_response.py
index b961e9c7..d717b8cf 100644
--- a/src/kernel/types/lookup_response.py
+++ b/src/kernel/types/lookup_response.py
@@ -4,13 +4,14 @@
from .target import Target
from .._models import BaseModel
-from .recommendation import Recommendation
+from .recommendation_result import RecommendationResult
__all__ = ["LookupResponse"]
class LookupResponse(BaseModel):
- recommendation: Optional[Recommendation] = None
+ recommendation: Optional[RecommendationResult] = None
+ """A recommendation or a structured no-recommendation result."""
target: Target
From 3927c961789b4427a72c979ee5bb3dbcae1b8c74 Mon Sep 17 00:00:00 2001
From: "kernel-internal[bot]"
<260533166+kernel-internal[bot]@users.noreply.github.com>
Date: Sun, 13 Sep 2026 22:46:11 +0000
Subject: [PATCH 257/366] feat: Add guarded vault card fill operations
Stainless-Generated-From: 8e37c63cadbed622dfdf688b8e94ae2e5116344e
---
api.md | 7 +-
src/kernel/resources/vaults/items.py | 230 +++++++++-
src/kernel/types/vaults/__init__.py | 7 +
...fill_vault_item_operation_request_param.py | 61 +++
.../fill_vault_item_operation_result.py | 27 ++
.../vaults/item_perform_operation_params.py | 34 +-
.../vaults/vault_card_fill_field_param.py | 60 +++
.../types/vaults/vault_fill_field_result.py | 35 ++
src/kernel/types/vaults/vault_item.py | 4 +-
.../vaults/vault_item_operation_response.py | 139 ++++++
tests/api_resources/vaults/test_items.py | 405 +++++++++++++++++-
11 files changed, 970 insertions(+), 39 deletions(-)
create mode 100644 src/kernel/types/vaults/fill_vault_item_operation_request_param.py
create mode 100644 src/kernel/types/vaults/fill_vault_item_operation_result.py
create mode 100644 src/kernel/types/vaults/vault_card_fill_field_param.py
create mode 100644 src/kernel/types/vaults/vault_fill_field_result.py
create mode 100644 src/kernel/types/vaults/vault_item_operation_response.py
diff --git a/api.md b/api.md
index 63957ab1..c00bf223 100644
--- a/api.md
+++ b/api.md
@@ -527,10 +527,15 @@ from kernel.types.vaults import (
AgentcardCheckoutAuthorization,
CardVaultItemSpec,
CardVaultItemState,
+ FillVaultItemOperationRequest,
+ FillVaultItemOperationResult,
VaultCardAliases,
+ VaultCardFillField,
+ VaultFillFieldResult,
VaultItem,
VaultItemAction,
VaultItemEvent,
+ VaultItemOperationResponse,
VaultPaymentMethod,
WalletVaultItemSpec,
WalletVaultItemState,
@@ -546,7 +551,7 @@ Methods:
- client.vaults.items.list(id_or_name) -> ItemListResponse
- client.vaults.items.delete(key, \*, id_or_name) -> None
- client.vaults.items.events(key, \*, id_or_name, \*\*params) -> ItemEventsResponse
-- client.vaults.items.perform_operation(key, \*, id_or_name, \*\*params) -> VaultItem
+- client.vaults.items.perform_operation(key, \*, id_or_name, \*\*params) -> VaultItemOperationResponse
- client.vaults.items.upsert(key, \*, id_or_name, \*\*params) -> VaultItem
# Credentials
diff --git a/src/kernel/resources/vaults/items.py b/src/kernel/resources/vaults/items.py
index ac45401a..309c6170 100644
--- a/src/kernel/resources/vaults/items.py
+++ b/src/kernel/resources/vaults/items.py
@@ -2,7 +2,7 @@
from __future__ import annotations
-from typing import Any, List, cast
+from typing import Any, List, Iterable, cast
from typing_extensions import Literal, overload
import httpx
@@ -29,6 +29,8 @@
from ...types.vaults.item_list_response import ItemListResponse
from ...types.vaults.item_events_response import ItemEventsResponse
from ...types.vaults.card_vault_item_spec_param import CardVaultItemSpecParam
+from ...types.vaults.vault_card_fill_field_param import VaultCardFillFieldParam
+from ...types.vaults.vault_item_operation_response import VaultItemOperationResponse
__all__ = ["ItemsResource", "AsyncItemsResource"]
@@ -289,6 +291,7 @@ def events(
cast_to=ItemEventsResponse,
)
+ @overload
def perform_operation(
self,
key: str,
@@ -301,17 +304,81 @@ def perform_operation(
extra_query: Query | None = None,
extra_body: Body | None = None,
timeout: float | httpx.Timeout | None | NotGiven = not_given,
- ) -> VaultItem:
+ ) -> VaultItemOperationResponse:
+ """
+ Retrieve the item first and invoke only an operation listed in
+ `available_operations`, following its natural-language description. Availability
+ is rechecked at execution time; unavailable operations return 409. Authorization
+ may call an external provider and returns the updated item. Link cards advertise
+ authorize when eligible. AgentCard cards are created with PUT and request
+ approval when their aliases are used at checkout; they do not expose authorize.
+ If spend-request creation is rate limited, returns HTTP 429 with code
+ `spend_request_rate_limited`; stop and back off before retrying.
+
+ Fill returns a value-free execution result. Validation failures before writing
+ return 400 (invalid request or targets), 403 (access or destination denied), 404
+ (resource not found), or 409 (item or browser not ready). Once writing starts,
+ known partial failures and indeterminate field outcomes return 200 with status
+ `failed` or `unknown`, not an automatic-retry signal. A transport error may
+ leave the outcome unknown; do not automatically retry.
+
+ Args:
+ extra_headers: Send extra headers
+
+ extra_query: Add additional query parameters to the request
+
+ extra_body: Add additional JSON properties to the request
+
+ timeout: Override the client-level default timeout for this request, in seconds
+ """
+ ...
+
+ @overload
+ def perform_operation(
+ self,
+ key: str,
+ *,
+ id_or_name: str,
+ browser_id: str,
+ fields: Iterable[VaultCardFillFieldParam],
+ page_url: str,
+ type: Literal["fill"],
+ timeout_ms: int | Omit = omit,
+ # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
+ # The extra values given here take precedence over values defined on the client or passed to this method.
+ extra_headers: Headers | None = None,
+ extra_query: Query | None = None,
+ extra_body: Body | None = None,
+ timeout: float | httpx.Timeout | None | NotGiven = not_given,
+ ) -> VaultItemOperationResponse:
"""
Retrieve the item first and invoke only an operation listed in
- `available_operations`, following its natural-language description. Operations
- may call an external provider and return updated state. Link cards advertise
- authorize. AgentCard cards are created with PUT and request approval when their
- aliases are used at checkout; they do not expose this operation. If
- spend-request creation is rate limited, returns HTTP 429 with code
+ `available_operations`, following its natural-language description. Availability
+ is rechecked at execution time; unavailable operations return 409. Authorization
+ may call an external provider and returns the updated item. Link cards advertise
+ authorize when eligible. AgentCard cards are created with PUT and request
+ approval when their aliases are used at checkout; they do not expose authorize.
+ If spend-request creation is rate limited, returns HTTP 429 with code
`spend_request_rate_limited`; stop and back off before retrying.
+ Fill returns a value-free execution result. Validation failures before writing
+ return 400 (invalid request or targets), 403 (access or destination denied), 404
+ (resource not found), or 409 (item or browser not ready). Once writing starts,
+ known partial failures and indeterminate field outcomes return 200 with status
+ `failed` or `unknown`, not an automatic-retry signal. A transport error may
+ leave the outcome unknown; do not automatically retry.
+
Args:
+ browser_id: Browser session ID, not a reusable browser name.
+
+ fields: Field bindings for this step. No two bindings may resolve to the same element.
+
+ page_url: Exact current top-level page URL, including path, query, and fragment. Must
+ match exactly one open page in the browser; zero or multiple matches fail. No
+ prefix or glob matching. Must use HTTPS without embedded credentials.
+
+ timeout_ms: Total operation deadline in milliseconds, not a per-field timeout.
+
extra_headers: Send extra headers
extra_query: Add additional query parameters to the request
@@ -320,19 +387,50 @@ def perform_operation(
timeout: Override the client-level default timeout for this request, in seconds
"""
+ ...
+
+ @required_args(["id_or_name", "type"], ["id_or_name", "browser_id", "fields", "page_url", "type"])
+ def perform_operation(
+ self,
+ key: str,
+ *,
+ id_or_name: str,
+ type: Literal["authorize"] | Literal["fill"],
+ browser_id: str | Omit = omit,
+ fields: Iterable[VaultCardFillFieldParam] | Omit = omit,
+ page_url: str | Omit = omit,
+ timeout_ms: int | Omit = omit,
+ # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
+ # The extra values given here take precedence over values defined on the client or passed to this method.
+ extra_headers: Headers | None = None,
+ extra_query: Query | None = None,
+ extra_body: Body | None = None,
+ timeout: float | httpx.Timeout | None | NotGiven = not_given,
+ ) -> VaultItemOperationResponse:
if not id_or_name:
raise ValueError(f"Expected a non-empty value for `id_or_name` but received {id_or_name!r}")
if not key:
raise ValueError(f"Expected a non-empty value for `key` but received {key!r}")
return cast(
- VaultItem,
+ VaultItemOperationResponse,
self._post(
path_template("/vaults/{id_or_name}/items/{key}/operations", id_or_name=id_or_name, key=key),
- body=maybe_transform({"type": type}, item_perform_operation_params.ItemPerformOperationParams),
+ body=maybe_transform(
+ {
+ "type": type,
+ "browser_id": browser_id,
+ "fields": fields,
+ "page_url": page_url,
+ "timeout_ms": timeout_ms,
+ },
+ item_perform_operation_params.ItemPerformOperationParams,
+ ),
options=make_request_options(
extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
),
- cast_to=cast(Any, VaultItem), # Union types cannot be passed in as arguments in the type system
+ cast_to=cast(
+ Any, VaultItemOperationResponse
+ ), # Union types cannot be passed in as arguments in the type system
),
)
@@ -707,6 +805,7 @@ async def events(
cast_to=ItemEventsResponse,
)
+ @overload
async def perform_operation(
self,
key: str,
@@ -719,17 +818,81 @@ async def perform_operation(
extra_query: Query | None = None,
extra_body: Body | None = None,
timeout: float | httpx.Timeout | None | NotGiven = not_given,
- ) -> VaultItem:
+ ) -> VaultItemOperationResponse:
+ """
+ Retrieve the item first and invoke only an operation listed in
+ `available_operations`, following its natural-language description. Availability
+ is rechecked at execution time; unavailable operations return 409. Authorization
+ may call an external provider and returns the updated item. Link cards advertise
+ authorize when eligible. AgentCard cards are created with PUT and request
+ approval when their aliases are used at checkout; they do not expose authorize.
+ If spend-request creation is rate limited, returns HTTP 429 with code
+ `spend_request_rate_limited`; stop and back off before retrying.
+
+ Fill returns a value-free execution result. Validation failures before writing
+ return 400 (invalid request or targets), 403 (access or destination denied), 404
+ (resource not found), or 409 (item or browser not ready). Once writing starts,
+ known partial failures and indeterminate field outcomes return 200 with status
+ `failed` or `unknown`, not an automatic-retry signal. A transport error may
+ leave the outcome unknown; do not automatically retry.
+
+ Args:
+ extra_headers: Send extra headers
+
+ extra_query: Add additional query parameters to the request
+
+ extra_body: Add additional JSON properties to the request
+
+ timeout: Override the client-level default timeout for this request, in seconds
+ """
+ ...
+
+ @overload
+ async def perform_operation(
+ self,
+ key: str,
+ *,
+ id_or_name: str,
+ browser_id: str,
+ fields: Iterable[VaultCardFillFieldParam],
+ page_url: str,
+ type: Literal["fill"],
+ timeout_ms: int | Omit = omit,
+ # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
+ # The extra values given here take precedence over values defined on the client or passed to this method.
+ extra_headers: Headers | None = None,
+ extra_query: Query | None = None,
+ extra_body: Body | None = None,
+ timeout: float | httpx.Timeout | None | NotGiven = not_given,
+ ) -> VaultItemOperationResponse:
"""
Retrieve the item first and invoke only an operation listed in
- `available_operations`, following its natural-language description. Operations
- may call an external provider and return updated state. Link cards advertise
- authorize. AgentCard cards are created with PUT and request approval when their
- aliases are used at checkout; they do not expose this operation. If
- spend-request creation is rate limited, returns HTTP 429 with code
+ `available_operations`, following its natural-language description. Availability
+ is rechecked at execution time; unavailable operations return 409. Authorization
+ may call an external provider and returns the updated item. Link cards advertise
+ authorize when eligible. AgentCard cards are created with PUT and request
+ approval when their aliases are used at checkout; they do not expose authorize.
+ If spend-request creation is rate limited, returns HTTP 429 with code
`spend_request_rate_limited`; stop and back off before retrying.
+ Fill returns a value-free execution result. Validation failures before writing
+ return 400 (invalid request or targets), 403 (access or destination denied), 404
+ (resource not found), or 409 (item or browser not ready). Once writing starts,
+ known partial failures and indeterminate field outcomes return 200 with status
+ `failed` or `unknown`, not an automatic-retry signal. A transport error may
+ leave the outcome unknown; do not automatically retry.
+
Args:
+ browser_id: Browser session ID, not a reusable browser name.
+
+ fields: Field bindings for this step. No two bindings may resolve to the same element.
+
+ page_url: Exact current top-level page URL, including path, query, and fragment. Must
+ match exactly one open page in the browser; zero or multiple matches fail. No
+ prefix or glob matching. Must use HTTPS without embedded credentials.
+
+ timeout_ms: Total operation deadline in milliseconds, not a per-field timeout.
+
extra_headers: Send extra headers
extra_query: Add additional query parameters to the request
@@ -738,21 +901,50 @@ async def perform_operation(
timeout: Override the client-level default timeout for this request, in seconds
"""
+ ...
+
+ @required_args(["id_or_name", "type"], ["id_or_name", "browser_id", "fields", "page_url", "type"])
+ async def perform_operation(
+ self,
+ key: str,
+ *,
+ id_or_name: str,
+ type: Literal["authorize"] | Literal["fill"],
+ browser_id: str | Omit = omit,
+ fields: Iterable[VaultCardFillFieldParam] | Omit = omit,
+ page_url: str | Omit = omit,
+ timeout_ms: int | Omit = omit,
+ # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
+ # The extra values given here take precedence over values defined on the client or passed to this method.
+ extra_headers: Headers | None = None,
+ extra_query: Query | None = None,
+ extra_body: Body | None = None,
+ timeout: float | httpx.Timeout | None | NotGiven = not_given,
+ ) -> VaultItemOperationResponse:
if not id_or_name:
raise ValueError(f"Expected a non-empty value for `id_or_name` but received {id_or_name!r}")
if not key:
raise ValueError(f"Expected a non-empty value for `key` but received {key!r}")
return cast(
- VaultItem,
+ VaultItemOperationResponse,
await self._post(
path_template("/vaults/{id_or_name}/items/{key}/operations", id_or_name=id_or_name, key=key),
body=await async_maybe_transform(
- {"type": type}, item_perform_operation_params.ItemPerformOperationParams
+ {
+ "type": type,
+ "browser_id": browser_id,
+ "fields": fields,
+ "page_url": page_url,
+ "timeout_ms": timeout_ms,
+ },
+ item_perform_operation_params.ItemPerformOperationParams,
),
options=make_request_options(
extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
),
- cast_to=cast(Any, VaultItem), # Union types cannot be passed in as arguments in the type system
+ cast_to=cast(
+ Any, VaultItemOperationResponse
+ ), # Union types cannot be passed in as arguments in the type system
),
)
diff --git a/src/kernel/types/vaults/__init__.py b/src/kernel/types/vaults/__init__.py
index 623e45d4..0f71f0c3 100644
--- a/src/kernel/types/vaults/__init__.py
+++ b/src/kernel/types/vaults/__init__.py
@@ -16,7 +16,14 @@
from .vault_payment_method import VaultPaymentMethod as VaultPaymentMethod
from .card_vault_item_state import CardVaultItemState as CardVaultItemState
from .wallet_vault_item_spec import WalletVaultItemSpec as WalletVaultItemSpec
+from .vault_fill_field_result import VaultFillFieldResult as VaultFillFieldResult
from .wallet_vault_item_state import WalletVaultItemState as WalletVaultItemState
from .card_vault_item_spec_param import CardVaultItemSpecParam as CardVaultItemSpecParam
+from .vault_card_fill_field_param import VaultCardFillFieldParam as VaultCardFillFieldParam
from .item_perform_operation_params import ItemPerformOperationParams as ItemPerformOperationParams
+from .vault_item_operation_response import VaultItemOperationResponse as VaultItemOperationResponse
from .agentcard_checkout_authorization import AgentcardCheckoutAuthorization as AgentcardCheckoutAuthorization
+from .fill_vault_item_operation_result import FillVaultItemOperationResult as FillVaultItemOperationResult
+from .fill_vault_item_operation_request_param import (
+ FillVaultItemOperationRequestParam as FillVaultItemOperationRequestParam,
+)
diff --git a/src/kernel/types/vaults/fill_vault_item_operation_request_param.py b/src/kernel/types/vaults/fill_vault_item_operation_request_param.py
new file mode 100644
index 00000000..a4613cd6
--- /dev/null
+++ b/src/kernel/types/vaults/fill_vault_item_operation_request_param.py
@@ -0,0 +1,61 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from __future__ import annotations
+
+from typing import Iterable
+from typing_extensions import Literal, Required, TypedDict
+
+from .vault_card_fill_field_param import VaultCardFillFieldParam
+
+__all__ = ["FillVaultItemOperationRequestParam"]
+
+
+class FillVaultItemOperationRequestParam(TypedDict, total=False):
+ """
+ Fill selected fields from one ready, unexpired card into a browser linked
+ to its vault. Only supported for card items created from Link wallets.
+ Only invoke when the item advertises `fill`. Browser and vault must belong
+ to the same project. Kernel checks access and allowed destinations before
+ filling; providing a page URL does not authorize a destination.
+
+ Find exactly one open page matching `page_url`. For each selector, search
+ the main frame and all descendant frames for editable inputs or selects
+ matched directly or contained within matching elements. Each selector must
+ resolve to one unique editable element across all frames; zero or multiple
+ candidates fail. Count each element once, even if multiple matching
+ containers contain it. Validate all bindings before filling.
+ Select elements match an option by its value, not its label.
+ If the page navigates or a target disappears during filling, stop rather
+ than selecting a different page or element.
+
+ Fill in request order and stop on the first failure. This operation is
+ not atomic: previously filled fields are not rolled back. Never submit
+ the form or click buttons, though input/change events may trigger site
+ behavior. Fill is the preferred browser-checkout path. Aliases remain an
+ alternative for explicitly chosen egress-substitution integrations. Do not
+ automatically retry or fall back to aliases after a failed or indeterminate
+ operation.
+
+ Secret values are never returned or included in operation logs, traces,
+ audit events, or error details. This does not prevent an agent with
+ unrestricted browser access from reading values from the page or other
+ browser observation surfaces.
+ """
+
+ browser_id: Required[str]
+ """Browser session ID, not a reusable browser name."""
+
+ fields: Required[Iterable[VaultCardFillFieldParam]]
+ """Field bindings for this step. No two bindings may resolve to the same element."""
+
+ page_url: Required[str]
+ """Exact current top-level page URL, including path, query, and fragment.
+
+ Must match exactly one open page in the browser; zero or multiple matches fail.
+ No prefix or glob matching. Must use HTTPS without embedded credentials.
+ """
+
+ type: Required[Literal["fill"]]
+
+ timeout_ms: int
+ """Total operation deadline in milliseconds, not a per-field timeout."""
diff --git a/src/kernel/types/vaults/fill_vault_item_operation_result.py b/src/kernel/types/vaults/fill_vault_item_operation_result.py
new file mode 100644
index 00000000..0f3f1855
--- /dev/null
+++ b/src/kernel/types/vaults/fill_vault_item_operation_result.py
@@ -0,0 +1,27 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from typing import List
+from typing_extensions import Literal
+
+from ..._models import BaseModel
+from .vault_fill_field_result import VaultFillFieldResult
+
+__all__ = ["FillVaultItemOperationResult"]
+
+
+class FillVaultItemOperationResult(BaseModel):
+ fields: List[VaultFillFieldResult]
+ """Exactly one result per request binding, in request order.
+
+ After the first failed or unknown field, all remaining fields are not_attempted.
+ """
+
+ status: Literal["completed", "failed", "unknown"]
+ """Completed only when all fields were filled.
+
+ Failed when execution stopped with known outcomes. Unknown when any field's
+ outcome cannot be determined. None of these statuses confirms payment or
+ merchant acceptance.
+ """
+
+ type: Literal["fill"]
diff --git a/src/kernel/types/vaults/item_perform_operation_params.py b/src/kernel/types/vaults/item_perform_operation_params.py
index 9d62f380..4a5e4361 100644
--- a/src/kernel/types/vaults/item_perform_operation_params.py
+++ b/src/kernel/types/vaults/item_perform_operation_params.py
@@ -2,12 +2,40 @@
from __future__ import annotations
-from typing_extensions import Literal, Required, TypedDict
+from typing import Union, Iterable
+from typing_extensions import Literal, Required, TypeAlias, TypedDict
-__all__ = ["ItemPerformOperationParams"]
+from .vault_card_fill_field_param import VaultCardFillFieldParam
+__all__ = ["ItemPerformOperationParams", "AuthorizeVaultItemOperationRequest", "FillVaultItemOperationRequest"]
-class ItemPerformOperationParams(TypedDict, total=False):
+
+class AuthorizeVaultItemOperationRequest(TypedDict, total=False):
id_or_name: Required[str]
type: Required[Literal["authorize"]]
+
+
+class FillVaultItemOperationRequest(TypedDict, total=False):
+ id_or_name: Required[str]
+
+ browser_id: Required[str]
+ """Browser session ID, not a reusable browser name."""
+
+ fields: Required[Iterable[VaultCardFillFieldParam]]
+ """Field bindings for this step. No two bindings may resolve to the same element."""
+
+ page_url: Required[str]
+ """Exact current top-level page URL, including path, query, and fragment.
+
+ Must match exactly one open page in the browser; zero or multiple matches fail.
+ No prefix or glob matching. Must use HTTPS without embedded credentials.
+ """
+
+ type: Required[Literal["fill"]]
+
+ timeout_ms: int
+ """Total operation deadline in milliseconds, not a per-field timeout."""
+
+
+ItemPerformOperationParams: TypeAlias = Union[AuthorizeVaultItemOperationRequest, FillVaultItemOperationRequest]
diff --git a/src/kernel/types/vaults/vault_card_fill_field_param.py b/src/kernel/types/vaults/vault_card_fill_field_param.py
new file mode 100644
index 00000000..e286291e
--- /dev/null
+++ b/src/kernel/types/vaults/vault_card_fill_field_param.py
@@ -0,0 +1,60 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from __future__ import annotations
+
+from typing import Union
+from typing_extensions import Literal, Required, TypeAlias, TypedDict
+
+__all__ = ["VaultCardFillFieldParam", "VaultCardStoredFillField", "VaultCardExpirationFillField"]
+
+
+class VaultCardStoredFillField(TypedDict, total=False):
+ field: Required[
+ Literal[
+ "number",
+ "exp_month",
+ "exp_year",
+ "cvc",
+ "billing_name",
+ "billing_line1",
+ "billing_line2",
+ "billing_city",
+ "billing_state",
+ "billing_postal_code",
+ "billing_country",
+ ]
+ ]
+ """Field in the decrypted card, not an alias.
+
+ Number and CVC preserve leading zeros; month uses two digits and year uses four
+ digits. Billing fields use the provider's stored billing address (name, line1,
+ line2, city, state, postal_code, country) without reformatting. Request only
+ needed billing fields. An absent or empty requested billing field returns 400
+ field_unavailable before any browser writes; it does not make other card fields
+ unavailable.
+ """
+
+ selector: Required[str]
+ """CSS selector for an editable input or select, or a containing element.
+
+ Must resolve to one unique editable element across all page frames.
+ """
+
+
+class VaultCardExpirationFillField(TypedDict, total=False):
+ """
+ Combined expiration derived from the stored month and year; not a separate stored secret.
+ """
+
+ field: Required[Literal["expiration"]]
+
+ format: Required[Literal["MM/YY", "MM/YYYY"]]
+
+ selector: Required[str]
+ """CSS selector for an editable input or select, or a containing element.
+
+ Must resolve to one unique editable element across all page frames.
+ """
+
+
+VaultCardFillFieldParam: TypeAlias = Union[VaultCardStoredFillField, VaultCardExpirationFillField]
diff --git a/src/kernel/types/vaults/vault_fill_field_result.py b/src/kernel/types/vaults/vault_fill_field_result.py
new file mode 100644
index 00000000..757b4d43
--- /dev/null
+++ b/src/kernel/types/vaults/vault_fill_field_result.py
@@ -0,0 +1,35 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from typing import Optional
+from typing_extensions import Literal
+
+from ..._models import BaseModel
+
+__all__ = ["VaultFillFieldResult"]
+
+
+class VaultFillFieldResult(BaseModel):
+ index: int
+ """Zero-based index into the request fields array."""
+
+ status: Literal["filled", "failed", "not_attempted", "unknown"]
+ """
+ Filled means the fill action completed, not that the website retained or
+ accepted the value.
+ """
+
+ error_code: Optional[
+ Literal[
+ "target_changed",
+ "element_not_found",
+ "ambiguous_selector",
+ "element_not_editable",
+ "option_not_found",
+ "timeout",
+ "execution_failed",
+ ]
+ ] = None
+ """Present only for failed or unknown fields.
+
+ Never includes secret values, DOM content, or raw browser errors.
+ """
diff --git a/src/kernel/types/vaults/vault_item.py b/src/kernel/types/vaults/vault_item.py
index 2cc7ab26..e6ca1561 100644
--- a/src/kernel/types/vaults/vault_item.py
+++ b/src/kernel/types/vaults/vault_item.py
@@ -43,7 +43,7 @@ class WalletVaultItemAvailableOperation(BaseModel):
description: str
- type: Literal["authorize"]
+ type: Literal["authorize", "fill"]
class WalletVaultItemExpanded(BaseModel):
@@ -107,7 +107,7 @@ class CardVaultItemAvailableOperation(BaseModel):
description: str
- type: Literal["authorize"]
+ type: Literal["authorize", "fill"]
class CardVaultItem(BaseModel):
diff --git a/src/kernel/types/vaults/vault_item_operation_response.py b/src/kernel/types/vaults/vault_item_operation_response.py
new file mode 100644
index 00000000..73502ccb
--- /dev/null
+++ b/src/kernel/types/vaults/vault_item_operation_response.py
@@ -0,0 +1,139 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from typing import List, Union, Optional
+from datetime import datetime
+from typing_extensions import Literal, TypeAlias
+
+from ..._models import BaseModel
+from .vault_item_action import VaultItemAction
+from .card_vault_item_spec import CardVaultItemSpec
+from .vault_payment_method import VaultPaymentMethod
+from .card_vault_item_state import CardVaultItemState
+from .wallet_vault_item_spec import WalletVaultItemSpec
+from .wallet_vault_item_state import WalletVaultItemState
+from .fill_vault_item_operation_result import FillVaultItemOperationResult
+
+__all__ = [
+ "VaultItemOperationResponse",
+ "WalletVaultItem",
+ "WalletVaultItemAvailableExpansion",
+ "WalletVaultItemAvailableOperation",
+ "WalletVaultItemExpanded",
+ "CardVaultItem",
+ "CardVaultItemAvailableExpansion",
+ "CardVaultItemAvailableOperation",
+]
+
+
+class WalletVaultItemAvailableExpansion(BaseModel):
+ """
+ Live data that can currently be requested by passing its type to the item GET expand parameter.
+ """
+
+ description: str
+
+ type: Literal["payment_methods"]
+
+
+class WalletVaultItemAvailableOperation(BaseModel):
+ """An operation that is currently valid for this item.
+
+ Read the description before invoking it through the item operations endpoint.
+ """
+
+ description: str
+
+ type: Literal["authorize", "fill"]
+
+
+class WalletVaultItemExpanded(BaseModel):
+ """Live, non-persisted data requested through the item GET expand parameter."""
+
+ payment_methods: Optional[List[VaultPaymentMethod]] = None
+
+
+class WalletVaultItem(BaseModel):
+ id: str
+
+ available_expansions: List[WalletVaultItemAvailableExpansion]
+
+ available_operations: List[WalletVaultItemAvailableOperation]
+
+ created_at: datetime
+
+ key: str
+ """Immutable item key assigned when the item is created."""
+
+ spec: WalletVaultItemSpec
+ """AgentCard wallet.
+
+ Omit provider_config to use Kernel-managed credentials, or select a
+ customer-owned configuration. Mode (sandbox vs live) is determined by the
+ selected credential; there is no per-item test flag. Without user_id, creation
+ returns a hosted enrollment action and Kernel polls until the user connects.
+ user_id may only reference a user already enrolled by a wallet in this
+ organization under the same configuration.
+ """
+
+ state: WalletVaultItemState
+
+ type: Literal["wallet"]
+
+ updated_at: datetime
+
+ action: Optional[VaultItemAction] = None
+
+ expanded: Optional[WalletVaultItemExpanded] = None
+ """Live, non-persisted data requested through the item GET expand parameter."""
+
+ expires_at: Optional[datetime] = None
+
+
+class CardVaultItemAvailableExpansion(BaseModel):
+ """
+ Live data that can currently be requested by passing its type to the item GET expand parameter.
+ """
+
+ description: str
+
+ type: Literal["payment_methods"]
+
+
+class CardVaultItemAvailableOperation(BaseModel):
+ """An operation that is currently valid for this item.
+
+ Read the description before invoking it through the item operations endpoint.
+ """
+
+ description: str
+
+ type: Literal["authorize", "fill"]
+
+
+class CardVaultItem(BaseModel):
+ id: str
+
+ available_expansions: List[CardVaultItemAvailableExpansion]
+
+ available_operations: List[CardVaultItemAvailableOperation]
+
+ created_at: datetime
+
+ key: str
+ """Immutable item key assigned when the item is created."""
+
+ spec: CardVaultItemSpec
+ """Live payment card. Test-mode card creation is not supported."""
+
+ state: CardVaultItemState
+
+ type: Literal["card"]
+
+ updated_at: datetime
+
+ action: Optional[VaultItemAction] = None
+
+ expires_at: Optional[datetime] = None
+
+
+VaultItemOperationResponse: TypeAlias = Union[WalletVaultItem, CardVaultItem, FillVaultItemOperationResult]
diff --git a/tests/api_resources/vaults/test_items.py b/tests/api_resources/vaults/test_items.py
index 9747f919..e63a7ceb 100644
--- a/tests/api_resources/vaults/test_items.py
+++ b/tests/api_resources/vaults/test_items.py
@@ -13,6 +13,7 @@
VaultItem,
ItemListResponse,
ItemEventsResponse,
+ VaultItemOperationResponse,
)
base_url = os.environ.get("TEST_API_BASE_URL", "http://127.0.0.1:4010")
@@ -392,17 +393,17 @@ def test_path_params_events(self, client: Kernel) -> None:
@pytest.mark.skip(reason="Mock server tests are disabled")
@parametrize
- def test_method_perform_operation(self, client: Kernel) -> None:
+ def test_method_perform_operation_overload_1(self, client: Kernel) -> None:
item = client.vaults.items.perform_operation(
key="key",
id_or_name="id_or_name",
type="authorize",
)
- assert_matches_type(VaultItem, item, path=["response"])
+ assert_matches_type(VaultItemOperationResponse, item, path=["response"])
@pytest.mark.skip(reason="Mock server tests are disabled")
@parametrize
- def test_raw_response_perform_operation(self, client: Kernel) -> None:
+ def test_raw_response_perform_operation_overload_1(self, client: Kernel) -> None:
response = client.vaults.items.with_raw_response.perform_operation(
key="key",
id_or_name="id_or_name",
@@ -412,11 +413,11 @@ def test_raw_response_perform_operation(self, client: Kernel) -> None:
assert response.is_closed is True
assert response.http_request.headers.get("X-Stainless-Lang") == "python"
item = response.parse()
- assert_matches_type(VaultItem, item, path=["response"])
+ assert_matches_type(VaultItemOperationResponse, item, path=["response"])
@pytest.mark.skip(reason="Mock server tests are disabled")
@parametrize
- def test_streaming_response_perform_operation(self, client: Kernel) -> None:
+ def test_streaming_response_perform_operation_overload_1(self, client: Kernel) -> None:
with client.vaults.items.with_streaming_response.perform_operation(
key="key",
id_or_name="id_or_name",
@@ -426,13 +427,13 @@ def test_streaming_response_perform_operation(self, client: Kernel) -> None:
assert response.http_request.headers.get("X-Stainless-Lang") == "python"
item = response.parse()
- assert_matches_type(VaultItem, item, path=["response"])
+ assert_matches_type(VaultItemOperationResponse, item, path=["response"])
assert cast(Any, response.is_closed) is True
@pytest.mark.skip(reason="Mock server tests are disabled")
@parametrize
- def test_path_params_perform_operation(self, client: Kernel) -> None:
+ def test_path_params_perform_operation_overload_1(self, client: Kernel) -> None:
with pytest.raises(ValueError, match=r"Expected a non-empty value for `id_or_name` but received ''"):
client.vaults.items.with_raw_response.perform_operation(
key="key",
@@ -447,6 +448,194 @@ def test_path_params_perform_operation(self, client: Kernel) -> None:
type="authorize",
)
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ def test_method_perform_operation_overload_2(self, client: Kernel) -> None:
+ item = client.vaults.items.perform_operation(
+ key="key",
+ id_or_name="id_or_name",
+ browser_id="browser-session-id",
+ fields=[
+ {
+ "field": "number",
+ "selector": "#card-number",
+ },
+ {
+ "field": "exp_month",
+ "selector": "#expiry-month",
+ },
+ {
+ "field": "exp_year",
+ "selector": "#expiry-year",
+ },
+ {
+ "field": "cvc",
+ "selector": "#security-code",
+ },
+ ],
+ page_url="https://shop.example/checkout",
+ type="fill",
+ )
+ assert_matches_type(VaultItemOperationResponse, item, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ def test_method_perform_operation_with_all_params_overload_2(self, client: Kernel) -> None:
+ item = client.vaults.items.perform_operation(
+ key="key",
+ id_or_name="id_or_name",
+ browser_id="browser-session-id",
+ fields=[
+ {
+ "field": "number",
+ "selector": "#card-number",
+ },
+ {
+ "field": "exp_month",
+ "selector": "#expiry-month",
+ },
+ {
+ "field": "exp_year",
+ "selector": "#expiry-year",
+ },
+ {
+ "field": "cvc",
+ "selector": "#security-code",
+ },
+ ],
+ page_url="https://shop.example/checkout",
+ type="fill",
+ timeout_ms=1,
+ )
+ assert_matches_type(VaultItemOperationResponse, item, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ def test_raw_response_perform_operation_overload_2(self, client: Kernel) -> None:
+ response = client.vaults.items.with_raw_response.perform_operation(
+ key="key",
+ id_or_name="id_or_name",
+ browser_id="browser-session-id",
+ fields=[
+ {
+ "field": "number",
+ "selector": "#card-number",
+ },
+ {
+ "field": "exp_month",
+ "selector": "#expiry-month",
+ },
+ {
+ "field": "exp_year",
+ "selector": "#expiry-year",
+ },
+ {
+ "field": "cvc",
+ "selector": "#security-code",
+ },
+ ],
+ page_url="https://shop.example/checkout",
+ type="fill",
+ )
+
+ assert response.is_closed is True
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+ item = response.parse()
+ assert_matches_type(VaultItemOperationResponse, item, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ def test_streaming_response_perform_operation_overload_2(self, client: Kernel) -> None:
+ with client.vaults.items.with_streaming_response.perform_operation(
+ key="key",
+ id_or_name="id_or_name",
+ browser_id="browser-session-id",
+ fields=[
+ {
+ "field": "number",
+ "selector": "#card-number",
+ },
+ {
+ "field": "exp_month",
+ "selector": "#expiry-month",
+ },
+ {
+ "field": "exp_year",
+ "selector": "#expiry-year",
+ },
+ {
+ "field": "cvc",
+ "selector": "#security-code",
+ },
+ ],
+ page_url="https://shop.example/checkout",
+ type="fill",
+ ) as response:
+ assert not response.is_closed
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+
+ item = response.parse()
+ assert_matches_type(VaultItemOperationResponse, item, path=["response"])
+
+ assert cast(Any, response.is_closed) is True
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ def test_path_params_perform_operation_overload_2(self, client: Kernel) -> None:
+ with pytest.raises(ValueError, match=r"Expected a non-empty value for `id_or_name` but received ''"):
+ client.vaults.items.with_raw_response.perform_operation(
+ key="key",
+ id_or_name="",
+ browser_id="browser-session-id",
+ fields=[
+ {
+ "field": "number",
+ "selector": "#card-number",
+ },
+ {
+ "field": "exp_month",
+ "selector": "#expiry-month",
+ },
+ {
+ "field": "exp_year",
+ "selector": "#expiry-year",
+ },
+ {
+ "field": "cvc",
+ "selector": "#security-code",
+ },
+ ],
+ page_url="https://shop.example/checkout",
+ type="fill",
+ )
+
+ with pytest.raises(ValueError, match=r"Expected a non-empty value for `key` but received ''"):
+ client.vaults.items.with_raw_response.perform_operation(
+ key="",
+ id_or_name="id_or_name",
+ browser_id="browser-session-id",
+ fields=[
+ {
+ "field": "number",
+ "selector": "#card-number",
+ },
+ {
+ "field": "exp_month",
+ "selector": "#expiry-month",
+ },
+ {
+ "field": "exp_year",
+ "selector": "#expiry-year",
+ },
+ {
+ "field": "cvc",
+ "selector": "#security-code",
+ },
+ ],
+ page_url="https://shop.example/checkout",
+ type="fill",
+ )
+
@pytest.mark.skip(reason="Mock server tests are disabled")
@parametrize
def test_method_upsert_overload_1(self, client: Kernel) -> None:
@@ -1088,17 +1277,17 @@ async def test_path_params_events(self, async_client: AsyncKernel) -> None:
@pytest.mark.skip(reason="Mock server tests are disabled")
@parametrize
- async def test_method_perform_operation(self, async_client: AsyncKernel) -> None:
+ async def test_method_perform_operation_overload_1(self, async_client: AsyncKernel) -> None:
item = await async_client.vaults.items.perform_operation(
key="key",
id_or_name="id_or_name",
type="authorize",
)
- assert_matches_type(VaultItem, item, path=["response"])
+ assert_matches_type(VaultItemOperationResponse, item, path=["response"])
@pytest.mark.skip(reason="Mock server tests are disabled")
@parametrize
- async def test_raw_response_perform_operation(self, async_client: AsyncKernel) -> None:
+ async def test_raw_response_perform_operation_overload_1(self, async_client: AsyncKernel) -> None:
response = await async_client.vaults.items.with_raw_response.perform_operation(
key="key",
id_or_name="id_or_name",
@@ -1108,11 +1297,11 @@ async def test_raw_response_perform_operation(self, async_client: AsyncKernel) -
assert response.is_closed is True
assert response.http_request.headers.get("X-Stainless-Lang") == "python"
item = await response.parse()
- assert_matches_type(VaultItem, item, path=["response"])
+ assert_matches_type(VaultItemOperationResponse, item, path=["response"])
@pytest.mark.skip(reason="Mock server tests are disabled")
@parametrize
- async def test_streaming_response_perform_operation(self, async_client: AsyncKernel) -> None:
+ async def test_streaming_response_perform_operation_overload_1(self, async_client: AsyncKernel) -> None:
async with async_client.vaults.items.with_streaming_response.perform_operation(
key="key",
id_or_name="id_or_name",
@@ -1122,13 +1311,13 @@ async def test_streaming_response_perform_operation(self, async_client: AsyncKer
assert response.http_request.headers.get("X-Stainless-Lang") == "python"
item = await response.parse()
- assert_matches_type(VaultItem, item, path=["response"])
+ assert_matches_type(VaultItemOperationResponse, item, 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_perform_operation(self, async_client: AsyncKernel) -> None:
+ async def test_path_params_perform_operation_overload_1(self, async_client: AsyncKernel) -> None:
with pytest.raises(ValueError, match=r"Expected a non-empty value for `id_or_name` but received ''"):
await async_client.vaults.items.with_raw_response.perform_operation(
key="key",
@@ -1143,6 +1332,194 @@ async def test_path_params_perform_operation(self, async_client: AsyncKernel) ->
type="authorize",
)
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ async def test_method_perform_operation_overload_2(self, async_client: AsyncKernel) -> None:
+ item = await async_client.vaults.items.perform_operation(
+ key="key",
+ id_or_name="id_or_name",
+ browser_id="browser-session-id",
+ fields=[
+ {
+ "field": "number",
+ "selector": "#card-number",
+ },
+ {
+ "field": "exp_month",
+ "selector": "#expiry-month",
+ },
+ {
+ "field": "exp_year",
+ "selector": "#expiry-year",
+ },
+ {
+ "field": "cvc",
+ "selector": "#security-code",
+ },
+ ],
+ page_url="https://shop.example/checkout",
+ type="fill",
+ )
+ assert_matches_type(VaultItemOperationResponse, item, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ async def test_method_perform_operation_with_all_params_overload_2(self, async_client: AsyncKernel) -> None:
+ item = await async_client.vaults.items.perform_operation(
+ key="key",
+ id_or_name="id_or_name",
+ browser_id="browser-session-id",
+ fields=[
+ {
+ "field": "number",
+ "selector": "#card-number",
+ },
+ {
+ "field": "exp_month",
+ "selector": "#expiry-month",
+ },
+ {
+ "field": "exp_year",
+ "selector": "#expiry-year",
+ },
+ {
+ "field": "cvc",
+ "selector": "#security-code",
+ },
+ ],
+ page_url="https://shop.example/checkout",
+ type="fill",
+ timeout_ms=1,
+ )
+ assert_matches_type(VaultItemOperationResponse, item, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ async def test_raw_response_perform_operation_overload_2(self, async_client: AsyncKernel) -> None:
+ response = await async_client.vaults.items.with_raw_response.perform_operation(
+ key="key",
+ id_or_name="id_or_name",
+ browser_id="browser-session-id",
+ fields=[
+ {
+ "field": "number",
+ "selector": "#card-number",
+ },
+ {
+ "field": "exp_month",
+ "selector": "#expiry-month",
+ },
+ {
+ "field": "exp_year",
+ "selector": "#expiry-year",
+ },
+ {
+ "field": "cvc",
+ "selector": "#security-code",
+ },
+ ],
+ page_url="https://shop.example/checkout",
+ type="fill",
+ )
+
+ assert response.is_closed is True
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+ item = await response.parse()
+ assert_matches_type(VaultItemOperationResponse, item, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ async def test_streaming_response_perform_operation_overload_2(self, async_client: AsyncKernel) -> None:
+ async with async_client.vaults.items.with_streaming_response.perform_operation(
+ key="key",
+ id_or_name="id_or_name",
+ browser_id="browser-session-id",
+ fields=[
+ {
+ "field": "number",
+ "selector": "#card-number",
+ },
+ {
+ "field": "exp_month",
+ "selector": "#expiry-month",
+ },
+ {
+ "field": "exp_year",
+ "selector": "#expiry-year",
+ },
+ {
+ "field": "cvc",
+ "selector": "#security-code",
+ },
+ ],
+ page_url="https://shop.example/checkout",
+ type="fill",
+ ) as response:
+ assert not response.is_closed
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+
+ item = await response.parse()
+ assert_matches_type(VaultItemOperationResponse, item, 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_perform_operation_overload_2(self, async_client: AsyncKernel) -> None:
+ with pytest.raises(ValueError, match=r"Expected a non-empty value for `id_or_name` but received ''"):
+ await async_client.vaults.items.with_raw_response.perform_operation(
+ key="key",
+ id_or_name="",
+ browser_id="browser-session-id",
+ fields=[
+ {
+ "field": "number",
+ "selector": "#card-number",
+ },
+ {
+ "field": "exp_month",
+ "selector": "#expiry-month",
+ },
+ {
+ "field": "exp_year",
+ "selector": "#expiry-year",
+ },
+ {
+ "field": "cvc",
+ "selector": "#security-code",
+ },
+ ],
+ page_url="https://shop.example/checkout",
+ type="fill",
+ )
+
+ with pytest.raises(ValueError, match=r"Expected a non-empty value for `key` but received ''"):
+ await async_client.vaults.items.with_raw_response.perform_operation(
+ key="",
+ id_or_name="id_or_name",
+ browser_id="browser-session-id",
+ fields=[
+ {
+ "field": "number",
+ "selector": "#card-number",
+ },
+ {
+ "field": "exp_month",
+ "selector": "#expiry-month",
+ },
+ {
+ "field": "exp_year",
+ "selector": "#expiry-year",
+ },
+ {
+ "field": "cvc",
+ "selector": "#security-code",
+ },
+ ],
+ page_url="https://shop.example/checkout",
+ type="fill",
+ )
+
@pytest.mark.skip(reason="Mock server tests are disabled")
@parametrize
async def test_method_upsert_overload_1(self, async_client: AsyncKernel) -> None:
From c43935913b8a471421ef8d96c9066c442ec95171 Mon Sep 17 00:00:00 2001
From: "kernel-internal[bot]"
<260533166+kernel-internal[bot]@users.noreply.github.com>
Date: Mon, 14 Sep 2026 17:05:49 +0000
Subject: [PATCH 258/366] feat: Add single-use AgentCard prepare_checkout for
Square
Stainless-Generated-From: 0c1e767db4c047801717ec350cd4b40cd7eace0c
---
api.md | 4 +
src/kernel/resources/vaults/items.py | 185 +++++++++++++++---
src/kernel/types/vaults/__init__.py | 8 +
.../vaults/agentcard_checkout_preparation.py | 43 ++++
...rize_vault_item_operation_request_param.py | 16 ++
.../types/vaults/card_vault_item_state.py | 38 +++-
.../vaults/item_perform_operation_params.py | 27 ++-
...kout_vault_item_operation_request_param.py | 27 +++
.../vaults/vault_checkout_context_param.py | 27 +++
src/kernel/types/vaults/vault_item.py | 4 +-
.../vaults/vault_item_operation_response.py | 4 +-
tests/api_resources/vaults/test_items.py | 180 ++++++++++++++++-
12 files changed, 513 insertions(+), 50 deletions(-)
create mode 100644 src/kernel/types/vaults/agentcard_checkout_preparation.py
create mode 100644 src/kernel/types/vaults/authorize_vault_item_operation_request_param.py
create mode 100644 src/kernel/types/vaults/prepare_checkout_vault_item_operation_request_param.py
create mode 100644 src/kernel/types/vaults/vault_checkout_context_param.py
diff --git a/api.md b/api.md
index c00bf223..1c9b02a3 100644
--- a/api.md
+++ b/api.md
@@ -525,12 +525,16 @@ Types:
```python
from kernel.types.vaults import (
AgentcardCheckoutAuthorization,
+ AgentcardCheckoutPreparation,
+ AuthorizeVaultItemOperationRequest,
CardVaultItemSpec,
CardVaultItemState,
FillVaultItemOperationRequest,
FillVaultItemOperationResult,
+ PrepareCheckoutVaultItemOperationRequest,
VaultCardAliases,
VaultCardFillField,
+ VaultCheckoutContext,
VaultFillFieldResult,
VaultItem,
VaultItemAction,
diff --git a/src/kernel/resources/vaults/items.py b/src/kernel/resources/vaults/items.py
index 309c6170..02315e2b 100644
--- a/src/kernel/resources/vaults/items.py
+++ b/src/kernel/resources/vaults/items.py
@@ -30,6 +30,7 @@
from ...types.vaults.item_events_response import ItemEventsResponse
from ...types.vaults.card_vault_item_spec_param import CardVaultItemSpecParam
from ...types.vaults.vault_card_fill_field_param import VaultCardFillFieldParam
+from ...types.vaults.vault_checkout_context_param import VaultCheckoutContextParam
from ...types.vaults.vault_item_operation_response import VaultItemOperationResponse
__all__ = ["ItemsResource", "AsyncItemsResource"]
@@ -212,10 +213,10 @@ def delete(
) -> None:
"""
Unresolved payment operations normally block deletion, including operations on
- child cards of a wallet. An AgentCard checkout whose create response returned no
- authorization ID may be explicitly abandoned by deleting that card directly;
- deleting its wallet or vault remains blocked. Deleting or recreating an item is
- not proof that a payment did not occur.
+ child cards of a wallet. An AgentCard card in recovery_required whose checkout
+ create response returned no authorization ID may be explicitly abandoned by
+ deleting that card directly; deleting its wallet or vault remains blocked.
+ Deleting or recreating an item is not proof that a payment did not occur.
Args:
extra_headers: Send extra headers
@@ -309,10 +310,13 @@ def perform_operation(
Retrieve the item first and invoke only an operation listed in
`available_operations`, following its natural-language description. Availability
is rechecked at execution time; unavailable operations return 409. Authorization
- may call an external provider and returns the updated item. Link cards advertise
- authorize when eligible. AgentCard cards are created with PUT and request
- approval when their aliases are used at checkout; they do not expose authorize.
- If spend-request creation is rate limited, returns HTTP 429 with code
+ and preparation may call an external provider and return updated state. Link
+ cards advertise authorize without checkout context. Eligible unused AgentCard
+ cards advertise prepare_checkout, which requires checkout context and obtains
+ device approval before native Square Pay. Keep the returned approval page open,
+ poll until ready_to_submit, then submit before preparation.expires_at. Unused
+ preparations expire automatically and cannot be reused. If spend-request
+ creation is rate limited, returns HTTP 429 with code
`spend_request_rate_limited`; stop and back off before retrying.
Fill returns a value-free execution result. Validation failures before writing
@@ -333,6 +337,58 @@ def perform_operation(
"""
...
+ @overload
+ def perform_operation(
+ self,
+ key: str,
+ *,
+ id_or_name: str,
+ checkout: VaultCheckoutContextParam,
+ type: Literal["prepare_checkout"],
+ # 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,
+ ) -> VaultItemOperationResponse:
+ """
+ Retrieve the item first and invoke only an operation listed in
+ `available_operations`, following its natural-language description. Availability
+ is rechecked at execution time; unavailable operations return 409. Authorization
+ and preparation may call an external provider and return updated state. Link
+ cards advertise authorize without checkout context. Eligible unused AgentCard
+ cards advertise prepare_checkout, which requires checkout context and obtains
+ device approval before native Square Pay. Keep the returned approval page open,
+ poll until ready_to_submit, then submit before preparation.expires_at. Unused
+ preparations expire automatically and cannot be reused. If spend-request
+ creation is rate limited, returns HTTP 429 with code
+ `spend_request_rate_limited`; stop and back off before retrying.
+
+ Fill returns a value-free execution result. Validation failures before writing
+ return 400 (invalid request or targets), 403 (access or destination denied), 404
+ (resource not found), or 409 (item or browser not ready). Once writing starts,
+ known partial failures and indeterminate field outcomes return 200 with status
+ `failed` or `unknown`, not an automatic-retry signal. A transport error may
+ leave the outcome unknown; do not automatically retry.
+
+ Args:
+ checkout: Required when preparing an unused AgentCard card for Square. Consent is bound to
+ this browser and declared merchant origin, not a tab. Wait for the item's
+ ready_to_submit status before native Pay and submit within its readiness
+ deadline. Unused preparations expire automatically; every preparation is
+ single-use, including after failure or expiry.
+
+ 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
+ """
+ ...
+
@overload
def perform_operation(
self,
@@ -355,10 +411,13 @@ def perform_operation(
Retrieve the item first and invoke only an operation listed in
`available_operations`, following its natural-language description. Availability
is rechecked at execution time; unavailable operations return 409. Authorization
- may call an external provider and returns the updated item. Link cards advertise
- authorize when eligible. AgentCard cards are created with PUT and request
- approval when their aliases are used at checkout; they do not expose authorize.
- If spend-request creation is rate limited, returns HTTP 429 with code
+ and preparation may call an external provider and return updated state. Link
+ cards advertise authorize without checkout context. Eligible unused AgentCard
+ cards advertise prepare_checkout, which requires checkout context and obtains
+ device approval before native Square Pay. Keep the returned approval page open,
+ poll until ready_to_submit, then submit before preparation.expires_at. Unused
+ preparations expire automatically and cannot be reused. If spend-request
+ creation is rate limited, returns HTTP 429 with code
`spend_request_rate_limited`; stop and back off before retrying.
Fill returns a value-free execution result. Validation failures before writing
@@ -389,13 +448,18 @@ def perform_operation(
"""
...
- @required_args(["id_or_name", "type"], ["id_or_name", "browser_id", "fields", "page_url", "type"])
+ @required_args(
+ ["id_or_name", "type"],
+ ["id_or_name", "checkout", "type"],
+ ["id_or_name", "browser_id", "fields", "page_url", "type"],
+ )
def perform_operation(
self,
key: str,
*,
id_or_name: str,
- type: Literal["authorize"] | Literal["fill"],
+ type: Literal["authorize"] | Literal["prepare_checkout"] | Literal["fill"],
+ checkout: VaultCheckoutContextParam | Omit = omit,
browser_id: str | Omit = omit,
fields: Iterable[VaultCardFillFieldParam] | Omit = omit,
page_url: str | Omit = omit,
@@ -418,6 +482,7 @@ def perform_operation(
body=maybe_transform(
{
"type": type,
+ "checkout": checkout,
"browser_id": browser_id,
"fields": fields,
"page_url": page_url,
@@ -726,10 +791,10 @@ async def delete(
) -> None:
"""
Unresolved payment operations normally block deletion, including operations on
- child cards of a wallet. An AgentCard checkout whose create response returned no
- authorization ID may be explicitly abandoned by deleting that card directly;
- deleting its wallet or vault remains blocked. Deleting or recreating an item is
- not proof that a payment did not occur.
+ child cards of a wallet. An AgentCard card in recovery_required whose checkout
+ create response returned no authorization ID may be explicitly abandoned by
+ deleting that card directly; deleting its wallet or vault remains blocked.
+ Deleting or recreating an item is not proof that a payment did not occur.
Args:
extra_headers: Send extra headers
@@ -823,10 +888,13 @@ async def perform_operation(
Retrieve the item first and invoke only an operation listed in
`available_operations`, following its natural-language description. Availability
is rechecked at execution time; unavailable operations return 409. Authorization
- may call an external provider and returns the updated item. Link cards advertise
- authorize when eligible. AgentCard cards are created with PUT and request
- approval when their aliases are used at checkout; they do not expose authorize.
- If spend-request creation is rate limited, returns HTTP 429 with code
+ and preparation may call an external provider and return updated state. Link
+ cards advertise authorize without checkout context. Eligible unused AgentCard
+ cards advertise prepare_checkout, which requires checkout context and obtains
+ device approval before native Square Pay. Keep the returned approval page open,
+ poll until ready_to_submit, then submit before preparation.expires_at. Unused
+ preparations expire automatically and cannot be reused. If spend-request
+ creation is rate limited, returns HTTP 429 with code
`spend_request_rate_limited`; stop and back off before retrying.
Fill returns a value-free execution result. Validation failures before writing
@@ -847,6 +915,58 @@ async def perform_operation(
"""
...
+ @overload
+ async def perform_operation(
+ self,
+ key: str,
+ *,
+ id_or_name: str,
+ checkout: VaultCheckoutContextParam,
+ type: Literal["prepare_checkout"],
+ # 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,
+ ) -> VaultItemOperationResponse:
+ """
+ Retrieve the item first and invoke only an operation listed in
+ `available_operations`, following its natural-language description. Availability
+ is rechecked at execution time; unavailable operations return 409. Authorization
+ and preparation may call an external provider and return updated state. Link
+ cards advertise authorize without checkout context. Eligible unused AgentCard
+ cards advertise prepare_checkout, which requires checkout context and obtains
+ device approval before native Square Pay. Keep the returned approval page open,
+ poll until ready_to_submit, then submit before preparation.expires_at. Unused
+ preparations expire automatically and cannot be reused. If spend-request
+ creation is rate limited, returns HTTP 429 with code
+ `spend_request_rate_limited`; stop and back off before retrying.
+
+ Fill returns a value-free execution result. Validation failures before writing
+ return 400 (invalid request or targets), 403 (access or destination denied), 404
+ (resource not found), or 409 (item or browser not ready). Once writing starts,
+ known partial failures and indeterminate field outcomes return 200 with status
+ `failed` or `unknown`, not an automatic-retry signal. A transport error may
+ leave the outcome unknown; do not automatically retry.
+
+ Args:
+ checkout: Required when preparing an unused AgentCard card for Square. Consent is bound to
+ this browser and declared merchant origin, not a tab. Wait for the item's
+ ready_to_submit status before native Pay and submit within its readiness
+ deadline. Unused preparations expire automatically; every preparation is
+ single-use, including after failure or expiry.
+
+ 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
+ """
+ ...
+
@overload
async def perform_operation(
self,
@@ -869,10 +989,13 @@ async def perform_operation(
Retrieve the item first and invoke only an operation listed in
`available_operations`, following its natural-language description. Availability
is rechecked at execution time; unavailable operations return 409. Authorization
- may call an external provider and returns the updated item. Link cards advertise
- authorize when eligible. AgentCard cards are created with PUT and request
- approval when their aliases are used at checkout; they do not expose authorize.
- If spend-request creation is rate limited, returns HTTP 429 with code
+ and preparation may call an external provider and return updated state. Link
+ cards advertise authorize without checkout context. Eligible unused AgentCard
+ cards advertise prepare_checkout, which requires checkout context and obtains
+ device approval before native Square Pay. Keep the returned approval page open,
+ poll until ready_to_submit, then submit before preparation.expires_at. Unused
+ preparations expire automatically and cannot be reused. If spend-request
+ creation is rate limited, returns HTTP 429 with code
`spend_request_rate_limited`; stop and back off before retrying.
Fill returns a value-free execution result. Validation failures before writing
@@ -903,13 +1026,18 @@ async def perform_operation(
"""
...
- @required_args(["id_or_name", "type"], ["id_or_name", "browser_id", "fields", "page_url", "type"])
+ @required_args(
+ ["id_or_name", "type"],
+ ["id_or_name", "checkout", "type"],
+ ["id_or_name", "browser_id", "fields", "page_url", "type"],
+ )
async def perform_operation(
self,
key: str,
*,
id_or_name: str,
- type: Literal["authorize"] | Literal["fill"],
+ type: Literal["authorize"] | Literal["prepare_checkout"] | Literal["fill"],
+ checkout: VaultCheckoutContextParam | Omit = omit,
browser_id: str | Omit = omit,
fields: Iterable[VaultCardFillFieldParam] | Omit = omit,
page_url: str | Omit = omit,
@@ -932,6 +1060,7 @@ async def perform_operation(
body=await async_maybe_transform(
{
"type": type,
+ "checkout": checkout,
"browser_id": browser_id,
"fields": fields,
"page_url": page_url,
diff --git a/src/kernel/types/vaults/__init__.py b/src/kernel/types/vaults/__init__.py
index 0f71f0c3..b48fd00d 100644
--- a/src/kernel/types/vaults/__init__.py
+++ b/src/kernel/types/vaults/__init__.py
@@ -20,10 +20,18 @@
from .wallet_vault_item_state import WalletVaultItemState as WalletVaultItemState
from .card_vault_item_spec_param import CardVaultItemSpecParam as CardVaultItemSpecParam
from .vault_card_fill_field_param import VaultCardFillFieldParam as VaultCardFillFieldParam
+from .vault_checkout_context_param import VaultCheckoutContextParam as VaultCheckoutContextParam
from .item_perform_operation_params import ItemPerformOperationParams as ItemPerformOperationParams
from .vault_item_operation_response import VaultItemOperationResponse as VaultItemOperationResponse
+from .agentcard_checkout_preparation import AgentcardCheckoutPreparation as AgentcardCheckoutPreparation
from .agentcard_checkout_authorization import AgentcardCheckoutAuthorization as AgentcardCheckoutAuthorization
from .fill_vault_item_operation_result import FillVaultItemOperationResult as FillVaultItemOperationResult
from .fill_vault_item_operation_request_param import (
FillVaultItemOperationRequestParam as FillVaultItemOperationRequestParam,
)
+from .authorize_vault_item_operation_request_param import (
+ AuthorizeVaultItemOperationRequestParam as AuthorizeVaultItemOperationRequestParam,
+)
+from .prepare_checkout_vault_item_operation_request_param import (
+ PrepareCheckoutVaultItemOperationRequestParam as PrepareCheckoutVaultItemOperationRequestParam,
+)
diff --git a/src/kernel/types/vaults/agentcard_checkout_preparation.py b/src/kernel/types/vaults/agentcard_checkout_preparation.py
new file mode 100644
index 00000000..8296b2fd
--- /dev/null
+++ b/src/kernel/types/vaults/agentcard_checkout_preparation.py
@@ -0,0 +1,43 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from typing import Optional
+from datetime import datetime
+from typing_extensions import Literal
+
+from ..._models import BaseModel
+
+__all__ = ["AgentcardCheckoutPreparation"]
+
+
+class AgentcardCheckoutPreparation(BaseModel):
+ """One-use Square checkout preparation.
+
+ Keep the approval page open through token handoff. The amount is display-only and does not constrain the merchant's eventual charge.
+ """
+
+ browser_id: str
+
+ created_at: datetime
+
+ environment: Literal["production", "sandbox"]
+
+ merchant_origin: str
+
+ status: Literal["creating", "awaiting_approval", "ready", "consumed", "cancelled", "expired", "unknown"]
+ """
+ Preparation consumed means egress claimed the preparation and it cannot be
+ reused. It does not mean the attempt settled. Use the enclosing item's status as
+ the lifecycle indicator; item consumed means the attempt settled, not that an
+ order or charge succeeded.
+ """
+
+ id: Optional[str] = None
+
+ approval_url: Optional[str] = None
+
+ expires_at: Optional[datetime] = None
+ """
+ When ready, the absolute deadline to submit the first native request; no later
+ than provider readiness expiry or 30 seconds after Kernel first observes
+ readiness. Polling never extends this deadline.
+ """
diff --git a/src/kernel/types/vaults/authorize_vault_item_operation_request_param.py b/src/kernel/types/vaults/authorize_vault_item_operation_request_param.py
new file mode 100644
index 00000000..10ba11bc
--- /dev/null
+++ b/src/kernel/types/vaults/authorize_vault_item_operation_request_param.py
@@ -0,0 +1,16 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from __future__ import annotations
+
+from typing_extensions import Literal, Required, TypedDict
+
+__all__ = ["AuthorizeVaultItemOperationRequestParam"]
+
+
+class AuthorizeVaultItemOperationRequestParam(TypedDict, total=False):
+ """Authorize a Link card using its existing purchase specification.
+
+ Use only after explicit user approval and when the item advertises authorize. Do not automatically retry provider failures or indeterminate outcomes. Checkout context is not accepted.
+ """
+
+ type: Required[Literal["authorize"]]
diff --git a/src/kernel/types/vaults/card_vault_item_state.py b/src/kernel/types/vaults/card_vault_item_state.py
index c64f2ab7..98459762 100644
--- a/src/kernel/types/vaults/card_vault_item_state.py
+++ b/src/kernel/types/vaults/card_vault_item_state.py
@@ -8,6 +8,7 @@
from ..._utils import PropertyInfo
from ..._models import BaseModel
from .vault_card_aliases import VaultCardAliases
+from .agentcard_checkout_preparation import AgentcardCheckoutPreparation
from .agentcard_checkout_authorization import AgentcardCheckoutAuthorization
__all__ = ["CardVaultItemState", "LinkCardState", "LinkCardStateMasks", "AgentCardCardState", "AgentCardCardStateMasks"]
@@ -76,14 +77,28 @@ def __getattr__(self, attr: str) -> str: ...
class AgentCardCardState(BaseModel):
provider: Literal["agentcard"]
- status: Literal["requested", "ready", "pending_approval", "degraded", "recovery_required"]
- """recovery_required means the original checkout outcome is unresolved.
-
- Automatic reuse is blocked. Known authorization IDs must be reconciled through
- provider observations or support. When no authorization ID was returned, an
- explicitly confirmed item deletion may abandon the unresolved attempt so the
- caller can create a replacement; deletion does not prove that the original
- attempt failed. It does not mean declined or expired.
+ status: Literal[
+ "requested",
+ "ready",
+ "preparing",
+ "ready_to_submit",
+ "pending_approval",
+ "consumed",
+ "stopped",
+ "outcome_unknown",
+ "degraded",
+ "recovery_required",
+ ]
+ """ready_to_submit is device readiness for at most 30 seconds.
+
+ consumed means the prepared attempt has settled, not that an order succeeded.
+ stopped cannot be reused. outcome_unknown requires merchant reconciliation and
+ blocks new requests. recovery_required means the original checkout outcome is
+ unresolved. Automatic reuse is blocked. Known authorization IDs must be
+ reconciled through provider observations or support. When no authorization ID
+ was returned, an explicitly confirmed item deletion may abandon the unresolved
+ attempt so the caller can create a replacement; deletion does not prove that the
+ original attempt failed. It does not mean declined or expired.
"""
aliases: Optional[VaultCardAliases] = None
@@ -96,6 +111,13 @@ class AgentCardCardState(BaseModel):
masks: Optional[AgentCardCardStateMasks] = None
+ preparation: Optional[AgentcardCheckoutPreparation] = None
+ """One-use Square checkout preparation.
+
+ Keep the approval page open through token handoff. The amount is display-only
+ and does not constrain the merchant's eventual charge.
+ """
+
status_reason: Optional[str] = None
diff --git a/src/kernel/types/vaults/item_perform_operation_params.py b/src/kernel/types/vaults/item_perform_operation_params.py
index 4a5e4361..c583f53b 100644
--- a/src/kernel/types/vaults/item_perform_operation_params.py
+++ b/src/kernel/types/vaults/item_perform_operation_params.py
@@ -6,8 +6,14 @@
from typing_extensions import Literal, Required, TypeAlias, TypedDict
from .vault_card_fill_field_param import VaultCardFillFieldParam
+from .vault_checkout_context_param import VaultCheckoutContextParam
-__all__ = ["ItemPerformOperationParams", "AuthorizeVaultItemOperationRequest", "FillVaultItemOperationRequest"]
+__all__ = [
+ "ItemPerformOperationParams",
+ "AuthorizeVaultItemOperationRequest",
+ "PrepareCheckoutVaultItemOperationRequest",
+ "FillVaultItemOperationRequest",
+]
class AuthorizeVaultItemOperationRequest(TypedDict, total=False):
@@ -16,6 +22,21 @@ class AuthorizeVaultItemOperationRequest(TypedDict, total=False):
type: Required[Literal["authorize"]]
+class PrepareCheckoutVaultItemOperationRequest(TypedDict, total=False):
+ id_or_name: Required[str]
+
+ checkout: Required[VaultCheckoutContextParam]
+ """Required when preparing an unused AgentCard card for Square.
+
+ Consent is bound to this browser and declared merchant origin, not a tab. Wait
+ for the item's ready_to_submit status before native Pay and submit within its
+ readiness deadline. Unused preparations expire automatically; every preparation
+ is single-use, including after failure or expiry.
+ """
+
+ type: Required[Literal["prepare_checkout"]]
+
+
class FillVaultItemOperationRequest(TypedDict, total=False):
id_or_name: Required[str]
@@ -38,4 +59,6 @@ class FillVaultItemOperationRequest(TypedDict, total=False):
"""Total operation deadline in milliseconds, not a per-field timeout."""
-ItemPerformOperationParams: TypeAlias = Union[AuthorizeVaultItemOperationRequest, FillVaultItemOperationRequest]
+ItemPerformOperationParams: TypeAlias = Union[
+ AuthorizeVaultItemOperationRequest, PrepareCheckoutVaultItemOperationRequest, FillVaultItemOperationRequest
+]
diff --git a/src/kernel/types/vaults/prepare_checkout_vault_item_operation_request_param.py b/src/kernel/types/vaults/prepare_checkout_vault_item_operation_request_param.py
new file mode 100644
index 00000000..9900c513
--- /dev/null
+++ b/src/kernel/types/vaults/prepare_checkout_vault_item_operation_request_param.py
@@ -0,0 +1,27 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from __future__ import annotations
+
+from typing_extensions import Literal, Required, TypedDict
+
+from .vault_checkout_context_param import VaultCheckoutContextParam
+
+__all__ = ["PrepareCheckoutVaultItemOperationRequestParam"]
+
+
+class PrepareCheckoutVaultItemOperationRequestParam(TypedDict, total=False):
+ """Prepare an unused AgentCard card for Square checkout.
+
+ Deliver the returned approval URL and keep the approval page open. Poll the item until ready_to_submit, then submit native Pay before preparation.expires_at. Readiness lasts at most 30 seconds. Unused preparations expire automatically. Preparations are single-use even after failure or expiry; do not automatically retry and reconcile uncertain outcomes with the merchant.
+ """
+
+ checkout: Required[VaultCheckoutContextParam]
+ """Required when preparing an unused AgentCard card for Square.
+
+ Consent is bound to this browser and declared merchant origin, not a tab. Wait
+ for the item's ready_to_submit status before native Pay and submit within its
+ readiness deadline. Unused preparations expire automatically; every preparation
+ is single-use, including after failure or expiry.
+ """
+
+ type: Required[Literal["prepare_checkout"]]
diff --git a/src/kernel/types/vaults/vault_checkout_context_param.py b/src/kernel/types/vaults/vault_checkout_context_param.py
new file mode 100644
index 00000000..8b08c40c
--- /dev/null
+++ b/src/kernel/types/vaults/vault_checkout_context_param.py
@@ -0,0 +1,27 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from __future__ import annotations
+
+from typing_extensions import Literal, Required, TypedDict
+
+__all__ = ["VaultCheckoutContextParam"]
+
+
+class VaultCheckoutContextParam(TypedDict, total=False):
+ """Required when preparing an unused AgentCard card for Square.
+
+ Consent is bound to this browser and declared merchant origin, not a tab. Wait for the item's ready_to_submit status before native Pay and submit within its readiness deadline. Unused preparations expire automatically; every preparation is single-use, including after failure or expiry.
+ """
+
+ browser_id: Required[str]
+ """Active browser session with this vault bound to it."""
+
+ environment: Required[Literal["production", "sandbox"]]
+ """Square environment, independent of the AgentCard credential mode."""
+
+ merchant_origin: Required[str]
+ """Canonical HTTPS origin of the top-level merchant document, not the Square
+ iframe.
+
+ HTTP localhost is accepted for tests.
+ """
diff --git a/src/kernel/types/vaults/vault_item.py b/src/kernel/types/vaults/vault_item.py
index e6ca1561..e33e12db 100644
--- a/src/kernel/types/vaults/vault_item.py
+++ b/src/kernel/types/vaults/vault_item.py
@@ -43,7 +43,7 @@ class WalletVaultItemAvailableOperation(BaseModel):
description: str
- type: Literal["authorize", "fill"]
+ type: Literal["authorize", "prepare_checkout", "fill"]
class WalletVaultItemExpanded(BaseModel):
@@ -107,7 +107,7 @@ class CardVaultItemAvailableOperation(BaseModel):
description: str
- type: Literal["authorize", "fill"]
+ type: Literal["authorize", "prepare_checkout", "fill"]
class CardVaultItem(BaseModel):
diff --git a/src/kernel/types/vaults/vault_item_operation_response.py b/src/kernel/types/vaults/vault_item_operation_response.py
index 73502ccb..a464a6b8 100644
--- a/src/kernel/types/vaults/vault_item_operation_response.py
+++ b/src/kernel/types/vaults/vault_item_operation_response.py
@@ -43,7 +43,7 @@ class WalletVaultItemAvailableOperation(BaseModel):
description: str
- type: Literal["authorize", "fill"]
+ type: Literal["authorize", "prepare_checkout", "fill"]
class WalletVaultItemExpanded(BaseModel):
@@ -107,7 +107,7 @@ class CardVaultItemAvailableOperation(BaseModel):
description: str
- type: Literal["authorize", "fill"]
+ type: Literal["authorize", "prepare_checkout", "fill"]
class CardVaultItem(BaseModel):
diff --git a/tests/api_resources/vaults/test_items.py b/tests/api_resources/vaults/test_items.py
index e63a7ceb..f9dde10b 100644
--- a/tests/api_resources/vaults/test_items.py
+++ b/tests/api_resources/vaults/test_items.py
@@ -451,6 +451,88 @@ def test_path_params_perform_operation_overload_1(self, client: Kernel) -> None:
@pytest.mark.skip(reason="Mock server tests are disabled")
@parametrize
def test_method_perform_operation_overload_2(self, client: Kernel) -> None:
+ item = client.vaults.items.perform_operation(
+ key="key",
+ id_or_name="id_or_name",
+ checkout={
+ "browser_id": "browser_id",
+ "environment": "production",
+ "merchant_origin": "merchant_origin",
+ },
+ type="prepare_checkout",
+ )
+ assert_matches_type(VaultItemOperationResponse, item, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ def test_raw_response_perform_operation_overload_2(self, client: Kernel) -> None:
+ response = client.vaults.items.with_raw_response.perform_operation(
+ key="key",
+ id_or_name="id_or_name",
+ checkout={
+ "browser_id": "browser_id",
+ "environment": "production",
+ "merchant_origin": "merchant_origin",
+ },
+ type="prepare_checkout",
+ )
+
+ assert response.is_closed is True
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+ item = response.parse()
+ assert_matches_type(VaultItemOperationResponse, item, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ def test_streaming_response_perform_operation_overload_2(self, client: Kernel) -> None:
+ with client.vaults.items.with_streaming_response.perform_operation(
+ key="key",
+ id_or_name="id_or_name",
+ checkout={
+ "browser_id": "browser_id",
+ "environment": "production",
+ "merchant_origin": "merchant_origin",
+ },
+ type="prepare_checkout",
+ ) as response:
+ assert not response.is_closed
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+
+ item = response.parse()
+ assert_matches_type(VaultItemOperationResponse, item, path=["response"])
+
+ assert cast(Any, response.is_closed) is True
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ def test_path_params_perform_operation_overload_2(self, client: Kernel) -> None:
+ with pytest.raises(ValueError, match=r"Expected a non-empty value for `id_or_name` but received ''"):
+ client.vaults.items.with_raw_response.perform_operation(
+ key="key",
+ id_or_name="",
+ checkout={
+ "browser_id": "browser_id",
+ "environment": "production",
+ "merchant_origin": "merchant_origin",
+ },
+ type="prepare_checkout",
+ )
+
+ with pytest.raises(ValueError, match=r"Expected a non-empty value for `key` but received ''"):
+ client.vaults.items.with_raw_response.perform_operation(
+ key="",
+ id_or_name="id_or_name",
+ checkout={
+ "browser_id": "browser_id",
+ "environment": "production",
+ "merchant_origin": "merchant_origin",
+ },
+ type="prepare_checkout",
+ )
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ def test_method_perform_operation_overload_3(self, client: Kernel) -> None:
item = client.vaults.items.perform_operation(
key="key",
id_or_name="id_or_name",
@@ -480,7 +562,7 @@ def test_method_perform_operation_overload_2(self, client: Kernel) -> None:
@pytest.mark.skip(reason="Mock server tests are disabled")
@parametrize
- def test_method_perform_operation_with_all_params_overload_2(self, client: Kernel) -> None:
+ def test_method_perform_operation_with_all_params_overload_3(self, client: Kernel) -> None:
item = client.vaults.items.perform_operation(
key="key",
id_or_name="id_or_name",
@@ -511,7 +593,7 @@ def test_method_perform_operation_with_all_params_overload_2(self, client: Kerne
@pytest.mark.skip(reason="Mock server tests are disabled")
@parametrize
- def test_raw_response_perform_operation_overload_2(self, client: Kernel) -> None:
+ def test_raw_response_perform_operation_overload_3(self, client: Kernel) -> None:
response = client.vaults.items.with_raw_response.perform_operation(
key="key",
id_or_name="id_or_name",
@@ -545,7 +627,7 @@ def test_raw_response_perform_operation_overload_2(self, client: Kernel) -> None
@pytest.mark.skip(reason="Mock server tests are disabled")
@parametrize
- def test_streaming_response_perform_operation_overload_2(self, client: Kernel) -> None:
+ def test_streaming_response_perform_operation_overload_3(self, client: Kernel) -> None:
with client.vaults.items.with_streaming_response.perform_operation(
key="key",
id_or_name="id_or_name",
@@ -581,7 +663,7 @@ def test_streaming_response_perform_operation_overload_2(self, client: Kernel) -
@pytest.mark.skip(reason="Mock server tests are disabled")
@parametrize
- def test_path_params_perform_operation_overload_2(self, client: Kernel) -> None:
+ def test_path_params_perform_operation_overload_3(self, client: Kernel) -> None:
with pytest.raises(ValueError, match=r"Expected a non-empty value for `id_or_name` but received ''"):
client.vaults.items.with_raw_response.perform_operation(
key="key",
@@ -1335,6 +1417,88 @@ async def test_path_params_perform_operation_overload_1(self, async_client: Asyn
@pytest.mark.skip(reason="Mock server tests are disabled")
@parametrize
async def test_method_perform_operation_overload_2(self, async_client: AsyncKernel) -> None:
+ item = await async_client.vaults.items.perform_operation(
+ key="key",
+ id_or_name="id_or_name",
+ checkout={
+ "browser_id": "browser_id",
+ "environment": "production",
+ "merchant_origin": "merchant_origin",
+ },
+ type="prepare_checkout",
+ )
+ assert_matches_type(VaultItemOperationResponse, item, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ async def test_raw_response_perform_operation_overload_2(self, async_client: AsyncKernel) -> None:
+ response = await async_client.vaults.items.with_raw_response.perform_operation(
+ key="key",
+ id_or_name="id_or_name",
+ checkout={
+ "browser_id": "browser_id",
+ "environment": "production",
+ "merchant_origin": "merchant_origin",
+ },
+ type="prepare_checkout",
+ )
+
+ assert response.is_closed is True
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+ item = await response.parse()
+ assert_matches_type(VaultItemOperationResponse, item, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ async def test_streaming_response_perform_operation_overload_2(self, async_client: AsyncKernel) -> None:
+ async with async_client.vaults.items.with_streaming_response.perform_operation(
+ key="key",
+ id_or_name="id_or_name",
+ checkout={
+ "browser_id": "browser_id",
+ "environment": "production",
+ "merchant_origin": "merchant_origin",
+ },
+ type="prepare_checkout",
+ ) as response:
+ assert not response.is_closed
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+
+ item = await response.parse()
+ assert_matches_type(VaultItemOperationResponse, item, 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_perform_operation_overload_2(self, async_client: AsyncKernel) -> None:
+ with pytest.raises(ValueError, match=r"Expected a non-empty value for `id_or_name` but received ''"):
+ await async_client.vaults.items.with_raw_response.perform_operation(
+ key="key",
+ id_or_name="",
+ checkout={
+ "browser_id": "browser_id",
+ "environment": "production",
+ "merchant_origin": "merchant_origin",
+ },
+ type="prepare_checkout",
+ )
+
+ with pytest.raises(ValueError, match=r"Expected a non-empty value for `key` but received ''"):
+ await async_client.vaults.items.with_raw_response.perform_operation(
+ key="",
+ id_or_name="id_or_name",
+ checkout={
+ "browser_id": "browser_id",
+ "environment": "production",
+ "merchant_origin": "merchant_origin",
+ },
+ type="prepare_checkout",
+ )
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ async def test_method_perform_operation_overload_3(self, async_client: AsyncKernel) -> None:
item = await async_client.vaults.items.perform_operation(
key="key",
id_or_name="id_or_name",
@@ -1364,7 +1528,7 @@ async def test_method_perform_operation_overload_2(self, async_client: AsyncKern
@pytest.mark.skip(reason="Mock server tests are disabled")
@parametrize
- async def test_method_perform_operation_with_all_params_overload_2(self, async_client: AsyncKernel) -> None:
+ async def test_method_perform_operation_with_all_params_overload_3(self, async_client: AsyncKernel) -> None:
item = await async_client.vaults.items.perform_operation(
key="key",
id_or_name="id_or_name",
@@ -1395,7 +1559,7 @@ async def test_method_perform_operation_with_all_params_overload_2(self, async_c
@pytest.mark.skip(reason="Mock server tests are disabled")
@parametrize
- async def test_raw_response_perform_operation_overload_2(self, async_client: AsyncKernel) -> None:
+ async def test_raw_response_perform_operation_overload_3(self, async_client: AsyncKernel) -> None:
response = await async_client.vaults.items.with_raw_response.perform_operation(
key="key",
id_or_name="id_or_name",
@@ -1429,7 +1593,7 @@ async def test_raw_response_perform_operation_overload_2(self, async_client: Asy
@pytest.mark.skip(reason="Mock server tests are disabled")
@parametrize
- async def test_streaming_response_perform_operation_overload_2(self, async_client: AsyncKernel) -> None:
+ async def test_streaming_response_perform_operation_overload_3(self, async_client: AsyncKernel) -> None:
async with async_client.vaults.items.with_streaming_response.perform_operation(
key="key",
id_or_name="id_or_name",
@@ -1465,7 +1629,7 @@ async def test_streaming_response_perform_operation_overload_2(self, async_clien
@pytest.mark.skip(reason="Mock server tests are disabled")
@parametrize
- async def test_path_params_perform_operation_overload_2(self, async_client: AsyncKernel) -> None:
+ async def test_path_params_perform_operation_overload_3(self, async_client: AsyncKernel) -> None:
with pytest.raises(ValueError, match=r"Expected a non-empty value for `id_or_name` but received ''"):
await async_client.vaults.items.with_raw_response.perform_operation(
key="key",
From 39ffff82be3987e2220d8d5057d8d2505991a3eb Mon Sep 17 00:00:00 2001
From: "kernel-internal[bot]"
<260533166+kernel-internal[bot]@users.noreply.github.com>
Date: Mon, 14 Sep 2026 20:28:09 +0000
Subject: [PATCH 259/366] feat: Accept a workload intent and use it to widen
vendor coverage
Stainless-Generated-From: 3973041af0408ac2efd51b5cfe1c472ab6708c7b
---
.../config_registry/config_registry.py | 20 +++++++++++++++++++
.../types/config_registry_resolve_params.py | 11 ++++++++++
src/kernel/types/config_registry_response.py | 10 ++++++++++
tests/api_resources/test_config_registry.py | 2 ++
4 files changed, 43 insertions(+)
diff --git a/src/kernel/resources/config_registry/config_registry.py b/src/kernel/resources/config_registry/config_registry.py
index 0b9d1824..27a8766c 100644
--- a/src/kernel/resources/config_registry/config_registry.py
+++ b/src/kernel/resources/config_registry/config_registry.py
@@ -165,6 +165,7 @@ def resolve(
*,
url: str,
allowed_proxy_countries: SequenceNotStr[str] | Omit = omit,
+ intent: 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,
@@ -184,6 +185,14 @@ def resolve(
configuration. Kernel may test a subset of allowed countries. When omitted,
Kernel uses its default country selection.
+ intent: Plain-language description of the workload you intend to run against this
+ target, in a sentence or two. Requires an https target, because the pass treats
+ any non-HTTPS destination as off-site and will not drive an http one. Kernel
+ uses it to drive the browser further into the site, where it can observe
+ protections that only appear once a session interacts. When this target already
+ has a verified configuration, the run confirms that one instead of re-deriving
+ the whole matrix, so supplying an intent narrows what can be recommended.
+
extra_headers: Send extra headers
extra_query: Add additional query parameters to the request
@@ -198,6 +207,7 @@ def resolve(
{
"url": url,
"allowed_proxy_countries": allowed_proxy_countries,
+ "intent": intent,
},
config_registry_resolve_params.ConfigRegistryResolveParams,
),
@@ -339,6 +349,7 @@ async def resolve(
*,
url: str,
allowed_proxy_countries: SequenceNotStr[str] | Omit = omit,
+ intent: 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,
@@ -358,6 +369,14 @@ async def resolve(
configuration. Kernel may test a subset of allowed countries. When omitted,
Kernel uses its default country selection.
+ intent: Plain-language description of the workload you intend to run against this
+ target, in a sentence or two. Requires an https target, because the pass treats
+ any non-HTTPS destination as off-site and will not drive an http one. Kernel
+ uses it to drive the browser further into the site, where it can observe
+ protections that only appear once a session interacts. When this target already
+ has a verified configuration, the run confirms that one instead of re-deriving
+ the whole matrix, so supplying an intent narrows what can be recommended.
+
extra_headers: Send extra headers
extra_query: Add additional query parameters to the request
@@ -372,6 +391,7 @@ async def resolve(
{
"url": url,
"allowed_proxy_countries": allowed_proxy_countries,
+ "intent": intent,
},
config_registry_resolve_params.ConfigRegistryResolveParams,
),
diff --git a/src/kernel/types/config_registry_resolve_params.py b/src/kernel/types/config_registry_resolve_params.py
index 644c4b7a..2c0ecc00 100644
--- a/src/kernel/types/config_registry_resolve_params.py
+++ b/src/kernel/types/config_registry_resolve_params.py
@@ -19,3 +19,14 @@ class ConfigRegistryResolveParams(TypedDict, total=False):
configuration. Kernel may test a subset of allowed countries. When omitted,
Kernel uses its default country selection.
"""
+
+ intent: str
+ """
+ Plain-language description of the workload you intend to run against this
+ target, in a sentence or two. Requires an https target, because the pass treats
+ any non-HTTPS destination as off-site and will not drive an http one. Kernel
+ uses it to drive the browser further into the site, where it can observe
+ protections that only appear once a session interacts. When this target already
+ has a verified configuration, the run confirms that one instead of re-deriving
+ the whole matrix, so supplying an intent narrows what can be recommended.
+ """
diff --git a/src/kernel/types/config_registry_response.py b/src/kernel/types/config_registry_response.py
index eead2467..dba10b9e 100644
--- a/src/kernel/types/config_registry_response.py
+++ b/src/kernel/types/config_registry_response.py
@@ -1,6 +1,7 @@
# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
from typing import Optional
+from typing_extensions import Literal
from .target import Target
from .._models import BaseModel
@@ -29,3 +30,12 @@ class ConfigRegistryResponse(BaseModel):
prevented success is useful without a configuration. Not verified against this
target. Null when nothing applicable was observed or no notes exist.
"""
+
+ workload_outcome: Optional[
+ Literal["completed", "turn_limit", "auth_required", "payment_required", "blocked", "error"]
+ ] = None
+ """How far the workload pass got, when an intent was supplied and a pass ran.
+
+ A run outcome rather than advice, so it is reported whether or not any guidance
+ could be assembled. Null when no intent was supplied or no pass ran.
+ """
diff --git a/tests/api_resources/test_config_registry.py b/tests/api_resources/test_config_registry.py
index 710d3088..18a3d598 100644
--- a/tests/api_resources/test_config_registry.py
+++ b/tests/api_resources/test_config_registry.py
@@ -119,6 +119,7 @@ def test_method_resolve_with_all_params(self, client: Kernel) -> None:
config_registry = client.config_registry.resolve(
url="https://example.com",
allowed_proxy_countries=["US"],
+ intent="search for a black hoodie and add it to the cart",
)
assert_matches_type(ConfigRegistryResponse, config_registry, path=["response"])
@@ -251,6 +252,7 @@ async def test_method_resolve_with_all_params(self, async_client: AsyncKernel) -
config_registry = await async_client.config_registry.resolve(
url="https://example.com",
allowed_proxy_countries=["US"],
+ intent="search for a black hoodie and add it to the cart",
)
assert_matches_type(ConfigRegistryResponse, config_registry, path=["response"])
From 0fa2fa34cd72d04a12c6f9afd61d87098d43799b Mon Sep 17 00:00:00 2001
From: "kernel-internal[bot]"
<260533166+kernel-internal[bot]@users.noreply.github.com>
Date: Mon, 14 Sep 2026 22:17:11 +0000
Subject: [PATCH 260/366] feat: Honor managed auth browser regions
Stainless-Generated-From: 8b6a7fa50a5a17f5627f4278907725ea8d3993b5
---
src/kernel/types/auth/managed_auth_browser_config.py | 9 +++++++++
.../types/auth/managed_auth_browser_config_param.py | 10 +++++++++-
tests/api_resources/auth/test_connections.py | 6 ++++++
3 files changed, 24 insertions(+), 1 deletion(-)
diff --git a/src/kernel/types/auth/managed_auth_browser_config.py b/src/kernel/types/auth/managed_auth_browser_config.py
index c7e565d1..3309904e 100644
--- a/src/kernel/types/auth/managed_auth_browser_config.py
+++ b/src/kernel/types/auth/managed_auth_browser_config.py
@@ -1,6 +1,7 @@
# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
from typing import Optional
+from typing_extensions import Literal
from ..._models import BaseModel
from ..browser_proxy_config import BrowserProxyConfig
@@ -108,6 +109,14 @@ class ManagedAuthBrowserConfig(BaseModel):
preserve or inherit the connection default.
"""
+ region: Optional[Literal["us-east", "eu-west", "ap-southeast"]] = None
+ """Browser region.
+
+ Omit on create to use us-east, on update to keep the current region, or on login
+ to inherit it. Login overrides apply only to that login. Non-default regions
+ require an eligible plan and organization access.
+ """
+
stealth: Optional[bool] = None
"""Whether managed auth browser sessions use stealth mode.
diff --git a/src/kernel/types/auth/managed_auth_browser_config_param.py b/src/kernel/types/auth/managed_auth_browser_config_param.py
index bfd09f95..6f8eab6d 100644
--- a/src/kernel/types/auth/managed_auth_browser_config_param.py
+++ b/src/kernel/types/auth/managed_auth_browser_config_param.py
@@ -3,7 +3,7 @@
from __future__ import annotations
from typing import Optional
-from typing_extensions import TypedDict
+from typing_extensions import Literal, TypedDict
from ..browser_proxy_config_param import BrowserProxyConfigParam
from ..browsers.browser_telemetry_categories_config_param import BrowserTelemetryCategoriesConfigParam
@@ -110,6 +110,14 @@ class ManagedAuthBrowserConfigParam(TypedDict, total=False):
preserve or inherit the connection default.
"""
+ region: Literal["us-east", "eu-west", "ap-southeast"]
+ """Browser region.
+
+ Omit on create to use us-east, on update to keep the current region, or on login
+ to inherit it. Login overrides apply only to that login. Non-default regions
+ require an eligible plan and organization access.
+ """
+
stealth: bool
"""Whether managed auth browser sessions use stealth mode.
diff --git a/tests/api_resources/auth/test_connections.py b/tests/api_resources/auth/test_connections.py
index 4dff13ee..53a55fa7 100644
--- a/tests/api_resources/auth/test_connections.py
+++ b/tests/api_resources/auth/test_connections.py
@@ -46,6 +46,7 @@ def test_method_create_with_all_params(self, client: Kernel) -> None:
"mode": "direct",
"name": "x",
},
+ "region": "us-east",
"stealth": False,
"telemetry": {
"browser": {
@@ -211,6 +212,7 @@ def test_method_update_with_all_params(self, client: Kernel) -> None:
"mode": "direct",
"name": "x",
},
+ "region": "us-east",
"stealth": False,
"telemetry": {
"browser": {
@@ -461,6 +463,7 @@ def test_method_login_with_all_params(self, client: Kernel) -> None:
"mode": "direct",
"name": "x",
},
+ "region": "us-east",
"stealth": False,
"telemetry": {
"browser": {
@@ -705,6 +708,7 @@ async def test_method_create_with_all_params(self, async_client: AsyncKernel) ->
"mode": "direct",
"name": "x",
},
+ "region": "us-east",
"stealth": False,
"telemetry": {
"browser": {
@@ -870,6 +874,7 @@ async def test_method_update_with_all_params(self, async_client: AsyncKernel) ->
"mode": "direct",
"name": "x",
},
+ "region": "us-east",
"stealth": False,
"telemetry": {
"browser": {
@@ -1120,6 +1125,7 @@ async def test_method_login_with_all_params(self, async_client: AsyncKernel) ->
"mode": "direct",
"name": "x",
},
+ "region": "us-east",
"stealth": False,
"telemetry": {
"browser": {
From 6143aa72764ef417a29e44dbb2b4ee9337ef9f51 Mon Sep 17 00:00:00 2001
From: "kernel-internal[bot]"
<260533166+kernel-internal[bot]@users.noreply.github.com>
Date: Mon, 14 Sep 2026 22:21:57 +0000
Subject: [PATCH 261/366] release: 0.102.0
---
.release-please-manifest.json | 2 +-
CHANGELOG.md | 14 ++++++++++++++
pyproject.toml | 2 +-
src/kernel/_version.py | 2 +-
4 files changed, 17 insertions(+), 3 deletions(-)
diff --git a/.release-please-manifest.json b/.release-please-manifest.json
index aebbc8bc..f70d9978 100644
--- a/.release-please-manifest.json
+++ b/.release-please-manifest.json
@@ -1,3 +1,3 @@
{
- ".": "0.101.0"
+ ".": "0.102.0"
}
\ No newline at end of file
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 77497eb7..80c45f21 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,19 @@
# Changelog
+## [0.102.0](https://github.com/kernel/kernel-python-sdk/compare/v0.101.0...v0.102.0) (2026-09-14)
+
+
+### Features
+
+* Accept a workload intent and use it to widen vendor coverage ([39ffff8](https://github.com/kernel/kernel-python-sdk/commit/39ffff82be3987e2220d8d5057d8d2505991a3eb))
+* Add guarded vault card fill operations ([3927c96](https://github.com/kernel/kernel-python-sdk/commit/3927c961789b4427a72c979ee5bb3dbcae1b8c74))
+* Add single-use AgentCard prepare_checkout for Square ([c439359](https://github.com/kernel/kernel-python-sdk/commit/c43935913b8a471421ef8d96c9066c442ec95171))
+* Allow replacing AgentCard cards after unknown creates ([22a8731](https://github.com/kernel/kernel-python-sdk/commit/22a87318e6ac2f1318a295b9d22de5b2306e66b2))
+* Document punctuation key sequences ([e7850fd](https://github.com/kernel/kernel-python-sdk/commit/e7850fd5757934c731373c5cb09b4444a29d7e84))
+* Honor managed auth browser regions ([0fa2fa3](https://github.com/kernel/kernel-python-sdk/commit/0fa2fa34cd72d04a12c6f9afd61d87098d43799b))
+* Reapply vendor guidance recommendations ([dfcc7ec](https://github.com/kernel/kernel-python-sdk/commit/dfcc7ecbaa087a07d11a5b0eb4738b41b8fadd8b))
+* Report proxy-restricted targets from config registry lookup ([c276ebc](https://github.com/kernel/kernel-python-sdk/commit/c276ebc6fd266924894b300b13ff02f30d0c8527))
+
## [0.101.0](https://github.com/kernel/kernel-python-sdk/compare/v0.100.0...v0.101.0) (2026-09-11)
diff --git a/pyproject.toml b/pyproject.toml
index ce24d358..473c0ec0 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -1,6 +1,6 @@
[project]
name = "kernel"
-version = "0.101.0"
+version = "0.102.0"
description = "The official Python library for the kernel API"
dynamic = ["readme"]
license = "Apache-2.0"
diff --git a/src/kernel/_version.py b/src/kernel/_version.py
index 058933d0..b5daff26 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.101.0" # x-release-please-version
+__version__ = "0.102.0" # x-release-please-version
From 4ce4ab9f533b4f5d9fe420f91f1a0054c0e13c1b Mon Sep 17 00:00:00 2001
From: "kernel-internal[bot]"
<260533166+kernel-internal[bot]@users.noreply.github.com>
Date: Mon, 14 Sep 2026 23:46:01 +0000
Subject: [PATCH 262/366] feat: chore(stlc): seal custom-code tracking files
Stainless-Generated-From: c38e2308cd507f413c42d0d729d082ccd535debe
---
api.md | 15 +
src/kernel/resources/vaults/items.py | 482 ++++++++++--
src/kernel/types/vaults/__init__.py | 24 +-
...lect_vault_item_operation_request_param.py | 27 +
.../vaults/credential_collection_action.py | 50 ++
.../credential_vault_field_definition.py | 31 +
.../credential_vault_field_input_param.py | 41 ++
.../vaults/credential_vault_field_state.py | 18 +
.../vaults/credential_vault_field_type.py | 7 +
.../credential_vault_field_update_param.py | 20 +
.../types/vaults/credential_vault_item.py | 93 +++
.../credential_vault_item_request_param.py | 36 +
.../vaults/credential_vault_item_spec.py | 19 +
.../credential_vault_item_spec_input_param.py | 27 +
...credential_vault_item_spec_update_param.py | 21 +
.../vaults/credential_vault_item_state.py | 20 +
...dential_vault_item_update_request_param.py | 38 +
...fill_vault_item_operation_request_param.py | 23 +-
.../vaults/item_perform_operation_params.py | 26 +-
.../types/vaults/item_retrieve_params.py | 6 +-
src/kernel/types/vaults/item_update_params.py | 32 +-
src/kernel/types/vaults/item_upsert_params.py | 17 +-
.../vaults/vault_card_fill_field_param.py | 60 --
.../types/vaults/vault_fill_field_param.py | 23 +
src/kernel/types/vaults/vault_item.py | 9 +-
.../vaults/vault_item_operation_response.py | 9 +-
tests/api_resources/vaults/test_items.py | 690 ++++++++++++++----
27 files changed, 1598 insertions(+), 266 deletions(-)
create mode 100644 src/kernel/types/vaults/collect_vault_item_operation_request_param.py
create mode 100644 src/kernel/types/vaults/credential_collection_action.py
create mode 100644 src/kernel/types/vaults/credential_vault_field_definition.py
create mode 100644 src/kernel/types/vaults/credential_vault_field_input_param.py
create mode 100644 src/kernel/types/vaults/credential_vault_field_state.py
create mode 100644 src/kernel/types/vaults/credential_vault_field_type.py
create mode 100644 src/kernel/types/vaults/credential_vault_field_update_param.py
create mode 100644 src/kernel/types/vaults/credential_vault_item.py
create mode 100644 src/kernel/types/vaults/credential_vault_item_request_param.py
create mode 100644 src/kernel/types/vaults/credential_vault_item_spec.py
create mode 100644 src/kernel/types/vaults/credential_vault_item_spec_input_param.py
create mode 100644 src/kernel/types/vaults/credential_vault_item_spec_update_param.py
create mode 100644 src/kernel/types/vaults/credential_vault_item_state.py
create mode 100644 src/kernel/types/vaults/credential_vault_item_update_request_param.py
delete mode 100644 src/kernel/types/vaults/vault_card_fill_field_param.py
create mode 100644 src/kernel/types/vaults/vault_fill_field_param.py
diff --git a/api.md b/api.md
index 1c9b02a3..fdb9696b 100644
--- a/api.md
+++ b/api.md
@@ -529,12 +529,27 @@ from kernel.types.vaults import (
AuthorizeVaultItemOperationRequest,
CardVaultItemSpec,
CardVaultItemState,
+ CollectVaultItemOperationRequest,
+ CredentialCollectionAction,
+ CredentialVaultFieldDefinition,
+ CredentialVaultFieldInput,
+ CredentialVaultFieldState,
+ CredentialVaultFieldType,
+ CredentialVaultFieldUpdate,
+ CredentialVaultItem,
+ CredentialVaultItemRequest,
+ CredentialVaultItemSpec,
+ CredentialVaultItemSpecInput,
+ CredentialVaultItemSpecUpdate,
+ CredentialVaultItemState,
+ CredentialVaultItemUpdateRequest,
FillVaultItemOperationRequest,
FillVaultItemOperationResult,
PrepareCheckoutVaultItemOperationRequest,
VaultCardAliases,
VaultCardFillField,
VaultCheckoutContext,
+ VaultFillField,
VaultFillFieldResult,
VaultItem,
VaultItemAction,
diff --git a/src/kernel/resources/vaults/items.py b/src/kernel/resources/vaults/items.py
index 02315e2b..507cf048 100644
--- a/src/kernel/resources/vaults/items.py
+++ b/src/kernel/resources/vaults/items.py
@@ -28,10 +28,12 @@
from ...types.vaults.vault_item import VaultItem
from ...types.vaults.item_list_response import ItemListResponse
from ...types.vaults.item_events_response import ItemEventsResponse
+from ...types.vaults.vault_fill_field_param import VaultFillFieldParam
from ...types.vaults.card_vault_item_spec_param import CardVaultItemSpecParam
-from ...types.vaults.vault_card_fill_field_param import VaultCardFillFieldParam
from ...types.vaults.vault_checkout_context_param import VaultCheckoutContextParam
from ...types.vaults.vault_item_operation_response import VaultItemOperationResponse
+from ...types.vaults.credential_vault_item_spec_input_param import CredentialVaultItemSpecInputParam
+from ...types.vaults.credential_vault_item_spec_update_param import CredentialVaultItemSpecUpdateParam
__all__ = ["ItemsResource", "AsyncItemsResource"]
@@ -75,13 +77,19 @@ def retrieve(
and live data that can be requested through `expand`. Read each operation's
description before using it. Expanded data is fetched from the provider and is
not persisted in the vault item. Requesting an unavailable expansion returns 409
- instead of a partial item.
+ instead of a partial item. Pending credential items return a collection action.
+ Kernel-hosted active collection links are renewed atomically on expiry for ready
+ or pending items without changing the item version. Invoke collect to open a
+ form for a ready item without clearing values. Sensitive credential values are
+ never returned.
Args:
expand: Live fields advertised by `available_expansions` to include in `expanded`.
- wait: Hold for up to this many seconds while the item is pending authorization or
- approval.
+ wait: Hold for up to this many seconds while the item is pending authorization,
+ approval, or credential collection. Return the current item when ready or when
+ the wait elapses. This does not wait for edits to an already-ready credential;
+ poll GET without wait and compare version to observe changes after collect.
extra_headers: Send extra headers
@@ -116,12 +124,14 @@ def retrieve(
),
)
+ @overload
def update(
self,
key: str,
*,
id_or_name: str,
spec: CardVaultItemSpecParam,
+ type: Literal["card"] | 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,
@@ -129,15 +139,19 @@ def update(
extra_body: Body | None = None,
timeout: float | httpx.Timeout | None | NotGiven = not_given,
) -> VaultItem:
- """Requested cards accept a replacement specification.
-
- Pending issuance requests
- may update provider-supported fields on their existing request, subject to
- atomic provider approval checks; omitted optional fields remain unchanged and
- explicit empty lists clear them. Wallet/provider binding and unsupported fields
- cannot change after authorization starts. An uncertain update enters
- recovery_required and must not be retried. Checkout cards may be edited between
- authorizations.
+ """
+ Credential updates require type credential and the current version, and change
+ only values or description; omitted values are preserved, nonempty strings
+ replace, and null or empty strings clear supported fields. Clearing required
+ text/email/password values returns pending_collection; browser forms still
+ require nonempty required inputs. Card updates may omit type for compatibility
+ with legacy requests. Requested cards accept a replacement specification.
+ Pending issuance requests may update provider-supported fields on their existing
+ request, subject to atomic provider approval checks; omitted optional fields
+ remain unchanged and explicit empty lists clear them. Wallet/provider binding
+ and unsupported fields cannot change after authorization starts. An uncertain
+ update enters recovery_required and must not be retried. Checkout cards may be
+ edited between authorizations.
Args:
spec: Live payment card. Test-mode card creation is not supported.
@@ -150,6 +164,74 @@ def update(
timeout: Override the client-level default timeout for this request, in seconds
"""
+ ...
+
+ @overload
+ def update(
+ self,
+ key: str,
+ *,
+ id_or_name: str,
+ spec: CredentialVaultItemSpecUpdateParam,
+ type: Literal["credential"],
+ version: int,
+ expected_item_id: str | Omit = omit,
+ # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
+ # The extra values given here take precedence over values defined on the client or passed to this method.
+ extra_headers: Headers | None = None,
+ extra_query: Query | None = None,
+ extra_body: Body | None = None,
+ timeout: float | httpx.Timeout | None | NotGiven = not_given,
+ ) -> VaultItem:
+ """
+ Credential updates require type credential and the current version, and change
+ only values or description; omitted values are preserved, nonempty strings
+ replace, and null or empty strings clear supported fields. Clearing required
+ text/email/password values returns pending_collection; browser forms still
+ require nonempty required inputs. Card updates may omit type for compatibility
+ with legacy requests. Requested cards accept a replacement specification.
+ Pending issuance requests may update provider-supported fields on their existing
+ request, subject to atomic provider approval checks; omitted optional fields
+ remain unchanged and explicit empty lists clear them. Wallet/provider binding
+ and unsupported fields cannot change after authorization starts. An uncertain
+ update enters recovery_required and must not be retried. Checkout cards may be
+ edited between authorizations.
+
+ Args:
+ version: Expected current item version from the latest read.
+
+ expected_item_id: Optional immutable item ID precondition. Returns 409 if the key now identifies a
+ different item. Accepted writes target this immutable ID, preventing
+ replacement-key races. Supply this when submitting a form bound to a previously
+ read item.
+
+ 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
+ """
+ ...
+
+ @required_args(["id_or_name", "spec"], ["id_or_name", "spec", "type", "version"])
+ def update(
+ self,
+ key: str,
+ *,
+ id_or_name: str,
+ spec: CardVaultItemSpecParam | CredentialVaultItemSpecUpdateParam,
+ type: Literal["card"] | Literal["credential"] | Omit = omit,
+ version: int | Omit = omit,
+ expected_item_id: str | Omit = omit,
+ # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
+ # The extra values given here take precedence over values defined on the client or passed to this method.
+ extra_headers: Headers | None = None,
+ extra_query: Query | None = None,
+ extra_body: Body | None = None,
+ timeout: float | httpx.Timeout | None | NotGiven = not_given,
+ ) -> VaultItem:
if not id_or_name:
raise ValueError(f"Expected a non-empty value for `id_or_name` but received {id_or_name!r}")
if not key:
@@ -158,7 +240,15 @@ def update(
VaultItem,
self._patch(
path_template("/vaults/{id_or_name}/items/{key}", id_or_name=id_or_name, key=key),
- body=maybe_transform({"spec": spec}, item_update_params.ItemUpdateParams),
+ body=maybe_transform(
+ {
+ "spec": spec,
+ "type": type,
+ "version": version,
+ "expected_item_id": expected_item_id,
+ },
+ item_update_params.ItemUpdateParams,
+ ),
options=make_request_options(
extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
),
@@ -177,8 +267,11 @@ def list(
extra_body: Body | None = None,
timeout: float | httpx.Timeout | None | NotGiven = not_given,
) -> ItemListResponse:
- """
- List vault items without secret values
+ """Credential entries include safe field metadata and non-sensitive values.
+
+ Listing
+ never creates or renews collection sessions; only an existing unexpired active
+ session is included. Use single-item GET or collect to obtain a fresh link.
Args:
extra_headers: Send extra headers
@@ -337,6 +430,51 @@ def perform_operation(
"""
...
+ @overload
+ def perform_operation(
+ self,
+ key: str,
+ *,
+ id_or_name: str,
+ type: Literal["collect"],
+ # 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,
+ ) -> VaultItemOperationResponse:
+ """
+ Retrieve the item first and invoke only an operation listed in
+ `available_operations`, following its natural-language description. Availability
+ is rechecked at execution time; unavailable operations return 409. Authorization
+ and preparation may call an external provider and return updated state. Link
+ cards advertise authorize without checkout context. Eligible unused AgentCard
+ cards advertise prepare_checkout, which requires checkout context and obtains
+ device approval before native Square Pay. Keep the returned approval page open,
+ poll until ready_to_submit, then submit before preparation.expires_at. Unused
+ preparations expire automatically and cannot be reused. If spend-request
+ creation is rate limited, returns HTTP 429 with code
+ `spend_request_rate_limited`; stop and back off before retrying.
+
+ Fill returns a value-free execution result. Validation failures before writing
+ return 400 (invalid request or targets), 403 (access or destination denied), 404
+ (resource not found), or 409 (item or browser not ready). Once writing starts,
+ known partial failures and indeterminate field outcomes return 200 with status
+ `failed` or `unknown`, not an automatic-retry signal. A transport error may
+ leave the outcome unknown; do not automatically retry.
+
+ Args:
+ extra_headers: Send extra headers
+
+ extra_query: Add additional query parameters to the request
+
+ extra_body: Add additional JSON properties to the request
+
+ timeout: Override the client-level default timeout for this request, in seconds
+ """
+ ...
+
@overload
def perform_operation(
self,
@@ -396,9 +534,9 @@ def perform_operation(
*,
id_or_name: str,
browser_id: str,
- fields: Iterable[VaultCardFillFieldParam],
- page_url: str,
+ fields: Iterable[VaultFillFieldParam],
type: Literal["fill"],
+ page_url: str | Omit = omit,
timeout_ms: int | Omit = omit,
# Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
# The extra values given here take precedence over values defined on the client or passed to this method.
@@ -434,7 +572,9 @@ def perform_operation(
page_url: Exact current top-level page URL, including path, query, and fragment. Must
match exactly one open page in the browser; zero or multiple matches fail. No
- prefix or glob matching. Must use HTTPS without embedded credentials.
+ prefix or glob matching. Required for cards, which must use HTTPS without
+ embedded credentials. Optional for credentials, where omission requires exactly
+ one open page.
timeout_ms: Total operation deadline in milliseconds, not a per-field timeout.
@@ -449,19 +589,17 @@ def perform_operation(
...
@required_args(
- ["id_or_name", "type"],
- ["id_or_name", "checkout", "type"],
- ["id_or_name", "browser_id", "fields", "page_url", "type"],
+ ["id_or_name", "type"], ["id_or_name", "checkout", "type"], ["id_or_name", "browser_id", "fields", "type"]
)
def perform_operation(
self,
key: str,
*,
id_or_name: str,
- type: Literal["authorize"] | Literal["prepare_checkout"] | Literal["fill"],
+ type: Literal["authorize"] | Literal["collect"] | Literal["prepare_checkout"] | Literal["fill"],
checkout: VaultCheckoutContextParam | Omit = omit,
browser_id: str | Omit = omit,
- fields: Iterable[VaultCardFillFieldParam] | Omit = omit,
+ fields: Iterable[VaultFillFieldParam] | Omit = omit,
page_url: str | Omit = omit,
timeout_ms: int | Omit = omit,
# Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
@@ -520,7 +658,10 @@ def upsert(
card in any lifecycle state without polling the provider, reauthorizing,
replacing aliases, or resetting recovery. Conflicting specifications return 409.
Provider-specific authorization requirements and retry behavior are described in
- the item's request schema.
+ the item's request schema. Do not use credential items to store, collect, or
+ fill credit card data, including card numbers (PANs), security codes (CVV/CVC),
+ or expiration dates. Use wallet and card item types for credit cards and payment
+ checkout instead.
Args:
spec: AgentCard wallet. Omit provider_config to use Kernel-managed credentials, or
@@ -561,7 +702,10 @@ def upsert(
card in any lifecycle state without polling the provider, reauthorizing,
replacing aliases, or resetting recovery. Conflicting specifications return 409.
Provider-specific authorization requirements and retry behavior are described in
- the item's request schema.
+ the item's request schema. Do not use credential items to store, collect, or
+ fill credit card data, including card numbers (PANs), security codes (CVV/CVC),
+ or expiration dates. Use wallet and card item types for credit cards and payment
+ checkout instead.
Args:
spec: Live payment card. Test-mode card creation is not supported.
@@ -576,14 +720,57 @@ def upsert(
"""
...
+ @overload
+ def upsert(
+ self,
+ key: str,
+ *,
+ id_or_name: str,
+ spec: CredentialVaultItemSpecInputParam,
+ type: Literal["credential"],
+ # 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,
+ ) -> VaultItem:
+ """
+ Create an item under a key unique within its vault, or retrieve the existing
+ item when its specification matches. An identical card PUT returns the existing
+ card in any lifecycle state without polling the provider, reauthorizing,
+ replacing aliases, or resetting recovery. Conflicting specifications return 409.
+ Provider-specific authorization requirements and retry behavior are described in
+ the item's request schema. Do not use credential items to store, collect, or
+ fill credit card data, including card numbers (PANs), security codes (CVV/CVC),
+ or expiration dates. Use wallet and card item types for credit cards and payment
+ checkout instead.
+
+ Args:
+ spec: Credential fields are for login and other non-payment credentials. Do not store,
+ collect, or fill credit card data in credential items. Use wallet and card item
+ types for credit cards and payment checkout instead.
+
+ 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
+ """
+ ...
+
@required_args(["id_or_name", "spec", "type"])
def upsert(
self,
key: str,
*,
id_or_name: str,
- spec: item_upsert_params.WalletVaultItemRequestSpec | CardVaultItemSpecParam,
- type: Literal["wallet"] | Literal["card"],
+ spec: item_upsert_params.WalletVaultItemRequestSpec
+ | CardVaultItemSpecParam
+ | CredentialVaultItemSpecInputParam,
+ type: Literal["wallet"] | Literal["card"] | Literal["credential"],
# 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,
@@ -653,13 +840,19 @@ async def retrieve(
and live data that can be requested through `expand`. Read each operation's
description before using it. Expanded data is fetched from the provider and is
not persisted in the vault item. Requesting an unavailable expansion returns 409
- instead of a partial item.
+ instead of a partial item. Pending credential items return a collection action.
+ Kernel-hosted active collection links are renewed atomically on expiry for ready
+ or pending items without changing the item version. Invoke collect to open a
+ form for a ready item without clearing values. Sensitive credential values are
+ never returned.
Args:
expand: Live fields advertised by `available_expansions` to include in `expanded`.
- wait: Hold for up to this many seconds while the item is pending authorization or
- approval.
+ wait: Hold for up to this many seconds while the item is pending authorization,
+ approval, or credential collection. Return the current item when ready or when
+ the wait elapses. This does not wait for edits to an already-ready credential;
+ poll GET without wait and compare version to observe changes after collect.
extra_headers: Send extra headers
@@ -694,12 +887,14 @@ async def retrieve(
),
)
+ @overload
async def update(
self,
key: str,
*,
id_or_name: str,
spec: CardVaultItemSpecParam,
+ type: Literal["card"] | 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,
@@ -707,15 +902,19 @@ async def update(
extra_body: Body | None = None,
timeout: float | httpx.Timeout | None | NotGiven = not_given,
) -> VaultItem:
- """Requested cards accept a replacement specification.
-
- Pending issuance requests
- may update provider-supported fields on their existing request, subject to
- atomic provider approval checks; omitted optional fields remain unchanged and
- explicit empty lists clear them. Wallet/provider binding and unsupported fields
- cannot change after authorization starts. An uncertain update enters
- recovery_required and must not be retried. Checkout cards may be edited between
- authorizations.
+ """
+ Credential updates require type credential and the current version, and change
+ only values or description; omitted values are preserved, nonempty strings
+ replace, and null or empty strings clear supported fields. Clearing required
+ text/email/password values returns pending_collection; browser forms still
+ require nonempty required inputs. Card updates may omit type for compatibility
+ with legacy requests. Requested cards accept a replacement specification.
+ Pending issuance requests may update provider-supported fields on their existing
+ request, subject to atomic provider approval checks; omitted optional fields
+ remain unchanged and explicit empty lists clear them. Wallet/provider binding
+ and unsupported fields cannot change after authorization starts. An uncertain
+ update enters recovery_required and must not be retried. Checkout cards may be
+ edited between authorizations.
Args:
spec: Live payment card. Test-mode card creation is not supported.
@@ -728,6 +927,74 @@ async def update(
timeout: Override the client-level default timeout for this request, in seconds
"""
+ ...
+
+ @overload
+ async def update(
+ self,
+ key: str,
+ *,
+ id_or_name: str,
+ spec: CredentialVaultItemSpecUpdateParam,
+ type: Literal["credential"],
+ version: int,
+ expected_item_id: str | Omit = omit,
+ # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
+ # The extra values given here take precedence over values defined on the client or passed to this method.
+ extra_headers: Headers | None = None,
+ extra_query: Query | None = None,
+ extra_body: Body | None = None,
+ timeout: float | httpx.Timeout | None | NotGiven = not_given,
+ ) -> VaultItem:
+ """
+ Credential updates require type credential and the current version, and change
+ only values or description; omitted values are preserved, nonempty strings
+ replace, and null or empty strings clear supported fields. Clearing required
+ text/email/password values returns pending_collection; browser forms still
+ require nonempty required inputs. Card updates may omit type for compatibility
+ with legacy requests. Requested cards accept a replacement specification.
+ Pending issuance requests may update provider-supported fields on their existing
+ request, subject to atomic provider approval checks; omitted optional fields
+ remain unchanged and explicit empty lists clear them. Wallet/provider binding
+ and unsupported fields cannot change after authorization starts. An uncertain
+ update enters recovery_required and must not be retried. Checkout cards may be
+ edited between authorizations.
+
+ Args:
+ version: Expected current item version from the latest read.
+
+ expected_item_id: Optional immutable item ID precondition. Returns 409 if the key now identifies a
+ different item. Accepted writes target this immutable ID, preventing
+ replacement-key races. Supply this when submitting a form bound to a previously
+ read item.
+
+ 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
+ """
+ ...
+
+ @required_args(["id_or_name", "spec"], ["id_or_name", "spec", "type", "version"])
+ async def update(
+ self,
+ key: str,
+ *,
+ id_or_name: str,
+ spec: CardVaultItemSpecParam | CredentialVaultItemSpecUpdateParam,
+ type: Literal["card"] | Literal["credential"] | Omit = omit,
+ version: int | Omit = omit,
+ expected_item_id: str | Omit = omit,
+ # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
+ # The extra values given here take precedence over values defined on the client or passed to this method.
+ extra_headers: Headers | None = None,
+ extra_query: Query | None = None,
+ extra_body: Body | None = None,
+ timeout: float | httpx.Timeout | None | NotGiven = not_given,
+ ) -> VaultItem:
if not id_or_name:
raise ValueError(f"Expected a non-empty value for `id_or_name` but received {id_or_name!r}")
if not key:
@@ -736,7 +1003,15 @@ async def update(
VaultItem,
await self._patch(
path_template("/vaults/{id_or_name}/items/{key}", id_or_name=id_or_name, key=key),
- body=await async_maybe_transform({"spec": spec}, item_update_params.ItemUpdateParams),
+ body=await async_maybe_transform(
+ {
+ "spec": spec,
+ "type": type,
+ "version": version,
+ "expected_item_id": expected_item_id,
+ },
+ item_update_params.ItemUpdateParams,
+ ),
options=make_request_options(
extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
),
@@ -755,8 +1030,11 @@ async def list(
extra_body: Body | None = None,
timeout: float | httpx.Timeout | None | NotGiven = not_given,
) -> ItemListResponse:
- """
- List vault items without secret values
+ """Credential entries include safe field metadata and non-sensitive values.
+
+ Listing
+ never creates or renews collection sessions; only an existing unexpired active
+ session is included. Use single-item GET or collect to obtain a fresh link.
Args:
extra_headers: Send extra headers
@@ -915,6 +1193,51 @@ async def perform_operation(
"""
...
+ @overload
+ async def perform_operation(
+ self,
+ key: str,
+ *,
+ id_or_name: str,
+ type: Literal["collect"],
+ # 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,
+ ) -> VaultItemOperationResponse:
+ """
+ Retrieve the item first and invoke only an operation listed in
+ `available_operations`, following its natural-language description. Availability
+ is rechecked at execution time; unavailable operations return 409. Authorization
+ and preparation may call an external provider and return updated state. Link
+ cards advertise authorize without checkout context. Eligible unused AgentCard
+ cards advertise prepare_checkout, which requires checkout context and obtains
+ device approval before native Square Pay. Keep the returned approval page open,
+ poll until ready_to_submit, then submit before preparation.expires_at. Unused
+ preparations expire automatically and cannot be reused. If spend-request
+ creation is rate limited, returns HTTP 429 with code
+ `spend_request_rate_limited`; stop and back off before retrying.
+
+ Fill returns a value-free execution result. Validation failures before writing
+ return 400 (invalid request or targets), 403 (access or destination denied), 404
+ (resource not found), or 409 (item or browser not ready). Once writing starts,
+ known partial failures and indeterminate field outcomes return 200 with status
+ `failed` or `unknown`, not an automatic-retry signal. A transport error may
+ leave the outcome unknown; do not automatically retry.
+
+ Args:
+ extra_headers: Send extra headers
+
+ extra_query: Add additional query parameters to the request
+
+ extra_body: Add additional JSON properties to the request
+
+ timeout: Override the client-level default timeout for this request, in seconds
+ """
+ ...
+
@overload
async def perform_operation(
self,
@@ -974,9 +1297,9 @@ async def perform_operation(
*,
id_or_name: str,
browser_id: str,
- fields: Iterable[VaultCardFillFieldParam],
- page_url: str,
+ fields: Iterable[VaultFillFieldParam],
type: Literal["fill"],
+ page_url: str | Omit = omit,
timeout_ms: int | Omit = omit,
# Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
# The extra values given here take precedence over values defined on the client or passed to this method.
@@ -1012,7 +1335,9 @@ async def perform_operation(
page_url: Exact current top-level page URL, including path, query, and fragment. Must
match exactly one open page in the browser; zero or multiple matches fail. No
- prefix or glob matching. Must use HTTPS without embedded credentials.
+ prefix or glob matching. Required for cards, which must use HTTPS without
+ embedded credentials. Optional for credentials, where omission requires exactly
+ one open page.
timeout_ms: Total operation deadline in milliseconds, not a per-field timeout.
@@ -1027,19 +1352,17 @@ async def perform_operation(
...
@required_args(
- ["id_or_name", "type"],
- ["id_or_name", "checkout", "type"],
- ["id_or_name", "browser_id", "fields", "page_url", "type"],
+ ["id_or_name", "type"], ["id_or_name", "checkout", "type"], ["id_or_name", "browser_id", "fields", "type"]
)
async def perform_operation(
self,
key: str,
*,
id_or_name: str,
- type: Literal["authorize"] | Literal["prepare_checkout"] | Literal["fill"],
+ type: Literal["authorize"] | Literal["collect"] | Literal["prepare_checkout"] | Literal["fill"],
checkout: VaultCheckoutContextParam | Omit = omit,
browser_id: str | Omit = omit,
- fields: Iterable[VaultCardFillFieldParam] | Omit = omit,
+ fields: Iterable[VaultFillFieldParam] | Omit = omit,
page_url: str | Omit = omit,
timeout_ms: int | Omit = omit,
# Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
@@ -1098,7 +1421,10 @@ async def upsert(
card in any lifecycle state without polling the provider, reauthorizing,
replacing aliases, or resetting recovery. Conflicting specifications return 409.
Provider-specific authorization requirements and retry behavior are described in
- the item's request schema.
+ the item's request schema. Do not use credential items to store, collect, or
+ fill credit card data, including card numbers (PANs), security codes (CVV/CVC),
+ or expiration dates. Use wallet and card item types for credit cards and payment
+ checkout instead.
Args:
spec: AgentCard wallet. Omit provider_config to use Kernel-managed credentials, or
@@ -1139,7 +1465,10 @@ async def upsert(
card in any lifecycle state without polling the provider, reauthorizing,
replacing aliases, or resetting recovery. Conflicting specifications return 409.
Provider-specific authorization requirements and retry behavior are described in
- the item's request schema.
+ the item's request schema. Do not use credential items to store, collect, or
+ fill credit card data, including card numbers (PANs), security codes (CVV/CVC),
+ or expiration dates. Use wallet and card item types for credit cards and payment
+ checkout instead.
Args:
spec: Live payment card. Test-mode card creation is not supported.
@@ -1154,14 +1483,57 @@ async def upsert(
"""
...
+ @overload
+ async def upsert(
+ self,
+ key: str,
+ *,
+ id_or_name: str,
+ spec: CredentialVaultItemSpecInputParam,
+ type: Literal["credential"],
+ # 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,
+ ) -> VaultItem:
+ """
+ Create an item under a key unique within its vault, or retrieve the existing
+ item when its specification matches. An identical card PUT returns the existing
+ card in any lifecycle state without polling the provider, reauthorizing,
+ replacing aliases, or resetting recovery. Conflicting specifications return 409.
+ Provider-specific authorization requirements and retry behavior are described in
+ the item's request schema. Do not use credential items to store, collect, or
+ fill credit card data, including card numbers (PANs), security codes (CVV/CVC),
+ or expiration dates. Use wallet and card item types for credit cards and payment
+ checkout instead.
+
+ Args:
+ spec: Credential fields are for login and other non-payment credentials. Do not store,
+ collect, or fill credit card data in credential items. Use wallet and card item
+ types for credit cards and payment checkout instead.
+
+ 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
+ """
+ ...
+
@required_args(["id_or_name", "spec", "type"])
async def upsert(
self,
key: str,
*,
id_or_name: str,
- spec: item_upsert_params.WalletVaultItemRequestSpec | CardVaultItemSpecParam,
- type: Literal["wallet"] | Literal["card"],
+ spec: item_upsert_params.WalletVaultItemRequestSpec
+ | CardVaultItemSpecParam
+ | CredentialVaultItemSpecInputParam,
+ type: Literal["wallet"] | Literal["card"] | Literal["credential"],
# 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,
diff --git a/src/kernel/types/vaults/__init__.py b/src/kernel/types/vaults/__init__.py
index b48fd00d..9188791e 100644
--- a/src/kernel/types/vaults/__init__.py
+++ b/src/kernel/types/vaults/__init__.py
@@ -15,20 +15,42 @@
from .item_retrieve_params import ItemRetrieveParams as ItemRetrieveParams
from .vault_payment_method import VaultPaymentMethod as VaultPaymentMethod
from .card_vault_item_state import CardVaultItemState as CardVaultItemState
+from .credential_vault_item import CredentialVaultItem as CredentialVaultItem
+from .vault_fill_field_param import VaultFillFieldParam as VaultFillFieldParam
from .wallet_vault_item_spec import WalletVaultItemSpec as WalletVaultItemSpec
from .vault_fill_field_result import VaultFillFieldResult as VaultFillFieldResult
from .wallet_vault_item_state import WalletVaultItemState as WalletVaultItemState
from .card_vault_item_spec_param import CardVaultItemSpecParam as CardVaultItemSpecParam
-from .vault_card_fill_field_param import VaultCardFillFieldParam as VaultCardFillFieldParam
+from .credential_vault_item_spec import CredentialVaultItemSpec as CredentialVaultItemSpec
+from .credential_vault_field_type import CredentialVaultFieldType as CredentialVaultFieldType
+from .credential_vault_item_state import CredentialVaultItemState as CredentialVaultItemState
+from .credential_collection_action import CredentialCollectionAction as CredentialCollectionAction
+from .credential_vault_field_state import CredentialVaultFieldState as CredentialVaultFieldState
from .vault_checkout_context_param import VaultCheckoutContextParam as VaultCheckoutContextParam
from .item_perform_operation_params import ItemPerformOperationParams as ItemPerformOperationParams
from .vault_item_operation_response import VaultItemOperationResponse as VaultItemOperationResponse
from .agentcard_checkout_preparation import AgentcardCheckoutPreparation as AgentcardCheckoutPreparation
from .agentcard_checkout_authorization import AgentcardCheckoutAuthorization as AgentcardCheckoutAuthorization
from .fill_vault_item_operation_result import FillVaultItemOperationResult as FillVaultItemOperationResult
+from .credential_vault_field_definition import CredentialVaultFieldDefinition as CredentialVaultFieldDefinition
+from .credential_vault_field_input_param import CredentialVaultFieldInputParam as CredentialVaultFieldInputParam
+from .credential_vault_field_update_param import CredentialVaultFieldUpdateParam as CredentialVaultFieldUpdateParam
+from .credential_vault_item_request_param import CredentialVaultItemRequestParam as CredentialVaultItemRequestParam
+from .credential_vault_item_spec_input_param import (
+ CredentialVaultItemSpecInputParam as CredentialVaultItemSpecInputParam,
+)
+from .credential_vault_item_spec_update_param import (
+ CredentialVaultItemSpecUpdateParam as CredentialVaultItemSpecUpdateParam,
+)
from .fill_vault_item_operation_request_param import (
FillVaultItemOperationRequestParam as FillVaultItemOperationRequestParam,
)
+from .collect_vault_item_operation_request_param import (
+ CollectVaultItemOperationRequestParam as CollectVaultItemOperationRequestParam,
+)
+from .credential_vault_item_update_request_param import (
+ CredentialVaultItemUpdateRequestParam as CredentialVaultItemUpdateRequestParam,
+)
from .authorize_vault_item_operation_request_param import (
AuthorizeVaultItemOperationRequestParam as AuthorizeVaultItemOperationRequestParam,
)
diff --git a/src/kernel/types/vaults/collect_vault_item_operation_request_param.py b/src/kernel/types/vaults/collect_vault_item_operation_request_param.py
new file mode 100644
index 00000000..2ce3031c
--- /dev/null
+++ b/src/kernel/types/vaults/collect_vault_item_operation_request_param.py
@@ -0,0 +1,27 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from __future__ import annotations
+
+from typing_extensions import Literal, Required, TypedDict
+
+__all__ = ["CollectVaultItemOperationRequestParam"]
+
+
+class CollectVaultItemOperationRequestParam(TypedDict, total=False):
+ """Return the credential item with its collection action.
+
+ Supported for ready
+ and pending_collection credential items. Always render the same form from
+ every form-supported field; totp fields have no form input and are omitted.
+ No caller-selected field subsets or form overrides are accepted.
+ Reuse an active Kernel-hosted session or renew an expired session atomically.
+ Customer-hosted forms use their own backend and ordinary item GET/PATCH. Opening
+ the form does not clear values or change readiness or item version.
+ To observe edits on a ready item, record its version and poll GET without
+ wait until the version changes, then reconcile the returned state. Version
+ changes may also come from PATCH; they do not identify a particular form
+ submission. Customer-hosted apps use their own submission callback, including
+ for unchanged forms. The wait parameter waits for readiness, not edits.
+ """
+
+ type: Required[Literal["collect"]]
diff --git a/src/kernel/types/vaults/credential_collection_action.py b/src/kernel/types/vaults/credential_collection_action.py
new file mode 100644
index 00000000..331e255c
--- /dev/null
+++ b/src/kernel/types/vaults/credential_collection_action.py
@@ -0,0 +1,50 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from datetime import datetime
+from typing_extensions import Literal
+
+from ..._models import BaseModel
+
+__all__ = ["CredentialCollectionAction"]
+
+
+class CredentialCollectionAction(BaseModel):
+ """
+ One schema-derived form for the item, available in ready or pending_collection
+ state. Render every form-supported field as editable; omit totp fields and
+ preserve their stored seeds. Prefill non-sensitive values,
+ and allow existing sensitive values to be preserved or replaced without ever
+ revealing them. No field subsets or per-request form configuration exist.
+ Validate required fields against the resulting values, including preserved
+ secrets. Submit changed values only, using the version used to render the form.
+ Scoped hosted submission rejects totp edits; seed writes require the ordinary
+ authenticated item API. Customer forms likewise omit totp from their payloads.
+ Save edits atomically. A successful hosted submission increments the version,
+ marks ready, and consumes the session; an empty edit may complete collection
+ while preserving values. A customer form uses PATCH for changed values and
+ does not send an empty PATCH when nothing changed.
+ Kernel-hosted bearer sessions require no Kernel account and are bound to the
+ item version. Expired, superseded, consumed, or deleted-item sessions cannot
+ submit. Authenticated item GET renews expired active sessions for ready or pending items; pending
+ items always receive an action. A ready item with no active session omits the
+ action until collect is invoked. Concurrent renewals return the same link.
+ Renewal changes neither values nor item version. An expired link cannot renew
+ itself. The hosted form handles its collection protocol; callers only open
+ the returned URL and do not extract or submit its token through the public API.
+ For customer-hosted forms, use @onkernel/vault-react and an authenticated
+ customer backend calling the ordinary item GET/PATCH API. Kernel does not
+ store customer collection URLs or authenticate the customer's end users.
+ Treat URLs and submitted values as secrets and exclude them from logs,
+ traces, and errors.
+ """
+
+ expires_at: datetime
+ """Expiry of the Kernel-hosted collection link (30 minutes after issuance)."""
+
+ name: Literal["collect"]
+
+ url: str
+ """Time-scoped hosted form URL (vault.kernel.sh in production).
+
+ Open this URL as returned; treat it as a secret.
+ """
diff --git a/src/kernel/types/vaults/credential_vault_field_definition.py b/src/kernel/types/vaults/credential_vault_field_definition.py
new file mode 100644
index 00000000..9350813b
--- /dev/null
+++ b/src/kernel/types/vaults/credential_vault_field_definition.py
@@ -0,0 +1,31 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from ..._models import BaseModel
+from .credential_vault_field_type import CredentialVaultFieldType
+
+__all__ = ["CredentialVaultFieldDefinition"]
+
+
+class CredentialVaultFieldDefinition(BaseModel):
+ required: bool
+ """Whether a nonempty value is required for readiness and form submission."""
+
+ sensitive: bool
+ """Whether the value is omitted from every item response.
+
+ Reserve true for secrets such as passwords, API tokens, and TOTP seeds. Ordinary
+ usernames and email addresses should be false so the form can display and
+ prefill them.
+ """
+
+ type: CredentialVaultFieldType
+ """
+ Text, email, and password have form inputs; totp does not and is omitted from
+ both Kernel-hosted and customer React forms. Password and totp must be
+ sensitive. A totp value is an RFC 4648 Base32 generator seed (case-insensitive,
+ optional trailing padding), not an otpauth URI or current code. Reject invalid
+ or empty decoded seeds. Browser fill generates an RFC 6238 code at execution
+ time using HMAC-SHA1, 6 digits, and a 30-second period. Preserve leading zeros;
+ never fill the seed. Custom algorithms, digits, periods, and form enrollment are
+ unsupported.
+ """
diff --git a/src/kernel/types/vaults/credential_vault_field_input_param.py b/src/kernel/types/vaults/credential_vault_field_input_param.py
new file mode 100644
index 00000000..2cf68978
--- /dev/null
+++ b/src/kernel/types/vaults/credential_vault_field_input_param.py
@@ -0,0 +1,41 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from __future__ import annotations
+
+from typing_extensions import Required, TypedDict
+
+from .credential_vault_field_type import CredentialVaultFieldType
+
+__all__ = ["CredentialVaultFieldInputParam"]
+
+
+class CredentialVaultFieldInputParam(TypedDict, total=False):
+ type: Required[CredentialVaultFieldType]
+ """
+ Text, email, and password have form inputs; totp does not and is omitted from
+ both Kernel-hosted and customer React forms. Password and totp must be
+ sensitive. A totp value is an RFC 4648 Base32 generator seed (case-insensitive,
+ optional trailing padding), not an otpauth URI or current code. Reject invalid
+ or empty decoded seeds. Browser fill generates an RFC 6238 code at execution
+ time using HMAC-SHA1, 6 digits, and a 30-second period. Preserve leading zeros;
+ never fill the seed. Custom algorithms, digits, periods, and form enrollment are
+ unsupported.
+ """
+
+ required: bool
+
+ sensitive: bool
+ """
+ Set false explicitly for ordinary usernames, email addresses, and other
+ non-secret identifiers. Reserve true for secrets such as passwords, API tokens,
+ and TOTP seeds. Password and totp fields must be true. Omission defaults to true
+ for safety; do not rely on that default for every field. False permits API reads
+ and form prefilling.
+ """
+
+ value: str
+ """
+ Optional initial value satisfying the declared type, at most 16 KiB in UTF-8
+ bytes. Omit to leave unset; null and empty strings are rejected on creation.
+ Sensitive values are encrypted and never copied into the returned spec.
+ """
diff --git a/src/kernel/types/vaults/credential_vault_field_state.py b/src/kernel/types/vaults/credential_vault_field_state.py
new file mode 100644
index 00000000..1c2935cd
--- /dev/null
+++ b/src/kernel/types/vaults/credential_vault_field_state.py
@@ -0,0 +1,18 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from typing import Optional
+
+from ..._models import BaseModel
+
+__all__ = ["CredentialVaultFieldState"]
+
+
+class CredentialVaultFieldState(BaseModel):
+ has_value: bool
+
+ value: Optional[str] = None
+ """Present exactly when has_value is true and the field is not sensitive.
+
+ Reflects the latest developer or human edit. For totp, has_value indicates a
+ stored seed; neither the seed nor a generated code is returned.
+ """
diff --git a/src/kernel/types/vaults/credential_vault_field_type.py b/src/kernel/types/vaults/credential_vault_field_type.py
new file mode 100644
index 00000000..cb400e9c
--- /dev/null
+++ b/src/kernel/types/vaults/credential_vault_field_type.py
@@ -0,0 +1,7 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from typing_extensions import Literal, TypeAlias
+
+__all__ = ["CredentialVaultFieldType"]
+
+CredentialVaultFieldType: TypeAlias = Literal["text", "email", "password", "totp"]
diff --git a/src/kernel/types/vaults/credential_vault_field_update_param.py b/src/kernel/types/vaults/credential_vault_field_update_param.py
new file mode 100644
index 00000000..9d4a532b
--- /dev/null
+++ b/src/kernel/types/vaults/credential_vault_field_update_param.py
@@ -0,0 +1,20 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from __future__ import annotations
+
+from typing import Optional
+from typing_extensions import Required, TypedDict
+
+__all__ = ["CredentialVaultFieldUpdateParam"]
+
+
+class CredentialVaultFieldUpdateParam(TypedDict, total=False):
+ value: Required[Optional[str]]
+ """
+ Replacement value (at most 16 KiB in UTF-8 bytes), or null or an empty string to
+ immediately clear the stored value. Clearing a required form-supported field
+ reopens collection; clearing an optional field does not prevent readiness.
+ Values must satisfy the declared field type. For totp, value is the generator
+ seed, never a current code. Clearing a required totp field returns 400 because
+ it cannot be collected in a form.
+ """
diff --git a/src/kernel/types/vaults/credential_vault_item.py b/src/kernel/types/vaults/credential_vault_item.py
new file mode 100644
index 00000000..79c12abc
--- /dev/null
+++ b/src/kernel/types/vaults/credential_vault_item.py
@@ -0,0 +1,93 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from typing import List, Optional
+from datetime import datetime
+from typing_extensions import Literal
+
+from ..._models import BaseModel
+from .credential_vault_item_spec import CredentialVaultItemSpec
+from .credential_vault_item_state import CredentialVaultItemState
+from .credential_collection_action import CredentialCollectionAction
+
+__all__ = ["CredentialVaultItem", "AvailableExpansion", "AvailableOperation"]
+
+
+class AvailableExpansion(BaseModel):
+ """
+ Live data that can currently be requested by passing its type to the item GET expand parameter.
+ """
+
+ description: str
+
+ type: Literal["payment_methods"]
+
+
+class AvailableOperation(BaseModel):
+ """An operation that is currently valid for this item.
+
+ Read the description before invoking it through the item operations endpoint.
+ """
+
+ description: str
+
+ type: Literal["authorize", "collect", "prepare_checkout", "fill"]
+
+
+class CredentialVaultItem(BaseModel):
+ id: str
+
+ available_expansions: List[AvailableExpansion]
+
+ available_operations: List[AvailableOperation]
+ """Advertises collect for ready and pending_collection items.
+
+ Browser fill is advertised only when separately implemented and eligible.
+ """
+
+ created_at: datetime
+
+ key: str
+ """Immutable item key assigned when the item is created."""
+
+ spec: CredentialVaultItemSpec
+
+ state: CredentialVaultItemState
+
+ type: Literal["credential"]
+
+ updated_at: datetime
+
+ version: int
+ """
+ Starts at 1 and increments on PATCH and successful hosted submission, but not
+ collection-link renewal.
+ """
+
+ action: Optional[CredentialCollectionAction] = None
+ """
+ One schema-derived form for the item, available in ready or pending_collection
+ state. Render every form-supported field as editable; omit totp fields and
+ preserve their stored seeds. Prefill non-sensitive values, and allow existing
+ sensitive values to be preserved or replaced without ever revealing them. No
+ field subsets or per-request form configuration exist. Validate required fields
+ against the resulting values, including preserved secrets. Submit changed values
+ only, using the version used to render the form. Scoped hosted submission
+ rejects totp edits; seed writes require the ordinary authenticated item API.
+ Customer forms likewise omit totp from their payloads. Save edits atomically. A
+ successful hosted submission increments the version, marks ready, and consumes
+ the session; an empty edit may complete collection while preserving values. A
+ customer form uses PATCH for changed values and does not send an empty PATCH
+ when nothing changed. Kernel-hosted bearer sessions require no Kernel account
+ and are bound to the item version. Expired, superseded, consumed, or
+ deleted-item sessions cannot submit. Authenticated item GET renews expired
+ active sessions for ready or pending items; pending items always receive an
+ action. A ready item with no active session omits the action until collect is
+ invoked. Concurrent renewals return the same link. Renewal changes neither
+ values nor item version. An expired link cannot renew itself. The hosted form
+ handles its collection protocol; callers only open the returned URL and do not
+ extract or submit its token through the public API. For customer-hosted forms,
+ use @onkernel/vault-react and an authenticated customer backend calling the
+ ordinary item GET/PATCH API. Kernel does not store customer collection URLs or
+ authenticate the customer's end users. Treat URLs and submitted values as
+ secrets and exclude them from logs, traces, and errors.
+ """
diff --git a/src/kernel/types/vaults/credential_vault_item_request_param.py b/src/kernel/types/vaults/credential_vault_item_request_param.py
new file mode 100644
index 00000000..579d7a0b
--- /dev/null
+++ b/src/kernel/types/vaults/credential_vault_item_request_param.py
@@ -0,0 +1,36 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from __future__ import annotations
+
+from typing_extensions import Literal, Required, TypedDict
+
+from .credential_vault_item_spec_input_param import CredentialVaultItemSpecInputParam
+
+__all__ = ["CredentialVaultItemRequestParam"]
+
+
+class CredentialVaultItemRequestParam(TypedDict, total=False):
+ """
+ Create a credential item without a wallet or external provider.
+ Do not use credential items to store, collect, or fill credit card data,
+ including card numbers (PANs), security codes (CVV/CVC), or expiration dates.
+ Use wallet and card item types for credit cards and payment checkout instead.
+ If all required fields have values, return ready without a collection action;
+ collect can still open its form. Otherwise return pending_collection with
+ a time-scoped Kernel-hosted collection action. Missing
+ optional fields alone do not trigger collection. Repeating the original
+ creation request returns the current item without overwriting later edits;
+ a different request at the same key returns 409. Use PATCH for updates.
+ Required totp fields must include a valid seed on creation; otherwise return
+ 400 rather than opening a form that cannot collect it. Optional totp fields
+ may be unset and populated later through PATCH.
+ """
+
+ spec: Required[CredentialVaultItemSpecInputParam]
+ """Credential fields are for login and other non-payment credentials.
+
+ Do not store, collect, or fill credit card data in credential items. Use wallet
+ and card item types for credit cards and payment checkout instead.
+ """
+
+ type: Required[Literal["credential"]]
diff --git a/src/kernel/types/vaults/credential_vault_item_spec.py b/src/kernel/types/vaults/credential_vault_item_spec.py
new file mode 100644
index 00000000..e953f416
--- /dev/null
+++ b/src/kernel/types/vaults/credential_vault_item_spec.py
@@ -0,0 +1,19 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from typing import Dict, Optional
+
+from ..._models import BaseModel
+from .credential_vault_field_definition import CredentialVaultFieldDefinition
+
+__all__ = ["CredentialVaultItemSpec"]
+
+
+class CredentialVaultItemSpec(BaseModel):
+ fields: Dict[str, CredentialVaultFieldDefinition]
+
+ description: Optional[str] = None
+ """
+ Recognizable site or service name displayed verbatim as the form title, without
+ suffixes such as sign-in credentials. Display text only, not an enforced
+ destination policy.
+ """
diff --git a/src/kernel/types/vaults/credential_vault_item_spec_input_param.py b/src/kernel/types/vaults/credential_vault_item_spec_input_param.py
new file mode 100644
index 00000000..a0e8850a
--- /dev/null
+++ b/src/kernel/types/vaults/credential_vault_item_spec_input_param.py
@@ -0,0 +1,27 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from __future__ import annotations
+
+from typing import Dict
+from typing_extensions import Required, TypedDict
+
+from .credential_vault_field_input_param import CredentialVaultFieldInputParam
+
+__all__ = ["CredentialVaultItemSpecInputParam"]
+
+
+class CredentialVaultItemSpecInputParam(TypedDict, total=False):
+ """Credential fields are for login and other non-payment credentials.
+
+ Do not store, collect, or fill credit card data in credential items. Use wallet and card item types for credit cards and payment checkout instead.
+ """
+
+ fields: Required[Dict[str, CredentialVaultFieldInputParam]]
+
+ description: str
+ """
+ The site's recognizable display name, used verbatim as the user-facing form
+ title (for example, Hacker News). Use only the site or service name; do not
+ append sign-in, login, credentials, or task instructions. This is display text,
+ not an enforced destination policy. At most 16 KiB in UTF-8 bytes.
+ """
diff --git a/src/kernel/types/vaults/credential_vault_item_spec_update_param.py b/src/kernel/types/vaults/credential_vault_item_spec_update_param.py
new file mode 100644
index 00000000..d7933b9b
--- /dev/null
+++ b/src/kernel/types/vaults/credential_vault_item_spec_update_param.py
@@ -0,0 +1,21 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from __future__ import annotations
+
+from typing import Dict
+from typing_extensions import TypedDict
+
+from .credential_vault_field_update_param import CredentialVaultFieldUpdateParam
+
+__all__ = ["CredentialVaultItemSpecUpdateParam"]
+
+
+class CredentialVaultItemSpecUpdateParam(TypedDict, total=False):
+ description: str
+ """
+ Recognizable site or service name used as the form title, without suffixes such
+ as sign-in credentials. An empty string clears it. Display text only, not an
+ enforced destination policy. The server also enforces a 16 KiB UTF-8 byte limit.
+ """
+
+ fields: Dict[str, CredentialVaultFieldUpdateParam]
diff --git a/src/kernel/types/vaults/credential_vault_item_state.py b/src/kernel/types/vaults/credential_vault_item_state.py
new file mode 100644
index 00000000..7d64ad95
--- /dev/null
+++ b/src/kernel/types/vaults/credential_vault_item_state.py
@@ -0,0 +1,20 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from typing import Dict
+from typing_extensions import Literal
+
+from ..._models import BaseModel
+from .credential_vault_field_state import CredentialVaultFieldState
+
+__all__ = ["CredentialVaultItemState"]
+
+
+class CredentialVaultItemState(BaseModel):
+ fields: Dict[str, CredentialVaultFieldState]
+ """Exactly one entry for each declared field."""
+
+ status: Literal["pending_collection", "ready"]
+ """Ready means all required fields have values, not that a login succeeded.
+
+ Optional fields may remain unset.
+ """
diff --git a/src/kernel/types/vaults/credential_vault_item_update_request_param.py b/src/kernel/types/vaults/credential_vault_item_update_request_param.py
new file mode 100644
index 00000000..b76c4b8f
--- /dev/null
+++ b/src/kernel/types/vaults/credential_vault_item_update_request_param.py
@@ -0,0 +1,38 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from __future__ import annotations
+
+from typing_extensions import Literal, Required, TypedDict
+
+from .credential_vault_item_spec_update_param import CredentialVaultItemSpecUpdateParam
+
+__all__ = ["CredentialVaultItemUpdateRequestParam"]
+
+
+class CredentialVaultItemUpdateRequestParam(TypedDict, total=False):
+ """Atomically update description and selected values.
+
+ Omitted properties are
+ preserved. Field names, types, required flags, and sensitivity cannot change.
+ Unknown field names return 400; stale versions or mismatched item types
+ return 409 without changing the item. A successful update increments version
+ and invalidates outstanding Kernel-hosted collection sessions. If required
+ values remain missing, return pending_collection and a fresh collection
+ action. Otherwise return ready without an action; collect can open the form
+ again without clearing values. Customer URLs have no Kernel-managed expiry.
+ """
+
+ spec: Required[CredentialVaultItemSpecUpdateParam]
+
+ type: Required[Literal["credential"]]
+
+ version: Required[int]
+ """Expected current item version from the latest read."""
+
+ expected_item_id: str
+ """Optional immutable item ID precondition.
+
+ Returns 409 if the key now identifies a different item. Accepted writes target
+ this immutable ID, preventing replacement-key races. Supply this when submitting
+ a form bound to a previously read item.
+ """
diff --git a/src/kernel/types/vaults/fill_vault_item_operation_request_param.py b/src/kernel/types/vaults/fill_vault_item_operation_request_param.py
index a4613cd6..1d8fc8be 100644
--- a/src/kernel/types/vaults/fill_vault_item_operation_request_param.py
+++ b/src/kernel/types/vaults/fill_vault_item_operation_request_param.py
@@ -5,20 +5,23 @@
from typing import Iterable
from typing_extensions import Literal, Required, TypedDict
-from .vault_card_fill_field_param import VaultCardFillFieldParam
+from .vault_fill_field_param import VaultFillFieldParam
__all__ = ["FillVaultItemOperationRequestParam"]
class FillVaultItemOperationRequestParam(TypedDict, total=False):
"""
- Fill selected fields from one ready, unexpired card into a browser linked
- to its vault. Only supported for card items created from Link wallets.
+ Fill selected fields from one ready credential or ready, unexpired Link card
+ into a browser linked to its vault.
Only invoke when the item advertises `fill`. Browser and vault must belong
to the same project. Kernel checks access and allowed destinations before
filling; providing a page URL does not authorize a destination.
- Find exactly one open page matching `page_url`. For each selector, search
+ Find exactly one open page matching `page_url`. Credential items may omit
+ `page_url` to require exactly one open page; cards require an HTTPS page URL.
+ Credentials have no destination allowlist. TOTP fields generate a current
+ code immediately before writing; their seeds never enter the browser. For each selector, search
the main frame and all descendant frames for editable inputs or selects
matched directly or contained within matching elements. Each selector must
resolve to one unique editable element across all frames; zero or multiple
@@ -45,17 +48,19 @@ class FillVaultItemOperationRequestParam(TypedDict, total=False):
browser_id: Required[str]
"""Browser session ID, not a reusable browser name."""
- fields: Required[Iterable[VaultCardFillFieldParam]]
+ fields: Required[Iterable[VaultFillFieldParam]]
"""Field bindings for this step. No two bindings may resolve to the same element."""
- page_url: Required[str]
+ type: Required[Literal["fill"]]
+
+ page_url: str
"""Exact current top-level page URL, including path, query, and fragment.
Must match exactly one open page in the browser; zero or multiple matches fail.
- No prefix or glob matching. Must use HTTPS without embedded credentials.
+ No prefix or glob matching. Required for cards, which must use HTTPS without
+ embedded credentials. Optional for credentials, where omission requires exactly
+ one open page.
"""
- type: Required[Literal["fill"]]
-
timeout_ms: int
"""Total operation deadline in milliseconds, not a per-field timeout."""
diff --git a/src/kernel/types/vaults/item_perform_operation_params.py b/src/kernel/types/vaults/item_perform_operation_params.py
index c583f53b..33c38043 100644
--- a/src/kernel/types/vaults/item_perform_operation_params.py
+++ b/src/kernel/types/vaults/item_perform_operation_params.py
@@ -5,12 +5,13 @@
from typing import Union, Iterable
from typing_extensions import Literal, Required, TypeAlias, TypedDict
-from .vault_card_fill_field_param import VaultCardFillFieldParam
+from .vault_fill_field_param import VaultFillFieldParam
from .vault_checkout_context_param import VaultCheckoutContextParam
__all__ = [
"ItemPerformOperationParams",
"AuthorizeVaultItemOperationRequest",
+ "CollectVaultItemOperationRequest",
"PrepareCheckoutVaultItemOperationRequest",
"FillVaultItemOperationRequest",
]
@@ -22,6 +23,12 @@ class AuthorizeVaultItemOperationRequest(TypedDict, total=False):
type: Required[Literal["authorize"]]
+class CollectVaultItemOperationRequest(TypedDict, total=False):
+ id_or_name: Required[str]
+
+ type: Required[Literal["collect"]]
+
+
class PrepareCheckoutVaultItemOperationRequest(TypedDict, total=False):
id_or_name: Required[str]
@@ -43,22 +50,27 @@ class FillVaultItemOperationRequest(TypedDict, total=False):
browser_id: Required[str]
"""Browser session ID, not a reusable browser name."""
- fields: Required[Iterable[VaultCardFillFieldParam]]
+ fields: Required[Iterable[VaultFillFieldParam]]
"""Field bindings for this step. No two bindings may resolve to the same element."""
- page_url: Required[str]
+ type: Required[Literal["fill"]]
+
+ page_url: str
"""Exact current top-level page URL, including path, query, and fragment.
Must match exactly one open page in the browser; zero or multiple matches fail.
- No prefix or glob matching. Must use HTTPS without embedded credentials.
+ No prefix or glob matching. Required for cards, which must use HTTPS without
+ embedded credentials. Optional for credentials, where omission requires exactly
+ one open page.
"""
- type: Required[Literal["fill"]]
-
timeout_ms: int
"""Total operation deadline in milliseconds, not a per-field timeout."""
ItemPerformOperationParams: TypeAlias = Union[
- AuthorizeVaultItemOperationRequest, PrepareCheckoutVaultItemOperationRequest, FillVaultItemOperationRequest
+ AuthorizeVaultItemOperationRequest,
+ CollectVaultItemOperationRequest,
+ PrepareCheckoutVaultItemOperationRequest,
+ FillVaultItemOperationRequest,
]
diff --git a/src/kernel/types/vaults/item_retrieve_params.py b/src/kernel/types/vaults/item_retrieve_params.py
index 976881b2..d07f8167 100644
--- a/src/kernel/types/vaults/item_retrieve_params.py
+++ b/src/kernel/types/vaults/item_retrieve_params.py
@@ -16,6 +16,8 @@ class ItemRetrieveParams(TypedDict, total=False):
wait: int
"""
- Hold for up to this many seconds while the item is pending authorization or
- approval.
+ Hold for up to this many seconds while the item is pending authorization,
+ approval, or credential collection. Return the current item when ready or when
+ the wait elapses. This does not wait for edits to an already-ready credential;
+ poll GET without wait and compare version to observe changes after collect.
"""
diff --git a/src/kernel/types/vaults/item_update_params.py b/src/kernel/types/vaults/item_update_params.py
index 1ea45428..136a04d0 100644
--- a/src/kernel/types/vaults/item_update_params.py
+++ b/src/kernel/types/vaults/item_update_params.py
@@ -2,15 +2,41 @@
from __future__ import annotations
-from typing_extensions import Required, TypedDict
+from typing import Union
+from typing_extensions import Literal, Required, TypeAlias, TypedDict
from .card_vault_item_spec_param import CardVaultItemSpecParam
+from .credential_vault_item_spec_update_param import CredentialVaultItemSpecUpdateParam
-__all__ = ["ItemUpdateParams"]
+__all__ = ["ItemUpdateParams", "CardVaultItemUpdateRequest", "CredentialVaultItemUpdateRequest"]
-class ItemUpdateParams(TypedDict, total=False):
+class CardVaultItemUpdateRequest(TypedDict, total=False):
id_or_name: Required[str]
spec: Required[CardVaultItemSpecParam]
"""Live payment card. Test-mode card creation is not supported."""
+
+ type: Literal["card"]
+
+
+class CredentialVaultItemUpdateRequest(TypedDict, total=False):
+ id_or_name: Required[str]
+
+ spec: Required[CredentialVaultItemSpecUpdateParam]
+
+ type: Required[Literal["credential"]]
+
+ version: Required[int]
+ """Expected current item version from the latest read."""
+
+ expected_item_id: str
+ """Optional immutable item ID precondition.
+
+ Returns 409 if the key now identifies a different item. Accepted writes target
+ this immutable ID, preventing replacement-key races. Supply this when submitting
+ a form bound to a previously read item.
+ """
+
+
+ItemUpdateParams: TypeAlias = Union[CardVaultItemUpdateRequest, CredentialVaultItemUpdateRequest]
diff --git a/src/kernel/types/vaults/item_upsert_params.py b/src/kernel/types/vaults/item_upsert_params.py
index 2a62d726..5890280a 100644
--- a/src/kernel/types/vaults/item_upsert_params.py
+++ b/src/kernel/types/vaults/item_upsert_params.py
@@ -6,6 +6,7 @@
from typing_extensions import Literal, Required, TypeAlias, TypedDict
from .card_vault_item_spec_param import CardVaultItemSpecParam
+from .credential_vault_item_spec_input_param import CredentialVaultItemSpecInputParam
__all__ = [
"ItemUpsertParams",
@@ -22,6 +23,7 @@
"WalletVaultItemRequestSpecAgentCardWalletVaultItemSpec",
"WalletVaultItemRequestSpecAgentCardWalletVaultItemSpecProviderConfig",
"CardVaultItemRequest",
+ "CredentialVaultItemRequest",
]
@@ -185,4 +187,17 @@ class CardVaultItemRequest(TypedDict, total=False):
type: Required[Literal["card"]]
-ItemUpsertParams: TypeAlias = Union[WalletVaultItemRequest, CardVaultItemRequest]
+class CredentialVaultItemRequest(TypedDict, total=False):
+ id_or_name: Required[str]
+
+ spec: Required[CredentialVaultItemSpecInputParam]
+ """Credential fields are for login and other non-payment credentials.
+
+ Do not store, collect, or fill credit card data in credential items. Use wallet
+ and card item types for credit cards and payment checkout instead.
+ """
+
+ type: Required[Literal["credential"]]
+
+
+ItemUpsertParams: TypeAlias = Union[WalletVaultItemRequest, CardVaultItemRequest, CredentialVaultItemRequest]
diff --git a/src/kernel/types/vaults/vault_card_fill_field_param.py b/src/kernel/types/vaults/vault_card_fill_field_param.py
deleted file mode 100644
index e286291e..00000000
--- a/src/kernel/types/vaults/vault_card_fill_field_param.py
+++ /dev/null
@@ -1,60 +0,0 @@
-# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
-
-from __future__ import annotations
-
-from typing import Union
-from typing_extensions import Literal, Required, TypeAlias, TypedDict
-
-__all__ = ["VaultCardFillFieldParam", "VaultCardStoredFillField", "VaultCardExpirationFillField"]
-
-
-class VaultCardStoredFillField(TypedDict, total=False):
- field: Required[
- Literal[
- "number",
- "exp_month",
- "exp_year",
- "cvc",
- "billing_name",
- "billing_line1",
- "billing_line2",
- "billing_city",
- "billing_state",
- "billing_postal_code",
- "billing_country",
- ]
- ]
- """Field in the decrypted card, not an alias.
-
- Number and CVC preserve leading zeros; month uses two digits and year uses four
- digits. Billing fields use the provider's stored billing address (name, line1,
- line2, city, state, postal_code, country) without reformatting. Request only
- needed billing fields. An absent or empty requested billing field returns 400
- field_unavailable before any browser writes; it does not make other card fields
- unavailable.
- """
-
- selector: Required[str]
- """CSS selector for an editable input or select, or a containing element.
-
- Must resolve to one unique editable element across all page frames.
- """
-
-
-class VaultCardExpirationFillField(TypedDict, total=False):
- """
- Combined expiration derived from the stored month and year; not a separate stored secret.
- """
-
- field: Required[Literal["expiration"]]
-
- format: Required[Literal["MM/YY", "MM/YYYY"]]
-
- selector: Required[str]
- """CSS selector for an editable input or select, or a containing element.
-
- Must resolve to one unique editable element across all page frames.
- """
-
-
-VaultCardFillFieldParam: TypeAlias = Union[VaultCardStoredFillField, VaultCardExpirationFillField]
diff --git a/src/kernel/types/vaults/vault_fill_field_param.py b/src/kernel/types/vaults/vault_fill_field_param.py
new file mode 100644
index 00000000..a7fc5255
--- /dev/null
+++ b/src/kernel/types/vaults/vault_fill_field_param.py
@@ -0,0 +1,23 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from __future__ import annotations
+
+from typing_extensions import Literal, Required, TypedDict
+
+__all__ = ["VaultFillFieldParam"]
+
+
+class VaultFillFieldParam(TypedDict, total=False):
+ field: Required[str]
+ """A declared credential field name or a supported card field.
+
+ Unset credential fields cannot be filled.
+ """
+
+ selector: Required[str]
+
+ format: Literal["MM/YY", "MM/YYYY"]
+ """Required only for a card's combined expiration field.
+
+ Forbidden for other card fields and all credential fields.
+ """
diff --git a/src/kernel/types/vaults/vault_item.py b/src/kernel/types/vaults/vault_item.py
index e33e12db..07253ea4 100644
--- a/src/kernel/types/vaults/vault_item.py
+++ b/src/kernel/types/vaults/vault_item.py
@@ -10,6 +10,7 @@
from .card_vault_item_spec import CardVaultItemSpec
from .vault_payment_method import VaultPaymentMethod
from .card_vault_item_state import CardVaultItemState
+from .credential_vault_item import CredentialVaultItem
from .wallet_vault_item_spec import WalletVaultItemSpec
from .wallet_vault_item_state import WalletVaultItemState
@@ -43,7 +44,7 @@ class WalletVaultItemAvailableOperation(BaseModel):
description: str
- type: Literal["authorize", "prepare_checkout", "fill"]
+ type: Literal["authorize", "collect", "prepare_checkout", "fill"]
class WalletVaultItemExpanded(BaseModel):
@@ -107,7 +108,7 @@ class CardVaultItemAvailableOperation(BaseModel):
description: str
- type: Literal["authorize", "prepare_checkout", "fill"]
+ type: Literal["authorize", "collect", "prepare_checkout", "fill"]
class CardVaultItem(BaseModel):
@@ -136,4 +137,6 @@ class CardVaultItem(BaseModel):
expires_at: Optional[datetime] = None
-VaultItem: TypeAlias = Annotated[Union[WalletVaultItem, CardVaultItem], PropertyInfo(discriminator="type")]
+VaultItem: TypeAlias = Annotated[
+ Union[WalletVaultItem, CardVaultItem, CredentialVaultItem], PropertyInfo(discriminator="type")
+]
diff --git a/src/kernel/types/vaults/vault_item_operation_response.py b/src/kernel/types/vaults/vault_item_operation_response.py
index a464a6b8..163f73a7 100644
--- a/src/kernel/types/vaults/vault_item_operation_response.py
+++ b/src/kernel/types/vaults/vault_item_operation_response.py
@@ -9,6 +9,7 @@
from .card_vault_item_spec import CardVaultItemSpec
from .vault_payment_method import VaultPaymentMethod
from .card_vault_item_state import CardVaultItemState
+from .credential_vault_item import CredentialVaultItem
from .wallet_vault_item_spec import WalletVaultItemSpec
from .wallet_vault_item_state import WalletVaultItemState
from .fill_vault_item_operation_result import FillVaultItemOperationResult
@@ -43,7 +44,7 @@ class WalletVaultItemAvailableOperation(BaseModel):
description: str
- type: Literal["authorize", "prepare_checkout", "fill"]
+ type: Literal["authorize", "collect", "prepare_checkout", "fill"]
class WalletVaultItemExpanded(BaseModel):
@@ -107,7 +108,7 @@ class CardVaultItemAvailableOperation(BaseModel):
description: str
- type: Literal["authorize", "prepare_checkout", "fill"]
+ type: Literal["authorize", "collect", "prepare_checkout", "fill"]
class CardVaultItem(BaseModel):
@@ -136,4 +137,6 @@ class CardVaultItem(BaseModel):
expires_at: Optional[datetime] = None
-VaultItemOperationResponse: TypeAlias = Union[WalletVaultItem, CardVaultItem, FillVaultItemOperationResult]
+VaultItemOperationResponse: TypeAlias = Union[
+ WalletVaultItem, CardVaultItem, CredentialVaultItem, FillVaultItemOperationResult
+]
diff --git a/tests/api_resources/vaults/test_items.py b/tests/api_resources/vaults/test_items.py
index f9dde10b..1eeb98e3 100644
--- a/tests/api_resources/vaults/test_items.py
+++ b/tests/api_resources/vaults/test_items.py
@@ -87,38 +87,38 @@ 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:
+ def test_method_update_overload_1(self, client: Kernel) -> None:
item = client.vaults.items.update(
key="x",
id_or_name="id_or_name",
spec={
- "amount": 3000,
- "context": "The order total changed to USD 30.00 including shipping and taxes for one notebook. Update this unapproved request rather than creating a second payment.",
- "currency": "usd",
- "merchant_name": "Example Store",
- "merchant_url": "https://store.example.com",
- "payment_method_id": "pm_example",
+ "amount": 1,
+ "context": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
+ "currency": "bFx",
+ "merchant_name": "x",
+ "merchant_url": "https://example.com",
+ "payment_method_id": "x",
"provider": "link",
- "wallet": "link-wallet",
+ "wallet": "wallet",
},
)
assert_matches_type(VaultItem, item, path=["response"])
@pytest.mark.skip(reason="Mock server tests are disabled")
@parametrize
- def test_method_update_with_all_params(self, client: Kernel) -> None:
+ def test_method_update_with_all_params_overload_1(self, client: Kernel) -> None:
item = client.vaults.items.update(
key="x",
id_or_name="id_or_name",
spec={
- "amount": 3000,
- "context": "The order total changed to USD 30.00 including shipping and taxes for one notebook. Update this unapproved request rather than creating a second payment.",
- "currency": "usd",
- "merchant_name": "Example Store",
- "merchant_url": "https://store.example.com",
- "payment_method_id": "pm_example",
+ "amount": 1,
+ "context": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
+ "currency": "bFx",
+ "merchant_name": "x",
+ "merchant_url": "https://example.com",
+ "payment_method_id": "x",
"provider": "link",
- "wallet": "link-wallet",
+ "wallet": "wallet",
"expires_at": 0,
"line_items": [
{
@@ -148,24 +148,25 @@ def test_method_update_with_all_params(self, client: Kernel) -> None:
}
],
},
+ type="card",
)
assert_matches_type(VaultItem, item, path=["response"])
@pytest.mark.skip(reason="Mock server tests are disabled")
@parametrize
- def test_raw_response_update(self, client: Kernel) -> None:
+ def test_raw_response_update_overload_1(self, client: Kernel) -> None:
response = client.vaults.items.with_raw_response.update(
key="x",
id_or_name="id_or_name",
spec={
- "amount": 3000,
- "context": "The order total changed to USD 30.00 including shipping and taxes for one notebook. Update this unapproved request rather than creating a second payment.",
- "currency": "usd",
- "merchant_name": "Example Store",
- "merchant_url": "https://store.example.com",
- "payment_method_id": "pm_example",
+ "amount": 1,
+ "context": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
+ "currency": "bFx",
+ "merchant_name": "x",
+ "merchant_url": "https://example.com",
+ "payment_method_id": "x",
"provider": "link",
- "wallet": "link-wallet",
+ "wallet": "wallet",
},
)
@@ -176,19 +177,19 @@ def test_raw_response_update(self, client: Kernel) -> None:
@pytest.mark.skip(reason="Mock server tests are disabled")
@parametrize
- def test_streaming_response_update(self, client: Kernel) -> None:
+ def test_streaming_response_update_overload_1(self, client: Kernel) -> None:
with client.vaults.items.with_streaming_response.update(
key="x",
id_or_name="id_or_name",
spec={
- "amount": 3000,
- "context": "The order total changed to USD 30.00 including shipping and taxes for one notebook. Update this unapproved request rather than creating a second payment.",
- "currency": "usd",
- "merchant_name": "Example Store",
- "merchant_url": "https://store.example.com",
- "payment_method_id": "pm_example",
+ "amount": 1,
+ "context": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
+ "currency": "bFx",
+ "merchant_name": "x",
+ "merchant_url": "https://example.com",
+ "payment_method_id": "x",
"provider": "link",
- "wallet": "link-wallet",
+ "wallet": "wallet",
},
) as response:
assert not response.is_closed
@@ -201,20 +202,20 @@ def test_streaming_response_update(self, client: Kernel) -> None:
@pytest.mark.skip(reason="Mock server tests are disabled")
@parametrize
- def test_path_params_update(self, client: Kernel) -> None:
+ def test_path_params_update_overload_1(self, client: Kernel) -> None:
with pytest.raises(ValueError, match=r"Expected a non-empty value for `id_or_name` but received ''"):
client.vaults.items.with_raw_response.update(
key="x",
id_or_name="",
spec={
- "amount": 3000,
- "context": "The order total changed to USD 30.00 including shipping and taxes for one notebook. Update this unapproved request rather than creating a second payment.",
- "currency": "usd",
- "merchant_name": "Example Store",
- "merchant_url": "https://store.example.com",
- "payment_method_id": "pm_example",
+ "amount": 1,
+ "context": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
+ "currency": "bFx",
+ "merchant_name": "x",
+ "merchant_url": "https://example.com",
+ "payment_method_id": "x",
"provider": "link",
- "wallet": "link-wallet",
+ "wallet": "wallet",
},
)
@@ -223,17 +224,100 @@ def test_path_params_update(self, client: Kernel) -> None:
key="",
id_or_name="id_or_name",
spec={
- "amount": 3000,
- "context": "The order total changed to USD 30.00 including shipping and taxes for one notebook. Update this unapproved request rather than creating a second payment.",
- "currency": "usd",
- "merchant_name": "Example Store",
- "merchant_url": "https://store.example.com",
- "payment_method_id": "pm_example",
+ "amount": 1,
+ "context": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
+ "currency": "bFx",
+ "merchant_name": "x",
+ "merchant_url": "https://example.com",
+ "payment_method_id": "x",
"provider": "link",
- "wallet": "link-wallet",
+ "wallet": "wallet",
},
)
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ def test_method_update_overload_2(self, client: Kernel) -> None:
+ item = client.vaults.items.update(
+ key="x",
+ id_or_name="id_or_name",
+ spec={},
+ type="credential",
+ version=1,
+ )
+ assert_matches_type(VaultItem, item, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ def test_method_update_with_all_params_overload_2(self, client: Kernel) -> None:
+ item = client.vaults.items.update(
+ key="x",
+ id_or_name="id_or_name",
+ spec={
+ "description": "description",
+ "fields": {"foo": {"value": "value"}},
+ },
+ type="credential",
+ version=1,
+ expected_item_id="x",
+ )
+ assert_matches_type(VaultItem, item, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ def test_raw_response_update_overload_2(self, client: Kernel) -> None:
+ response = client.vaults.items.with_raw_response.update(
+ key="x",
+ id_or_name="id_or_name",
+ spec={},
+ type="credential",
+ version=1,
+ )
+
+ assert response.is_closed is True
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+ item = response.parse()
+ assert_matches_type(VaultItem, item, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ def test_streaming_response_update_overload_2(self, client: Kernel) -> None:
+ with client.vaults.items.with_streaming_response.update(
+ key="x",
+ id_or_name="id_or_name",
+ spec={},
+ type="credential",
+ version=1,
+ ) as response:
+ assert not response.is_closed
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+
+ item = response.parse()
+ assert_matches_type(VaultItem, item, 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_overload_2(self, client: Kernel) -> None:
+ with pytest.raises(ValueError, match=r"Expected a non-empty value for `id_or_name` but received ''"):
+ client.vaults.items.with_raw_response.update(
+ key="x",
+ id_or_name="",
+ spec={},
+ type="credential",
+ version=1,
+ )
+
+ with pytest.raises(ValueError, match=r"Expected a non-empty value for `key` but received ''"):
+ client.vaults.items.with_raw_response.update(
+ key="",
+ id_or_name="id_or_name",
+ spec={},
+ type="credential",
+ version=1,
+ )
+
@pytest.mark.skip(reason="Mock server tests are disabled")
@parametrize
def test_method_list(self, client: Kernel) -> None:
@@ -451,6 +535,63 @@ def test_path_params_perform_operation_overload_1(self, client: Kernel) -> None:
@pytest.mark.skip(reason="Mock server tests are disabled")
@parametrize
def test_method_perform_operation_overload_2(self, client: Kernel) -> None:
+ item = client.vaults.items.perform_operation(
+ key="key",
+ id_or_name="id_or_name",
+ type="collect",
+ )
+ assert_matches_type(VaultItemOperationResponse, item, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ def test_raw_response_perform_operation_overload_2(self, client: Kernel) -> None:
+ response = client.vaults.items.with_raw_response.perform_operation(
+ key="key",
+ id_or_name="id_or_name",
+ type="collect",
+ )
+
+ assert response.is_closed is True
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+ item = response.parse()
+ assert_matches_type(VaultItemOperationResponse, item, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ def test_streaming_response_perform_operation_overload_2(self, client: Kernel) -> None:
+ with client.vaults.items.with_streaming_response.perform_operation(
+ key="key",
+ id_or_name="id_or_name",
+ type="collect",
+ ) as response:
+ assert not response.is_closed
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+
+ item = response.parse()
+ assert_matches_type(VaultItemOperationResponse, item, path=["response"])
+
+ assert cast(Any, response.is_closed) is True
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ def test_path_params_perform_operation_overload_2(self, client: Kernel) -> None:
+ with pytest.raises(ValueError, match=r"Expected a non-empty value for `id_or_name` but received ''"):
+ client.vaults.items.with_raw_response.perform_operation(
+ key="key",
+ id_or_name="",
+ type="collect",
+ )
+
+ with pytest.raises(ValueError, match=r"Expected a non-empty value for `key` but received ''"):
+ client.vaults.items.with_raw_response.perform_operation(
+ key="",
+ id_or_name="id_or_name",
+ type="collect",
+ )
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ def test_method_perform_operation_overload_3(self, client: Kernel) -> None:
item = client.vaults.items.perform_operation(
key="key",
id_or_name="id_or_name",
@@ -465,7 +606,7 @@ def test_method_perform_operation_overload_2(self, client: Kernel) -> None:
@pytest.mark.skip(reason="Mock server tests are disabled")
@parametrize
- def test_raw_response_perform_operation_overload_2(self, client: Kernel) -> None:
+ def test_raw_response_perform_operation_overload_3(self, client: Kernel) -> None:
response = client.vaults.items.with_raw_response.perform_operation(
key="key",
id_or_name="id_or_name",
@@ -484,7 +625,7 @@ def test_raw_response_perform_operation_overload_2(self, client: Kernel) -> None
@pytest.mark.skip(reason="Mock server tests are disabled")
@parametrize
- def test_streaming_response_perform_operation_overload_2(self, client: Kernel) -> None:
+ def test_streaming_response_perform_operation_overload_3(self, client: Kernel) -> None:
with client.vaults.items.with_streaming_response.perform_operation(
key="key",
id_or_name="id_or_name",
@@ -505,7 +646,7 @@ def test_streaming_response_perform_operation_overload_2(self, client: Kernel) -
@pytest.mark.skip(reason="Mock server tests are disabled")
@parametrize
- def test_path_params_perform_operation_overload_2(self, client: Kernel) -> None:
+ def test_path_params_perform_operation_overload_3(self, client: Kernel) -> None:
with pytest.raises(ValueError, match=r"Expected a non-empty value for `id_or_name` but received ''"):
client.vaults.items.with_raw_response.perform_operation(
key="key",
@@ -532,7 +673,7 @@ def test_path_params_perform_operation_overload_2(self, client: Kernel) -> None:
@pytest.mark.skip(reason="Mock server tests are disabled")
@parametrize
- def test_method_perform_operation_overload_3(self, client: Kernel) -> None:
+ def test_method_perform_operation_overload_4(self, client: Kernel) -> None:
item = client.vaults.items.perform_operation(
key="key",
id_or_name="id_or_name",
@@ -555,14 +696,13 @@ def test_method_perform_operation_overload_3(self, client: Kernel) -> None:
"selector": "#security-code",
},
],
- page_url="https://shop.example/checkout",
type="fill",
)
assert_matches_type(VaultItemOperationResponse, item, path=["response"])
@pytest.mark.skip(reason="Mock server tests are disabled")
@parametrize
- def test_method_perform_operation_with_all_params_overload_3(self, client: Kernel) -> None:
+ def test_method_perform_operation_with_all_params_overload_4(self, client: Kernel) -> None:
item = client.vaults.items.perform_operation(
key="key",
id_or_name="id_or_name",
@@ -571,29 +711,33 @@ def test_method_perform_operation_with_all_params_overload_3(self, client: Kerne
{
"field": "number",
"selector": "#card-number",
+ "format": "MM/YY",
},
{
"field": "exp_month",
"selector": "#expiry-month",
+ "format": "MM/YY",
},
{
"field": "exp_year",
"selector": "#expiry-year",
+ "format": "MM/YY",
},
{
"field": "cvc",
"selector": "#security-code",
+ "format": "MM/YY",
},
],
- page_url="https://shop.example/checkout",
type="fill",
+ page_url="https://shop.example/checkout",
timeout_ms=1,
)
assert_matches_type(VaultItemOperationResponse, item, path=["response"])
@pytest.mark.skip(reason="Mock server tests are disabled")
@parametrize
- def test_raw_response_perform_operation_overload_3(self, client: Kernel) -> None:
+ def test_raw_response_perform_operation_overload_4(self, client: Kernel) -> None:
response = client.vaults.items.with_raw_response.perform_operation(
key="key",
id_or_name="id_or_name",
@@ -616,7 +760,6 @@ def test_raw_response_perform_operation_overload_3(self, client: Kernel) -> None
"selector": "#security-code",
},
],
- page_url="https://shop.example/checkout",
type="fill",
)
@@ -627,7 +770,7 @@ def test_raw_response_perform_operation_overload_3(self, client: Kernel) -> None
@pytest.mark.skip(reason="Mock server tests are disabled")
@parametrize
- def test_streaming_response_perform_operation_overload_3(self, client: Kernel) -> None:
+ def test_streaming_response_perform_operation_overload_4(self, client: Kernel) -> None:
with client.vaults.items.with_streaming_response.perform_operation(
key="key",
id_or_name="id_or_name",
@@ -650,7 +793,6 @@ def test_streaming_response_perform_operation_overload_3(self, client: Kernel) -
"selector": "#security-code",
},
],
- page_url="https://shop.example/checkout",
type="fill",
) as response:
assert not response.is_closed
@@ -663,7 +805,7 @@ def test_streaming_response_perform_operation_overload_3(self, client: Kernel) -
@pytest.mark.skip(reason="Mock server tests are disabled")
@parametrize
- def test_path_params_perform_operation_overload_3(self, client: Kernel) -> None:
+ def test_path_params_perform_operation_overload_4(self, client: Kernel) -> None:
with pytest.raises(ValueError, match=r"Expected a non-empty value for `id_or_name` but received ''"):
client.vaults.items.with_raw_response.perform_operation(
key="key",
@@ -687,7 +829,6 @@ def test_path_params_perform_operation_overload_3(self, client: Kernel) -> None:
"selector": "#security-code",
},
],
- page_url="https://shop.example/checkout",
type="fill",
)
@@ -714,7 +855,6 @@ def test_path_params_perform_operation_overload_3(self, client: Kernel) -> None:
"selector": "#security-code",
},
],
- page_url="https://shop.example/checkout",
type="fill",
)
@@ -982,6 +1122,89 @@ def test_path_params_upsert_overload_2(self, client: Kernel) -> None:
type="card",
)
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ def test_method_upsert_overload_3(self, client: Kernel) -> None:
+ item = client.vaults.items.upsert(
+ key="x",
+ id_or_name="id_or_name",
+ spec={"fields": {"foo": {"type": "text"}}},
+ type="credential",
+ )
+ assert_matches_type(VaultItem, item, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ def test_method_upsert_with_all_params_overload_3(self, client: Kernel) -> None:
+ item = client.vaults.items.upsert(
+ key="x",
+ id_or_name="id_or_name",
+ spec={
+ "fields": {
+ "foo": {
+ "type": "text",
+ "required": True,
+ "sensitive": True,
+ "value": "x",
+ }
+ },
+ "description": "description",
+ },
+ type="credential",
+ )
+ assert_matches_type(VaultItem, item, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ def test_raw_response_upsert_overload_3(self, client: Kernel) -> None:
+ response = client.vaults.items.with_raw_response.upsert(
+ key="x",
+ id_or_name="id_or_name",
+ spec={"fields": {"foo": {"type": "text"}}},
+ type="credential",
+ )
+
+ assert response.is_closed is True
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+ item = response.parse()
+ assert_matches_type(VaultItem, item, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ def test_streaming_response_upsert_overload_3(self, client: Kernel) -> None:
+ with client.vaults.items.with_streaming_response.upsert(
+ key="x",
+ id_or_name="id_or_name",
+ spec={"fields": {"foo": {"type": "text"}}},
+ type="credential",
+ ) as response:
+ assert not response.is_closed
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+
+ item = response.parse()
+ assert_matches_type(VaultItem, item, path=["response"])
+
+ assert cast(Any, response.is_closed) is True
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ def test_path_params_upsert_overload_3(self, client: Kernel) -> None:
+ with pytest.raises(ValueError, match=r"Expected a non-empty value for `id_or_name` but received ''"):
+ client.vaults.items.with_raw_response.upsert(
+ key="x",
+ id_or_name="",
+ spec={"fields": {"foo": {"type": "text"}}},
+ type="credential",
+ )
+
+ with pytest.raises(ValueError, match=r"Expected a non-empty value for `key` but received ''"):
+ client.vaults.items.with_raw_response.upsert(
+ key="",
+ id_or_name="id_or_name",
+ spec={"fields": {"foo": {"type": "text"}}},
+ type="credential",
+ )
+
class TestAsyncItems:
parametrize = pytest.mark.parametrize(
@@ -1053,38 +1276,38 @@ 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:
+ async def test_method_update_overload_1(self, async_client: AsyncKernel) -> None:
item = await async_client.vaults.items.update(
key="x",
id_or_name="id_or_name",
spec={
- "amount": 3000,
- "context": "The order total changed to USD 30.00 including shipping and taxes for one notebook. Update this unapproved request rather than creating a second payment.",
- "currency": "usd",
- "merchant_name": "Example Store",
- "merchant_url": "https://store.example.com",
- "payment_method_id": "pm_example",
+ "amount": 1,
+ "context": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
+ "currency": "bFx",
+ "merchant_name": "x",
+ "merchant_url": "https://example.com",
+ "payment_method_id": "x",
"provider": "link",
- "wallet": "link-wallet",
+ "wallet": "wallet",
},
)
assert_matches_type(VaultItem, item, 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:
+ async def test_method_update_with_all_params_overload_1(self, async_client: AsyncKernel) -> None:
item = await async_client.vaults.items.update(
key="x",
id_or_name="id_or_name",
spec={
- "amount": 3000,
- "context": "The order total changed to USD 30.00 including shipping and taxes for one notebook. Update this unapproved request rather than creating a second payment.",
- "currency": "usd",
- "merchant_name": "Example Store",
- "merchant_url": "https://store.example.com",
- "payment_method_id": "pm_example",
+ "amount": 1,
+ "context": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
+ "currency": "bFx",
+ "merchant_name": "x",
+ "merchant_url": "https://example.com",
+ "payment_method_id": "x",
"provider": "link",
- "wallet": "link-wallet",
+ "wallet": "wallet",
"expires_at": 0,
"line_items": [
{
@@ -1114,24 +1337,25 @@ async def test_method_update_with_all_params(self, async_client: AsyncKernel) ->
}
],
},
+ type="card",
)
assert_matches_type(VaultItem, item, path=["response"])
@pytest.mark.skip(reason="Mock server tests are disabled")
@parametrize
- async def test_raw_response_update(self, async_client: AsyncKernel) -> None:
+ async def test_raw_response_update_overload_1(self, async_client: AsyncKernel) -> None:
response = await async_client.vaults.items.with_raw_response.update(
key="x",
id_or_name="id_or_name",
spec={
- "amount": 3000,
- "context": "The order total changed to USD 30.00 including shipping and taxes for one notebook. Update this unapproved request rather than creating a second payment.",
- "currency": "usd",
- "merchant_name": "Example Store",
- "merchant_url": "https://store.example.com",
- "payment_method_id": "pm_example",
+ "amount": 1,
+ "context": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
+ "currency": "bFx",
+ "merchant_name": "x",
+ "merchant_url": "https://example.com",
+ "payment_method_id": "x",
"provider": "link",
- "wallet": "link-wallet",
+ "wallet": "wallet",
},
)
@@ -1142,19 +1366,19 @@ async def test_raw_response_update(self, async_client: AsyncKernel) -> None:
@pytest.mark.skip(reason="Mock server tests are disabled")
@parametrize
- async def test_streaming_response_update(self, async_client: AsyncKernel) -> None:
+ async def test_streaming_response_update_overload_1(self, async_client: AsyncKernel) -> None:
async with async_client.vaults.items.with_streaming_response.update(
key="x",
id_or_name="id_or_name",
spec={
- "amount": 3000,
- "context": "The order total changed to USD 30.00 including shipping and taxes for one notebook. Update this unapproved request rather than creating a second payment.",
- "currency": "usd",
- "merchant_name": "Example Store",
- "merchant_url": "https://store.example.com",
- "payment_method_id": "pm_example",
+ "amount": 1,
+ "context": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
+ "currency": "bFx",
+ "merchant_name": "x",
+ "merchant_url": "https://example.com",
+ "payment_method_id": "x",
"provider": "link",
- "wallet": "link-wallet",
+ "wallet": "wallet",
},
) as response:
assert not response.is_closed
@@ -1167,20 +1391,20 @@ async def test_streaming_response_update(self, async_client: AsyncKernel) -> Non
@pytest.mark.skip(reason="Mock server tests are disabled")
@parametrize
- async def test_path_params_update(self, async_client: AsyncKernel) -> None:
+ async def test_path_params_update_overload_1(self, async_client: AsyncKernel) -> None:
with pytest.raises(ValueError, match=r"Expected a non-empty value for `id_or_name` but received ''"):
await async_client.vaults.items.with_raw_response.update(
key="x",
id_or_name="",
spec={
- "amount": 3000,
- "context": "The order total changed to USD 30.00 including shipping and taxes for one notebook. Update this unapproved request rather than creating a second payment.",
- "currency": "usd",
- "merchant_name": "Example Store",
- "merchant_url": "https://store.example.com",
- "payment_method_id": "pm_example",
+ "amount": 1,
+ "context": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
+ "currency": "bFx",
+ "merchant_name": "x",
+ "merchant_url": "https://example.com",
+ "payment_method_id": "x",
"provider": "link",
- "wallet": "link-wallet",
+ "wallet": "wallet",
},
)
@@ -1189,17 +1413,100 @@ async def test_path_params_update(self, async_client: AsyncKernel) -> None:
key="",
id_or_name="id_or_name",
spec={
- "amount": 3000,
- "context": "The order total changed to USD 30.00 including shipping and taxes for one notebook. Update this unapproved request rather than creating a second payment.",
- "currency": "usd",
- "merchant_name": "Example Store",
- "merchant_url": "https://store.example.com",
- "payment_method_id": "pm_example",
+ "amount": 1,
+ "context": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
+ "currency": "bFx",
+ "merchant_name": "x",
+ "merchant_url": "https://example.com",
+ "payment_method_id": "x",
"provider": "link",
- "wallet": "link-wallet",
+ "wallet": "wallet",
},
)
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ async def test_method_update_overload_2(self, async_client: AsyncKernel) -> None:
+ item = await async_client.vaults.items.update(
+ key="x",
+ id_or_name="id_or_name",
+ spec={},
+ type="credential",
+ version=1,
+ )
+ assert_matches_type(VaultItem, item, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ async def test_method_update_with_all_params_overload_2(self, async_client: AsyncKernel) -> None:
+ item = await async_client.vaults.items.update(
+ key="x",
+ id_or_name="id_or_name",
+ spec={
+ "description": "description",
+ "fields": {"foo": {"value": "value"}},
+ },
+ type="credential",
+ version=1,
+ expected_item_id="x",
+ )
+ assert_matches_type(VaultItem, item, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ async def test_raw_response_update_overload_2(self, async_client: AsyncKernel) -> None:
+ response = await async_client.vaults.items.with_raw_response.update(
+ key="x",
+ id_or_name="id_or_name",
+ spec={},
+ type="credential",
+ version=1,
+ )
+
+ assert response.is_closed is True
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+ item = await response.parse()
+ assert_matches_type(VaultItem, item, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ async def test_streaming_response_update_overload_2(self, async_client: AsyncKernel) -> None:
+ async with async_client.vaults.items.with_streaming_response.update(
+ key="x",
+ id_or_name="id_or_name",
+ spec={},
+ type="credential",
+ version=1,
+ ) as response:
+ assert not response.is_closed
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+
+ item = await response.parse()
+ assert_matches_type(VaultItem, item, 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_overload_2(self, async_client: AsyncKernel) -> None:
+ with pytest.raises(ValueError, match=r"Expected a non-empty value for `id_or_name` but received ''"):
+ await async_client.vaults.items.with_raw_response.update(
+ key="x",
+ id_or_name="",
+ spec={},
+ type="credential",
+ version=1,
+ )
+
+ with pytest.raises(ValueError, match=r"Expected a non-empty value for `key` but received ''"):
+ await async_client.vaults.items.with_raw_response.update(
+ key="",
+ id_or_name="id_or_name",
+ spec={},
+ type="credential",
+ version=1,
+ )
+
@pytest.mark.skip(reason="Mock server tests are disabled")
@parametrize
async def test_method_list(self, async_client: AsyncKernel) -> None:
@@ -1417,6 +1724,63 @@ async def test_path_params_perform_operation_overload_1(self, async_client: Asyn
@pytest.mark.skip(reason="Mock server tests are disabled")
@parametrize
async def test_method_perform_operation_overload_2(self, async_client: AsyncKernel) -> None:
+ item = await async_client.vaults.items.perform_operation(
+ key="key",
+ id_or_name="id_or_name",
+ type="collect",
+ )
+ assert_matches_type(VaultItemOperationResponse, item, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ async def test_raw_response_perform_operation_overload_2(self, async_client: AsyncKernel) -> None:
+ response = await async_client.vaults.items.with_raw_response.perform_operation(
+ key="key",
+ id_or_name="id_or_name",
+ type="collect",
+ )
+
+ assert response.is_closed is True
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+ item = await response.parse()
+ assert_matches_type(VaultItemOperationResponse, item, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ async def test_streaming_response_perform_operation_overload_2(self, async_client: AsyncKernel) -> None:
+ async with async_client.vaults.items.with_streaming_response.perform_operation(
+ key="key",
+ id_or_name="id_or_name",
+ type="collect",
+ ) as response:
+ assert not response.is_closed
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+
+ item = await response.parse()
+ assert_matches_type(VaultItemOperationResponse, item, 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_perform_operation_overload_2(self, async_client: AsyncKernel) -> None:
+ with pytest.raises(ValueError, match=r"Expected a non-empty value for `id_or_name` but received ''"):
+ await async_client.vaults.items.with_raw_response.perform_operation(
+ key="key",
+ id_or_name="",
+ type="collect",
+ )
+
+ with pytest.raises(ValueError, match=r"Expected a non-empty value for `key` but received ''"):
+ await async_client.vaults.items.with_raw_response.perform_operation(
+ key="",
+ id_or_name="id_or_name",
+ type="collect",
+ )
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ async def test_method_perform_operation_overload_3(self, async_client: AsyncKernel) -> None:
item = await async_client.vaults.items.perform_operation(
key="key",
id_or_name="id_or_name",
@@ -1431,7 +1795,7 @@ async def test_method_perform_operation_overload_2(self, async_client: AsyncKern
@pytest.mark.skip(reason="Mock server tests are disabled")
@parametrize
- async def test_raw_response_perform_operation_overload_2(self, async_client: AsyncKernel) -> None:
+ async def test_raw_response_perform_operation_overload_3(self, async_client: AsyncKernel) -> None:
response = await async_client.vaults.items.with_raw_response.perform_operation(
key="key",
id_or_name="id_or_name",
@@ -1450,7 +1814,7 @@ async def test_raw_response_perform_operation_overload_2(self, async_client: Asy
@pytest.mark.skip(reason="Mock server tests are disabled")
@parametrize
- async def test_streaming_response_perform_operation_overload_2(self, async_client: AsyncKernel) -> None:
+ async def test_streaming_response_perform_operation_overload_3(self, async_client: AsyncKernel) -> None:
async with async_client.vaults.items.with_streaming_response.perform_operation(
key="key",
id_or_name="id_or_name",
@@ -1471,7 +1835,7 @@ async def test_streaming_response_perform_operation_overload_2(self, async_clien
@pytest.mark.skip(reason="Mock server tests are disabled")
@parametrize
- async def test_path_params_perform_operation_overload_2(self, async_client: AsyncKernel) -> None:
+ async def test_path_params_perform_operation_overload_3(self, async_client: AsyncKernel) -> None:
with pytest.raises(ValueError, match=r"Expected a non-empty value for `id_or_name` but received ''"):
await async_client.vaults.items.with_raw_response.perform_operation(
key="key",
@@ -1498,7 +1862,7 @@ async def test_path_params_perform_operation_overload_2(self, async_client: Asyn
@pytest.mark.skip(reason="Mock server tests are disabled")
@parametrize
- async def test_method_perform_operation_overload_3(self, async_client: AsyncKernel) -> None:
+ async def test_method_perform_operation_overload_4(self, async_client: AsyncKernel) -> None:
item = await async_client.vaults.items.perform_operation(
key="key",
id_or_name="id_or_name",
@@ -1521,14 +1885,13 @@ async def test_method_perform_operation_overload_3(self, async_client: AsyncKern
"selector": "#security-code",
},
],
- page_url="https://shop.example/checkout",
type="fill",
)
assert_matches_type(VaultItemOperationResponse, item, path=["response"])
@pytest.mark.skip(reason="Mock server tests are disabled")
@parametrize
- async def test_method_perform_operation_with_all_params_overload_3(self, async_client: AsyncKernel) -> None:
+ async def test_method_perform_operation_with_all_params_overload_4(self, async_client: AsyncKernel) -> None:
item = await async_client.vaults.items.perform_operation(
key="key",
id_or_name="id_or_name",
@@ -1537,29 +1900,33 @@ async def test_method_perform_operation_with_all_params_overload_3(self, async_c
{
"field": "number",
"selector": "#card-number",
+ "format": "MM/YY",
},
{
"field": "exp_month",
"selector": "#expiry-month",
+ "format": "MM/YY",
},
{
"field": "exp_year",
"selector": "#expiry-year",
+ "format": "MM/YY",
},
{
"field": "cvc",
"selector": "#security-code",
+ "format": "MM/YY",
},
],
- page_url="https://shop.example/checkout",
type="fill",
+ page_url="https://shop.example/checkout",
timeout_ms=1,
)
assert_matches_type(VaultItemOperationResponse, item, path=["response"])
@pytest.mark.skip(reason="Mock server tests are disabled")
@parametrize
- async def test_raw_response_perform_operation_overload_3(self, async_client: AsyncKernel) -> None:
+ async def test_raw_response_perform_operation_overload_4(self, async_client: AsyncKernel) -> None:
response = await async_client.vaults.items.with_raw_response.perform_operation(
key="key",
id_or_name="id_or_name",
@@ -1582,7 +1949,6 @@ async def test_raw_response_perform_operation_overload_3(self, async_client: Asy
"selector": "#security-code",
},
],
- page_url="https://shop.example/checkout",
type="fill",
)
@@ -1593,7 +1959,7 @@ async def test_raw_response_perform_operation_overload_3(self, async_client: Asy
@pytest.mark.skip(reason="Mock server tests are disabled")
@parametrize
- async def test_streaming_response_perform_operation_overload_3(self, async_client: AsyncKernel) -> None:
+ async def test_streaming_response_perform_operation_overload_4(self, async_client: AsyncKernel) -> None:
async with async_client.vaults.items.with_streaming_response.perform_operation(
key="key",
id_or_name="id_or_name",
@@ -1616,7 +1982,6 @@ async def test_streaming_response_perform_operation_overload_3(self, async_clien
"selector": "#security-code",
},
],
- page_url="https://shop.example/checkout",
type="fill",
) as response:
assert not response.is_closed
@@ -1629,7 +1994,7 @@ async def test_streaming_response_perform_operation_overload_3(self, async_clien
@pytest.mark.skip(reason="Mock server tests are disabled")
@parametrize
- async def test_path_params_perform_operation_overload_3(self, async_client: AsyncKernel) -> None:
+ async def test_path_params_perform_operation_overload_4(self, async_client: AsyncKernel) -> None:
with pytest.raises(ValueError, match=r"Expected a non-empty value for `id_or_name` but received ''"):
await async_client.vaults.items.with_raw_response.perform_operation(
key="key",
@@ -1653,7 +2018,6 @@ async def test_path_params_perform_operation_overload_3(self, async_client: Asyn
"selector": "#security-code",
},
],
- page_url="https://shop.example/checkout",
type="fill",
)
@@ -1680,7 +2044,6 @@ async def test_path_params_perform_operation_overload_3(self, async_client: Asyn
"selector": "#security-code",
},
],
- page_url="https://shop.example/checkout",
type="fill",
)
@@ -1947,3 +2310,86 @@ async def test_path_params_upsert_overload_2(self, async_client: AsyncKernel) ->
},
type="card",
)
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ async def test_method_upsert_overload_3(self, async_client: AsyncKernel) -> None:
+ item = await async_client.vaults.items.upsert(
+ key="x",
+ id_or_name="id_or_name",
+ spec={"fields": {"foo": {"type": "text"}}},
+ type="credential",
+ )
+ assert_matches_type(VaultItem, item, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ async def test_method_upsert_with_all_params_overload_3(self, async_client: AsyncKernel) -> None:
+ item = await async_client.vaults.items.upsert(
+ key="x",
+ id_or_name="id_or_name",
+ spec={
+ "fields": {
+ "foo": {
+ "type": "text",
+ "required": True,
+ "sensitive": True,
+ "value": "x",
+ }
+ },
+ "description": "description",
+ },
+ type="credential",
+ )
+ assert_matches_type(VaultItem, item, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ async def test_raw_response_upsert_overload_3(self, async_client: AsyncKernel) -> None:
+ response = await async_client.vaults.items.with_raw_response.upsert(
+ key="x",
+ id_or_name="id_or_name",
+ spec={"fields": {"foo": {"type": "text"}}},
+ type="credential",
+ )
+
+ assert response.is_closed is True
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+ item = await response.parse()
+ assert_matches_type(VaultItem, item, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ async def test_streaming_response_upsert_overload_3(self, async_client: AsyncKernel) -> None:
+ async with async_client.vaults.items.with_streaming_response.upsert(
+ key="x",
+ id_or_name="id_or_name",
+ spec={"fields": {"foo": {"type": "text"}}},
+ type="credential",
+ ) as response:
+ assert not response.is_closed
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+
+ item = await response.parse()
+ assert_matches_type(VaultItem, item, 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_upsert_overload_3(self, async_client: AsyncKernel) -> None:
+ with pytest.raises(ValueError, match=r"Expected a non-empty value for `id_or_name` but received ''"):
+ await async_client.vaults.items.with_raw_response.upsert(
+ key="x",
+ id_or_name="",
+ spec={"fields": {"foo": {"type": "text"}}},
+ type="credential",
+ )
+
+ with pytest.raises(ValueError, match=r"Expected a non-empty value for `key` but received ''"):
+ await async_client.vaults.items.with_raw_response.upsert(
+ key="",
+ id_or_name="id_or_name",
+ spec={"fields": {"foo": {"type": "text"}}},
+ type="credential",
+ )
From c11035847c9f6dd23b7cdd5efa0a8b188d5a850a Mon Sep 17 00:00:00 2001
From: "kernel-internal[bot]"
<260533166+kernel-internal[bot]@users.noreply.github.com>
Date: Mon, 14 Sep 2026 23:56:19 +0000
Subject: [PATCH 263/366] feat: Expose managed auth field input modes
Stainless-Generated-From: 641e17ff11ca542895bb45f2e6c06d75f136aa5d
---
src/kernel/types/auth/connection_follow_response.py | 3 +++
src/kernel/types/auth/managed_auth.py | 3 +++
2 files changed, 6 insertions(+)
diff --git a/src/kernel/types/auth/connection_follow_response.py b/src/kernel/types/auth/connection_follow_response.py
index ff6dcf0b..c7690794 100644
--- a/src/kernel/types/auth/connection_follow_response.py
+++ b/src/kernel/types/auth/connection_follow_response.py
@@ -109,6 +109,9 @@ class ManagedAuthStateEventField(BaseModel):
hint: Optional[str] = None
"""Context shown near the field, including a masked code destination."""
+ input_mode: Optional[Literal["text", "email", "tel", "numeric"]] = None
+ """Virtual keyboard hint, independent of field type and browser validation."""
+
label: Optional[str] = None
"""Human-readable label shown to the user."""
diff --git a/src/kernel/types/auth/managed_auth.py b/src/kernel/types/auth/managed_auth.py
index a358197e..62fa7852 100644
--- a/src/kernel/types/auth/managed_auth.py
+++ b/src/kernel/types/auth/managed_auth.py
@@ -218,6 +218,9 @@ class Field(BaseModel):
hint: Optional[str] = None
"""Context shown near the field, including a masked code destination."""
+ input_mode: Optional[Literal["text", "email", "tel", "numeric"]] = None
+ """Virtual keyboard hint, independent of field type and browser validation."""
+
label: Optional[str] = None
"""Human-readable label shown to the user."""
From 0737fe974b15c6e422c8722d1d76c97c811533d7 Mon Sep 17 00:00:00 2001
From: "kernel-internal[bot]"
<260533166+kernel-internal[bot]@users.noreply.github.com>
Date: Tue, 15 Sep 2026 00:17:29 +0000
Subject: [PATCH 264/366] feat: Return working config registry configurations
Stainless-Generated-From: fafe851cda2bec958cc113405ced9ecd6d1c35e3
---
src/kernel/types/config_registry_response.py | 9 ++++++++-
src/kernel/types/lookup_response.py | 9 ++++++++-
2 files changed, 16 insertions(+), 2 deletions(-)
diff --git a/src/kernel/types/config_registry_response.py b/src/kernel/types/config_registry_response.py
index dba10b9e..cad09105 100644
--- a/src/kernel/types/config_registry_response.py
+++ b/src/kernel/types/config_registry_response.py
@@ -1,11 +1,12 @@
# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
-from typing import Optional
+from typing import List, Optional
from typing_extensions import Literal
from .target import Target
from .._models import BaseModel
from .analysis import Analysis
+from .recommendation import Recommendation
from .recommendation_result import RecommendationResult
__all__ = ["ConfigRegistryResponse"]
@@ -23,6 +24,12 @@ class ConfigRegistryResponse(BaseModel):
target: Target
+ working_configurations: List[Recommendation]
+ """
+ Working configurations for the target, ordered with the recommended
+ configuration first.
+ """
+
guidance: Optional[str] = None
"""Short advisory markdown to facilitate navigating this target.
diff --git a/src/kernel/types/lookup_response.py b/src/kernel/types/lookup_response.py
index d717b8cf..c92e163e 100644
--- a/src/kernel/types/lookup_response.py
+++ b/src/kernel/types/lookup_response.py
@@ -1,9 +1,10 @@
# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
-from typing import Optional
+from typing import List, Optional
from .target import Target
from .._models import BaseModel
+from .recommendation import Recommendation
from .recommendation_result import RecommendationResult
__all__ = ["LookupResponse"]
@@ -15,6 +16,12 @@ class LookupResponse(BaseModel):
target: Target
+ working_configurations: List[Recommendation]
+ """
+ Working configurations for the target, ordered with the recommended
+ configuration first.
+ """
+
guidance: Optional[str] = None
"""Short advisory markdown to facilitate navigating this target.
From aad62f97e2f6c3ee590775916364c19980dc9456 Mon Sep 17 00:00:00 2001
From: "kernel-internal[bot]"
<260533166+kernel-internal[bot]@users.noreply.github.com>
Date: Tue, 15 Sep 2026 15:06:51 +0000
Subject: [PATCH 265/366] fix: disable automatic vault operation retries in
SDKs
Stainless-Generated-From: 068a33988f678bd0ddafaacdda5828037155b2ed
---
src/kernel/resources/vaults/items.py | 4 +-
tests/test_vault_operation_retries.py | 74 +++++++++++++++++++++++++++
2 files changed, 76 insertions(+), 2 deletions(-)
create mode 100644 tests/test_vault_operation_retries.py
diff --git a/src/kernel/resources/vaults/items.py b/src/kernel/resources/vaults/items.py
index 507cf048..ea0fbf83 100644
--- a/src/kernel/resources/vaults/items.py
+++ b/src/kernel/resources/vaults/items.py
@@ -615,7 +615,7 @@ def perform_operation(
raise ValueError(f"Expected a non-empty value for `key` but received {key!r}")
return cast(
VaultItemOperationResponse,
- self._post(
+ self._client.with_options(max_retries=0).post(
path_template("/vaults/{id_or_name}/items/{key}/operations", id_or_name=id_or_name, key=key),
body=maybe_transform(
{
@@ -1378,7 +1378,7 @@ async def perform_operation(
raise ValueError(f"Expected a non-empty value for `key` but received {key!r}")
return cast(
VaultItemOperationResponse,
- await self._post(
+ await self._client.with_options(max_retries=0).post(
path_template("/vaults/{id_or_name}/items/{key}/operations", id_or_name=id_or_name, key=key),
body=await async_maybe_transform(
{
diff --git a/tests/test_vault_operation_retries.py b/tests/test_vault_operation_retries.py
new file mode 100644
index 00000000..f25520e7
--- /dev/null
+++ b/tests/test_vault_operation_retries.py
@@ -0,0 +1,74 @@
+from __future__ import annotations
+
+import httpx
+import pytest
+
+from kernel import Kernel, AsyncKernel, APIStatusError, APIConnectionError
+
+
+@pytest.mark.parametrize("failure", ["connection", 409, 429, 500])
+def test_fill_does_not_retry(failure: str | int) -> None:
+ calls = 0
+
+ def handle(request: httpx.Request) -> httpx.Response:
+ nonlocal calls
+ calls += 1
+ if isinstance(failure, str):
+ raise httpx.ReadError("connection lost", request=request)
+ return httpx.Response(failure, json={})
+
+ with Kernel(
+ api_key="test", max_retries=1, http_client=httpx.Client(transport=httpx.MockTransport(handle))
+ ) as client:
+ with pytest.raises((APIConnectionError, APIStatusError)):
+ client.vaults.items.perform_operation(
+ "login",
+ id_or_name="vault",
+ type="fill",
+ browser_id="browser",
+ fields=[{"field": "password", "selector": "#password"}],
+ )
+ assert calls == 1
+ assert client.max_retries == 1
+
+
+@pytest.mark.parametrize("failure", ["connection", 409, 429, 500])
+async def test_async_fill_does_not_retry(failure: str | int) -> None:
+ calls = 0
+
+ def handle(request: httpx.Request) -> httpx.Response:
+ nonlocal calls
+ calls += 1
+ if isinstance(failure, str):
+ raise httpx.ReadError("connection lost", request=request)
+ return httpx.Response(failure, json={})
+
+ async with AsyncKernel(
+ api_key="test", max_retries=1, http_client=httpx.AsyncClient(transport=httpx.MockTransport(handle))
+ ) as client:
+ with pytest.raises((APIConnectionError, APIStatusError)):
+ await client.vaults.items.perform_operation(
+ "login",
+ id_or_name="vault",
+ type="fill",
+ browser_id="browser",
+ fields=[{"field": "password", "selector": "#password"}],
+ )
+ assert calls == 1
+ assert client.max_retries == 1
+
+
+def test_reads_keep_client_retries() -> None:
+ calls = 0
+
+ def handle(request: httpx.Request) -> httpx.Response:
+ nonlocal calls
+ calls += 1
+ raise httpx.ReadError("connection lost", request=request)
+
+ with Kernel(
+ api_key="test", max_retries=1, http_client=httpx.Client(transport=httpx.MockTransport(handle))
+ ) as client:
+ with pytest.raises(APIConnectionError):
+ client.vaults.items.retrieve("login", id_or_name="vault")
+ assert calls == 2
From 678511b8b45aa30bac3b122005cb9f091862ba19 Mon Sep 17 00:00:00 2001
From: "kernel-internal[bot]"
<260533166+kernel-internal[bot]@users.noreply.github.com>
Date: Tue, 15 Sep 2026 15:11:43 +0000
Subject: [PATCH 266/366] release: 0.103.0
---
.release-please-manifest.json | 2 +-
CHANGELOG.md | 14 ++++++++++++++
pyproject.toml | 2 +-
src/kernel/_version.py | 2 +-
4 files changed, 17 insertions(+), 3 deletions(-)
diff --git a/.release-please-manifest.json b/.release-please-manifest.json
index f70d9978..508a7081 100644
--- a/.release-please-manifest.json
+++ b/.release-please-manifest.json
@@ -1,3 +1,3 @@
{
- ".": "0.102.0"
+ ".": "0.103.0"
}
\ No newline at end of file
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 80c45f21..14213f10 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,19 @@
# Changelog
+## [0.103.0](https://github.com/kernel/kernel-python-sdk/compare/v0.102.0...v0.103.0) (2026-09-15)
+
+
+### Features
+
+* chore(stlc): seal custom-code tracking files ([4ce4ab9](https://github.com/kernel/kernel-python-sdk/commit/4ce4ab9f533b4f5d9fe420f91f1a0054c0e13c1b))
+* Expose managed auth field input modes ([c110358](https://github.com/kernel/kernel-python-sdk/commit/c11035847c9f6dd23b7cdd5efa0a8b188d5a850a))
+* Return working config registry configurations ([0737fe9](https://github.com/kernel/kernel-python-sdk/commit/0737fe974b15c6e422c8722d1d76c97c811533d7))
+
+
+### Bug Fixes
+
+* disable automatic vault operation retries in SDKs ([aad62f9](https://github.com/kernel/kernel-python-sdk/commit/aad62f97e2f6c3ee590775916364c19980dc9456))
+
## [0.102.0](https://github.com/kernel/kernel-python-sdk/compare/v0.101.0...v0.102.0) (2026-09-14)
diff --git a/pyproject.toml b/pyproject.toml
index 473c0ec0..e8a7f193 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -1,6 +1,6 @@
[project]
name = "kernel"
-version = "0.102.0"
+version = "0.103.0"
description = "The official Python library for the kernel API"
dynamic = ["readme"]
license = "Apache-2.0"
diff --git a/src/kernel/_version.py b/src/kernel/_version.py
index b5daff26..033ca8e7 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.102.0" # x-release-please-version
+__version__ = "0.103.0" # x-release-please-version
From 9b5cb34e8fb19c22a44c42aca9e1fc61bf7744a9 Mon Sep 17 00:00:00 2001
From: "kernel-internal[bot]"
<260533166+kernel-internal[bot]@users.noreply.github.com>
Date: Tue, 15 Sep 2026 18:23:59 +0000
Subject: [PATCH 267/366] feat: Rename workers bot profiler dependency
Stainless-Generated-From: fa1448d7c5f036b517534546789f98487075e641
---
src/kernel/resources/browsers/webmcp.py | 30 ++++++++++++-------
.../types/browsers/invocation_result.py | 8 ++++-
2 files changed, 27 insertions(+), 11 deletions(-)
diff --git a/src/kernel/resources/browsers/webmcp.py b/src/kernel/resources/browsers/webmcp.py
index 00f571de..450a5c4f 100644
--- a/src/kernel/resources/browsers/webmcp.py
+++ b/src/kernel/resources/browsers/webmcp.py
@@ -60,11 +60,16 @@ def invoke_tool(
extra_body: Body | None = None,
timeout: float | httpx.Timeout | None | NotGiven = not_given,
) -> InvocationResult:
- """
- Invokes the exact live registration identified by tool_ref and waits
- synchronously for its result. Navigation during execution is allowed. If the tab
- or embedded frame disappears, or the request times out after invocation begins,
- the response reports outcome_unknown and the tool is not retried.
+ """Invokes the exact live registration identified by tool_ref.
+
+ Non-autosubmit
+ declarative form tools return after their fields are populated with an
+ awaiting_submission status. Other tools wait for a terminal result, including
+ across navigation. Inspect a populated form, obtain any required confirmation,
+ then submit through Playwright or computer interaction without invoking the tool
+ again. If the tab or embedded frame disappears, or the request times out after
+ invocation begins, the response reports outcome_unknown and the tool is not
+ retried.
Args:
input: Tool input, limited to 1 MiB after JSON serialization.
@@ -168,11 +173,16 @@ async def invoke_tool(
extra_body: Body | None = None,
timeout: float | httpx.Timeout | None | NotGiven = not_given,
) -> InvocationResult:
- """
- Invokes the exact live registration identified by tool_ref and waits
- synchronously for its result. Navigation during execution is allowed. If the tab
- or embedded frame disappears, or the request times out after invocation begins,
- the response reports outcome_unknown and the tool is not retried.
+ """Invokes the exact live registration identified by tool_ref.
+
+ Non-autosubmit
+ declarative form tools return after their fields are populated with an
+ awaiting_submission status. Other tools wait for a terminal result, including
+ across navigation. Inspect a populated form, obtain any required confirmation,
+ then submit through Playwright or computer interaction without invoking the tool
+ again. If the tab or embedded frame disappears, or the request times out after
+ invocation begins, the response reports outcome_unknown and the tool is not
+ retried.
Args:
input: Tool input, limited to 1 MiB after JSON serialization.
diff --git a/src/kernel/types/browsers/invocation_result.py b/src/kernel/types/browsers/invocation_result.py
index 7ecd69ce..c8c06188 100644
--- a/src/kernel/types/browsers/invocation_result.py
+++ b/src/kernel/types/browsers/invocation_result.py
@@ -11,7 +11,13 @@
class InvocationResult(BaseModel):
invocation_id: str
- status: Literal["completed", "canceled", "error"]
+ status: Literal["completed", "canceled", "error", "awaiting_submission"]
+ """
+ awaiting_submission means a non-autosubmit declarative form was populated but
+ not submitted. Inspect the form, obtain any required confirmation, then submit
+ through Playwright or computer interaction without invoking the tool again. The
+ other statuses are terminal results.
+ """
error_text: Optional[str] = None
From b8cbc39370caf1e7ecb0222ae42c7a4c4241181f Mon Sep 17 00:00:00 2001
From: "kernel-internal[bot]"
<260533166+kernel-internal[bot]@users.noreply.github.com>
Date: Tue, 15 Sep 2026 18:28:05 +0000
Subject: [PATCH 268/366] release: 0.104.0
---
.release-please-manifest.json | 2 +-
CHANGELOG.md | 7 +++++++
pyproject.toml | 2 +-
src/kernel/_version.py | 2 +-
4 files changed, 10 insertions(+), 3 deletions(-)
diff --git a/.release-please-manifest.json b/.release-please-manifest.json
index 508a7081..19c0e9cc 100644
--- a/.release-please-manifest.json
+++ b/.release-please-manifest.json
@@ -1,3 +1,3 @@
{
- ".": "0.103.0"
+ ".": "0.104.0"
}
\ No newline at end of file
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 14213f10..d05919d0 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,12 @@
# Changelog
+## [0.104.0](https://github.com/kernel/kernel-python-sdk/compare/v0.103.0...v0.104.0) (2026-09-15)
+
+
+### Features
+
+* Rename workers bot profiler dependency ([9b5cb34](https://github.com/kernel/kernel-python-sdk/commit/9b5cb34e8fb19c22a44c42aca9e1fc61bf7744a9))
+
## [0.103.0](https://github.com/kernel/kernel-python-sdk/compare/v0.102.0...v0.103.0) (2026-09-15)
diff --git a/pyproject.toml b/pyproject.toml
index e8a7f193..6b6ca1ef 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -1,6 +1,6 @@
[project]
name = "kernel"
-version = "0.103.0"
+version = "0.104.0"
description = "The official Python library for the kernel API"
dynamic = ["readme"]
license = "Apache-2.0"
diff --git a/src/kernel/_version.py b/src/kernel/_version.py
index 033ca8e7..0a7fea06 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.103.0" # x-release-please-version
+__version__ = "0.104.0" # x-release-please-version
From a72eb537f286d56461ae94bd62166b45f1d7bf8d Mon Sep 17 00:00:00 2001
From: rgarcia <72655+rgarcia@users.noreply.github.com>
Date: Tue, 15 Sep 2026 18:33:37 +0000
Subject: [PATCH 269/366] docs: Describe the WebMCP status in release notes
---
CHANGELOG.md | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index d05919d0..eb98375c 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -5,7 +5,7 @@
### Features
-* Rename workers bot profiler dependency ([9b5cb34](https://github.com/kernel/kernel-python-sdk/commit/9b5cb34e8fb19c22a44c42aca9e1fc61bf7744a9))
+* Expose the WebMCP `awaiting_submission` status ([9b5cb34](https://github.com/kernel/kernel-python-sdk/commit/9b5cb34e8fb19c22a44c42aca9e1fc61bf7744a9))
## [0.103.0](https://github.com/kernel/kernel-python-sdk/compare/v0.102.0...v0.103.0) (2026-09-15)
From 76dee03d215481de6ee42b2cba631b5f5893c1a3 Mon Sep 17 00:00:00 2001
From: "kernel-internal[bot]"
<260533166+kernel-internal[bot]@users.noreply.github.com>
Date: Tue, 15 Sep 2026 19:58:01 +0000
Subject: [PATCH 270/366] feat: Square UKP and Hypeman placement load factors
Stainless-Generated-From: 65cfe5b8da1de339f51b2027237b88f0746f376b
---
src/kernel/types/analysis.py | 6 ++++++
1 file changed, 6 insertions(+)
diff --git a/src/kernel/types/analysis.py b/src/kernel/types/analysis.py
index 34184ae4..6ce7c3b1 100644
--- a/src/kernel/types/analysis.py
+++ b/src/kernel/types/analysis.py
@@ -31,3 +31,9 @@ class Analysis(BaseModel):
status: Literal["running", "completed", "failed", "canceled", "expired"]
"""Lifecycle status of a background analysis."""
+
+ intent: Optional[str] = None
+ """The workload description supplied for this analysis.
+
+ Null when the analysis only tested connectivity.
+ """
From 37d5e9c3d5db76bece8f8ba8d5358b939bc79315 Mon Sep 17 00:00:00 2001
From: "kernel-internal[bot]"
<260533166+kernel-internal[bot]@users.noreply.github.com>
Date: Tue, 15 Sep 2026 20:48:48 +0000
Subject: [PATCH 271/366] feat: Add processor-bound AgentCard preparation
contracts
Stainless-Generated-From: 1b2956dd517385b95316303d9f6b5e962f8d2a40
---
api.md | 1 +
src/kernel/resources/vaults/items.py | 20 ++++++------
src/kernel/types/vaults/__init__.py | 1 +
.../vaults/agentcard_checkout_preparation.py | 7 ++--
.../vaults/agentcard_prepared_processor.py | 7 ++++
.../types/vaults/card_vault_item_state.py | 2 +-
.../vaults/item_perform_operation_params.py | 12 +++----
...kout_vault_item_operation_request_param.py | 14 ++++----
.../vaults/vault_checkout_context_param.py | 28 +++++++++++-----
tests/api_resources/vaults/test_items.py | 32 +++++++++++++++++++
10 files changed, 90 insertions(+), 34 deletions(-)
create mode 100644 src/kernel/types/vaults/agentcard_prepared_processor.py
diff --git a/api.md b/api.md
index fdb9696b..dc22d102 100644
--- a/api.md
+++ b/api.md
@@ -526,6 +526,7 @@ Types:
from kernel.types.vaults import (
AgentcardCheckoutAuthorization,
AgentcardCheckoutPreparation,
+ AgentcardPreparedProcessor,
AuthorizeVaultItemOperationRequest,
CardVaultItemSpec,
CardVaultItemState,
diff --git a/src/kernel/resources/vaults/items.py b/src/kernel/resources/vaults/items.py
index ea0fbf83..cd53e796 100644
--- a/src/kernel/resources/vaults/items.py
+++ b/src/kernel/resources/vaults/items.py
@@ -511,11 +511,11 @@ def perform_operation(
leave the outcome unknown; do not automatically retry.
Args:
- checkout: Required when preparing an unused AgentCard card for Square. Consent is bound to
- this browser and declared merchant origin, not a tab. Wait for the item's
- ready_to_submit status before native Pay and submit within its readiness
- deadline. Unused preparations expire automatically; every preparation is
- single-use, including after failure or expiry.
+ checkout: Required when preparing an unused AgentCard card for a supported tokenization
+ processor. Consent is bound to this browser and declared merchant origin, not a
+ tab. Wait for the item's ready_to_submit status before native Pay and submit
+ within its readiness deadline. Unused preparations expire automatically; every
+ preparation is single-use, including after failure or expiry.
extra_headers: Send extra headers
@@ -1274,11 +1274,11 @@ async def perform_operation(
leave the outcome unknown; do not automatically retry.
Args:
- checkout: Required when preparing an unused AgentCard card for Square. Consent is bound to
- this browser and declared merchant origin, not a tab. Wait for the item's
- ready_to_submit status before native Pay and submit within its readiness
- deadline. Unused preparations expire automatically; every preparation is
- single-use, including after failure or expiry.
+ checkout: Required when preparing an unused AgentCard card for a supported tokenization
+ processor. Consent is bound to this browser and declared merchant origin, not a
+ tab. Wait for the item's ready_to_submit status before native Pay and submit
+ within its readiness deadline. Unused preparations expire automatically; every
+ preparation is single-use, including after failure or expiry.
extra_headers: Send extra headers
diff --git a/src/kernel/types/vaults/__init__.py b/src/kernel/types/vaults/__init__.py
index 9188791e..e2dd28eb 100644
--- a/src/kernel/types/vaults/__init__.py
+++ b/src/kernel/types/vaults/__init__.py
@@ -24,6 +24,7 @@
from .credential_vault_item_spec import CredentialVaultItemSpec as CredentialVaultItemSpec
from .credential_vault_field_type import CredentialVaultFieldType as CredentialVaultFieldType
from .credential_vault_item_state import CredentialVaultItemState as CredentialVaultItemState
+from .agentcard_prepared_processor import AgentcardPreparedProcessor as AgentcardPreparedProcessor
from .credential_collection_action import CredentialCollectionAction as CredentialCollectionAction
from .credential_vault_field_state import CredentialVaultFieldState as CredentialVaultFieldState
from .vault_checkout_context_param import VaultCheckoutContextParam as VaultCheckoutContextParam
diff --git a/src/kernel/types/vaults/agentcard_checkout_preparation.py b/src/kernel/types/vaults/agentcard_checkout_preparation.py
index 8296b2fd..7f9da71f 100644
--- a/src/kernel/types/vaults/agentcard_checkout_preparation.py
+++ b/src/kernel/types/vaults/agentcard_checkout_preparation.py
@@ -5,12 +5,13 @@
from typing_extensions import Literal
from ..._models import BaseModel
+from .agentcard_prepared_processor import AgentcardPreparedProcessor
__all__ = ["AgentcardCheckoutPreparation"]
class AgentcardCheckoutPreparation(BaseModel):
- """One-use Square checkout preparation.
+ """One-use processor-bound checkout preparation.
Keep the approval page open through token handoff. The amount is display-only and does not constrain the merchant's eventual charge.
"""
@@ -19,10 +20,12 @@ class AgentcardCheckoutPreparation(BaseModel):
created_at: datetime
- environment: Literal["production", "sandbox"]
+ environment: Literal["production", "sandbox", "shared"]
merchant_origin: str
+ psp: AgentcardPreparedProcessor
+
status: Literal["creating", "awaiting_approval", "ready", "consumed", "cancelled", "expired", "unknown"]
"""
Preparation consumed means egress claimed the preparation and it cannot be
diff --git a/src/kernel/types/vaults/agentcard_prepared_processor.py b/src/kernel/types/vaults/agentcard_prepared_processor.py
new file mode 100644
index 00000000..ebf94c86
--- /dev/null
+++ b/src/kernel/types/vaults/agentcard_prepared_processor.py
@@ -0,0 +1,7 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from typing_extensions import Literal, TypeAlias
+
+__all__ = ["AgentcardPreparedProcessor"]
+
+AgentcardPreparedProcessor: TypeAlias = Literal["square", "braintree", "worldpay", "bambora", "mercado_pago"]
diff --git a/src/kernel/types/vaults/card_vault_item_state.py b/src/kernel/types/vaults/card_vault_item_state.py
index 98459762..4785f3ac 100644
--- a/src/kernel/types/vaults/card_vault_item_state.py
+++ b/src/kernel/types/vaults/card_vault_item_state.py
@@ -112,7 +112,7 @@ class AgentCardCardState(BaseModel):
masks: Optional[AgentCardCardStateMasks] = None
preparation: Optional[AgentcardCheckoutPreparation] = None
- """One-use Square checkout preparation.
+ """One-use processor-bound checkout preparation.
Keep the approval page open through token handoff. The amount is display-only
and does not constrain the merchant's eventual charge.
diff --git a/src/kernel/types/vaults/item_perform_operation_params.py b/src/kernel/types/vaults/item_perform_operation_params.py
index 33c38043..c1fb80c9 100644
--- a/src/kernel/types/vaults/item_perform_operation_params.py
+++ b/src/kernel/types/vaults/item_perform_operation_params.py
@@ -33,12 +33,12 @@ class PrepareCheckoutVaultItemOperationRequest(TypedDict, total=False):
id_or_name: Required[str]
checkout: Required[VaultCheckoutContextParam]
- """Required when preparing an unused AgentCard card for Square.
-
- Consent is bound to this browser and declared merchant origin, not a tab. Wait
- for the item's ready_to_submit status before native Pay and submit within its
- readiness deadline. Unused preparations expire automatically; every preparation
- is single-use, including after failure or expiry.
+ """
+ Required when preparing an unused AgentCard card for a supported tokenization
+ processor. Consent is bound to this browser and declared merchant origin, not a
+ tab. Wait for the item's ready_to_submit status before native Pay and submit
+ within its readiness deadline. Unused preparations expire automatically; every
+ preparation is single-use, including after failure or expiry.
"""
type: Required[Literal["prepare_checkout"]]
diff --git a/src/kernel/types/vaults/prepare_checkout_vault_item_operation_request_param.py b/src/kernel/types/vaults/prepare_checkout_vault_item_operation_request_param.py
index 9900c513..72fdb4d2 100644
--- a/src/kernel/types/vaults/prepare_checkout_vault_item_operation_request_param.py
+++ b/src/kernel/types/vaults/prepare_checkout_vault_item_operation_request_param.py
@@ -10,18 +10,18 @@
class PrepareCheckoutVaultItemOperationRequestParam(TypedDict, total=False):
- """Prepare an unused AgentCard card for Square checkout.
+ """Prepare an unused AgentCard card for a supported tokenization checkout.
Deliver the returned approval URL and keep the approval page open. Poll the item until ready_to_submit, then submit native Pay before preparation.expires_at. Readiness lasts at most 30 seconds. Unused preparations expire automatically. Preparations are single-use even after failure or expiry; do not automatically retry and reconcile uncertain outcomes with the merchant.
"""
checkout: Required[VaultCheckoutContextParam]
- """Required when preparing an unused AgentCard card for Square.
-
- Consent is bound to this browser and declared merchant origin, not a tab. Wait
- for the item's ready_to_submit status before native Pay and submit within its
- readiness deadline. Unused preparations expire automatically; every preparation
- is single-use, including after failure or expiry.
+ """
+ Required when preparing an unused AgentCard card for a supported tokenization
+ processor. Consent is bound to this browser and declared merchant origin, not a
+ tab. Wait for the item's ready_to_submit status before native Pay and submit
+ within its readiness deadline. Unused preparations expire automatically; every
+ preparation is single-use, including after failure or expiry.
"""
type: Required[Literal["prepare_checkout"]]
diff --git a/src/kernel/types/vaults/vault_checkout_context_param.py b/src/kernel/types/vaults/vault_checkout_context_param.py
index 8b08c40c..f5fda5db 100644
--- a/src/kernel/types/vaults/vault_checkout_context_param.py
+++ b/src/kernel/types/vaults/vault_checkout_context_param.py
@@ -4,24 +4,36 @@
from typing_extensions import Literal, Required, TypedDict
+from .agentcard_prepared_processor import AgentcardPreparedProcessor
+
__all__ = ["VaultCheckoutContextParam"]
class VaultCheckoutContextParam(TypedDict, total=False):
- """Required when preparing an unused AgentCard card for Square.
-
- Consent is bound to this browser and declared merchant origin, not a tab. Wait for the item's ready_to_submit status before native Pay and submit within its readiness deadline. Unused preparations expire automatically; every preparation is single-use, including after failure or expiry.
+ """
+ Required when preparing an unused AgentCard card for a supported tokenization processor. Consent is bound to this browser and declared merchant origin, not a tab. Wait for the item's ready_to_submit status before native Pay and submit within its readiness deadline. Unused preparations expire automatically; every preparation is single-use, including after failure or expiry.
"""
browser_id: Required[str]
"""Active browser session with this vault bound to it."""
- environment: Required[Literal["production", "sandbox"]]
- """Square environment, independent of the AgentCard credential mode."""
+ environment: Required[Literal["production", "sandbox", "shared"]]
+ """
+ Use production or sandbox for Square, Braintree and Worldpay; shared for Bambora
+ and Mercado Pago. Shared endpoints do not establish test mode. Merchant
+ credentials/configuration determine processor test mode, independently of the
+ AgentCard credential mode.
+ """
merchant_origin: Required[str]
- """Canonical HTTPS origin of the top-level merchant document, not the Square
- iframe.
+ """
+ Canonical HTTPS origin of the top-level merchant document, not a processor
+ iframe. HTTP localhost is accepted for tests.
+ """
+
+ psp: AgentcardPreparedProcessor
+ """Tokenization processor.
- HTTP localhost is accepted for tests.
+ Omit for Square compatibility. Non-Square processors require multi-processor
+ preparation enablement.
"""
diff --git a/tests/api_resources/vaults/test_items.py b/tests/api_resources/vaults/test_items.py
index 1eeb98e3..b80631eb 100644
--- a/tests/api_resources/vaults/test_items.py
+++ b/tests/api_resources/vaults/test_items.py
@@ -604,6 +604,22 @@ def test_method_perform_operation_overload_3(self, client: Kernel) -> None:
)
assert_matches_type(VaultItemOperationResponse, item, path=["response"])
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ def test_method_perform_operation_with_all_params_overload_3(self, client: Kernel) -> None:
+ item = client.vaults.items.perform_operation(
+ key="key",
+ id_or_name="id_or_name",
+ checkout={
+ "browser_id": "browser_id",
+ "environment": "production",
+ "merchant_origin": "merchant_origin",
+ "psp": "square",
+ },
+ type="prepare_checkout",
+ )
+ assert_matches_type(VaultItemOperationResponse, item, path=["response"])
+
@pytest.mark.skip(reason="Mock server tests are disabled")
@parametrize
def test_raw_response_perform_operation_overload_3(self, client: Kernel) -> None:
@@ -1793,6 +1809,22 @@ async def test_method_perform_operation_overload_3(self, async_client: AsyncKern
)
assert_matches_type(VaultItemOperationResponse, item, path=["response"])
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ async def test_method_perform_operation_with_all_params_overload_3(self, async_client: AsyncKernel) -> None:
+ item = await async_client.vaults.items.perform_operation(
+ key="key",
+ id_or_name="id_or_name",
+ checkout={
+ "browser_id": "browser_id",
+ "environment": "production",
+ "merchant_origin": "merchant_origin",
+ "psp": "square",
+ },
+ type="prepare_checkout",
+ )
+ assert_matches_type(VaultItemOperationResponse, item, path=["response"])
+
@pytest.mark.skip(reason="Mock server tests are disabled")
@parametrize
async def test_raw_response_perform_operation_overload_3(self, async_client: AsyncKernel) -> None:
From afe2feaf2dcea1f4fc160d66cfe9f3711b61a447 Mon Sep 17 00:00:00 2001
From: "kernel-internal[bot]"
<260533166+kernel-internal[bot]@users.noreply.github.com>
Date: Wed, 16 Sep 2026 00:43:42 +0000
Subject: [PATCH 272/366] feat: Polish and publish the Config Registry API
Stainless-Generated-From: c13de5a1ff00481bd0125cfd74527f4bae1a301f
---
.stats.yml | 2 +-
api.md | 1 +
.../resources/config_registry/analyses.py | 86 +++++++++++++++++++
.../config_registry/config_registry.py | 10 ++-
.../types/config_registry_resolve_params.py | 5 +-
src/kernel/types/evidence.py | 10 +--
src/kernel/types/recommendation.py | 13 ++-
.../config_registry/test_analyses.py | 84 ++++++++++++++++++
8 files changed, 191 insertions(+), 20 deletions(-)
diff --git a/.stats.yml b/.stats.yml
index a0ee50ed..88ce9db5 100644
--- a/.stats.yml
+++ b/.stats.yml
@@ -1 +1 @@
-configured_endpoints: 163
+configured_endpoints: 164
diff --git a/api.md b/api.md
index dc22d102..0679de62 100644
--- a/api.md
+++ b/api.md
@@ -109,6 +109,7 @@ Methods:
- client.config_registry.analyses.retrieve(id) -> ConfigRegistryResponse
- client.config_registry.analyses.list(\*\*params) -> SyncOffsetPagination[AnalysisSummary]
+- client.config_registry.analyses.cancel(id) -> ConfigRegistryResponse
# Browsers
diff --git a/src/kernel/resources/config_registry/analyses.py b/src/kernel/resources/config_registry/analyses.py
index c0cbd2c7..3d975f32 100644
--- a/src/kernel/resources/config_registry/analyses.py
+++ b/src/kernel/resources/config_registry/analyses.py
@@ -126,6 +126,43 @@ def list(
model=AnalysisSummary,
)
+ def cancel(
+ self,
+ id: str,
+ *,
+ # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
+ # The extra values given here take precedence over values defined on the client or passed to this method.
+ extra_headers: Headers | None = None,
+ extra_query: Query | None = None,
+ extra_body: Body | None = None,
+ timeout: float | httpx.Timeout | None | NotGiven = not_given,
+ ) -> ConfigRegistryResponse:
+ """Requests cancellation of a running project-scoped analysis.
+
+ Cancellation is
+ asynchronous; poll the analysis until its status becomes canceled. Repeating the
+ request after the analysis reaches a terminal state returns the existing
+ outcome.
+
+ Args:
+ extra_headers: Send extra headers
+
+ extra_query: Add additional query parameters to the request
+
+ extra_body: Add additional JSON properties to the request
+
+ timeout: Override the client-level default timeout for this request, in seconds
+ """
+ if not id:
+ raise ValueError(f"Expected a non-empty value for `id` but received {id!r}")
+ return self._post(
+ path_template("/config-registry/analyses/{id}/cancel", id=id),
+ options=make_request_options(
+ extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
+ ),
+ cast_to=ConfigRegistryResponse,
+ )
+
class AsyncAnalysesResource(AsyncAPIResource):
"""Resolve browser and proxy recommendations for bot-protected sites."""
@@ -230,6 +267,43 @@ def list(
model=AnalysisSummary,
)
+ async def cancel(
+ self,
+ id: str,
+ *,
+ # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
+ # The extra values given here take precedence over values defined on the client or passed to this method.
+ extra_headers: Headers | None = None,
+ extra_query: Query | None = None,
+ extra_body: Body | None = None,
+ timeout: float | httpx.Timeout | None | NotGiven = not_given,
+ ) -> ConfigRegistryResponse:
+ """Requests cancellation of a running project-scoped analysis.
+
+ Cancellation is
+ asynchronous; poll the analysis until its status becomes canceled. Repeating the
+ request after the analysis reaches a terminal state returns the existing
+ outcome.
+
+ Args:
+ extra_headers: Send extra headers
+
+ extra_query: Add additional query parameters to the request
+
+ extra_body: Add additional JSON properties to the request
+
+ timeout: Override the client-level default timeout for this request, in seconds
+ """
+ if not id:
+ raise ValueError(f"Expected a non-empty value for `id` but received {id!r}")
+ return await self._post(
+ path_template("/config-registry/analyses/{id}/cancel", id=id),
+ options=make_request_options(
+ extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
+ ),
+ cast_to=ConfigRegistryResponse,
+ )
+
class AnalysesResourceWithRawResponse:
def __init__(self, analyses: AnalysesResource) -> None:
@@ -241,6 +315,9 @@ def __init__(self, analyses: AnalysesResource) -> None:
self.list = to_raw_response_wrapper(
analyses.list,
)
+ self.cancel = to_raw_response_wrapper(
+ analyses.cancel,
+ )
class AsyncAnalysesResourceWithRawResponse:
@@ -253,6 +330,9 @@ def __init__(self, analyses: AsyncAnalysesResource) -> None:
self.list = async_to_raw_response_wrapper(
analyses.list,
)
+ self.cancel = async_to_raw_response_wrapper(
+ analyses.cancel,
+ )
class AnalysesResourceWithStreamingResponse:
@@ -265,6 +345,9 @@ def __init__(self, analyses: AnalysesResource) -> None:
self.list = to_streamed_response_wrapper(
analyses.list,
)
+ self.cancel = to_streamed_response_wrapper(
+ analyses.cancel,
+ )
class AsyncAnalysesResourceWithStreamingResponse:
@@ -277,3 +360,6 @@ def __init__(self, analyses: AsyncAnalysesResource) -> None:
self.list = async_to_streamed_response_wrapper(
analyses.list,
)
+ self.cancel = async_to_streamed_response_wrapper(
+ analyses.cancel,
+ )
diff --git a/src/kernel/resources/config_registry/config_registry.py b/src/kernel/resources/config_registry/config_registry.py
index 27a8766c..39faa9f5 100644
--- a/src/kernel/resources/config_registry/config_registry.py
+++ b/src/kernel/resources/config_registry/config_registry.py
@@ -190,8 +190,9 @@ def resolve(
any non-HTTPS destination as off-site and will not drive an http one. Kernel
uses it to drive the browser further into the site, where it can observe
protections that only appear once a session interacts. When this target already
- has a verified configuration, the run confirms that one instead of re-deriving
- the whole matrix, so supplying an intent narrows what can be recommended.
+ has a recommended configuration, the run confirms that one instead of
+ re-deriving the whole matrix, so supplying an intent narrows what can be
+ recommended.
extra_headers: Send extra headers
@@ -374,8 +375,9 @@ async def resolve(
any non-HTTPS destination as off-site and will not drive an http one. Kernel
uses it to drive the browser further into the site, where it can observe
protections that only appear once a session interacts. When this target already
- has a verified configuration, the run confirms that one instead of re-deriving
- the whole matrix, so supplying an intent narrows what can be recommended.
+ has a recommended configuration, the run confirms that one instead of
+ re-deriving the whole matrix, so supplying an intent narrows what can be
+ recommended.
extra_headers: Send extra headers
diff --git a/src/kernel/types/config_registry_resolve_params.py b/src/kernel/types/config_registry_resolve_params.py
index 2c0ecc00..e1a063dc 100644
--- a/src/kernel/types/config_registry_resolve_params.py
+++ b/src/kernel/types/config_registry_resolve_params.py
@@ -27,6 +27,7 @@ class ConfigRegistryResolveParams(TypedDict, total=False):
any non-HTTPS destination as off-site and will not drive an http one. Kernel
uses it to drive the browser further into the site, where it can observe
protections that only appear once a session interacts. When this target already
- has a verified configuration, the run confirms that one instead of re-deriving
- the whole matrix, so supplying an intent narrows what can be recommended.
+ has a recommended configuration, the run confirms that one instead of
+ re-deriving the whole matrix, so supplying an intent narrows what can be
+ recommended.
"""
diff --git a/src/kernel/types/evidence.py b/src/kernel/types/evidence.py
index 1a777de9..c53f2842 100644
--- a/src/kernel/types/evidence.py
+++ b/src/kernel/types/evidence.py
@@ -30,9 +30,9 @@ class Evidence(BaseModel):
success_rate: float
"""Accessed trials divided by judged trials. Inconclusive trials are excluded."""
- last_verified_at: Optional[datetime] = None
- """Most recent contributing run where this config met the success threshold.
-
- Omitted for knowledge assembled from runs that did not independently meet the
- threshold.
+ last_supported_at: Optional[datetime] = None
+ """
+ Most recent contributing run whose evidence supported recommending this
+ configuration. Omitted when no individual run independently met the
+ recommendation threshold.
"""
diff --git a/src/kernel/types/recommendation.py b/src/kernel/types/recommendation.py
index 74b1761a..e7d21b8e 100644
--- a/src/kernel/types/recommendation.py
+++ b/src/kernel/types/recommendation.py
@@ -17,7 +17,11 @@ class Recommendation(BaseModel):
evidence: Evidence
match_scope: Literal["exact", "host", "domain"]
- """Specificity of knowledge matched for this recommendation."""
+ """Specificity of knowledge matched for this recommendation.
+
+ Exact matches use knowledge for the requested target; host and domain matches
+ use broader fallback knowledge.
+ """
matched_target: str
"""Target value that supplied the recommendation."""
@@ -26,10 +30,3 @@ class Recommendation(BaseModel):
"""Proxy recipe for the recommended browser."""
type: Literal["recommendation"]
-
- verification: Literal["verified", "inferred"]
- """
- Exact matches meet the evidence threshold; host and domain fallbacks are
- inferred. Check evidence.last_verified_at for successful verification age and
- last_observed_at for the latest evidence.
- """
diff --git a/tests/api_resources/config_registry/test_analyses.py b/tests/api_resources/config_registry/test_analyses.py
index 50b608df..0a9c8607 100644
--- a/tests/api_resources/config_registry/test_analyses.py
+++ b/tests/api_resources/config_registry/test_analyses.py
@@ -98,6 +98,48 @@ def test_streaming_response_list(self, client: Kernel) -> None:
assert cast(Any, response.is_closed) is True
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ def test_method_cancel(self, client: Kernel) -> None:
+ analysis = client.config_registry.analyses.cancel(
+ "id",
+ )
+ assert_matches_type(ConfigRegistryResponse, analysis, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ def test_raw_response_cancel(self, client: Kernel) -> None:
+ response = client.config_registry.analyses.with_raw_response.cancel(
+ "id",
+ )
+
+ assert response.is_closed is True
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+ analysis = response.parse()
+ assert_matches_type(ConfigRegistryResponse, analysis, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ def test_streaming_response_cancel(self, client: Kernel) -> None:
+ with client.config_registry.analyses.with_streaming_response.cancel(
+ "id",
+ ) as response:
+ assert not response.is_closed
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+
+ analysis = response.parse()
+ assert_matches_type(ConfigRegistryResponse, analysis, path=["response"])
+
+ assert cast(Any, response.is_closed) is True
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ def test_path_params_cancel(self, client: Kernel) -> None:
+ with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"):
+ client.config_registry.analyses.with_raw_response.cancel(
+ "",
+ )
+
class TestAsyncAnalyses:
parametrize = pytest.mark.parametrize(
@@ -183,3 +225,45 @@ async def test_streaming_response_list(self, async_client: AsyncKernel) -> None:
assert_matches_type(AsyncOffsetPagination[AnalysisSummary], analysis, path=["response"])
assert cast(Any, response.is_closed) is True
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ async def test_method_cancel(self, async_client: AsyncKernel) -> None:
+ analysis = await async_client.config_registry.analyses.cancel(
+ "id",
+ )
+ assert_matches_type(ConfigRegistryResponse, analysis, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ async def test_raw_response_cancel(self, async_client: AsyncKernel) -> None:
+ response = await async_client.config_registry.analyses.with_raw_response.cancel(
+ "id",
+ )
+
+ assert response.is_closed is True
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+ analysis = await response.parse()
+ assert_matches_type(ConfigRegistryResponse, analysis, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ async def test_streaming_response_cancel(self, async_client: AsyncKernel) -> None:
+ async with async_client.config_registry.analyses.with_streaming_response.cancel(
+ "id",
+ ) as response:
+ assert not response.is_closed
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+
+ analysis = await response.parse()
+ assert_matches_type(ConfigRegistryResponse, analysis, 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_cancel(self, async_client: AsyncKernel) -> None:
+ with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"):
+ await async_client.config_registry.analyses.with_raw_response.cancel(
+ "",
+ )
From 82a475dae250abce535476386be88033a5314656 Mon Sep 17 00:00:00 2001
From: "kernel-internal[bot]"
<260533166+kernel-internal[bot]@users.noreply.github.com>
Date: Wed, 16 Sep 2026 17:17:47 +0000
Subject: [PATCH 273/366] feat: Add start_url to browser session updates
Stainless-Generated-From: 245527a0a5fccc56447f7227e933bc235a78cfe4
---
src/kernel/resources/browsers/browsers.py | 14 ++++++++++++++
src/kernel/types/browser_create_response.py | 2 +-
src/kernel/types/browser_list_response.py | 2 +-
src/kernel/types/browser_pool_acquire_response.py | 2 +-
src/kernel/types/browser_retrieve_response.py | 2 +-
src/kernel/types/browser_update_params.py | 8 ++++++++
src/kernel/types/browser_update_response.py | 2 +-
.../types/invocation_list_browsers_response.py | 2 +-
src/kernel/types/vaults/card_vault_item_state.py | 7 +++++--
.../fill_vault_item_operation_request_param.py | 7 +++----
src/kernel/types/vaults/vault_item.py | 4 ++++
.../types/vaults/vault_item_operation_response.py | 4 ++++
tests/api_resources/test_browsers.py | 2 ++
13 files changed, 46 insertions(+), 12 deletions(-)
diff --git a/src/kernel/resources/browsers/browsers.py b/src/kernel/resources/browsers/browsers.py
index 219dd462..49dc4063 100644
--- a/src/kernel/resources/browsers/browsers.py
+++ b/src/kernel/resources/browsers/browsers.py
@@ -394,6 +394,7 @@ def update(
profile: BrowserProfile | Omit = omit,
proxy: BrowserProxyConfigParam | Omit = omit,
proxy_id: Optional[str] | Omit = omit,
+ start_url: str | Omit = omit,
tags: Optional[TagsParam] | Omit = omit,
telemetry: Optional[browser_update_params.Telemetry] | Omit = omit,
viewport: browser_update_params.Viewport | Omit = omit,
@@ -428,6 +429,11 @@ def update(
proxy_id: ID of the proxy to use. Omit to leave unchanged, set to empty string to remove
proxy. Deprecated in favor of proxy.
+ start_url: Optional URL to navigate the browser to after applying this update. When a
+ profile is loaded in the same update, this overrides the profile's restored
+ tabs. Navigation is best-effort, so failures do not fail the update. Omit or set
+ to an empty string to leave the current page unchanged.
+
tags: User-defined key-value tags for the browser session. Omit to leave unchanged.
Provide a map to replace the entire tag set (full replace, not a merge). Set to
an empty object ({}) to clear all tags. Up to 50 pairs.
@@ -459,6 +465,7 @@ def update(
"profile": profile,
"proxy": proxy,
"proxy_id": proxy_id,
+ "start_url": start_url,
"tags": tags,
"telemetry": telemetry,
"viewport": viewport,
@@ -1024,6 +1031,7 @@ async def update(
profile: BrowserProfile | Omit = omit,
proxy: BrowserProxyConfigParam | Omit = omit,
proxy_id: Optional[str] | Omit = omit,
+ start_url: str | Omit = omit,
tags: Optional[TagsParam] | Omit = omit,
telemetry: Optional[browser_update_params.Telemetry] | Omit = omit,
viewport: browser_update_params.Viewport | Omit = omit,
@@ -1058,6 +1066,11 @@ async def update(
proxy_id: ID of the proxy to use. Omit to leave unchanged, set to empty string to remove
proxy. Deprecated in favor of proxy.
+ start_url: Optional URL to navigate the browser to after applying this update. When a
+ profile is loaded in the same update, this overrides the profile's restored
+ tabs. Navigation is best-effort, so failures do not fail the update. Omit or set
+ to an empty string to leave the current page unchanged.
+
tags: User-defined key-value tags for the browser session. Omit to leave unchanged.
Provide a map to replace the entire tag set (full replace, not a merge). Set to
an empty object ({}) to clear all tags. Up to 50 pairs.
@@ -1089,6 +1102,7 @@ async def update(
"profile": profile,
"proxy": proxy,
"proxy_id": proxy_id,
+ "start_url": start_url,
"tags": tags,
"telemetry": telemetry,
"viewport": viewport,
diff --git a/src/kernel/types/browser_create_response.py b/src/kernel/types/browser_create_response.py
index 678bcbcb..a5e674f7 100644
--- a/src/kernel/types/browser_create_response.py
+++ b/src/kernel/types/browser_create_response.py
@@ -106,7 +106,7 @@ class BrowserCreateResponse(BaseModel):
"""
start_url: Optional[str] = None
- """URL the session was asked to navigate to on creation, if any.
+ """URL the session was most recently asked to navigate to, if any.
Recorded for debugging. Navigation is fire-and-forget — the URL is dispatched to
the browser without waiting for it to load, and any errors (DNS failure, bad
diff --git a/src/kernel/types/browser_list_response.py b/src/kernel/types/browser_list_response.py
index aa281a39..3cc62ebe 100644
--- a/src/kernel/types/browser_list_response.py
+++ b/src/kernel/types/browser_list_response.py
@@ -106,7 +106,7 @@ class BrowserListResponse(BaseModel):
"""
start_url: Optional[str] = None
- """URL the session was asked to navigate to on creation, if any.
+ """URL the session was most recently asked to navigate to, if any.
Recorded for debugging. Navigation is fire-and-forget — the URL is dispatched to
the browser without waiting for it to load, and any errors (DNS failure, bad
diff --git a/src/kernel/types/browser_pool_acquire_response.py b/src/kernel/types/browser_pool_acquire_response.py
index d472b5e0..c223c927 100644
--- a/src/kernel/types/browser_pool_acquire_response.py
+++ b/src/kernel/types/browser_pool_acquire_response.py
@@ -106,7 +106,7 @@ class BrowserPoolAcquireResponse(BaseModel):
"""
start_url: Optional[str] = None
- """URL the session was asked to navigate to on creation, if any.
+ """URL the session was most recently asked to navigate to, if any.
Recorded for debugging. Navigation is fire-and-forget — the URL is dispatched to
the browser without waiting for it to load, and any errors (DNS failure, bad
diff --git a/src/kernel/types/browser_retrieve_response.py b/src/kernel/types/browser_retrieve_response.py
index e6881ab1..936b41c6 100644
--- a/src/kernel/types/browser_retrieve_response.py
+++ b/src/kernel/types/browser_retrieve_response.py
@@ -106,7 +106,7 @@ class BrowserRetrieveResponse(BaseModel):
"""
start_url: Optional[str] = None
- """URL the session was asked to navigate to on creation, if any.
+ """URL the session was most recently asked to navigate to, if any.
Recorded for debugging. Navigation is fire-and-forget — the URL is dispatched to
the browser without waiting for it to load, and any errors (DNS failure, bad
diff --git a/src/kernel/types/browser_update_params.py b/src/kernel/types/browser_update_params.py
index e7769e61..c0592e28 100644
--- a/src/kernel/types/browser_update_params.py
+++ b/src/kernel/types/browser_update_params.py
@@ -59,6 +59,14 @@ class BrowserUpdateParams(TypedDict, total=False):
favor of proxy.
"""
+ start_url: str
+ """Optional URL to navigate the browser to after applying this update.
+
+ When a profile is loaded in the same update, this overrides the profile's
+ restored tabs. Navigation is best-effort, so failures do not fail the update.
+ Omit or set to an empty string to leave the current page unchanged.
+ """
+
tags: Optional[TagsParam]
"""User-defined key-value tags for the browser session.
diff --git a/src/kernel/types/browser_update_response.py b/src/kernel/types/browser_update_response.py
index 65be1ba3..d2296c21 100644
--- a/src/kernel/types/browser_update_response.py
+++ b/src/kernel/types/browser_update_response.py
@@ -106,7 +106,7 @@ class BrowserUpdateResponse(BaseModel):
"""
start_url: Optional[str] = None
- """URL the session was asked to navigate to on creation, if any.
+ """URL the session was most recently asked to navigate to, if any.
Recorded for debugging. Navigation is fire-and-forget — the URL is dispatched to
the browser without waiting for it to load, and any errors (DNS failure, bad
diff --git a/src/kernel/types/invocation_list_browsers_response.py b/src/kernel/types/invocation_list_browsers_response.py
index bb9d423d..a88f33f9 100644
--- a/src/kernel/types/invocation_list_browsers_response.py
+++ b/src/kernel/types/invocation_list_browsers_response.py
@@ -106,7 +106,7 @@ class Browser(BaseModel):
"""
start_url: Optional[str] = None
- """URL the session was asked to navigate to on creation, if any.
+ """URL the session was most recently asked to navigate to, if any.
Recorded for debugging. Navigation is fire-and-forget — the URL is dispatched to
the browser without waiting for it to load, and any errors (DNS failure, bad
diff --git a/src/kernel/types/vaults/card_vault_item_state.py b/src/kernel/types/vaults/card_vault_item_state.py
index 4785f3ac..a1cb0b68 100644
--- a/src/kernel/types/vaults/card_vault_item_state.py
+++ b/src/kernel/types/vaults/card_vault_item_state.py
@@ -33,6 +33,11 @@ def __getattr__(self, attr: str) -> str: ...
class LinkCardState(BaseModel):
+ """Issued Link cards retain encrypted card material for the fill operation.
+
+ Link cards do not expose aliases or support egress substitution.
+ """
+
provider: Literal["link"]
status: Literal[
@@ -47,8 +52,6 @@ class LinkCardState(BaseModel):
caller-asserted reconciliation operation.
"""
- aliases: Optional[VaultCardAliases] = None
-
domains: Optional[List[str]] = None
masks: Optional[LinkCardStateMasks] = None
diff --git a/src/kernel/types/vaults/fill_vault_item_operation_request_param.py b/src/kernel/types/vaults/fill_vault_item_operation_request_param.py
index 1d8fc8be..012ec2b9 100644
--- a/src/kernel/types/vaults/fill_vault_item_operation_request_param.py
+++ b/src/kernel/types/vaults/fill_vault_item_operation_request_param.py
@@ -34,10 +34,9 @@ class FillVaultItemOperationRequestParam(TypedDict, total=False):
Fill in request order and stop on the first failure. This operation is
not atomic: previously filled fields are not rolled back. Never submit
the form or click buttons, though input/change events may trigger site
- behavior. Fill is the preferred browser-checkout path. Aliases remain an
- alternative for explicitly chosen egress-substitution integrations. Do not
- automatically retry or fall back to aliases after a failed or indeterminate
- operation.
+ behavior. Link cards use fill for browser checkout and do not expose
+ aliases or support egress substitution. Do not automatically retry a
+ failed or indeterminate operation.
Secret values are never returned or included in operation logs, traces,
audit events, or error details. This does not prevent an agent with
diff --git a/src/kernel/types/vaults/vault_item.py b/src/kernel/types/vaults/vault_item.py
index 07253ea4..b9723f4a 100644
--- a/src/kernel/types/vaults/vault_item.py
+++ b/src/kernel/types/vaults/vault_item.py
@@ -127,6 +127,10 @@ class CardVaultItem(BaseModel):
"""Live payment card. Test-mode card creation is not supported."""
state: CardVaultItemState
+ """Issued Link cards retain encrypted card material for the fill operation.
+
+ Link cards do not expose aliases or support egress substitution.
+ """
type: Literal["card"]
diff --git a/src/kernel/types/vaults/vault_item_operation_response.py b/src/kernel/types/vaults/vault_item_operation_response.py
index 163f73a7..33dcd8b3 100644
--- a/src/kernel/types/vaults/vault_item_operation_response.py
+++ b/src/kernel/types/vaults/vault_item_operation_response.py
@@ -127,6 +127,10 @@ class CardVaultItem(BaseModel):
"""Live payment card. Test-mode card creation is not supported."""
state: CardVaultItemState
+ """Issued Link cards retain encrypted card material for the fill operation.
+
+ Link cards do not expose aliases or support egress substitution.
+ """
type: Literal["card"]
diff --git a/tests/api_resources/test_browsers.py b/tests/api_resources/test_browsers.py
index 406b0ce8..28189f15 100644
--- a/tests/api_resources/test_browsers.py
+++ b/tests/api_resources/test_browsers.py
@@ -207,6 +207,7 @@ def test_method_update_with_all_params(self, client: Kernel) -> None:
"name": "x",
},
proxy_id="proxy_id",
+ start_url="https://example.com",
tags={
"team": "backend",
"env": "staging",
@@ -680,6 +681,7 @@ async def test_method_update_with_all_params(self, async_client: AsyncKernel) ->
"name": "x",
},
proxy_id="proxy_id",
+ start_url="https://example.com",
tags={
"team": "backend",
"env": "staging",
From bb426056a26874bd4de958d8444986e7d97fb4a0 Mon Sep 17 00:00:00 2001
From: "stainless-app[bot]"
<142633134+stainless-app[bot]@users.noreply.github.com>
Date: Wed, 16 Sep 2026 17:40:21 +0000
Subject: [PATCH 274/366] release: 0.106.0
---
.release-please-manifest.json | 2 +-
CHANGELOG.md | 11 +++++++++++
pyproject.toml | 2 +-
src/kernel/_version.py | 2 +-
4 files changed, 14 insertions(+), 3 deletions(-)
diff --git a/.release-please-manifest.json b/.release-please-manifest.json
index 19c0e9cc..f371d275 100644
--- a/.release-please-manifest.json
+++ b/.release-please-manifest.json
@@ -1,3 +1,3 @@
{
- ".": "0.104.0"
+ ".": "0.106.0"
}
\ No newline at end of file
diff --git a/CHANGELOG.md b/CHANGELOG.md
index eb98375c..cb10a2df 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,16 @@
# Changelog
+## 0.106.0 (2026-09-16)
+
+Full Changelog: [v0.104.0...v0.106.0](https://github.com/kernel/kernel-python-sdk/compare/v0.104.0...v0.106.0)
+
+### Features
+
+* Add processor-bound AgentCard preparation contracts ([37d5e9c](https://github.com/kernel/kernel-python-sdk/commit/37d5e9c3d5db76bece8f8ba8d5358b939bc79315))
+* Add start_url to browser session updates ([82a475d](https://github.com/kernel/kernel-python-sdk/commit/82a475dae250abce535476386be88033a5314656))
+* Polish and publish the Config Registry API ([afe2fea](https://github.com/kernel/kernel-python-sdk/commit/afe2feaf2dcea1f4fc160d66cfe9f3711b61a447))
+* Square UKP and Hypeman placement load factors ([76dee03](https://github.com/kernel/kernel-python-sdk/commit/76dee03d215481de6ee42b2cba631b5f5893c1a3))
+
## [0.104.0](https://github.com/kernel/kernel-python-sdk/compare/v0.103.0...v0.104.0) (2026-09-15)
diff --git a/pyproject.toml b/pyproject.toml
index 6b6ca1ef..a89b3709 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -1,6 +1,6 @@
[project]
name = "kernel"
-version = "0.104.0"
+version = "0.106.0"
description = "The official Python library for the kernel API"
dynamic = ["readme"]
license = "Apache-2.0"
diff --git a/src/kernel/_version.py b/src/kernel/_version.py
index 0a7fea06..66f10fd8 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.104.0" # x-release-please-version
+__version__ = "0.106.0" # x-release-please-version
From 82d9f8400a761a37debabeb79e6e5d8bec2cbe32 Mon Sep 17 00:00:00 2001
From: "kernel-internal[bot]"
<260533166+kernel-internal[bot]@users.noreply.github.com>
Date: Wed, 16 Sep 2026 17:50:00 +0000
Subject: [PATCH 275/366] feat: Return Link spend request errors directly
Stainless-Generated-From: 9598a45cc204663dc64d611c88b0a7a457b642f0
---
src/kernel/resources/vaults/items.py | 48 ++++++++++++-------
.../types/vaults/card_vault_item_spec.py | 5 +-
.../vaults/card_vault_item_spec_param.py | 5 +-
3 files changed, 40 insertions(+), 18 deletions(-)
diff --git a/src/kernel/resources/vaults/items.py b/src/kernel/resources/vaults/items.py
index cd53e796..8c720ee5 100644
--- a/src/kernel/resources/vaults/items.py
+++ b/src/kernel/resources/vaults/items.py
@@ -409,8 +409,10 @@ def perform_operation(
device approval before native Square Pay. Keep the returned approval page open,
poll until ready_to_submit, then submit before preparation.expires_at. Unused
preparations expire automatically and cannot be reused. If spend-request
- creation is rate limited, returns HTTP 429 with code
- `spend_request_rate_limited`; stop and back off before retrying.
+ creation is rejected with a non-retryable provider error, the card item is
+ deleted and the provider's error code and message are returned. Rate limits
+ return HTTP 429 and retain the card item; stop, back off, and retry the same
+ authorize operation.
Fill returns a value-free execution result. Validation failures before writing
return 400 (invalid request or targets), 403 (access or destination denied), 404
@@ -454,8 +456,10 @@ def perform_operation(
device approval before native Square Pay. Keep the returned approval page open,
poll until ready_to_submit, then submit before preparation.expires_at. Unused
preparations expire automatically and cannot be reused. If spend-request
- creation is rate limited, returns HTTP 429 with code
- `spend_request_rate_limited`; stop and back off before retrying.
+ creation is rejected with a non-retryable provider error, the card item is
+ deleted and the provider's error code and message are returned. Rate limits
+ return HTTP 429 and retain the card item; stop, back off, and retry the same
+ authorize operation.
Fill returns a value-free execution result. Validation failures before writing
return 400 (invalid request or targets), 403 (access or destination denied), 404
@@ -500,8 +504,10 @@ def perform_operation(
device approval before native Square Pay. Keep the returned approval page open,
poll until ready_to_submit, then submit before preparation.expires_at. Unused
preparations expire automatically and cannot be reused. If spend-request
- creation is rate limited, returns HTTP 429 with code
- `spend_request_rate_limited`; stop and back off before retrying.
+ creation is rejected with a non-retryable provider error, the card item is
+ deleted and the provider's error code and message are returned. Rate limits
+ return HTTP 429 and retain the card item; stop, back off, and retry the same
+ authorize operation.
Fill returns a value-free execution result. Validation failures before writing
return 400 (invalid request or targets), 403 (access or destination denied), 404
@@ -555,8 +561,10 @@ def perform_operation(
device approval before native Square Pay. Keep the returned approval page open,
poll until ready_to_submit, then submit before preparation.expires_at. Unused
preparations expire automatically and cannot be reused. If spend-request
- creation is rate limited, returns HTTP 429 with code
- `spend_request_rate_limited`; stop and back off before retrying.
+ creation is rejected with a non-retryable provider error, the card item is
+ deleted and the provider's error code and message are returned. Rate limits
+ return HTTP 429 and retain the card item; stop, back off, and retry the same
+ authorize operation.
Fill returns a value-free execution result. Validation failures before writing
return 400 (invalid request or targets), 403 (access or destination denied), 404
@@ -1172,8 +1180,10 @@ async def perform_operation(
device approval before native Square Pay. Keep the returned approval page open,
poll until ready_to_submit, then submit before preparation.expires_at. Unused
preparations expire automatically and cannot be reused. If spend-request
- creation is rate limited, returns HTTP 429 with code
- `spend_request_rate_limited`; stop and back off before retrying.
+ creation is rejected with a non-retryable provider error, the card item is
+ deleted and the provider's error code and message are returned. Rate limits
+ return HTTP 429 and retain the card item; stop, back off, and retry the same
+ authorize operation.
Fill returns a value-free execution result. Validation failures before writing
return 400 (invalid request or targets), 403 (access or destination denied), 404
@@ -1217,8 +1227,10 @@ async def perform_operation(
device approval before native Square Pay. Keep the returned approval page open,
poll until ready_to_submit, then submit before preparation.expires_at. Unused
preparations expire automatically and cannot be reused. If spend-request
- creation is rate limited, returns HTTP 429 with code
- `spend_request_rate_limited`; stop and back off before retrying.
+ creation is rejected with a non-retryable provider error, the card item is
+ deleted and the provider's error code and message are returned. Rate limits
+ return HTTP 429 and retain the card item; stop, back off, and retry the same
+ authorize operation.
Fill returns a value-free execution result. Validation failures before writing
return 400 (invalid request or targets), 403 (access or destination denied), 404
@@ -1263,8 +1275,10 @@ async def perform_operation(
device approval before native Square Pay. Keep the returned approval page open,
poll until ready_to_submit, then submit before preparation.expires_at. Unused
preparations expire automatically and cannot be reused. If spend-request
- creation is rate limited, returns HTTP 429 with code
- `spend_request_rate_limited`; stop and back off before retrying.
+ creation is rejected with a non-retryable provider error, the card item is
+ deleted and the provider's error code and message are returned. Rate limits
+ return HTTP 429 and retain the card item; stop, back off, and retry the same
+ authorize operation.
Fill returns a value-free execution result. Validation failures before writing
return 400 (invalid request or targets), 403 (access or destination denied), 404
@@ -1318,8 +1332,10 @@ async def perform_operation(
device approval before native Square Pay. Keep the returned approval page open,
poll until ready_to_submit, then submit before preparation.expires_at. Unused
preparations expire automatically and cannot be reused. If spend-request
- creation is rate limited, returns HTTP 429 with code
- `spend_request_rate_limited`; stop and back off before retrying.
+ creation is rejected with a non-retryable provider error, the card item is
+ deleted and the provider's error code and message are returned. Rate limits
+ return HTTP 429 and retain the card item; stop, back off, and retry the same
+ authorize operation.
Fill returns a value-free execution result. Validation failures before writing
return 400 (invalid request or targets), 403 (access or destination denied), 404
diff --git a/src/kernel/types/vaults/card_vault_item_spec.py b/src/kernel/types/vaults/card_vault_item_spec.py
index d66a5453..0bb0b77a 100644
--- a/src/kernel/types/vaults/card_vault_item_spec.py
+++ b/src/kernel/types/vaults/card_vault_item_spec.py
@@ -59,7 +59,10 @@ class LinkCardVaultItemSpec(BaseModel):
"""Live payment card. Test-mode card creation is not supported."""
amount: int
- """Integer amount in minor currency units."""
+ """Integer amount in minor currency units.
+
+ Link permits at most 50000 per spend request.
+ """
context: str
diff --git a/src/kernel/types/vaults/card_vault_item_spec_param.py b/src/kernel/types/vaults/card_vault_item_spec_param.py
index 3cced6ea..a1680703 100644
--- a/src/kernel/types/vaults/card_vault_item_spec_param.py
+++ b/src/kernel/types/vaults/card_vault_item_spec_param.py
@@ -58,7 +58,10 @@ class LinkCardVaultItemSpec(TypedDict, total=False):
"""Live payment card. Test-mode card creation is not supported."""
amount: Required[int]
- """Integer amount in minor currency units."""
+ """Integer amount in minor currency units.
+
+ Link permits at most 50000 per spend request.
+ """
context: Required[str]
From 755dba5d1273ef9083ffbc4bc7f4adf30aea2fe9 Mon Sep 17 00:00:00 2001
From: "kernel-internal[bot]"
<260533166+kernel-internal[bot]@users.noreply.github.com>
Date: Wed, 16 Sep 2026 18:10:48 +0000
Subject: [PATCH 276/366] feat: Expose persistent Browser REPL through the API
Stainless-Generated-From: 1422c8f3402ddd8a238fda63746dda102aa53937
---
.stats.yml | 2 +-
api.md | 6 +
src/kernel/_client.py | 6 -
src/kernel/resources/browsers/browsers.py | 152 +++++++++++++++++-
src/kernel/types/__init__.py | 5 +
src/kernel/types/browser_repl_content.py | 14 ++
.../types/browser_repl_image_content.py | 15 ++
src/kernel/types/browser_repl_params.py | 25 +++
src/kernel/types/browser_repl_result.py | 43 +++++
src/kernel/types/browser_repl_text_content.py | 19 +++
tests/api_resources/test_browsers.py | 115 +++++++++++++
11 files changed, 391 insertions(+), 11 deletions(-)
create mode 100644 src/kernel/types/browser_repl_content.py
create mode 100644 src/kernel/types/browser_repl_image_content.py
create mode 100644 src/kernel/types/browser_repl_params.py
create mode 100644 src/kernel/types/browser_repl_result.py
create mode 100644 src/kernel/types/browser_repl_text_content.py
diff --git a/.stats.yml b/.stats.yml
index 88ce9db5..40c9d3b0 100644
--- a/.stats.yml
+++ b/.stats.yml
@@ -1 +1 @@
-configured_endpoints: 164
+configured_endpoints: 165
diff --git a/api.md b/api.md
index 0679de62..90bb1465 100644
--- a/api.md
+++ b/api.md
@@ -124,6 +124,11 @@ from kernel.types import (
BrowserProxy,
BrowserProxyConfig,
BrowserProxyMode,
+ BrowserReplContent,
+ BrowserReplImageContent,
+ BrowserReplRequest,
+ BrowserReplResult,
+ BrowserReplTextContent,
BrowserUsage,
Profile,
Tags,
@@ -145,6 +150,7 @@ Methods:
- client.browsers.curl(id_or_name, \*\*params) -> BrowserCurlResponse
- client.browsers.delete_by_id(id_or_name) -> None
- client.browsers.load_extensions(id_or_name, \*\*params) -> None
+- client.browsers.repl(id_or_name, \*\*params) -> BrowserReplResult
## Telemetry
diff --git a/src/kernel/_client.py b/src/kernel/_client.py
index f4c9e48f..13a6e3dc 100644
--- a/src/kernel/_client.py
+++ b/src/kernel/_client.py
@@ -247,7 +247,6 @@ def config_registry(self) -> ConfigRegistryResource:
@cached_property
def browsers(self) -> BrowsersResource:
- """Create and manage browser sessions."""
from .resources.browsers import BrowsersResource
return BrowsersResource(self)
@@ -652,7 +651,6 @@ def config_registry(self) -> AsyncConfigRegistryResource:
@cached_property
def browsers(self) -> AsyncBrowsersResource:
- """Create and manage browser sessions."""
from .resources.browsers import AsyncBrowsersResource
return AsyncBrowsersResource(self)
@@ -959,7 +957,6 @@ def config_registry(self) -> config_registry.ConfigRegistryResourceWithRawRespon
@cached_property
def browsers(self) -> browsers.BrowsersResourceWithRawResponse:
- """Create and manage browser sessions."""
from .resources.browsers import BrowsersResourceWithRawResponse
return BrowsersResourceWithRawResponse(self._client.browsers)
@@ -1098,7 +1095,6 @@ def config_registry(self) -> config_registry.AsyncConfigRegistryResourceWithRawR
@cached_property
def browsers(self) -> browsers.AsyncBrowsersResourceWithRawResponse:
- """Create and manage browser sessions."""
from .resources.browsers import AsyncBrowsersResourceWithRawResponse
return AsyncBrowsersResourceWithRawResponse(self._client.browsers)
@@ -1237,7 +1233,6 @@ def config_registry(self) -> config_registry.ConfigRegistryResourceWithStreaming
@cached_property
def browsers(self) -> browsers.BrowsersResourceWithStreamingResponse:
- """Create and manage browser sessions."""
from .resources.browsers import BrowsersResourceWithStreamingResponse
return BrowsersResourceWithStreamingResponse(self._client.browsers)
@@ -1376,7 +1371,6 @@ def config_registry(self) -> config_registry.AsyncConfigRegistryResourceWithStre
@cached_property
def browsers(self) -> browsers.AsyncBrowsersResourceWithStreamingResponse:
- """Create and manage browser sessions."""
from .resources.browsers import AsyncBrowsersResourceWithStreamingResponse
return AsyncBrowsersResourceWithStreamingResponse(self._client.browsers)
diff --git a/src/kernel/resources/browsers/browsers.py b/src/kernel/resources/browsers/browsers.py
index 49dc4063..d2e519bd 100644
--- a/src/kernel/resources/browsers/browsers.py
+++ b/src/kernel/resources/browsers/browsers.py
@@ -36,6 +36,7 @@
BrowserMemoryRequest,
browser_curl_params,
browser_list_params,
+ browser_repl_params,
browser_create_params,
browser_update_params,
browser_retrieve_params,
@@ -95,6 +96,7 @@
from ...pagination import SyncOffsetPagination, AsyncOffsetPagination
from ..._base_client import AsyncPaginator, make_request_options
from ...types.tags_param import TagsParam
+from ...types.browser_repl_result import BrowserReplResult
from ...types.browser_curl_response import BrowserCurlResponse
from ...types.browser_list_response import BrowserListResponse
from ...types.vault_reference_param import VaultReferenceParam
@@ -118,8 +120,6 @@
class BrowsersResource(SyncAPIResource):
- """Create and manage browser sessions."""
-
@cached_property
def telemetry(self) -> TelemetryResource:
"""
@@ -753,10 +753,75 @@ def load_extensions(
cast_to=NoneType,
)
+ def repl(
+ self,
+ id_or_name: str,
+ *,
+ code: str,
+ reset: bool | Omit = omit,
+ timeout_sec: int | Omit = omit,
+ # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
+ # The extra values given here take precedence over values defined on the client or passed to this method.
+ extra_headers: Headers | None = None,
+ extra_query: Query | None = None,
+ extra_body: Body | None = None,
+ timeout: float | httpx.Timeout | None | NotGiven = not_given,
+ ) -> BrowserReplResult:
+ """
+ Execute JavaScript in a persistent Node.js runtime inside the browser VM.
+ Top-level bindings, closures, mutations, and dynamically imported modules
+ persist across calls until the REPL is reset or replaced. Start with
+ `repl.help()` to list available methods, or call `repl.help("click")` for
+ detailed help.
+
+ Expression values are ignored. Emit ordered text or image output with
+ `repl.write(...)`, console methods, or `repl.emitImage(...)`. The runtime also
+ exposes browser-control helpers, WebMCP, Patchright, Playwright, and raw CDP.
+
+ Executions are serialized. A timeout, crash, OOM, or protocol failure terminates
+ the REPL and changes its `repl_id`. This is unrestricted code execution inside
+ the browser VM and is not sandboxed.
+
+ Args:
+ code: JavaScript evaluated in a persistent Node.js runtime. Top-level bindings persist
+ until the browser VM's API process exits, the REPL is reset, or the REPL is
+ terminated after a crash or timeout. Static top-level imports are unsupported;
+ use dynamic `import()`. Expression values are ignored; emit output with
+ `repl.write(...)`, console methods, or `repl.emitImage(...)`. May be empty only
+ when `reset` is true.
+
+ reset: Terminate the current REPL, start a fresh one, and then evaluate code.
+
+ timeout_sec: Maximum execution time in seconds. Default is 60.
+
+ extra_headers: Send extra headers
+
+ extra_query: Add additional query parameters to the request
+
+ extra_body: Add additional JSON properties to the request
+
+ timeout: Override the client-level default timeout for this request, in seconds
+ """
+ if not id_or_name:
+ raise ValueError(f"Expected a non-empty value for `id_or_name` but received {id_or_name!r}")
+ return self._post(
+ path_template("/browsers/{id_or_name}/repl", id_or_name=id_or_name),
+ body=maybe_transform(
+ {
+ "code": code,
+ "reset": reset,
+ "timeout_sec": timeout_sec,
+ },
+ browser_repl_params.BrowserReplParams,
+ ),
+ options=make_request_options(
+ extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
+ ),
+ cast_to=BrowserReplResult,
+ )
-class AsyncBrowsersResource(AsyncAPIResource):
- """Create and manage browser sessions."""
+class AsyncBrowsersResource(AsyncAPIResource):
@cached_property
def telemetry(self) -> AsyncTelemetryResource:
"""
@@ -1390,6 +1455,73 @@ async def load_extensions(
cast_to=NoneType,
)
+ async def repl(
+ self,
+ id_or_name: str,
+ *,
+ code: str,
+ reset: bool | Omit = omit,
+ timeout_sec: int | Omit = omit,
+ # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
+ # The extra values given here take precedence over values defined on the client or passed to this method.
+ extra_headers: Headers | None = None,
+ extra_query: Query | None = None,
+ extra_body: Body | None = None,
+ timeout: float | httpx.Timeout | None | NotGiven = not_given,
+ ) -> BrowserReplResult:
+ """
+ Execute JavaScript in a persistent Node.js runtime inside the browser VM.
+ Top-level bindings, closures, mutations, and dynamically imported modules
+ persist across calls until the REPL is reset or replaced. Start with
+ `repl.help()` to list available methods, or call `repl.help("click")` for
+ detailed help.
+
+ Expression values are ignored. Emit ordered text or image output with
+ `repl.write(...)`, console methods, or `repl.emitImage(...)`. The runtime also
+ exposes browser-control helpers, WebMCP, Patchright, Playwright, and raw CDP.
+
+ Executions are serialized. A timeout, crash, OOM, or protocol failure terminates
+ the REPL and changes its `repl_id`. This is unrestricted code execution inside
+ the browser VM and is not sandboxed.
+
+ Args:
+ code: JavaScript evaluated in a persistent Node.js runtime. Top-level bindings persist
+ until the browser VM's API process exits, the REPL is reset, or the REPL is
+ terminated after a crash or timeout. Static top-level imports are unsupported;
+ use dynamic `import()`. Expression values are ignored; emit output with
+ `repl.write(...)`, console methods, or `repl.emitImage(...)`. May be empty only
+ when `reset` is true.
+
+ reset: Terminate the current REPL, start a fresh one, and then evaluate code.
+
+ timeout_sec: Maximum execution time in seconds. Default is 60.
+
+ extra_headers: Send extra headers
+
+ extra_query: Add additional query parameters to the request
+
+ extra_body: Add additional JSON properties to the request
+
+ timeout: Override the client-level default timeout for this request, in seconds
+ """
+ if not id_or_name:
+ raise ValueError(f"Expected a non-empty value for `id_or_name` but received {id_or_name!r}")
+ return await self._post(
+ path_template("/browsers/{id_or_name}/repl", id_or_name=id_or_name),
+ body=await async_maybe_transform(
+ {
+ "code": code,
+ "reset": reset,
+ "timeout_sec": timeout_sec,
+ },
+ browser_repl_params.BrowserReplParams,
+ ),
+ options=make_request_options(
+ extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
+ ),
+ cast_to=BrowserReplResult,
+ )
+
class BrowsersResourceWithRawResponse:
def __init__(self, browsers: BrowsersResource) -> None:
@@ -1416,6 +1548,9 @@ def __init__(self, browsers: BrowsersResource) -> None:
self.load_extensions = to_raw_response_wrapper(
browsers.load_extensions,
)
+ self.repl = to_raw_response_wrapper(
+ browsers.repl,
+ )
@cached_property
def telemetry(self) -> TelemetryResourceWithRawResponse:
@@ -1485,6 +1620,9 @@ def __init__(self, browsers: AsyncBrowsersResource) -> None:
self.load_extensions = async_to_raw_response_wrapper(
browsers.load_extensions,
)
+ self.repl = async_to_raw_response_wrapper(
+ browsers.repl,
+ )
@cached_property
def telemetry(self) -> AsyncTelemetryResourceWithRawResponse:
@@ -1554,6 +1692,9 @@ def __init__(self, browsers: BrowsersResource) -> None:
self.load_extensions = to_streamed_response_wrapper(
browsers.load_extensions,
)
+ self.repl = to_streamed_response_wrapper(
+ browsers.repl,
+ )
@cached_property
def telemetry(self) -> TelemetryResourceWithStreamingResponse:
@@ -1623,6 +1764,9 @@ def __init__(self, browsers: AsyncBrowsersResource) -> None:
self.load_extensions = async_to_streamed_response_wrapper(
browsers.load_extensions,
)
+ self.repl = async_to_streamed_response_wrapper(
+ browsers.repl,
+ )
@cached_property
def telemetry(self) -> AsyncTelemetryResourceWithStreamingResponse:
diff --git a/src/kernel/types/__init__.py b/src/kernel/types/__init__.py
index 70e9581a..0d25350f 100644
--- a/src/kernel/types/__init__.py
+++ b/src/kernel/types/__init__.py
@@ -48,6 +48,8 @@
from .api_key_list_params import APIKeyListParams as APIKeyListParams
from .browser_curl_params import BrowserCurlParams as BrowserCurlParams
from .browser_list_params import BrowserListParams as BrowserListParams
+from .browser_repl_params import BrowserReplParams as BrowserReplParams
+from .browser_repl_result import BrowserReplResult as BrowserReplResult
from .credential_provider import CredentialProvider as CredentialProvider
from .profile_list_params import ProfileListParams as ProfileListParams
from .project_list_params import ProjectListParams as ProjectListParams
@@ -56,6 +58,7 @@
from .proxy_update_params import ProxyUpdateParams as ProxyUpdateParams
from .vault_upsert_params import VaultUpsertParams as VaultUpsertParams
from .browser_proxy_config import BrowserProxyConfig as BrowserProxyConfig
+from .browser_repl_content import BrowserReplContent as BrowserReplContent
from .proxy_check_response import ProxyCheckResponse as ProxyCheckResponse
from .api_key_create_params import APIKeyCreateParams as APIKeyCreateParams
from .api_key_rotate_params import APIKeyRotateParams as APIKeyRotateParams
@@ -104,12 +107,14 @@
from .invocation_follow_params import InvocationFollowParams as InvocationFollowParams
from .invocation_list_response import InvocationListResponse as InvocationListResponse
from .invocation_update_params import InvocationUpdateParams as InvocationUpdateParams
+from .browser_repl_text_content import BrowserReplTextContent as BrowserReplTextContent
from .browser_retrieve_response import BrowserRetrieveResponse as BrowserRetrieveResponse
from .extension_upload_response import ExtensionUploadResponse as ExtensionUploadResponse
from .browser_pool_create_params import BrowserPoolCreateParams as BrowserPoolCreateParams
from .browser_pool_delete_params import BrowserPoolDeleteParams as BrowserPoolDeleteParams
from .browser_pool_update_params import BrowserPoolUpdateParams as BrowserPoolUpdateParams
from .browser_proxy_config_param import BrowserProxyConfigParam as BrowserProxyConfigParam
+from .browser_repl_image_content import BrowserReplImageContent as BrowserReplImageContent
from .deployment_create_response import DeploymentCreateResponse as DeploymentCreateResponse
from .deployment_follow_response import DeploymentFollowResponse as DeploymentFollowResponse
from .invocation_create_response import InvocationCreateResponse as InvocationCreateResponse
diff --git a/src/kernel/types/browser_repl_content.py b/src/kernel/types/browser_repl_content.py
new file mode 100644
index 00000000..70af04d7
--- /dev/null
+++ b/src/kernel/types/browser_repl_content.py
@@ -0,0 +1,14 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from typing import Union
+from typing_extensions import Annotated, TypeAlias
+
+from .._utils import PropertyInfo
+from .browser_repl_text_content import BrowserReplTextContent
+from .browser_repl_image_content import BrowserReplImageContent
+
+__all__ = ["BrowserReplContent"]
+
+BrowserReplContent: TypeAlias = Annotated[
+ Union[BrowserReplTextContent, BrowserReplImageContent], PropertyInfo(discriminator="type")
+]
diff --git a/src/kernel/types/browser_repl_image_content.py b/src/kernel/types/browser_repl_image_content.py
new file mode 100644
index 00000000..4af25aa9
--- /dev/null
+++ b/src/kernel/types/browser_repl_image_content.py
@@ -0,0 +1,15 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from typing_extensions import Literal
+
+from .._models import BaseModel
+
+__all__ = ["BrowserReplImageContent"]
+
+
+class BrowserReplImageContent(BaseModel):
+ data_b64: str
+
+ mime_type: str
+
+ type: Literal["image"]
diff --git a/src/kernel/types/browser_repl_params.py b/src/kernel/types/browser_repl_params.py
new file mode 100644
index 00000000..28df3a41
--- /dev/null
+++ b/src/kernel/types/browser_repl_params.py
@@ -0,0 +1,25 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from __future__ import annotations
+
+from typing_extensions import Required, TypedDict
+
+__all__ = ["BrowserReplParams"]
+
+
+class BrowserReplParams(TypedDict, total=False):
+ code: Required[str]
+ """JavaScript evaluated in a persistent Node.js runtime.
+
+ Top-level bindings persist until the browser VM's API process exits, the REPL is
+ reset, or the REPL is terminated after a crash or timeout. Static top-level
+ imports are unsupported; use dynamic `import()`. Expression values are ignored;
+ emit output with `repl.write(...)`, console methods, or `repl.emitImage(...)`.
+ May be empty only when `reset` is true.
+ """
+
+ reset: bool
+ """Terminate the current REPL, start a fresh one, and then evaluate code."""
+
+ timeout_sec: int
+ """Maximum execution time in seconds. Default is 60."""
diff --git a/src/kernel/types/browser_repl_result.py b/src/kernel/types/browser_repl_result.py
new file mode 100644
index 00000000..d5815501
--- /dev/null
+++ b/src/kernel/types/browser_repl_result.py
@@ -0,0 +1,43 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from typing import List, Optional
+
+from .._models import BaseModel
+from .browser_repl_content import BrowserReplContent
+
+__all__ = ["BrowserReplResult"]
+
+
+class BrowserReplResult(BaseModel):
+ """Result of Browser REPL code execution."""
+
+ repl_id: str
+ """CUID2 identifying the exact state-holding REPL process used for this execution.
+
+ Stable across calls and Chromium reconnects; changes after an API restart,
+ explicit reset, execution timeout, or REPL crash.
+ """
+
+ success: bool
+ """Whether the code executed successfully."""
+
+ content: Optional[List[BrowserReplContent]] = None
+ """Optional ordered text/image output produced by the execution."""
+
+ content_truncated: Optional[bool] = None
+ """True if text or image output was dropped or truncated due to response limits."""
+
+ duration_ms: Optional[int] = None
+ """Wall-clock execution time in milliseconds."""
+
+ error: Optional[str] = None
+ """Error message if execution failed."""
+
+ repl_terminated: Optional[bool] = None
+ """
+ True if the REPL identified by `repl_id` was terminated by this request. The
+ next request lazily starts a fresh REPL with a new `repl_id`.
+ """
+
+ stack: Optional[str] = None
+ """Stack trace if execution failed."""
diff --git a/src/kernel/types/browser_repl_text_content.py b/src/kernel/types/browser_repl_text_content.py
new file mode 100644
index 00000000..034d3691
--- /dev/null
+++ b/src/kernel/types/browser_repl_text_content.py
@@ -0,0 +1,19 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from typing_extensions import Literal
+
+from .._models import BaseModel
+
+__all__ = ["BrowserReplTextContent"]
+
+
+class BrowserReplTextContent(BaseModel):
+ channel: Literal["write", "stdout", "stderr"]
+ """
+ `write` is emitted by `repl.write`; `stdout` and `stderr` are emitted by console
+ methods.
+ """
+
+ text: str
+
+ type: Literal["text"]
diff --git a/tests/api_resources/test_browsers.py b/tests/api_resources/test_browsers.py
index 28189f15..ffa5f2e9 100644
--- a/tests/api_resources/test_browsers.py
+++ b/tests/api_resources/test_browsers.py
@@ -10,6 +10,7 @@
from kernel import Kernel, AsyncKernel
from tests.utils import assert_matches_type
from kernel.types import (
+ BrowserReplResult,
BrowserCurlResponse,
BrowserListResponse,
BrowserCreateResponse,
@@ -492,6 +493,63 @@ def test_path_params_load_extensions(self, client: Kernel) -> None:
],
)
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ def test_method_repl(self, client: Kernel) -> None:
+ browser = client.browsers.repl(
+ id_or_name="htzv5orfit78e1m2biiifpbv",
+ code="code",
+ )
+ assert_matches_type(BrowserReplResult, browser, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ def test_method_repl_with_all_params(self, client: Kernel) -> None:
+ browser = client.browsers.repl(
+ id_or_name="htzv5orfit78e1m2biiifpbv",
+ code="code",
+ reset=True,
+ timeout_sec=1,
+ )
+ assert_matches_type(BrowserReplResult, browser, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ def test_raw_response_repl(self, client: Kernel) -> None:
+ response = client.browsers.with_raw_response.repl(
+ id_or_name="htzv5orfit78e1m2biiifpbv",
+ code="code",
+ )
+
+ assert response.is_closed is True
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+ browser = response.parse()
+ assert_matches_type(BrowserReplResult, browser, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ def test_streaming_response_repl(self, client: Kernel) -> None:
+ with client.browsers.with_streaming_response.repl(
+ id_or_name="htzv5orfit78e1m2biiifpbv",
+ code="code",
+ ) as response:
+ assert not response.is_closed
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+
+ browser = response.parse()
+ assert_matches_type(BrowserReplResult, browser, path=["response"])
+
+ assert cast(Any, response.is_closed) is True
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ def test_path_params_repl(self, client: Kernel) -> None:
+ with pytest.raises(ValueError, match=r"Expected a non-empty value for `id_or_name` but received ''"):
+ client.browsers.with_raw_response.repl(
+ id_or_name="",
+ code="code",
+ )
+
class TestAsyncBrowsers:
parametrize = pytest.mark.parametrize(
@@ -965,3 +1023,60 @@ async def test_path_params_load_extensions(self, async_client: AsyncKernel) -> N
}
],
)
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ async def test_method_repl(self, async_client: AsyncKernel) -> None:
+ browser = await async_client.browsers.repl(
+ id_or_name="htzv5orfit78e1m2biiifpbv",
+ code="code",
+ )
+ assert_matches_type(BrowserReplResult, browser, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ async def test_method_repl_with_all_params(self, async_client: AsyncKernel) -> None:
+ browser = await async_client.browsers.repl(
+ id_or_name="htzv5orfit78e1m2biiifpbv",
+ code="code",
+ reset=True,
+ timeout_sec=1,
+ )
+ assert_matches_type(BrowserReplResult, browser, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ async def test_raw_response_repl(self, async_client: AsyncKernel) -> None:
+ response = await async_client.browsers.with_raw_response.repl(
+ id_or_name="htzv5orfit78e1m2biiifpbv",
+ code="code",
+ )
+
+ assert response.is_closed is True
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+ browser = await response.parse()
+ assert_matches_type(BrowserReplResult, browser, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ async def test_streaming_response_repl(self, async_client: AsyncKernel) -> None:
+ async with async_client.browsers.with_streaming_response.repl(
+ id_or_name="htzv5orfit78e1m2biiifpbv",
+ code="code",
+ ) as response:
+ assert not response.is_closed
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+
+ browser = await response.parse()
+ assert_matches_type(BrowserReplResult, browser, 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_repl(self, async_client: AsyncKernel) -> None:
+ with pytest.raises(ValueError, match=r"Expected a non-empty value for `id_or_name` but received ''"):
+ await async_client.browsers.with_raw_response.repl(
+ id_or_name="",
+ code="code",
+ )
From 12ced8647c23aa43c625b288ed48c7cdf8da9125 Mon Sep 17 00:00:00 2001
From: "kernel-internal[bot]"
<260533166+kernel-internal[bot]@users.noreply.github.com>
Date: Wed, 16 Sep 2026 18:31:29 +0000
Subject: [PATCH 277/366] feat: Classify unevaluable config registry targets
Stainless-Generated-From: 89ffaef01313311d22f91e6eba840096c085be85
---
src/kernel/types/no_recommendation.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/kernel/types/no_recommendation.py b/src/kernel/types/no_recommendation.py
index 05faec86..40edb9f0 100644
--- a/src/kernel/types/no_recommendation.py
+++ b/src/kernel/types/no_recommendation.py
@@ -8,7 +8,7 @@
class NoRecommendation(BaseModel):
- code: Literal["proxy_restricted", "no_working_configuration", "inconclusive"]
+ code: Literal["proxy_restricted", "target_not_evaluable", "no_working_configuration", "inconclusive"]
"""
Machine-readable reason Kernel cannot currently provide a config recommendation.
"""
From 7c83d879bad3151782085d507aad48c3b1ea5b85 Mon Sep 17 00:00:00 2001
From: "kernel-internal[bot]"
<260533166+kernel-internal[bot]@users.noreply.github.com>
Date: Wed, 16 Sep 2026 18:36:15 +0000
Subject: [PATCH 278/366] release: 0.107.0
---
.release-please-manifest.json | 2 +-
CHANGELOG.md | 9 +++++++++
pyproject.toml | 2 +-
src/kernel/_version.py | 2 +-
4 files changed, 12 insertions(+), 3 deletions(-)
diff --git a/.release-please-manifest.json b/.release-please-manifest.json
index f371d275..bd34dce0 100644
--- a/.release-please-manifest.json
+++ b/.release-please-manifest.json
@@ -1,3 +1,3 @@
{
- ".": "0.106.0"
+ ".": "0.107.0"
}
\ No newline at end of file
diff --git a/CHANGELOG.md b/CHANGELOG.md
index cb10a2df..235126cc 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,14 @@
# Changelog
+## [0.107.0](https://github.com/kernel/kernel-python-sdk/compare/v0.106.0...v0.107.0) (2026-09-16)
+
+
+### Features
+
+* Classify unevaluable config registry targets ([12ced86](https://github.com/kernel/kernel-python-sdk/commit/12ced8647c23aa43c625b288ed48c7cdf8da9125))
+* Expose persistent Browser REPL through the API ([755dba5](https://github.com/kernel/kernel-python-sdk/commit/755dba5d1273ef9083ffbc4bc7f4adf30aea2fe9))
+* Return Link spend request errors directly ([82d9f84](https://github.com/kernel/kernel-python-sdk/commit/82d9f8400a761a37debabeb79e6e5d8bec2cbe32))
+
## 0.106.0 (2026-09-16)
Full Changelog: [v0.104.0...v0.106.0](https://github.com/kernel/kernel-python-sdk/compare/v0.104.0...v0.106.0)
diff --git a/pyproject.toml b/pyproject.toml
index a89b3709..885ce9e3 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -1,6 +1,6 @@
[project]
name = "kernel"
-version = "0.106.0"
+version = "0.107.0"
description = "The official Python library for the kernel API"
dynamic = ["readme"]
license = "Apache-2.0"
diff --git a/src/kernel/_version.py b/src/kernel/_version.py
index 66f10fd8..9445a514 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.106.0" # x-release-please-version
+__version__ = "0.107.0" # x-release-please-version
From e38c8878f9f80f5d28fce9f92df6f0a18a48e2bb Mon Sep 17 00:00:00 2001
From: "kernel-internal[bot]"
<260533166+kernel-internal[bot]@users.noreply.github.com>
Date: Thu, 17 Sep 2026 18:45:43 +0000
Subject: [PATCH 279/366] feat: chore(stlc): seal custom-code tracking files
Stainless-Generated-From: fce86e75c1181e24c4e991e93be3817902ec3ab4
---
src/kernel/lib/config_registry_wait.py | 63 ++++++
.../resources/config_registry/analyses.py | 107 +++++++++++
tests/test_config_registry_wait.py | 181 ++++++++++++++++++
3 files changed, 351 insertions(+)
create mode 100644 src/kernel/lib/config_registry_wait.py
create mode 100644 tests/test_config_registry_wait.py
diff --git a/src/kernel/lib/config_registry_wait.py b/src/kernel/lib/config_registry_wait.py
new file mode 100644
index 00000000..fbf399a8
--- /dev/null
+++ b/src/kernel/lib/config_registry_wait.py
@@ -0,0 +1,63 @@
+from __future__ import annotations
+
+import math
+import time
+import random
+from datetime import datetime
+
+from .._types import Omit, Headers
+from .._exceptions import KernelError
+from ..types.config_registry_response import ConfigRegistryResponse
+
+DEFAULT_CONFIG_REGISTRY_POLL_INTERVAL = 5.0
+_TERMINAL_STATUSES = frozenset({"completed", "failed", "canceled", "expired"})
+
+
+def validate_wait_options(poll_interval: float, max_wait_seconds: float | None) -> None:
+ if not math.isfinite(poll_interval) or poll_interval <= 0:
+ raise ValueError("Expected a finite, positive value for `poll_interval`")
+ if max_wait_seconds is not None and (not math.isfinite(max_wait_seconds) or max_wait_seconds < 0):
+ raise ValueError("Expected a finite, non-negative value for `max_wait_seconds`")
+
+
+def poll_headers(extra_headers: Headers | None) -> Headers:
+ headers: dict[str, str | Omit] = dict(extra_headers or {})
+ headers["X-Stainless-Poll-Helper"] = "true"
+ return headers
+
+
+def poll_delay(poll_interval: float) -> float:
+ return poll_interval * random.uniform(0.9, 1.1)
+
+
+def wait_timeout_error(id: str, polls: int, last_status: str | None, started_at: float) -> TimeoutError:
+ elapsed = time.monotonic() - started_at
+ return TimeoutError(
+ f"Timed out waiting for config registry analysis {id!r} after {elapsed:.1f}s "
+ f"and {polls} polls; last status was {last_status!r}"
+ )
+
+
+def analysis_finished(response: ConfigRegistryResponse, requested_id: str) -> tuple[bool, str]:
+ analysis = response.analysis
+ if analysis is None:
+ raise KernelError(f"Config registry response for {requested_id!r} is missing an analysis")
+
+ analysis_id = getattr(analysis, "id", None)
+ if not isinstance(analysis_id, str) or not analysis_id:
+ raise KernelError(f"Config registry response for {requested_id!r} has no valid analysis ID")
+ if analysis_id != requested_id:
+ raise KernelError(f"Config registry response for {requested_id!r} returned analysis {analysis_id!r}")
+
+ status = getattr(analysis, "status", None)
+ if not isinstance(status, str) or not status:
+ raise KernelError(f"Config registry analysis {requested_id!r} has no valid status")
+
+ if "finished_at" not in analysis.model_fields_set:
+ raise KernelError(f"Config registry analysis {requested_id!r} is missing `finished_at`")
+
+ finished_at = getattr(analysis, "finished_at", None)
+ if finished_at is not None and not isinstance(finished_at, datetime):
+ raise KernelError(f"Config registry analysis {requested_id!r} has an invalid `finished_at`")
+
+ return finished_at is not None or status in _TERMINAL_STATUSES, status
diff --git a/src/kernel/resources/config_registry/analyses.py b/src/kernel/resources/config_registry/analyses.py
index 3d975f32..bbaeff64 100644
--- a/src/kernel/resources/config_registry/analyses.py
+++ b/src/kernel/resources/config_registry/analyses.py
@@ -2,6 +2,8 @@
from __future__ import annotations
+import time
+
import httpx
from ..._types import Body, Omit, Query, Headers, NotGiven, omit, not_given
@@ -18,6 +20,14 @@
from ..._base_client import AsyncPaginator, make_request_options
from ...types.config_registry import analysis_list_params
from ...types.analysis_summary import AnalysisSummary
+from ...lib.config_registry_wait import (
+ DEFAULT_CONFIG_REGISTRY_POLL_INTERVAL,
+ poll_delay,
+ poll_headers,
+ analysis_finished,
+ wait_timeout_error,
+ validate_wait_options,
+)
from ...types.config_registry_response import ConfigRegistryResponse
__all__ = ["AnalysesResource", "AsyncAnalysesResource"]
@@ -79,6 +89,54 @@ def retrieve(
cast_to=ConfigRegistryResponse,
)
+ def wait_for_result(
+ self,
+ id: str,
+ *,
+ poll_interval: float = DEFAULT_CONFIG_REGISTRY_POLL_INTERVAL,
+ max_wait_seconds: float | None = None,
+ extra_headers: Headers | None = None,
+ extra_query: Query | None = None,
+ extra_body: Body | None = None,
+ timeout: float | httpx.Timeout | None | NotGiven = not_given,
+ ) -> ConfigRegistryResponse:
+ """Wait for an analysis to finish and return its complete result.
+
+ The first retrieval happens immediately. ``max_wait_seconds`` is a soft
+ polling deadline: an in-flight request and its normal retries may finish
+ after it. Timing out does not cancel the remote analysis.
+ """
+ validate_wait_options(poll_interval, max_wait_seconds)
+ started_at = time.monotonic()
+ deadline = started_at + max_wait_seconds if max_wait_seconds is not None else None
+ headers = poll_headers(extra_headers)
+ polls = 0
+ last_status: str | None = None
+
+ while True:
+ if polls > 0 and deadline is not None and time.monotonic() >= deadline:
+ raise wait_timeout_error(id, polls, last_status, started_at)
+
+ response = self.retrieve(
+ id,
+ extra_headers=headers,
+ extra_query=extra_query,
+ extra_body=extra_body,
+ timeout=timeout,
+ )
+ polls += 1
+ finished, last_status = analysis_finished(response, id)
+ if finished:
+ return response
+
+ delay = poll_delay(poll_interval)
+ if deadline is not None:
+ remaining = deadline - time.monotonic()
+ if remaining <= 0:
+ raise wait_timeout_error(id, polls, last_status, started_at)
+ delay = min(delay, remaining)
+ self._sleep(delay)
+
def list(
self,
*,
@@ -220,6 +278,55 @@ async def retrieve(
cast_to=ConfigRegistryResponse,
)
+ async def wait_for_result(
+ self,
+ id: str,
+ *,
+ poll_interval: float = DEFAULT_CONFIG_REGISTRY_POLL_INTERVAL,
+ max_wait_seconds: float | None = None,
+ extra_headers: Headers | None = None,
+ extra_query: Query | None = None,
+ extra_body: Body | None = None,
+ timeout: float | httpx.Timeout | None | NotGiven = not_given,
+ ) -> ConfigRegistryResponse:
+ """Wait for an analysis to finish and return its complete result.
+
+ The first retrieval happens immediately. ``max_wait_seconds`` is a soft
+ polling deadline: an in-flight request and its normal retries may finish
+ after it. Timing out does not cancel the remote analysis. Cancelling the
+ calling task stops the wait without cancelling the remote analysis.
+ """
+ validate_wait_options(poll_interval, max_wait_seconds)
+ started_at = time.monotonic()
+ deadline = started_at + max_wait_seconds if max_wait_seconds is not None else None
+ headers = poll_headers(extra_headers)
+ polls = 0
+ last_status: str | None = None
+
+ while True:
+ if polls > 0 and deadline is not None and time.monotonic() >= deadline:
+ raise wait_timeout_error(id, polls, last_status, started_at)
+
+ response = await self.retrieve(
+ id,
+ extra_headers=headers,
+ extra_query=extra_query,
+ extra_body=extra_body,
+ timeout=timeout,
+ )
+ polls += 1
+ finished, last_status = analysis_finished(response, id)
+ if finished:
+ return response
+
+ delay = poll_delay(poll_interval)
+ if deadline is not None:
+ remaining = deadline - time.monotonic()
+ if remaining <= 0:
+ raise wait_timeout_error(id, polls, last_status, started_at)
+ delay = min(delay, remaining)
+ await self._sleep(delay)
+
def list(
self,
*,
diff --git a/tests/test_config_registry_wait.py b/tests/test_config_registry_wait.py
new file mode 100644
index 00000000..e668aa8c
--- /dev/null
+++ b/tests/test_config_registry_wait.py
@@ -0,0 +1,181 @@
+from __future__ import annotations
+
+import asyncio
+from typing import Any
+from collections.abc import Iterator
+
+import httpx
+import pytest
+
+from kernel import Kernel, AsyncKernel
+from kernel._exceptions import KernelError
+
+
+def analysis_response(
+ status: str,
+ *,
+ analysis_id: str = "analysis-1",
+ finished_at: str | None = None,
+) -> dict[str, Any]:
+ return {
+ "analysis": {
+ "id": analysis_id,
+ "created_at": "2026-09-16T00:00:00Z",
+ "expires_at": "2026-09-16T00:45:00Z",
+ "failure": None,
+ "finished_at": finished_at,
+ "status": status,
+ "intent": None,
+ },
+ "recommendation": None,
+ "target": {
+ "domain": "example.com",
+ "host": "example.com",
+ "normalized": "https://example.com/",
+ },
+ "working_configurations": [],
+ "guidance": None,
+ "workload_outcome": None,
+ }
+
+
+def response_sequence(payloads: list[dict[str, Any]]) -> tuple[httpx.MockTransport, list[httpx.Request]]:
+ remaining: Iterator[dict[str, Any]] = iter(payloads)
+ requests: list[httpx.Request] = []
+
+ def handler(request: httpx.Request) -> httpx.Response:
+ requests.append(request)
+ return httpx.Response(200, json=next(remaining))
+
+ return httpx.MockTransport(handler), requests
+
+
+def test_wait_for_result_polls_unknown_unfinished_status_until_finished() -> None:
+ transport, requests = response_sequence(
+ [
+ analysis_response("running"),
+ analysis_response("queued"),
+ analysis_response("archived", finished_at="2026-09-16T00:01:00Z"),
+ ]
+ )
+
+ with httpx.Client(transport=transport) as http_client:
+ client = Kernel(api_key="test", base_url="https://api.example", http_client=http_client)
+ result = client.config_registry.analyses.wait_for_result(
+ "analysis-1",
+ poll_interval=0.001,
+ extra_headers={"X-Test": "preserved", "X-Stainless-Poll-Helper": "caller"},
+ )
+
+ assert result.analysis is not None
+ assert result.analysis.status == "archived"
+ assert len(requests) == 3
+ assert all(request.headers["X-Test"] == "preserved" for request in requests)
+ assert all(request.headers["X-Stainless-Poll-Helper"] == "true" for request in requests)
+
+
+@pytest.mark.parametrize("status", ["completed", "failed", "canceled", "expired"])
+def test_wait_for_result_returns_known_terminal_status_without_finished_at(status: str) -> None:
+ transport, requests = response_sequence([analysis_response(status)])
+
+ with httpx.Client(transport=transport) as http_client:
+ client = Kernel(api_key="test", base_url="https://api.example", http_client=http_client)
+ result = client.config_registry.analyses.wait_for_result("analysis-1")
+
+ assert result.analysis is not None
+ assert result.analysis.status == status
+ assert len(requests) == 1
+
+
+def test_wait_for_result_zero_max_wait_reads_once_then_times_out() -> None:
+ transport, requests = response_sequence([analysis_response("running")])
+
+ with httpx.Client(transport=transport) as http_client:
+ client = Kernel(api_key="test", base_url="https://api.example", http_client=http_client)
+ with pytest.raises(TimeoutError, match="analysis-1"):
+ client.config_registry.analyses.wait_for_result("analysis-1", max_wait_seconds=0)
+
+ assert len(requests) == 1
+
+
+@pytest.mark.parametrize(
+ "payload, message",
+ [
+ ({"analysis": None}, "missing an analysis"),
+ (analysis_response("running", analysis_id="analysis-2"), "analysis-2"),
+ ],
+)
+def test_wait_for_result_rejects_invalid_analysis_response(payload: dict[str, Any], message: str) -> None:
+ transport, _ = response_sequence([payload])
+
+ with httpx.Client(transport=transport) as http_client:
+ client = Kernel(api_key="test", base_url="https://api.example", http_client=http_client)
+ with pytest.raises(KernelError, match=message):
+ client.config_registry.analyses.wait_for_result("analysis-1")
+
+
+def test_wait_for_result_rejects_missing_finished_at() -> None:
+ payload = analysis_response("running")
+ analysis = payload["analysis"]
+ assert isinstance(analysis, dict)
+ del analysis["finished_at"]
+ transport, _ = response_sequence([payload])
+
+ with httpx.Client(transport=transport) as http_client:
+ client = Kernel(api_key="test", base_url="https://api.example", http_client=http_client)
+ with pytest.raises(KernelError, match="finished_at"):
+ client.config_registry.analyses.wait_for_result("analysis-1")
+
+
+@pytest.mark.parametrize(
+ "kwargs",
+ [
+ {"poll_interval": 0},
+ {"poll_interval": float("nan")},
+ {"max_wait_seconds": -1},
+ {"max_wait_seconds": float("inf")},
+ ],
+)
+def test_wait_for_result_rejects_invalid_timing_options(kwargs: dict[str, float]) -> None:
+ client = Kernel(api_key="test", base_url="https://api.example")
+
+ with pytest.raises(ValueError):
+ client.config_registry.analyses.wait_for_result("analysis-1", **kwargs) # type: ignore[arg-type]
+
+
+async def test_async_wait_for_result_matches_sync_behavior() -> None:
+ transport, requests = response_sequence(
+ [
+ analysis_response("running"),
+ analysis_response("completed", finished_at="2026-09-16T00:01:00Z"),
+ ]
+ )
+
+ async with httpx.AsyncClient(transport=transport) as http_client:
+ client = AsyncKernel(api_key="test", base_url="https://api.example", http_client=http_client)
+ result = await client.config_registry.analyses.wait_for_result("analysis-1", poll_interval=0.001)
+
+ assert result.analysis is not None
+ assert result.analysis.status == "completed"
+ assert len(requests) == 2
+
+
+async def test_async_wait_for_result_is_cancellable_during_poll_sleep() -> None:
+ first_request = asyncio.Event()
+ requests: list[httpx.Request] = []
+
+ def handler(request: httpx.Request) -> httpx.Response:
+ requests.append(request)
+ first_request.set()
+ return httpx.Response(200, json=analysis_response("running"))
+
+ async with httpx.AsyncClient(transport=httpx.MockTransport(handler)) as http_client:
+ client = AsyncKernel(api_key="test", base_url="https://api.example", http_client=http_client)
+ task = asyncio.create_task(client.config_registry.analyses.wait_for_result("analysis-1", poll_interval=60))
+ await first_request.wait()
+ await asyncio.sleep(0)
+ task.cancel()
+ with pytest.raises(asyncio.CancelledError):
+ await task
+
+ assert len(requests) == 1
From 450f4a59491a1c8a5f1173ee4448757d23fc4424 Mon Sep 17 00:00:00 2001
From: "kernel-internal[bot]"
<260533166+kernel-internal[bot]@users.noreply.github.com>
Date: Thu, 17 Sep 2026 18:50:36 +0000
Subject: [PATCH 280/366] release: 0.108.0
---
.release-please-manifest.json | 2 +-
CHANGELOG.md | 7 +++++++
pyproject.toml | 2 +-
src/kernel/_version.py | 2 +-
4 files changed, 10 insertions(+), 3 deletions(-)
diff --git a/.release-please-manifest.json b/.release-please-manifest.json
index bd34dce0..3e05ae56 100644
--- a/.release-please-manifest.json
+++ b/.release-please-manifest.json
@@ -1,3 +1,3 @@
{
- ".": "0.107.0"
+ ".": "0.108.0"
}
\ No newline at end of file
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 235126cc..3961edaf 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,12 @@
# Changelog
+## [0.108.0](https://github.com/kernel/kernel-python-sdk/compare/v0.107.0...v0.108.0) (2026-09-17)
+
+
+### Features
+
+* chore(stlc): seal custom-code tracking files ([e38c887](https://github.com/kernel/kernel-python-sdk/commit/e38c8878f9f80f5d28fce9f92df6f0a18a48e2bb))
+
## [0.107.0](https://github.com/kernel/kernel-python-sdk/compare/v0.106.0...v0.107.0) (2026-09-16)
diff --git a/pyproject.toml b/pyproject.toml
index 885ce9e3..3a9c0dee 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -1,6 +1,6 @@
[project]
name = "kernel"
-version = "0.107.0"
+version = "0.108.0"
description = "The official Python library for the kernel API"
dynamic = ["readme"]
license = "Apache-2.0"
diff --git a/src/kernel/_version.py b/src/kernel/_version.py
index 9445a514..5c651d4a 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.107.0" # x-release-please-version
+__version__ = "0.108.0" # x-release-please-version
From 13781cb075baaa794642c266152ea3d95cb25496 Mon Sep 17 00:00:00 2001
From: Sayan- <1415138+Sayan-@users.noreply.github.com>
Date: Thu, 17 Sep 2026 19:33:05 +0000
Subject: [PATCH 281/366] docs: correct 0.108.0 changelog
---
CHANGELOG.md | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 3961edaf..1417baaa 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -5,7 +5,7 @@
### Features
-* chore(stlc): seal custom-code tracking files ([e38c887](https://github.com/kernel/kernel-python-sdk/commit/e38c8878f9f80f5d28fce9f92df6f0a18a48e2bb))
+* feat: add config registry analysis waiter ([e38c887](https://github.com/kernel/kernel-python-sdk/commit/e38c8878f9f80f5d28fce9f92df6f0a18a48e2bb))
## [0.107.0](https://github.com/kernel/kernel-python-sdk/compare/v0.106.0...v0.107.0) (2026-09-16)
From 8b22180766c592d1480e26fc781ffd5e1b623051 Mon Sep 17 00:00:00 2001
From: "kernel-internal[bot]"
<260533166+kernel-internal[bot]@users.noreply.github.com>
Date: Thu, 17 Sep 2026 19:57:57 +0000
Subject: [PATCH 282/366] feat: Expose organization-wide concurrent browser
capacity
Stainless-Generated-From: 3cbbdadb5bad3b3d3192976206eaf515db7a1263
---
src/kernel/resources/organization/limits.py | 10 ++++++++--
src/kernel/types/organization/org_limits.py | 13 +++++++++++++
2 files changed, 21 insertions(+), 2 deletions(-)
diff --git a/src/kernel/resources/organization/limits.py b/src/kernel/resources/organization/limits.py
index 9dcd9dbb..5654164b 100644
--- a/src/kernel/resources/organization/limits.py
+++ b/src/kernel/resources/organization/limits.py
@@ -55,7 +55,10 @@ def retrieve(
extra_body: Body | None = None,
timeout: float | httpx.Timeout | None | NotGiven = not_given,
) -> OrgLimits:
- """Get the organization's effective limits and managed auth and vault usage."""
+ """
+ Get the organization's effective limits and current concurrency, managed auth,
+ and vault usage.
+ """
return self._get(
"/org/limits",
options=make_request_options(
@@ -138,7 +141,10 @@ async def retrieve(
extra_body: Body | None = None,
timeout: float | httpx.Timeout | None | NotGiven = not_given,
) -> OrgLimits:
- """Get the organization's effective limits and managed auth and vault usage."""
+ """
+ Get the organization's effective limits and current concurrency, managed auth,
+ and vault usage.
+ """
return await self._get(
"/org/limits",
options=make_request_options(
diff --git a/src/kernel/types/organization/org_limits.py b/src/kernel/types/organization/org_limits.py
index 4e24a5f0..fbe26e76 100644
--- a/src/kernel/types/organization/org_limits.py
+++ b/src/kernel/types/organization/org_limits.py
@@ -15,6 +15,19 @@ class OrgLimits(BaseModel):
remaining capacity before a create is rejected with 403 insufficient_plan.
"""
+ concurrent_sessions_available: Optional[int] = None
+ """Number of concurrent browser slots currently available to the organization.
+
+ This is the effective concurrency limit minus active on-demand sessions and
+ browser pool reservations, floored at zero. Null when usage cannot be read.
+ """
+
+ concurrent_sessions_used: Optional[int] = None
+ """
+ Current organization-wide concurrent browser usage, including active on-demand
+ sessions and browser pool reservations. Null when usage cannot be read.
+ """
+
max_auth_connections: Optional[int] = None
"""Maximum managed auth connections the organization's plan allows.
From 1a408c41c0fd043ff69187f7d62bc721f9e5f867 Mon Sep 17 00:00:00 2001
From: "kernel-internal[bot]"
<260533166+kernel-internal[bot]@users.noreply.github.com>
Date: Thu, 17 Sep 2026 21:07:10 +0000
Subject: [PATCH 283/366] feat: Preserve credential field order
Stainless-Generated-From: 0df566e7c79bb73166acc226effc1c9f331264be
---
src/kernel/resources/vaults/items.py | 8 +-
.../credential_vault_field_definition.py | 3 +
.../credential_vault_field_input_param.py | 3 +
.../credential_vault_item_request_param.py | 4 +-
.../vaults/credential_vault_item_spec.py | 7 +-
.../credential_vault_item_spec_input_param.py | 11 +-
src/kernel/types/vaults/item_upsert_params.py | 4 +-
tests/api_resources/vaults/test_items.py | 104 +++++++++++++++---
8 files changed, 119 insertions(+), 25 deletions(-)
diff --git a/src/kernel/resources/vaults/items.py b/src/kernel/resources/vaults/items.py
index 8c720ee5..a9058c09 100644
--- a/src/kernel/resources/vaults/items.py
+++ b/src/kernel/resources/vaults/items.py
@@ -757,7 +757,9 @@ def upsert(
Args:
spec: Credential fields are for login and other non-payment credentials. Do not store,
collect, or fill credit card data in credential items. Use wallet and card item
- types for credit cards and payment checkout instead.
+ types for credit cards and payment checkout instead. Field order is preserved in
+ the user-facing collection form, so list fields in the same top-to-bottom order
+ as the website.
extra_headers: Send extra headers
@@ -1528,7 +1530,9 @@ async def upsert(
Args:
spec: Credential fields are for login and other non-payment credentials. Do not store,
collect, or fill credit card data in credential items. Use wallet and card item
- types for credit cards and payment checkout instead.
+ types for credit cards and payment checkout instead. Field order is preserved in
+ the user-facing collection form, so list fields in the same top-to-bottom order
+ as the website.
extra_headers: Send extra headers
diff --git a/src/kernel/types/vaults/credential_vault_field_definition.py b/src/kernel/types/vaults/credential_vault_field_definition.py
index 9350813b..0ea64ec7 100644
--- a/src/kernel/types/vaults/credential_vault_field_definition.py
+++ b/src/kernel/types/vaults/credential_vault_field_definition.py
@@ -7,6 +7,9 @@
class CredentialVaultFieldDefinition(BaseModel):
+ name: str
+ """Stable field name used to key values, updates, and browser fills."""
+
required: bool
"""Whether a nonempty value is required for readiness and form submission."""
diff --git a/src/kernel/types/vaults/credential_vault_field_input_param.py b/src/kernel/types/vaults/credential_vault_field_input_param.py
index 2cf68978..c5795ce1 100644
--- a/src/kernel/types/vaults/credential_vault_field_input_param.py
+++ b/src/kernel/types/vaults/credential_vault_field_input_param.py
@@ -10,6 +10,9 @@
class CredentialVaultFieldInputParam(TypedDict, total=False):
+ name: Required[str]
+ """Unique stable field name used to key values, updates, and browser fills."""
+
type: Required[CredentialVaultFieldType]
"""
Text, email, and password have form inputs; totp does not and is omitted from
diff --git a/src/kernel/types/vaults/credential_vault_item_request_param.py b/src/kernel/types/vaults/credential_vault_item_request_param.py
index 579d7a0b..169af036 100644
--- a/src/kernel/types/vaults/credential_vault_item_request_param.py
+++ b/src/kernel/types/vaults/credential_vault_item_request_param.py
@@ -30,7 +30,9 @@ class CredentialVaultItemRequestParam(TypedDict, total=False):
"""Credential fields are for login and other non-payment credentials.
Do not store, collect, or fill credit card data in credential items. Use wallet
- and card item types for credit cards and payment checkout instead.
+ and card item types for credit cards and payment checkout instead. Field order
+ is preserved in the user-facing collection form, so list fields in the same
+ top-to-bottom order as the website.
"""
type: Required[Literal["credential"]]
diff --git a/src/kernel/types/vaults/credential_vault_item_spec.py b/src/kernel/types/vaults/credential_vault_item_spec.py
index e953f416..307d2b25 100644
--- a/src/kernel/types/vaults/credential_vault_item_spec.py
+++ b/src/kernel/types/vaults/credential_vault_item_spec.py
@@ -1,6 +1,6 @@
# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
-from typing import Dict, Optional
+from typing import List, Optional
from ..._models import BaseModel
from .credential_vault_field_definition import CredentialVaultFieldDefinition
@@ -9,7 +9,10 @@
class CredentialVaultItemSpec(BaseModel):
- fields: Dict[str, CredentialVaultFieldDefinition]
+ fields: List[CredentialVaultFieldDefinition]
+ """
+ Ordered field definitions rendered in this order by credential collection forms.
+ """
description: Optional[str] = None
"""
diff --git a/src/kernel/types/vaults/credential_vault_item_spec_input_param.py b/src/kernel/types/vaults/credential_vault_item_spec_input_param.py
index a0e8850a..ab4eaee8 100644
--- a/src/kernel/types/vaults/credential_vault_item_spec_input_param.py
+++ b/src/kernel/types/vaults/credential_vault_item_spec_input_param.py
@@ -2,7 +2,7 @@
from __future__ import annotations
-from typing import Dict
+from typing import Iterable
from typing_extensions import Required, TypedDict
from .credential_vault_field_input_param import CredentialVaultFieldInputParam
@@ -13,10 +13,15 @@
class CredentialVaultItemSpecInputParam(TypedDict, total=False):
"""Credential fields are for login and other non-payment credentials.
- Do not store, collect, or fill credit card data in credential items. Use wallet and card item types for credit cards and payment checkout instead.
+ Do not store, collect, or fill credit card data in credential items. Use wallet and card item types for credit cards and payment checkout instead. Field order is preserved in the user-facing collection form, so list fields in the same top-to-bottom order as the website.
"""
- fields: Required[Dict[str, CredentialVaultFieldInputParam]]
+ fields: Required[Iterable[CredentialVaultFieldInputParam]]
+ """Ordered field definitions.
+
+ Use the website's top-to-bottom field order; the collection form renders this
+ order unchanged.
+ """
description: str
"""
diff --git a/src/kernel/types/vaults/item_upsert_params.py b/src/kernel/types/vaults/item_upsert_params.py
index 5890280a..4e4799dd 100644
--- a/src/kernel/types/vaults/item_upsert_params.py
+++ b/src/kernel/types/vaults/item_upsert_params.py
@@ -194,7 +194,9 @@ class CredentialVaultItemRequest(TypedDict, total=False):
"""Credential fields are for login and other non-payment credentials.
Do not store, collect, or fill credit card data in credential items. Use wallet
- and card item types for credit cards and payment checkout instead.
+ and card item types for credit cards and payment checkout instead. Field order
+ is preserved in the user-facing collection form, so list fields in the same
+ top-to-bottom order as the website.
"""
type: Required[Literal["credential"]]
diff --git a/tests/api_resources/vaults/test_items.py b/tests/api_resources/vaults/test_items.py
index b80631eb..75e84e15 100644
--- a/tests/api_resources/vaults/test_items.py
+++ b/tests/api_resources/vaults/test_items.py
@@ -1144,7 +1144,14 @@ def test_method_upsert_overload_3(self, client: Kernel) -> None:
item = client.vaults.items.upsert(
key="x",
id_or_name="id_or_name",
- spec={"fields": {"foo": {"type": "text"}}},
+ spec={
+ "fields": [
+ {
+ "name": "name",
+ "type": "text",
+ }
+ ]
+ },
type="credential",
)
assert_matches_type(VaultItem, item, path=["response"])
@@ -1156,14 +1163,15 @@ def test_method_upsert_with_all_params_overload_3(self, client: Kernel) -> None:
key="x",
id_or_name="id_or_name",
spec={
- "fields": {
- "foo": {
+ "fields": [
+ {
+ "name": "name",
"type": "text",
"required": True,
"sensitive": True,
"value": "x",
}
- },
+ ],
"description": "description",
},
type="credential",
@@ -1176,7 +1184,14 @@ def test_raw_response_upsert_overload_3(self, client: Kernel) -> None:
response = client.vaults.items.with_raw_response.upsert(
key="x",
id_or_name="id_or_name",
- spec={"fields": {"foo": {"type": "text"}}},
+ spec={
+ "fields": [
+ {
+ "name": "name",
+ "type": "text",
+ }
+ ]
+ },
type="credential",
)
@@ -1191,7 +1206,14 @@ def test_streaming_response_upsert_overload_3(self, client: Kernel) -> None:
with client.vaults.items.with_streaming_response.upsert(
key="x",
id_or_name="id_or_name",
- spec={"fields": {"foo": {"type": "text"}}},
+ spec={
+ "fields": [
+ {
+ "name": "name",
+ "type": "text",
+ }
+ ]
+ },
type="credential",
) as response:
assert not response.is_closed
@@ -1209,7 +1231,14 @@ def test_path_params_upsert_overload_3(self, client: Kernel) -> None:
client.vaults.items.with_raw_response.upsert(
key="x",
id_or_name="",
- spec={"fields": {"foo": {"type": "text"}}},
+ spec={
+ "fields": [
+ {
+ "name": "name",
+ "type": "text",
+ }
+ ]
+ },
type="credential",
)
@@ -1217,7 +1246,14 @@ def test_path_params_upsert_overload_3(self, client: Kernel) -> None:
client.vaults.items.with_raw_response.upsert(
key="",
id_or_name="id_or_name",
- spec={"fields": {"foo": {"type": "text"}}},
+ spec={
+ "fields": [
+ {
+ "name": "name",
+ "type": "text",
+ }
+ ]
+ },
type="credential",
)
@@ -2349,7 +2385,14 @@ async def test_method_upsert_overload_3(self, async_client: AsyncKernel) -> None
item = await async_client.vaults.items.upsert(
key="x",
id_or_name="id_or_name",
- spec={"fields": {"foo": {"type": "text"}}},
+ spec={
+ "fields": [
+ {
+ "name": "name",
+ "type": "text",
+ }
+ ]
+ },
type="credential",
)
assert_matches_type(VaultItem, item, path=["response"])
@@ -2361,14 +2404,15 @@ async def test_method_upsert_with_all_params_overload_3(self, async_client: Asyn
key="x",
id_or_name="id_or_name",
spec={
- "fields": {
- "foo": {
+ "fields": [
+ {
+ "name": "name",
"type": "text",
"required": True,
"sensitive": True,
"value": "x",
}
- },
+ ],
"description": "description",
},
type="credential",
@@ -2381,7 +2425,14 @@ async def test_raw_response_upsert_overload_3(self, async_client: AsyncKernel) -
response = await async_client.vaults.items.with_raw_response.upsert(
key="x",
id_or_name="id_or_name",
- spec={"fields": {"foo": {"type": "text"}}},
+ spec={
+ "fields": [
+ {
+ "name": "name",
+ "type": "text",
+ }
+ ]
+ },
type="credential",
)
@@ -2396,7 +2447,14 @@ async def test_streaming_response_upsert_overload_3(self, async_client: AsyncKer
async with async_client.vaults.items.with_streaming_response.upsert(
key="x",
id_or_name="id_or_name",
- spec={"fields": {"foo": {"type": "text"}}},
+ spec={
+ "fields": [
+ {
+ "name": "name",
+ "type": "text",
+ }
+ ]
+ },
type="credential",
) as response:
assert not response.is_closed
@@ -2414,7 +2472,14 @@ async def test_path_params_upsert_overload_3(self, async_client: AsyncKernel) ->
await async_client.vaults.items.with_raw_response.upsert(
key="x",
id_or_name="",
- spec={"fields": {"foo": {"type": "text"}}},
+ spec={
+ "fields": [
+ {
+ "name": "name",
+ "type": "text",
+ }
+ ]
+ },
type="credential",
)
@@ -2422,6 +2487,13 @@ async def test_path_params_upsert_overload_3(self, async_client: AsyncKernel) ->
await async_client.vaults.items.with_raw_response.upsert(
key="",
id_or_name="id_or_name",
- spec={"fields": {"foo": {"type": "text"}}},
+ spec={
+ "fields": [
+ {
+ "name": "name",
+ "type": "text",
+ }
+ ]
+ },
type="credential",
)
From 569a604ad58c278c83bda5068c8a6080334d085f Mon Sep 17 00:00:00 2001
From: "kernel-internal[bot]"
<260533166+kernel-internal[bot]@users.noreply.github.com>
Date: Thu, 17 Sep 2026 21:32:13 +0000
Subject: [PATCH 284/366] feat: Accept opaque AgentCard vaulted card IDs
Stainless-Generated-From: 77ed6712c80370958baad94c48aa88b305a33690
---
src/kernel/types/vaults/card_vault_item_spec.py | 5 +++--
src/kernel/types/vaults/card_vault_item_spec_param.py | 5 +++--
2 files changed, 6 insertions(+), 4 deletions(-)
diff --git a/src/kernel/types/vaults/card_vault_item_spec.py b/src/kernel/types/vaults/card_vault_item_spec.py
index 0bb0b77a..0c73d9bf 100644
--- a/src/kernel/types/vaults/card_vault_item_spec.py
+++ b/src/kernel/types/vaults/card_vault_item_spec.py
@@ -113,9 +113,10 @@ class AgentCardCardVaultItemSpec(BaseModel):
"""Wallet item key used to authorize checkouts."""
card_id: Optional[str] = None
- """AgentCard vaulted card to pay with.
+ """Opaque card ID returned by AgentCard for a card in the connected wallet.
- Omitted, the cardholder picks on the approval screen.
+ Pass it through unchanged without assuming a prefix or format. Omitted, the
+ cardholder picks on the approval screen.
"""
diff --git a/src/kernel/types/vaults/card_vault_item_spec_param.py b/src/kernel/types/vaults/card_vault_item_spec_param.py
index a1680703..7172d066 100644
--- a/src/kernel/types/vaults/card_vault_item_spec_param.py
+++ b/src/kernel/types/vaults/card_vault_item_spec_param.py
@@ -112,9 +112,10 @@ class AgentCardCardVaultItemSpec(TypedDict, total=False):
"""Wallet item key used to authorize checkouts."""
card_id: str
- """AgentCard vaulted card to pay with.
+ """Opaque card ID returned by AgentCard for a card in the connected wallet.
- Omitted, the cardholder picks on the approval screen.
+ Pass it through unchanged without assuming a prefix or format. Omitted, the
+ cardholder picks on the approval screen.
"""
From 79fa360ccad3a8403ce45b4694dee6ddfb47f04e Mon Sep 17 00:00:00 2001
From: "kernel-internal[bot]"
<260533166+kernel-internal[bot]@users.noreply.github.com>
Date: Thu, 17 Sep 2026 21:36:54 +0000
Subject: [PATCH 285/366] release: 0.109.0
---
.release-please-manifest.json | 2 +-
CHANGELOG.md | 9 +++++++++
pyproject.toml | 2 +-
src/kernel/_version.py | 2 +-
4 files changed, 12 insertions(+), 3 deletions(-)
diff --git a/.release-please-manifest.json b/.release-please-manifest.json
index 3e05ae56..3ca49358 100644
--- a/.release-please-manifest.json
+++ b/.release-please-manifest.json
@@ -1,3 +1,3 @@
{
- ".": "0.108.0"
+ ".": "0.109.0"
}
\ No newline at end of file
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 1417baaa..6d7f91fb 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,14 @@
# Changelog
+## [0.109.0](https://github.com/kernel/kernel-python-sdk/compare/v0.108.0...v0.109.0) (2026-09-17)
+
+
+### Features
+
+* Accept opaque AgentCard vaulted card IDs ([569a604](https://github.com/kernel/kernel-python-sdk/commit/569a604ad58c278c83bda5068c8a6080334d085f))
+* Expose organization-wide concurrent browser capacity ([8b22180](https://github.com/kernel/kernel-python-sdk/commit/8b22180766c592d1480e26fc781ffd5e1b623051))
+* Preserve credential field order ([1a408c4](https://github.com/kernel/kernel-python-sdk/commit/1a408c41c0fd043ff69187f7d62bc721f9e5f867))
+
## [0.108.0](https://github.com/kernel/kernel-python-sdk/compare/v0.107.0...v0.108.0) (2026-09-17)
diff --git a/pyproject.toml b/pyproject.toml
index 3a9c0dee..e98679a6 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -1,6 +1,6 @@
[project]
name = "kernel"
-version = "0.108.0"
+version = "0.109.0"
description = "The official Python library for the kernel API"
dynamic = ["readme"]
license = "Apache-2.0"
diff --git a/src/kernel/_version.py b/src/kernel/_version.py
index 5c651d4a..a6550577 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.108.0" # x-release-please-version
+__version__ = "0.109.0" # x-release-please-version
From 63de01fee886a6758fa613a279d6c401d32a0aff Mon Sep 17 00:00:00 2001
From: "kernel-internal[bot]"
<260533166+kernel-internal[bot]@users.noreply.github.com>
Date: Fri, 18 Sep 2026 19:44:37 +0000
Subject: [PATCH 286/366] feat: Add credential field labels to the API
Stainless-Generated-From: c78ab6e55680756f338514b9fb95457f9256470d
---
.../types/vaults/credential_vault_field_definition.py | 10 ++++++++++
.../types/vaults/credential_vault_field_input_param.py | 8 ++++++++
tests/api_resources/vaults/test_items.py | 2 ++
3 files changed, 20 insertions(+)
diff --git a/src/kernel/types/vaults/credential_vault_field_definition.py b/src/kernel/types/vaults/credential_vault_field_definition.py
index 0ea64ec7..8c60748d 100644
--- a/src/kernel/types/vaults/credential_vault_field_definition.py
+++ b/src/kernel/types/vaults/credential_vault_field_definition.py
@@ -1,5 +1,7 @@
# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+from typing import Optional
+
from ..._models import BaseModel
from .credential_vault_field_type import CredentialVaultFieldType
@@ -32,3 +34,11 @@ class CredentialVaultFieldDefinition(BaseModel):
never fill the seed. Custom algorithms, digits, periods, and form enrollment are
unsupported.
"""
+
+ label: Optional[str] = None
+ """Optional human-readable display label.
+
+ It is returned as non-secret metadata and never affects value keys, updates, or
+ browser fills. Use single-line, trimmed display text without control or
+ formatting characters. The server enforces a 128-byte UTF-8 limit.
+ """
diff --git a/src/kernel/types/vaults/credential_vault_field_input_param.py b/src/kernel/types/vaults/credential_vault_field_input_param.py
index c5795ce1..8e5faf5a 100644
--- a/src/kernel/types/vaults/credential_vault_field_input_param.py
+++ b/src/kernel/types/vaults/credential_vault_field_input_param.py
@@ -25,6 +25,14 @@ class CredentialVaultFieldInputParam(TypedDict, total=False):
unsupported.
"""
+ label: str
+ """Optional human-readable display label.
+
+ It is returned as non-secret metadata and never affects value keys, updates, or
+ browser fills. Use single-line, trimmed display text without control or
+ formatting characters. The server enforces a 128-byte UTF-8 limit.
+ """
+
required: bool
sensitive: bool
diff --git a/tests/api_resources/vaults/test_items.py b/tests/api_resources/vaults/test_items.py
index 75e84e15..61f350e4 100644
--- a/tests/api_resources/vaults/test_items.py
+++ b/tests/api_resources/vaults/test_items.py
@@ -1167,6 +1167,7 @@ def test_method_upsert_with_all_params_overload_3(self, client: Kernel) -> None:
{
"name": "name",
"type": "text",
+ "label": "label",
"required": True,
"sensitive": True,
"value": "x",
@@ -2408,6 +2409,7 @@ async def test_method_upsert_with_all_params_overload_3(self, async_client: Asyn
{
"name": "name",
"type": "text",
+ "label": "label",
"required": True,
"sensitive": True,
"value": "x",
From acd254d357c20db202d8ba4896e016ff723a1f56 Mon Sep 17 00:00:00 2001
From: "kernel-internal[bot]"
<260533166+kernel-internal[bot]@users.noreply.github.com>
Date: Fri, 18 Sep 2026 19:49:14 +0000
Subject: [PATCH 287/366] release: 0.110.0
---
.release-please-manifest.json | 2 +-
CHANGELOG.md | 7 +++++++
pyproject.toml | 2 +-
src/kernel/_version.py | 2 +-
4 files changed, 10 insertions(+), 3 deletions(-)
diff --git a/.release-please-manifest.json b/.release-please-manifest.json
index 3ca49358..c37434e9 100644
--- a/.release-please-manifest.json
+++ b/.release-please-manifest.json
@@ -1,3 +1,3 @@
{
- ".": "0.109.0"
+ ".": "0.110.0"
}
\ No newline at end of file
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 6d7f91fb..67530bf8 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,12 @@
# Changelog
+## [0.110.0](https://github.com/kernel/kernel-python-sdk/compare/v0.109.0...v0.110.0) (2026-09-18)
+
+
+### Features
+
+* Add credential field labels to the API ([63de01f](https://github.com/kernel/kernel-python-sdk/commit/63de01fee886a6758fa613a279d6c401d32a0aff))
+
## [0.109.0](https://github.com/kernel/kernel-python-sdk/compare/v0.108.0...v0.109.0) (2026-09-17)
diff --git a/pyproject.toml b/pyproject.toml
index e98679a6..ef25af35 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -1,6 +1,6 @@
[project]
name = "kernel"
-version = "0.109.0"
+version = "0.110.0"
description = "The official Python library for the kernel API"
dynamic = ["readme"]
license = "Apache-2.0"
diff --git a/src/kernel/_version.py b/src/kernel/_version.py
index a6550577..78e78477 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.109.0" # x-release-please-version
+__version__ = "0.110.0" # x-release-please-version
From 64805ae68f0978264fb77801e8e94a470a039e94 Mon Sep 17 00:00:00 2001
From: "kernel-internal[bot]"
<260533166+kernel-internal[bot]@users.noreply.github.com>
Date: Fri, 18 Sep 2026 23:28:11 +0000
Subject: [PATCH 288/366] feat: Publish restricted_route_unavailable and
unknown proxy_error codes
Stainless-Generated-From: 30b8dbda88df8b8b5d482c48532d5826e1ce35a4
---
.../browsers/browser_proxy_error_event.py | 19 ++++++++++++++++---
1 file changed, 16 insertions(+), 3 deletions(-)
diff --git a/src/kernel/types/browsers/browser_proxy_error_event.py b/src/kernel/types/browsers/browser_proxy_error_event.py
index d8c9dd93..5d40f9ed 100644
--- a/src/kernel/types/browsers/browser_proxy_error_event.py
+++ b/src/kernel/types/browsers/browser_proxy_error_event.py
@@ -22,18 +22,23 @@ class Data(BrowserEventContext):
"provider_unreachable",
"provider_rejected",
"origin_tls_timeout",
+ "origin_response_incomplete",
"proxy_unavailable",
+ "restricted_route_unavailable",
"upstream_timeout",
"upstream_dns_failure",
"upstream_connect_failed",
+ "unknown",
]
"""
Proxy-layer error code: the X-Kernel-Proxy-Error response header value from a
branded 5xx error page served by the metro egress host-proxy. Values mirror what
the proxy emits: destination_blocked, provider_blacklisted,
- provider_unreachable, provider_rejected, origin_tls_timeout, proxy_unavailable,
- upstream_timeout, upstream_dns_failure, upstream_connect_failed. Unknown header
- values are dropped.
+ provider_unreachable, provider_rejected, origin_tls_timeout,
+ origin_response_incomplete, proxy_unavailable, restricted_route_unavailable,
+ upstream_timeout, upstream_dns_failure, upstream_connect_failed. A header value
+ the browser image does not recognize is reported as unknown, with the header
+ value in raw_code.
"""
request_id: str
@@ -45,6 +50,14 @@ class Data(BrowserEventContext):
method: Optional[str] = None
"""HTTP method of the failed request, when known."""
+ raw_code: Optional[str] = None
+ """Sanitized X-Kernel-Proxy-Error header value, present only when code is unknown.
+
+ Surrounding whitespace is removed, the value is lowercased, characters outside
+ [a-z0-9_] are replaced with \\__, and the result is truncated to at most 64
+ characters.
+ """
+
resource_type: Optional[str] = None
"""CDP Network.ResourceType for the request, when known."""
From 5f2df0a4170aa146eabb23f576195d1a06621925 Mon Sep 17 00:00:00 2001
From: "kernel-internal[bot]"
<260533166+kernel-internal[bot]@users.noreply.github.com>
Date: Mon, 21 Sep 2026 15:57:52 +0000
Subject: [PATCH 289/366] feat: Support native prepared Adyen Sessions checkout
Stainless-Generated-From: 1c59aa3eefd9ebe82d2a948a4b96dfe9903207a1
---
src/kernel/resources/vaults/items.py | 4 ++--
.../types/vaults/agentcard_checkout_preparation.py | 2 +-
.../types/vaults/agentcard_prepared_processor.py | 2 +-
src/kernel/types/vaults/card_vault_item_state.py | 6 ++++--
.../types/vaults/item_perform_operation_params.py | 2 +-
..._checkout_vault_item_operation_request_param.py | 4 ++--
.../types/vaults/vault_checkout_context_param.py | 14 ++++++++------
7 files changed, 19 insertions(+), 15 deletions(-)
diff --git a/src/kernel/resources/vaults/items.py b/src/kernel/resources/vaults/items.py
index a9058c09..acaf0315 100644
--- a/src/kernel/resources/vaults/items.py
+++ b/src/kernel/resources/vaults/items.py
@@ -517,7 +517,7 @@ def perform_operation(
leave the outcome unknown; do not automatically retry.
Args:
- checkout: Required when preparing an unused AgentCard card for a supported tokenization
+ checkout: Required when preparing an unused AgentCard card for a supported checkout
processor. Consent is bound to this browser and declared merchant origin, not a
tab. Wait for the item's ready_to_submit status before native Pay and submit
within its readiness deadline. Unused preparations expire automatically; every
@@ -1290,7 +1290,7 @@ async def perform_operation(
leave the outcome unknown; do not automatically retry.
Args:
- checkout: Required when preparing an unused AgentCard card for a supported tokenization
+ checkout: Required when preparing an unused AgentCard card for a supported checkout
processor. Consent is bound to this browser and declared merchant origin, not a
tab. Wait for the item's ready_to_submit status before native Pay and submit
within its readiness deadline. Unused preparations expire automatically; every
diff --git a/src/kernel/types/vaults/agentcard_checkout_preparation.py b/src/kernel/types/vaults/agentcard_checkout_preparation.py
index 7f9da71f..419f3fdd 100644
--- a/src/kernel/types/vaults/agentcard_checkout_preparation.py
+++ b/src/kernel/types/vaults/agentcard_checkout_preparation.py
@@ -13,7 +13,7 @@
class AgentcardCheckoutPreparation(BaseModel):
"""One-use processor-bound checkout preparation.
- Keep the approval page open through token handoff. The amount is display-only and does not constrain the merchant's eventual charge.
+ Keep the approval page open through device handoff, including Adyen encryption. The amount is declared by the caller and does not constrain the merchant's eventual charge. Adyen device approval and browser Authorised responses are not capture or fulfillment evidence.
"""
browser_id: str
diff --git a/src/kernel/types/vaults/agentcard_prepared_processor.py b/src/kernel/types/vaults/agentcard_prepared_processor.py
index ebf94c86..41c3c7c2 100644
--- a/src/kernel/types/vaults/agentcard_prepared_processor.py
+++ b/src/kernel/types/vaults/agentcard_prepared_processor.py
@@ -4,4 +4,4 @@
__all__ = ["AgentcardPreparedProcessor"]
-AgentcardPreparedProcessor: TypeAlias = Literal["square", "braintree", "worldpay", "bambora", "mercado_pago"]
+AgentcardPreparedProcessor: TypeAlias = Literal["square", "braintree", "worldpay", "bambora", "mercado_pago", "adyen"]
diff --git a/src/kernel/types/vaults/card_vault_item_state.py b/src/kernel/types/vaults/card_vault_item_state.py
index a1cb0b68..df7a4306 100644
--- a/src/kernel/types/vaults/card_vault_item_state.py
+++ b/src/kernel/types/vaults/card_vault_item_state.py
@@ -117,8 +117,10 @@ class AgentCardCardState(BaseModel):
preparation: Optional[AgentcardCheckoutPreparation] = None
"""One-use processor-bound checkout preparation.
- Keep the approval page open through token handoff. The amount is display-only
- and does not constrain the merchant's eventual charge.
+ Keep the approval page open through device handoff, including Adyen encryption.
+ The amount is declared by the caller and does not constrain the merchant's
+ eventual charge. Adyen device approval and browser Authorised responses are not
+ capture or fulfillment evidence.
"""
status_reason: Optional[str] = None
diff --git a/src/kernel/types/vaults/item_perform_operation_params.py b/src/kernel/types/vaults/item_perform_operation_params.py
index c1fb80c9..7e866ff3 100644
--- a/src/kernel/types/vaults/item_perform_operation_params.py
+++ b/src/kernel/types/vaults/item_perform_operation_params.py
@@ -34,7 +34,7 @@ class PrepareCheckoutVaultItemOperationRequest(TypedDict, total=False):
checkout: Required[VaultCheckoutContextParam]
"""
- Required when preparing an unused AgentCard card for a supported tokenization
+ Required when preparing an unused AgentCard card for a supported checkout
processor. Consent is bound to this browser and declared merchant origin, not a
tab. Wait for the item's ready_to_submit status before native Pay and submit
within its readiness deadline. Unused preparations expire automatically; every
diff --git a/src/kernel/types/vaults/prepare_checkout_vault_item_operation_request_param.py b/src/kernel/types/vaults/prepare_checkout_vault_item_operation_request_param.py
index 72fdb4d2..73d92e08 100644
--- a/src/kernel/types/vaults/prepare_checkout_vault_item_operation_request_param.py
+++ b/src/kernel/types/vaults/prepare_checkout_vault_item_operation_request_param.py
@@ -10,14 +10,14 @@
class PrepareCheckoutVaultItemOperationRequestParam(TypedDict, total=False):
- """Prepare an unused AgentCard card for a supported tokenization checkout.
+ """Prepare an unused AgentCard card for a supported checkout.
Deliver the returned approval URL and keep the approval page open. Poll the item until ready_to_submit, then submit native Pay before preparation.expires_at. Readiness lasts at most 30 seconds. Unused preparations expire automatically. Preparations are single-use even after failure or expiry; do not automatically retry and reconcile uncertain outcomes with the merchant.
"""
checkout: Required[VaultCheckoutContextParam]
"""
- Required when preparing an unused AgentCard card for a supported tokenization
+ Required when preparing an unused AgentCard card for a supported checkout
processor. Consent is bound to this browser and declared merchant origin, not a
tab. Wait for the item's ready_to_submit status before native Pay and submit
within its readiness deadline. Unused preparations expire automatically; every
diff --git a/src/kernel/types/vaults/vault_checkout_context_param.py b/src/kernel/types/vaults/vault_checkout_context_param.py
index f5fda5db..545f2dff 100644
--- a/src/kernel/types/vaults/vault_checkout_context_param.py
+++ b/src/kernel/types/vaults/vault_checkout_context_param.py
@@ -11,7 +11,7 @@
class VaultCheckoutContextParam(TypedDict, total=False):
"""
- Required when preparing an unused AgentCard card for a supported tokenization processor. Consent is bound to this browser and declared merchant origin, not a tab. Wait for the item's ready_to_submit status before native Pay and submit within its readiness deadline. Unused preparations expire automatically; every preparation is single-use, including after failure or expiry.
+ Required when preparing an unused AgentCard card for a supported checkout processor. Consent is bound to this browser and declared merchant origin, not a tab. Wait for the item's ready_to_submit status before native Pay and submit within its readiness deadline. Unused preparations expire automatically; every preparation is single-use, including after failure or expiry.
"""
browser_id: Required[str]
@@ -19,8 +19,8 @@ class VaultCheckoutContextParam(TypedDict, total=False):
environment: Required[Literal["production", "sandbox", "shared"]]
"""
- Use production or sandbox for Square, Braintree and Worldpay; shared for Bambora
- and Mercado Pago. Shared endpoints do not establish test mode. Merchant
+ Use production or sandbox for Square, Braintree, Worldpay and Adyen; shared for
+ Bambora and Mercado Pago. Shared endpoints do not establish test mode. Merchant
credentials/configuration determine processor test mode, independently of the
AgentCard credential mode.
"""
@@ -32,8 +32,10 @@ class VaultCheckoutContextParam(TypedDict, total=False):
"""
psp: AgentcardPreparedProcessor
- """Tokenization processor.
+ """Checkout processor.
- Omit for Square compatibility. Non-Square processors require multi-processor
- preparation enablement.
+ Omit for Square compatibility. Adyen supports fresh-card Sessions requests on
+ Adyen hosts only. Use public dummy card fields, not vault aliases. The unique
+ armed preparation is associated with the subsequent eligible request from this
+ browser and declared merchant origin; competing preparations are rejected.
"""
From 41059ae6d345b924d7e508e43a8b1d0842c58556 Mon Sep 17 00:00:00 2001
From: "kernel-internal[bot]"
<260533166+kernel-internal[bot]@users.noreply.github.com>
Date: Mon, 21 Sep 2026 18:58:00 +0000
Subject: [PATCH 290/366] feat: Distinguish optimistic managed auth reauth
Stainless-Generated-From: 31a3e7fafe482f5af315d990f10750d7a0110016
---
src/kernel/types/auth/managed_auth.py | 21 ++++++++++++++-------
1 file changed, 14 insertions(+), 7 deletions(-)
diff --git a/src/kernel/types/auth/managed_auth.py b/src/kernel/types/auth/managed_auth.py
index 62fa7852..be202e61 100644
--- a/src/kernel/types/auth/managed_auth.py
+++ b/src/kernel/types/auth/managed_auth.py
@@ -367,10 +367,12 @@ class ManagedAuth(BaseModel):
can_reauth: Optional[bool] = None
"""
- Whether Kernel can automatically re-authenticate this connection when the
- session expires. Requires a prior successful login plus either a Kernel
- credential or an external credential reference. See `can_reauth_reason` for the
- specific outcome.
+ Whether this connection's stored requirements are eligible for unattended
+ re-authentication. A true value can represent either fully satisfiable
+ requirements or a best-effort attempt. It does not account for whether automatic
+ re-authentication is enabled or parent workflow state such as an active flow or
+ circuit-breaker cooldown, so it does not guarantee an attempt on the next health
+ check. See `can_reauth_reason` for the specific outcome.
"""
can_reauth_reason: Optional[
@@ -381,6 +383,7 @@ class ManagedAuth(BaseModel):
"viable_plans_found",
"no_requirements_recorded",
"totp_reauth_allowed",
+ "optimistic_totp_attempt",
"requirements_satisfiable",
"no_prior_successful_login",
"no_credential",
@@ -395,7 +398,7 @@ class ManagedAuth(BaseModel):
] = None
"""
Machine-readable reason for the current value of `can_reauth`. Affirmative
- values (re-auth is possible):
+ values (requirements are eligible for unattended re-authentication):
- `external_credential` — an external credential provider is attached
- `cua_has_credential` — CUA flow with a stored credential
@@ -404,8 +407,12 @@ class ManagedAuth(BaseModel):
- `viable_plans_found` — at least one stored login plan can be replayed
- `no_requirements_recorded` — no recorded credential requirements to fail
against
- - `totp_reauth_allowed` — TOTP is the only recorded requirement and is safe to
- attempt automatically
+ - `totp_reauth_allowed` — TOTP is the only recorded requirement and a stored
+ secret can generate the code
+ - `optimistic_totp_attempt` — TOTP was previously required but no reusable
+ secret is stored; the connection remains eligible for a
+ circuit-breaker-bounded attempt because the site may not challenge returning
+ sessions
- `requirements_satisfiable` — recorded requirements contain no recognized
blocker
From 17a7960d9304fdceb54d59b09277651b6166fdb1 Mon Sep 17 00:00:00 2001
From: "kernel-internal[bot]"
<260533166+kernel-internal[bot]@users.noreply.github.com>
Date: Mon, 21 Sep 2026 19:22:18 +0000
Subject: [PATCH 291/366] feat: Persist stable managed auth completion
timestamps
Stainless-Generated-From: 6427817227cf91c697de16f53ac309df45b1b532
---
src/kernel/types/auth/managed_auth_timeline_event.py | 8 ++++++++
1 file changed, 8 insertions(+)
diff --git a/src/kernel/types/auth/managed_auth_timeline_event.py b/src/kernel/types/auth/managed_auth_timeline_event.py
index b2e0213b..79eee70c 100644
--- a/src/kernel/types/auth/managed_auth_timeline_event.py
+++ b/src/kernel/types/auth/managed_auth_timeline_event.py
@@ -38,6 +38,14 @@ class ManagedAuthTimelineEvent(BaseModel):
browser_session_id: Optional[str] = None
"""Browser session that produced the event, if one was created."""
+ completed_at: Optional[datetime] = None
+ """When the login/reauth attempt first reached a terminal status.
+
+ Stable across retries and subsequent cleanup writes. Absent for in-progress
+ attempts, health checks, and historical attempts without a recorded completion
+ time.
+ """
+
error_code: Optional[str] = None
"""Machine-readable error code. Present when a login/reauth event failed."""
From e230ce3a684ce7723256170443ba0b84b08310a4 Mon Sep 17 00:00:00 2001
From: "kernel-internal[bot]"
<260533166+kernel-internal[bot]@users.noreply.github.com>
Date: Mon, 21 Sep 2026 19:52:02 +0000
Subject: [PATCH 292/366] feat: Allow managed auth logins to disable learned
skills
Stainless-Generated-From: 9c2ffa1553e5fe09f6a36fa423af3e7308eb7d14
---
src/kernel/resources/auth/connections.py | 12 ++++++++++++
src/kernel/types/auth/connection_login_params.py | 9 ++++++++-
tests/api_resources/auth/test_connections.py | 2 ++
3 files changed, 22 insertions(+), 1 deletion(-)
diff --git a/src/kernel/resources/auth/connections.py b/src/kernel/resources/auth/connections.py
index 62858fdd..fae767b7 100644
--- a/src/kernel/resources/auth/connections.py
+++ b/src/kernel/resources/auth/connections.py
@@ -477,6 +477,7 @@ def login(
browser_telemetry: Optional[connection_login_params.BrowserTelemetry] | Omit = omit,
proxy: connection_login_params.Proxy | Omit = omit,
record_session: bool | Omit = omit,
+ skill_mode: Literal["enabled", "disabled"] | 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,
@@ -502,6 +503,10 @@ def login(
record_session: Override the connection's default for recording this login's browser session.
When omitted, the connection's record_session default is used.
+ skill_mode: Controls whether this login reads and writes learned domain skills. Automatic
+ reauths inherit the selected mode until a later accepted login sets enabled or
+ omits this field. Defaults to enabled when omitted.
+
extra_headers: Send extra headers
extra_query: Add additional query parameters to the request
@@ -520,6 +525,7 @@ def login(
"browser_telemetry": browser_telemetry,
"proxy": proxy,
"record_session": record_session,
+ "skill_mode": skill_mode,
},
connection_login_params.ConnectionLoginParams,
),
@@ -1100,6 +1106,7 @@ async def login(
browser_telemetry: Optional[connection_login_params.BrowserTelemetry] | Omit = omit,
proxy: connection_login_params.Proxy | Omit = omit,
record_session: bool | Omit = omit,
+ skill_mode: Literal["enabled", "disabled"] | 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,
@@ -1125,6 +1132,10 @@ async def login(
record_session: Override the connection's default for recording this login's browser session.
When omitted, the connection's record_session default is used.
+ skill_mode: Controls whether this login reads and writes learned domain skills. Automatic
+ reauths inherit the selected mode until a later accepted login sets enabled or
+ omits this field. Defaults to enabled when omitted.
+
extra_headers: Send extra headers
extra_query: Add additional query parameters to the request
@@ -1143,6 +1154,7 @@ async def login(
"browser_telemetry": browser_telemetry,
"proxy": proxy,
"record_session": record_session,
+ "skill_mode": skill_mode,
},
connection_login_params.ConnectionLoginParams,
),
diff --git a/src/kernel/types/auth/connection_login_params.py b/src/kernel/types/auth/connection_login_params.py
index cbe2bf3b..0d62506d 100644
--- a/src/kernel/types/auth/connection_login_params.py
+++ b/src/kernel/types/auth/connection_login_params.py
@@ -3,7 +3,7 @@
from __future__ import annotations
from typing import Optional
-from typing_extensions import TypedDict
+from typing_extensions import Literal, TypedDict
from .managed_auth_browser_config_param import ManagedAuthBrowserConfigParam
from ..browsers.browser_telemetry_categories_config_param import BrowserTelemetryCategoriesConfigParam
@@ -40,6 +40,13 @@ class ConnectionLoginParams(TypedDict, total=False):
When omitted, the connection's record_session default is used.
"""
+ skill_mode: Literal["enabled", "disabled"]
+ """Controls whether this login reads and writes learned domain skills.
+
+ Automatic reauths inherit the selected mode until a later accepted login sets
+ enabled or omits this field. Defaults to enabled when omitted.
+ """
+
class BrowserTelemetryExportOtlpDestination(TypedDict, total=False):
"""OTLP destination to export this session's captured telemetry to.
diff --git a/tests/api_resources/auth/test_connections.py b/tests/api_resources/auth/test_connections.py
index 53a55fa7..311b9971 100644
--- a/tests/api_resources/auth/test_connections.py
+++ b/tests/api_resources/auth/test_connections.py
@@ -525,6 +525,7 @@ def test_method_login_with_all_params(self, client: Kernel) -> None:
"name": "name",
},
record_session=True,
+ skill_mode="enabled",
)
assert_matches_type(LoginResponse, connection, path=["response"])
@@ -1187,6 +1188,7 @@ async def test_method_login_with_all_params(self, async_client: AsyncKernel) ->
"name": "name",
},
record_session=True,
+ skill_mode="enabled",
)
assert_matches_type(LoginResponse, connection, path=["response"])
From 76b3d8871e29d47cfda20f68ab23152daf1790c6 Mon Sep 17 00:00:00 2001
From: "kernel-internal[bot]"
<260533166+kernel-internal[bot]@users.noreply.github.com>
Date: Tue, 22 Sep 2026 15:02:56 +0000
Subject: [PATCH 293/366] feat: Implement Search API v1 providers
Stainless-Generated-From: 88a2e9eb1b30837e304011693f8598389cfb45bb
---
.stats.yml | 2 +-
api.md | 37 +
src/kernel/_client.py | 44 ++
src/kernel/resources/__init__.py | 14 +
src/kernel/resources/search/__init__.py | 47 ++
src/kernel/resources/search/contents.py | 220 ++++++
src/kernel/resources/search/providers.py | 187 +++++
src/kernel/resources/search/search.py | 536 +++++++++++++++
src/kernel/types/__init__.py | 8 +
src/kernel/types/attempt.py | 20 +
src/kernel/types/provider_target_param.py | 636 ++++++++++++++++++
src/kernel/types/result.py | 115 ++++
src/kernel/types/search/__init__.py | 9 +
.../types/search/content_fetch_params.py | 97 +++
src/kernel/types/search/provider.py | 136 ++++
.../types/search/provider_list_params.py | 12 +
.../types/search/provider_list_response.py | 10 +
src/kernel/types/search/search.py | 56 ++
src/kernel/types/search_create_params.py | 186 +++++
src/kernel/types/strategy_param.py | 60 ++
src/kernel/types/usage.py | 24 +
src/kernel/types/warning.py | 24 +
tests/api_resources/search/__init__.py | 1 +
tests/api_resources/search/test_contents.py | 150 +++++
tests/api_resources/search/test_providers.py | 96 +++
tests/api_resources/test_search.py | 265 ++++++++
26 files changed, 2991 insertions(+), 1 deletion(-)
create mode 100644 src/kernel/resources/search/__init__.py
create mode 100644 src/kernel/resources/search/contents.py
create mode 100644 src/kernel/resources/search/providers.py
create mode 100644 src/kernel/resources/search/search.py
create mode 100644 src/kernel/types/attempt.py
create mode 100644 src/kernel/types/provider_target_param.py
create mode 100644 src/kernel/types/result.py
create mode 100644 src/kernel/types/search/__init__.py
create mode 100644 src/kernel/types/search/content_fetch_params.py
create mode 100644 src/kernel/types/search/provider.py
create mode 100644 src/kernel/types/search/provider_list_params.py
create mode 100644 src/kernel/types/search/provider_list_response.py
create mode 100644 src/kernel/types/search/search.py
create mode 100644 src/kernel/types/search_create_params.py
create mode 100644 src/kernel/types/strategy_param.py
create mode 100644 src/kernel/types/usage.py
create mode 100644 src/kernel/types/warning.py
create mode 100644 tests/api_resources/search/__init__.py
create mode 100644 tests/api_resources/search/test_contents.py
create mode 100644 tests/api_resources/search/test_providers.py
create mode 100644 tests/api_resources/test_search.py
diff --git a/.stats.yml b/.stats.yml
index 40c9d3b0..e8ac4eb6 100644
--- a/.stats.yml
+++ b/.stats.yml
@@ -1 +1 @@
-configured_endpoints: 165
+configured_endpoints: 169
diff --git a/api.md b/api.md
index 90bb1465..7e82ddfb 100644
--- a/api.md
+++ b/api.md
@@ -735,3 +735,40 @@ Methods:
- client.credential_providers.delete(id) -> None
- client.credential_providers.list_items(id) -> CredentialProviderListItemsResponse
- client.credential_providers.test(id) -> CredentialProviderTestResult
+
+# Search
+
+Types:
+
+```python
+from kernel.types import Attempt, ProviderTarget, Request, Result, Search, Strategy, Usage, Warning
+```
+
+Methods:
+
+- client.search.create(\*\*params) -> Search
+- client.search.retrieve(id) -> Search
+
+## Contents
+
+Types:
+
+```python
+from kernel.types.search import FetchRequest, Response
+```
+
+Methods:
+
+- client.search.contents.fetch(id, \*\*params) -> None
+
+## Providers
+
+Types:
+
+```python
+from kernel.types.search import Provider, ProviderListResponse
+```
+
+Methods:
+
+- client.search.providers.list(\*\*params) -> ProviderListResponse
diff --git a/src/kernel/_client.py b/src/kernel/_client.py
index 13a6e3dc..4ffa9faf 100644
--- a/src/kernel/_client.py
+++ b/src/kernel/_client.py
@@ -57,6 +57,7 @@
from .resources import (
apps,
auth,
+ search,
vaults,
proxies,
api_keys,
@@ -85,6 +86,7 @@
from .resources.deployments import DeploymentsResource, AsyncDeploymentsResource
from .resources.invocations import InvocationsResource, AsyncInvocationsResource
from .resources.browser_pools import BrowserPoolsResource, AsyncBrowserPoolsResource
+ from .resources.search.search import SearchResource, AsyncSearchResource
from .resources.vaults.vaults import VaultsResource, AsyncVaultsResource
from .resources.browsers.browsers import BrowsersResource, AsyncBrowsersResource
from .resources.projects.projects import ProjectsResource, AsyncProjectsResource
@@ -348,6 +350,13 @@ def credential_providers(self) -> CredentialProvidersResource:
return CredentialProvidersResource(self)
+ @cached_property
+ def search(self) -> SearchResource:
+ """Search the web and retrieve content for selected results."""
+ from .resources.search import SearchResource
+
+ return SearchResource(self)
+
@cached_property
def with_raw_response(self) -> KernelWithRawResponse:
return KernelWithRawResponse(self)
@@ -752,6 +761,13 @@ def credential_providers(self) -> AsyncCredentialProvidersResource:
return AsyncCredentialProvidersResource(self)
+ @cached_property
+ def search(self) -> AsyncSearchResource:
+ """Search the web and retrieve content for selected results."""
+ from .resources.search import AsyncSearchResource
+
+ return AsyncSearchResource(self)
+
@cached_property
def with_raw_response(self) -> AsyncKernelWithRawResponse:
return AsyncKernelWithRawResponse(self)
@@ -1058,6 +1074,13 @@ def credential_providers(self) -> credential_providers.CredentialProvidersResour
return CredentialProvidersResourceWithRawResponse(self._client.credential_providers)
+ @cached_property
+ def search(self) -> search.SearchResourceWithRawResponse:
+ """Search the web and retrieve content for selected results."""
+ from .resources.search import SearchResourceWithRawResponse
+
+ return SearchResourceWithRawResponse(self._client.search)
+
class AsyncKernelWithRawResponse:
_client: AsyncKernel
@@ -1196,6 +1219,13 @@ def credential_providers(self) -> credential_providers.AsyncCredentialProvidersR
return AsyncCredentialProvidersResourceWithRawResponse(self._client.credential_providers)
+ @cached_property
+ def search(self) -> search.AsyncSearchResourceWithRawResponse:
+ """Search the web and retrieve content for selected results."""
+ from .resources.search import AsyncSearchResourceWithRawResponse
+
+ return AsyncSearchResourceWithRawResponse(self._client.search)
+
class KernelWithStreamedResponse:
_client: Kernel
@@ -1334,6 +1364,13 @@ def credential_providers(self) -> credential_providers.CredentialProvidersResour
return CredentialProvidersResourceWithStreamingResponse(self._client.credential_providers)
+ @cached_property
+ def search(self) -> search.SearchResourceWithStreamingResponse:
+ """Search the web and retrieve content for selected results."""
+ from .resources.search import SearchResourceWithStreamingResponse
+
+ return SearchResourceWithStreamingResponse(self._client.search)
+
class AsyncKernelWithStreamedResponse:
_client: AsyncKernel
@@ -1472,6 +1509,13 @@ def credential_providers(self) -> credential_providers.AsyncCredentialProvidersR
return AsyncCredentialProvidersResourceWithStreamingResponse(self._client.credential_providers)
+ @cached_property
+ def search(self) -> search.AsyncSearchResourceWithStreamingResponse:
+ """Search the web and retrieve content for selected results."""
+ from .resources.search import AsyncSearchResourceWithStreamingResponse
+
+ return AsyncSearchResourceWithStreamingResponse(self._client.search)
+
Client = Kernel
diff --git a/src/kernel/resources/__init__.py b/src/kernel/resources/__init__.py
index 21426930..4be8e2ba 100644
--- a/src/kernel/resources/__init__.py
+++ b/src/kernel/resources/__init__.py
@@ -16,6 +16,14 @@
AuthResourceWithStreamingResponse,
AsyncAuthResourceWithStreamingResponse,
)
+from .search import (
+ SearchResource,
+ AsyncSearchResource,
+ SearchResourceWithRawResponse,
+ AsyncSearchResourceWithRawResponse,
+ SearchResourceWithStreamingResponse,
+ AsyncSearchResourceWithStreamingResponse,
+)
from .vaults import (
VaultsResource,
AsyncVaultsResource,
@@ -268,4 +276,10 @@
"AsyncCredentialProvidersResourceWithRawResponse",
"CredentialProvidersResourceWithStreamingResponse",
"AsyncCredentialProvidersResourceWithStreamingResponse",
+ "SearchResource",
+ "AsyncSearchResource",
+ "SearchResourceWithRawResponse",
+ "AsyncSearchResourceWithRawResponse",
+ "SearchResourceWithStreamingResponse",
+ "AsyncSearchResourceWithStreamingResponse",
]
diff --git a/src/kernel/resources/search/__init__.py b/src/kernel/resources/search/__init__.py
new file mode 100644
index 00000000..0ae39afe
--- /dev/null
+++ b/src/kernel/resources/search/__init__.py
@@ -0,0 +1,47 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from .search import (
+ SearchResource,
+ AsyncSearchResource,
+ SearchResourceWithRawResponse,
+ AsyncSearchResourceWithRawResponse,
+ SearchResourceWithStreamingResponse,
+ AsyncSearchResourceWithStreamingResponse,
+)
+from .contents import (
+ ContentsResource,
+ AsyncContentsResource,
+ ContentsResourceWithRawResponse,
+ AsyncContentsResourceWithRawResponse,
+ ContentsResourceWithStreamingResponse,
+ AsyncContentsResourceWithStreamingResponse,
+)
+from .providers import (
+ ProvidersResource,
+ AsyncProvidersResource,
+ ProvidersResourceWithRawResponse,
+ AsyncProvidersResourceWithRawResponse,
+ ProvidersResourceWithStreamingResponse,
+ AsyncProvidersResourceWithStreamingResponse,
+)
+
+__all__ = [
+ "ContentsResource",
+ "AsyncContentsResource",
+ "ContentsResourceWithRawResponse",
+ "AsyncContentsResourceWithRawResponse",
+ "ContentsResourceWithStreamingResponse",
+ "AsyncContentsResourceWithStreamingResponse",
+ "ProvidersResource",
+ "AsyncProvidersResource",
+ "ProvidersResourceWithRawResponse",
+ "AsyncProvidersResourceWithRawResponse",
+ "ProvidersResourceWithStreamingResponse",
+ "AsyncProvidersResourceWithStreamingResponse",
+ "SearchResource",
+ "AsyncSearchResource",
+ "SearchResourceWithRawResponse",
+ "AsyncSearchResourceWithRawResponse",
+ "SearchResourceWithStreamingResponse",
+ "AsyncSearchResourceWithStreamingResponse",
+]
diff --git a/src/kernel/resources/search/contents.py b/src/kernel/resources/search/contents.py
new file mode 100644
index 00000000..f41983c0
--- /dev/null
+++ b/src/kernel/resources/search/contents.py
@@ -0,0 +1,220 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from __future__ import annotations
+
+import httpx
+
+from ..._types import Body, Omit, Query, Headers, NoneType, NotGiven, SequenceNotStr, omit, not_given
+from ..._utils import path_template, maybe_transform, async_maybe_transform
+from ..._compat import cached_property
+from ..._resource import SyncAPIResource, AsyncAPIResource
+from ..._response import (
+ to_raw_response_wrapper,
+ to_streamed_response_wrapper,
+ async_to_raw_response_wrapper,
+ async_to_streamed_response_wrapper,
+)
+from ..._base_client import make_request_options
+from ...types.search import content_fetch_params
+
+__all__ = ["ContentsResource", "AsyncContentsResource"]
+
+
+class ContentsResource(SyncAPIResource):
+ """Search the web and retrieve content for selected results."""
+
+ @cached_property
+ def with_raw_response(self) -> ContentsResourceWithRawResponse:
+ """
+ This property can be used as a prefix for any HTTP method call to return
+ the raw response object instead of the parsed content.
+
+ For more information, see https://www.github.com/kernel/kernel-python-sdk#accessing-raw-response-data-eg-headers
+ """
+ return ContentsResourceWithRawResponse(self)
+
+ @cached_property
+ def with_streaming_response(self) -> ContentsResourceWithStreamingResponse:
+ """
+ An alternative to `.with_raw_response` that doesn't eagerly read the response body.
+
+ For more information, see https://www.github.com/kernel/kernel-python-sdk#with_streaming_response
+ """
+ return ContentsResourceWithStreamingResponse(self)
+
+ def fetch(
+ self,
+ id: str,
+ *,
+ content: content_fetch_params.Content | Omit = omit,
+ limit: int | Omit = omit,
+ result_ids: SequenceNotStr[str] | Omit = omit,
+ timeout_ms: int | Omit = omit,
+ # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
+ # The extra values given here take precedence over values defined on the client or passed to this method.
+ extra_headers: Headers | None = None,
+ extra_query: Query | None = None,
+ extra_body: Body | None = None,
+ timeout: float | httpx.Timeout | None | NotGiven = not_given,
+ ) -> None:
+ """
+ Deferred result-content retrieval is reserved but not available in this release.
+ Requests return 404 until the retrieval implementation is shipped. X-Request-Id
+ identifies this request separately from the search resource.
+
+ Args:
+ content: Defaults to source:auto when omitted.
+
+ limit: Maximum number of search results to fetch when result_ids is omitted, starting
+ from rank 1. Mutually exclusive with result_ids.
+
+ result_ids: Kernel-generated IDs from the referenced retained search, in desired response
+ order. They are not provider-standard IDs. Mutually exclusive with limit.
+
+ timeout_ms: Overall deadline across all selected results.
+
+ extra_headers: Send extra headers
+
+ extra_query: Add additional query parameters to the request
+
+ extra_body: Add additional JSON properties to the request
+
+ timeout: Override the client-level default timeout for this request, in seconds
+ """
+ if not id:
+ raise ValueError(f"Expected a non-empty value for `id` but received {id!r}")
+ extra_headers = {"Accept": "*/*", **(extra_headers or {})}
+ return self._post(
+ path_template("/search/{id}/contents", id=id),
+ body=maybe_transform(
+ {
+ "content": content,
+ "limit": limit,
+ "result_ids": result_ids,
+ "timeout_ms": timeout_ms,
+ },
+ content_fetch_params.ContentFetchParams,
+ ),
+ options=make_request_options(
+ extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
+ ),
+ cast_to=NoneType,
+ )
+
+
+class AsyncContentsResource(AsyncAPIResource):
+ """Search the web and retrieve content for selected results."""
+
+ @cached_property
+ def with_raw_response(self) -> AsyncContentsResourceWithRawResponse:
+ """
+ This property can be used as a prefix for any HTTP method call to return
+ the raw response object instead of the parsed content.
+
+ For more information, see https://www.github.com/kernel/kernel-python-sdk#accessing-raw-response-data-eg-headers
+ """
+ return AsyncContentsResourceWithRawResponse(self)
+
+ @cached_property
+ def with_streaming_response(self) -> AsyncContentsResourceWithStreamingResponse:
+ """
+ An alternative to `.with_raw_response` that doesn't eagerly read the response body.
+
+ For more information, see https://www.github.com/kernel/kernel-python-sdk#with_streaming_response
+ """
+ return AsyncContentsResourceWithStreamingResponse(self)
+
+ async def fetch(
+ self,
+ id: str,
+ *,
+ content: content_fetch_params.Content | Omit = omit,
+ limit: int | Omit = omit,
+ result_ids: SequenceNotStr[str] | Omit = omit,
+ timeout_ms: int | Omit = omit,
+ # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
+ # The extra values given here take precedence over values defined on the client or passed to this method.
+ extra_headers: Headers | None = None,
+ extra_query: Query | None = None,
+ extra_body: Body | None = None,
+ timeout: float | httpx.Timeout | None | NotGiven = not_given,
+ ) -> None:
+ """
+ Deferred result-content retrieval is reserved but not available in this release.
+ Requests return 404 until the retrieval implementation is shipped. X-Request-Id
+ identifies this request separately from the search resource.
+
+ Args:
+ content: Defaults to source:auto when omitted.
+
+ limit: Maximum number of search results to fetch when result_ids is omitted, starting
+ from rank 1. Mutually exclusive with result_ids.
+
+ result_ids: Kernel-generated IDs from the referenced retained search, in desired response
+ order. They are not provider-standard IDs. Mutually exclusive with limit.
+
+ timeout_ms: Overall deadline across all selected results.
+
+ extra_headers: Send extra headers
+
+ extra_query: Add additional query parameters to the request
+
+ extra_body: Add additional JSON properties to the request
+
+ timeout: Override the client-level default timeout for this request, in seconds
+ """
+ if not id:
+ raise ValueError(f"Expected a non-empty value for `id` but received {id!r}")
+ extra_headers = {"Accept": "*/*", **(extra_headers or {})}
+ return await self._post(
+ path_template("/search/{id}/contents", id=id),
+ body=await async_maybe_transform(
+ {
+ "content": content,
+ "limit": limit,
+ "result_ids": result_ids,
+ "timeout_ms": timeout_ms,
+ },
+ content_fetch_params.ContentFetchParams,
+ ),
+ options=make_request_options(
+ extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
+ ),
+ cast_to=NoneType,
+ )
+
+
+class ContentsResourceWithRawResponse:
+ def __init__(self, contents: ContentsResource) -> None:
+ self._contents = contents
+
+ self.fetch = to_raw_response_wrapper(
+ contents.fetch,
+ )
+
+
+class AsyncContentsResourceWithRawResponse:
+ def __init__(self, contents: AsyncContentsResource) -> None:
+ self._contents = contents
+
+ self.fetch = async_to_raw_response_wrapper(
+ contents.fetch,
+ )
+
+
+class ContentsResourceWithStreamingResponse:
+ def __init__(self, contents: ContentsResource) -> None:
+ self._contents = contents
+
+ self.fetch = to_streamed_response_wrapper(
+ contents.fetch,
+ )
+
+
+class AsyncContentsResourceWithStreamingResponse:
+ def __init__(self, contents: AsyncContentsResource) -> None:
+ self._contents = contents
+
+ self.fetch = async_to_streamed_response_wrapper(
+ contents.fetch,
+ )
diff --git a/src/kernel/resources/search/providers.py b/src/kernel/resources/search/providers.py
new file mode 100644
index 00000000..f913f1cd
--- /dev/null
+++ b/src/kernel/resources/search/providers.py
@@ -0,0 +1,187 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from __future__ import annotations
+
+from typing_extensions import Literal
+
+import httpx
+
+from ..._types import Body, Omit, Query, Headers, NotGiven, omit, not_given
+from ..._utils import maybe_transform, async_maybe_transform
+from ..._compat import cached_property
+from ..._resource import SyncAPIResource, AsyncAPIResource
+from ..._response import (
+ to_raw_response_wrapper,
+ to_streamed_response_wrapper,
+ async_to_raw_response_wrapper,
+ async_to_streamed_response_wrapper,
+)
+from ..._base_client import make_request_options
+from ...types.search import provider_list_params
+from ...types.search.provider_list_response import ProviderListResponse
+
+__all__ = ["ProvidersResource", "AsyncProvidersResource"]
+
+
+class ProvidersResource(SyncAPIResource):
+ """Search the web and retrieve content for selected results."""
+
+ @cached_property
+ def with_raw_response(self) -> ProvidersResourceWithRawResponse:
+ """
+ This property can be used as a prefix for any HTTP method call to return
+ the raw response object instead of the parsed content.
+
+ For more information, see https://www.github.com/kernel/kernel-python-sdk#accessing-raw-response-data-eg-headers
+ """
+ return ProvidersResourceWithRawResponse(self)
+
+ @cached_property
+ def with_streaming_response(self) -> ProvidersResourceWithStreamingResponse:
+ """
+ An alternative to `.with_raw_response` that doesn't eagerly read the response body.
+
+ For more information, see https://www.github.com/kernel/kernel-python-sdk#with_streaming_response
+ """
+ return ProvidersResourceWithStreamingResponse(self)
+
+ def list(
+ self,
+ *,
+ slug: Literal["brave", "exa", "perplexity", "context", "parallel", "valyu", "octen", "you", "tavily", "serpapi"]
+ | 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,
+ ) -> ProviderListResponse:
+ """Lists providers, capabilities, and machine-readable native-option schemas.
+
+ Auto
+ and fallback are strategies, not provider entries. The list is not paginated and
+ contains no latency benchmarks. X-Request-Id identifies the request.
+
+ Args:
+ slug: Optional concrete provider slug filter. Omit to list every provider.
+
+ 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
+ """
+ return self._get(
+ "/search/providers",
+ options=make_request_options(
+ extra_headers=extra_headers,
+ extra_query=extra_query,
+ extra_body=extra_body,
+ timeout=timeout,
+ query=maybe_transform({"slug": slug}, provider_list_params.ProviderListParams),
+ ),
+ cast_to=ProviderListResponse,
+ )
+
+
+class AsyncProvidersResource(AsyncAPIResource):
+ """Search the web and retrieve content for selected results."""
+
+ @cached_property
+ def with_raw_response(self) -> AsyncProvidersResourceWithRawResponse:
+ """
+ This property can be used as a prefix for any HTTP method call to return
+ the raw response object instead of the parsed content.
+
+ For more information, see https://www.github.com/kernel/kernel-python-sdk#accessing-raw-response-data-eg-headers
+ """
+ return AsyncProvidersResourceWithRawResponse(self)
+
+ @cached_property
+ def with_streaming_response(self) -> AsyncProvidersResourceWithStreamingResponse:
+ """
+ An alternative to `.with_raw_response` that doesn't eagerly read the response body.
+
+ For more information, see https://www.github.com/kernel/kernel-python-sdk#with_streaming_response
+ """
+ return AsyncProvidersResourceWithStreamingResponse(self)
+
+ async def list(
+ self,
+ *,
+ slug: Literal["brave", "exa", "perplexity", "context", "parallel", "valyu", "octen", "you", "tavily", "serpapi"]
+ | 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,
+ ) -> ProviderListResponse:
+ """Lists providers, capabilities, and machine-readable native-option schemas.
+
+ Auto
+ and fallback are strategies, not provider entries. The list is not paginated and
+ contains no latency benchmarks. X-Request-Id identifies the request.
+
+ Args:
+ slug: Optional concrete provider slug filter. Omit to list every provider.
+
+ 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
+ """
+ return await self._get(
+ "/search/providers",
+ options=make_request_options(
+ extra_headers=extra_headers,
+ extra_query=extra_query,
+ extra_body=extra_body,
+ timeout=timeout,
+ query=await async_maybe_transform({"slug": slug}, provider_list_params.ProviderListParams),
+ ),
+ cast_to=ProviderListResponse,
+ )
+
+
+class ProvidersResourceWithRawResponse:
+ def __init__(self, providers: ProvidersResource) -> None:
+ self._providers = providers
+
+ self.list = to_raw_response_wrapper(
+ providers.list,
+ )
+
+
+class AsyncProvidersResourceWithRawResponse:
+ def __init__(self, providers: AsyncProvidersResource) -> None:
+ self._providers = providers
+
+ self.list = async_to_raw_response_wrapper(
+ providers.list,
+ )
+
+
+class ProvidersResourceWithStreamingResponse:
+ def __init__(self, providers: ProvidersResource) -> None:
+ self._providers = providers
+
+ self.list = to_streamed_response_wrapper(
+ providers.list,
+ )
+
+
+class AsyncProvidersResourceWithStreamingResponse:
+ def __init__(self, providers: AsyncProvidersResource) -> None:
+ self._providers = providers
+
+ self.list = async_to_streamed_response_wrapper(
+ providers.list,
+ )
diff --git a/src/kernel/resources/search/search.py b/src/kernel/resources/search/search.py
new file mode 100644
index 00000000..850363c6
--- /dev/null
+++ b/src/kernel/resources/search/search.py
@@ -0,0 +1,536 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from __future__ import annotations
+
+from typing import Union
+from datetime import date
+from typing_extensions import Literal
+
+import httpx
+
+from ...types import search_create_params
+from ..._types import Body, Omit, Query, Headers, NotGiven, SequenceNotStr, omit, not_given
+from ..._utils import path_template, maybe_transform, async_maybe_transform
+from .contents import (
+ ContentsResource,
+ AsyncContentsResource,
+ ContentsResourceWithRawResponse,
+ AsyncContentsResourceWithRawResponse,
+ ContentsResourceWithStreamingResponse,
+ AsyncContentsResourceWithStreamingResponse,
+)
+from ..._compat import cached_property
+from .providers import (
+ ProvidersResource,
+ AsyncProvidersResource,
+ ProvidersResourceWithRawResponse,
+ AsyncProvidersResourceWithRawResponse,
+ ProvidersResourceWithStreamingResponse,
+ AsyncProvidersResourceWithStreamingResponse,
+)
+from ..._resource import SyncAPIResource, AsyncAPIResource
+from ..._response import (
+ to_raw_response_wrapper,
+ to_streamed_response_wrapper,
+ async_to_raw_response_wrapper,
+ async_to_streamed_response_wrapper,
+)
+from ..._base_client import make_request_options
+from ...types.search.search import Search
+from ...types.strategy_param import StrategyParam
+
+__all__ = ["SearchResource", "AsyncSearchResource"]
+
+
+class SearchResource(SyncAPIResource):
+ """Search the web and retrieve content for selected results."""
+
+ @cached_property
+ def contents(self) -> ContentsResource:
+ """Search the web and retrieve content for selected results."""
+ return ContentsResource(self._client)
+
+ @cached_property
+ def providers(self) -> ProvidersResource:
+ """Search the web and retrieve content for selected results."""
+ return ProvidersResource(self._client)
+
+ @cached_property
+ def with_raw_response(self) -> SearchResourceWithRawResponse:
+ """
+ This property can be used as a prefix for any HTTP method call to return
+ the raw response object instead of the parsed content.
+
+ For more information, see https://www.github.com/kernel/kernel-python-sdk#accessing-raw-response-data-eg-headers
+ """
+ return SearchResourceWithRawResponse(self)
+
+ @cached_property
+ def with_streaming_response(self) -> SearchResourceWithStreamingResponse:
+ """
+ An alternative to `.with_raw_response` that doesn't eagerly read the response body.
+
+ For more information, see https://www.github.com/kernel/kernel-python-sdk#with_streaming_response
+ """
+ return SearchResourceWithStreamingResponse(self)
+
+ def create(
+ self,
+ *,
+ query: str,
+ content: search_create_params.Content | Omit = omit,
+ country: str | Omit = omit,
+ end_date: Union[str, date] | Omit = omit,
+ exclude_domains: SequenceNotStr[str] | Omit = omit,
+ include_domains: SequenceNotStr[str] | Omit = omit,
+ include_raw: bool | Omit = omit,
+ language: str | Omit = omit,
+ max_results: int | Omit = omit,
+ recency: Literal["hour", "day", "week", "month", "year"] | Omit = omit,
+ safe_search: Literal["off", "moderate", "strict"] | Omit = omit,
+ start_date: Union[str, date] | Omit = omit,
+ strategy: StrategyParam | Omit = omit,
+ strict_params: bool | Omit = omit,
+ timeout_ms: int | Omit = omit,
+ # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
+ # The extra values given here take precedence over values defined on the client or passed to this method.
+ extra_headers: Headers | None = None,
+ extra_query: Query | None = None,
+ extra_body: Body | None = None,
+ timeout: float | httpx.Timeout | None | NotGiven = not_given,
+ ) -> Search:
+ """Returns ranked results from one serving provider.
+
+ The default strategy selects a
+ provider that supports the requested options. The fallback strategy tries
+ providers in the supplied order. Results are not blended across providers.
+ Portable filters may be approximated or omitted according to provider
+ capabilities; warnings describe those outcomes unless strict_params is true.
+ Native options apply only to their selected provider.
+
+ Args:
+ query: Primary search query. A provider-native multi-query option applies only to that
+ provider; other providers in a fallback chain receive this query.
+
+ content: Optional portable content retrieval. Pass true for defaults or an options
+ object. Omission never starts Kernel browser work; provider-supplied content is
+ still returned when available, including when requested through native options.
+ Both inline and deferred retrieval use the same options schema.
+
+ country: ISO 3166-1 alpha-2 search locale preference.
+
+ end_date: Inclusive publication-date upper bound; must not precede start_date. If recency
+ is also supplied, recency takes precedence with a warning. Unsupported or
+ approximated filtering is reported, or rejected under strict_params.
+
+ exclude_domains: Hostname exclusions, with the same best-effort/strict behavior as
+ include_domains. Provider-specific combinations that cannot be represented are
+ reported via warnings or rejected in strict mode.
+
+ include_domains: Hostname inclusion preference, matching a hostname and its subdomains. Empty
+ means unrestricted. Translated, emulated, or dropped with a warning according to
+ provider capability unless strict_params is true. Native boost modes remain
+ advisory and are identified in warnings.
+
+ include_raw: Include untouched per-result payloads and the full serving-provider response in
+ raw fields. Off by default; native top-level outputs such as answer remain
+ available without it.
+
+ language: BCP 47 search language preference.
+
+ max_results: Requested result count from 1 through 100. The effective count is clamped to the
+ serving provider's cap with a warning. Effective native counts are the lower of
+ this limit and supplied provider-native count aliases. Strict mode rejects
+ unsupported counts.
+
+ recency: Relative search window. Takes precedence over start_date/end_date with a warning
+ if both are set. Provider-native recency behavior is retained, including
+ documented hour-to-day widening. Unsupported filters are rejected only in strict
+ mode.
+
+ safe_search: Optional safety preference. Omit to use provider defaults. Unsupported values
+ are dropped with a warning unless strict_params is true. A search filter is not
+ an authorization boundary.
+
+ start_date: Inclusive publication-date lower bound. If recency is also supplied, recency
+ takes precedence with a warning. Provider date semantics, precision, and
+ unsupported filters are reported; unknown source dates are not fabricated or
+ universally post-filtered.
+
+ strategy: Omitted strategy defaults to auto.
+
+ strict_params: When false, unsupported portable parameters are omitted and approximations are
+ described in warnings. When true, every supplied portable parameter must be
+ honored exactly. Requests that cannot be served with those parameters are
+ rejected. This does not guarantee identical rankings or document timestamps
+ across indexes. Authentication and project isolation are always enforced.
+
+ timeout_ms: Overall deadline across search attempts and inline retrieval. No new attempt
+ starts after the deadline. Completed search results survive inline retrieval
+ timeouts.
+
+ 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
+ """
+ return self._post(
+ "/search",
+ body=maybe_transform(
+ {
+ "query": query,
+ "content": content,
+ "country": country,
+ "end_date": end_date,
+ "exclude_domains": exclude_domains,
+ "include_domains": include_domains,
+ "include_raw": include_raw,
+ "language": language,
+ "max_results": max_results,
+ "recency": recency,
+ "safe_search": safe_search,
+ "start_date": start_date,
+ "strategy": strategy,
+ "strict_params": strict_params,
+ "timeout_ms": timeout_ms,
+ },
+ search_create_params.SearchCreateParams,
+ ),
+ options=make_request_options(
+ extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
+ ),
+ cast_to=Search,
+ )
+
+ def retrieve(
+ self,
+ id: str,
+ *,
+ # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
+ # The extra values given here take precedence over values defined on the client or passed to this method.
+ extra_headers: Headers | None = None,
+ extra_query: Query | None = None,
+ extra_body: Body | None = None,
+ timeout: float | httpx.Timeout | None | NotGiven = not_given,
+ ) -> Search:
+ """
+ Returns the retained search resource exactly as it was returned by POST /search:
+ results, attempts, warnings, usage, and expires_at. No provider is called and
+ nothing is billed. Use it to look up a search by ID for debugging, cost review,
+ or to recover result IDs before calling the contents endpoint. Inline content
+ fetched at search time is included; content fetched later through the contents
+ endpoint is not merged in. Missing, expired, or inaccessible searches
+ return 404.
+
+ Args:
+ extra_headers: Send extra headers
+
+ extra_query: Add additional query parameters to the request
+
+ extra_body: Add additional JSON properties to the request
+
+ timeout: Override the client-level default timeout for this request, in seconds
+ """
+ if not id:
+ raise ValueError(f"Expected a non-empty value for `id` but received {id!r}")
+ return self._get(
+ path_template("/search/{id}", id=id),
+ options=make_request_options(
+ extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
+ ),
+ cast_to=Search,
+ )
+
+
+class AsyncSearchResource(AsyncAPIResource):
+ """Search the web and retrieve content for selected results."""
+
+ @cached_property
+ def contents(self) -> AsyncContentsResource:
+ """Search the web and retrieve content for selected results."""
+ return AsyncContentsResource(self._client)
+
+ @cached_property
+ def providers(self) -> AsyncProvidersResource:
+ """Search the web and retrieve content for selected results."""
+ return AsyncProvidersResource(self._client)
+
+ @cached_property
+ def with_raw_response(self) -> AsyncSearchResourceWithRawResponse:
+ """
+ This property can be used as a prefix for any HTTP method call to return
+ the raw response object instead of the parsed content.
+
+ For more information, see https://www.github.com/kernel/kernel-python-sdk#accessing-raw-response-data-eg-headers
+ """
+ return AsyncSearchResourceWithRawResponse(self)
+
+ @cached_property
+ def with_streaming_response(self) -> AsyncSearchResourceWithStreamingResponse:
+ """
+ An alternative to `.with_raw_response` that doesn't eagerly read the response body.
+
+ For more information, see https://www.github.com/kernel/kernel-python-sdk#with_streaming_response
+ """
+ return AsyncSearchResourceWithStreamingResponse(self)
+
+ async def create(
+ self,
+ *,
+ query: str,
+ content: search_create_params.Content | Omit = omit,
+ country: str | Omit = omit,
+ end_date: Union[str, date] | Omit = omit,
+ exclude_domains: SequenceNotStr[str] | Omit = omit,
+ include_domains: SequenceNotStr[str] | Omit = omit,
+ include_raw: bool | Omit = omit,
+ language: str | Omit = omit,
+ max_results: int | Omit = omit,
+ recency: Literal["hour", "day", "week", "month", "year"] | Omit = omit,
+ safe_search: Literal["off", "moderate", "strict"] | Omit = omit,
+ start_date: Union[str, date] | Omit = omit,
+ strategy: StrategyParam | Omit = omit,
+ strict_params: bool | Omit = omit,
+ timeout_ms: int | Omit = omit,
+ # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
+ # The extra values given here take precedence over values defined on the client or passed to this method.
+ extra_headers: Headers | None = None,
+ extra_query: Query | None = None,
+ extra_body: Body | None = None,
+ timeout: float | httpx.Timeout | None | NotGiven = not_given,
+ ) -> Search:
+ """Returns ranked results from one serving provider.
+
+ The default strategy selects a
+ provider that supports the requested options. The fallback strategy tries
+ providers in the supplied order. Results are not blended across providers.
+ Portable filters may be approximated or omitted according to provider
+ capabilities; warnings describe those outcomes unless strict_params is true.
+ Native options apply only to their selected provider.
+
+ Args:
+ query: Primary search query. A provider-native multi-query option applies only to that
+ provider; other providers in a fallback chain receive this query.
+
+ content: Optional portable content retrieval. Pass true for defaults or an options
+ object. Omission never starts Kernel browser work; provider-supplied content is
+ still returned when available, including when requested through native options.
+ Both inline and deferred retrieval use the same options schema.
+
+ country: ISO 3166-1 alpha-2 search locale preference.
+
+ end_date: Inclusive publication-date upper bound; must not precede start_date. If recency
+ is also supplied, recency takes precedence with a warning. Unsupported or
+ approximated filtering is reported, or rejected under strict_params.
+
+ exclude_domains: Hostname exclusions, with the same best-effort/strict behavior as
+ include_domains. Provider-specific combinations that cannot be represented are
+ reported via warnings or rejected in strict mode.
+
+ include_domains: Hostname inclusion preference, matching a hostname and its subdomains. Empty
+ means unrestricted. Translated, emulated, or dropped with a warning according to
+ provider capability unless strict_params is true. Native boost modes remain
+ advisory and are identified in warnings.
+
+ include_raw: Include untouched per-result payloads and the full serving-provider response in
+ raw fields. Off by default; native top-level outputs such as answer remain
+ available without it.
+
+ language: BCP 47 search language preference.
+
+ max_results: Requested result count from 1 through 100. The effective count is clamped to the
+ serving provider's cap with a warning. Effective native counts are the lower of
+ this limit and supplied provider-native count aliases. Strict mode rejects
+ unsupported counts.
+
+ recency: Relative search window. Takes precedence over start_date/end_date with a warning
+ if both are set. Provider-native recency behavior is retained, including
+ documented hour-to-day widening. Unsupported filters are rejected only in strict
+ mode.
+
+ safe_search: Optional safety preference. Omit to use provider defaults. Unsupported values
+ are dropped with a warning unless strict_params is true. A search filter is not
+ an authorization boundary.
+
+ start_date: Inclusive publication-date lower bound. If recency is also supplied, recency
+ takes precedence with a warning. Provider date semantics, precision, and
+ unsupported filters are reported; unknown source dates are not fabricated or
+ universally post-filtered.
+
+ strategy: Omitted strategy defaults to auto.
+
+ strict_params: When false, unsupported portable parameters are omitted and approximations are
+ described in warnings. When true, every supplied portable parameter must be
+ honored exactly. Requests that cannot be served with those parameters are
+ rejected. This does not guarantee identical rankings or document timestamps
+ across indexes. Authentication and project isolation are always enforced.
+
+ timeout_ms: Overall deadline across search attempts and inline retrieval. No new attempt
+ starts after the deadline. Completed search results survive inline retrieval
+ timeouts.
+
+ 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
+ """
+ return await self._post(
+ "/search",
+ body=await async_maybe_transform(
+ {
+ "query": query,
+ "content": content,
+ "country": country,
+ "end_date": end_date,
+ "exclude_domains": exclude_domains,
+ "include_domains": include_domains,
+ "include_raw": include_raw,
+ "language": language,
+ "max_results": max_results,
+ "recency": recency,
+ "safe_search": safe_search,
+ "start_date": start_date,
+ "strategy": strategy,
+ "strict_params": strict_params,
+ "timeout_ms": timeout_ms,
+ },
+ search_create_params.SearchCreateParams,
+ ),
+ options=make_request_options(
+ extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
+ ),
+ cast_to=Search,
+ )
+
+ async def retrieve(
+ self,
+ id: str,
+ *,
+ # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
+ # The extra values given here take precedence over values defined on the client or passed to this method.
+ extra_headers: Headers | None = None,
+ extra_query: Query | None = None,
+ extra_body: Body | None = None,
+ timeout: float | httpx.Timeout | None | NotGiven = not_given,
+ ) -> Search:
+ """
+ Returns the retained search resource exactly as it was returned by POST /search:
+ results, attempts, warnings, usage, and expires_at. No provider is called and
+ nothing is billed. Use it to look up a search by ID for debugging, cost review,
+ or to recover result IDs before calling the contents endpoint. Inline content
+ fetched at search time is included; content fetched later through the contents
+ endpoint is not merged in. Missing, expired, or inaccessible searches
+ return 404.
+
+ Args:
+ extra_headers: Send extra headers
+
+ extra_query: Add additional query parameters to the request
+
+ extra_body: Add additional JSON properties to the request
+
+ timeout: Override the client-level default timeout for this request, in seconds
+ """
+ if not id:
+ raise ValueError(f"Expected a non-empty value for `id` but received {id!r}")
+ return await self._get(
+ path_template("/search/{id}", id=id),
+ options=make_request_options(
+ extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
+ ),
+ cast_to=Search,
+ )
+
+
+class SearchResourceWithRawResponse:
+ def __init__(self, search: SearchResource) -> None:
+ self._search = search
+
+ self.create = to_raw_response_wrapper(
+ search.create,
+ )
+ self.retrieve = to_raw_response_wrapper(
+ search.retrieve,
+ )
+
+ @cached_property
+ def contents(self) -> ContentsResourceWithRawResponse:
+ """Search the web and retrieve content for selected results."""
+ return ContentsResourceWithRawResponse(self._search.contents)
+
+ @cached_property
+ def providers(self) -> ProvidersResourceWithRawResponse:
+ """Search the web and retrieve content for selected results."""
+ return ProvidersResourceWithRawResponse(self._search.providers)
+
+
+class AsyncSearchResourceWithRawResponse:
+ def __init__(self, search: AsyncSearchResource) -> None:
+ self._search = search
+
+ self.create = async_to_raw_response_wrapper(
+ search.create,
+ )
+ self.retrieve = async_to_raw_response_wrapper(
+ search.retrieve,
+ )
+
+ @cached_property
+ def contents(self) -> AsyncContentsResourceWithRawResponse:
+ """Search the web and retrieve content for selected results."""
+ return AsyncContentsResourceWithRawResponse(self._search.contents)
+
+ @cached_property
+ def providers(self) -> AsyncProvidersResourceWithRawResponse:
+ """Search the web and retrieve content for selected results."""
+ return AsyncProvidersResourceWithRawResponse(self._search.providers)
+
+
+class SearchResourceWithStreamingResponse:
+ def __init__(self, search: SearchResource) -> None:
+ self._search = search
+
+ self.create = to_streamed_response_wrapper(
+ search.create,
+ )
+ self.retrieve = to_streamed_response_wrapper(
+ search.retrieve,
+ )
+
+ @cached_property
+ def contents(self) -> ContentsResourceWithStreamingResponse:
+ """Search the web and retrieve content for selected results."""
+ return ContentsResourceWithStreamingResponse(self._search.contents)
+
+ @cached_property
+ def providers(self) -> ProvidersResourceWithStreamingResponse:
+ """Search the web and retrieve content for selected results."""
+ return ProvidersResourceWithStreamingResponse(self._search.providers)
+
+
+class AsyncSearchResourceWithStreamingResponse:
+ def __init__(self, search: AsyncSearchResource) -> None:
+ self._search = search
+
+ self.create = async_to_streamed_response_wrapper(
+ search.create,
+ )
+ self.retrieve = async_to_streamed_response_wrapper(
+ search.retrieve,
+ )
+
+ @cached_property
+ def contents(self) -> AsyncContentsResourceWithStreamingResponse:
+ """Search the web and retrieve content for selected results."""
+ return AsyncContentsResourceWithStreamingResponse(self._search.contents)
+
+ @cached_property
+ def providers(self) -> AsyncProvidersResourceWithStreamingResponse:
+ """Search the web and retrieve content for selected results."""
+ return AsyncProvidersResourceWithStreamingResponse(self._search.providers)
diff --git a/src/kernel/types/__init__.py b/src/kernel/types/__init__.py
index 0d25350f..968e4021 100644
--- a/src/kernel/types/__init__.py
+++ b/src/kernel/types/__init__.py
@@ -6,7 +6,10 @@
from .. import _compat
from .tags import Tags as Tags
from .proxy import Proxy as Proxy
+from .usage import Usage as Usage
from .vault import Vault as Vault
+from .result import Result as Result
+from .search import Search as Search
from .shared import (
LogEvent as LogEvent,
AppAction as AppAction,
@@ -20,9 +23,11 @@
)
from .target import Target as Target
from .api_key import APIKey as APIKey
+from .attempt import Attempt as Attempt
from .browser import Browser as Browser
from .profile import Profile as Profile
from .project import Project as Project
+from .warning import Warning as Warning
from .analysis import Analysis as Analysis
from .evidence import Evidence as Evidence
from .credential import Credential as Credential
@@ -32,6 +37,7 @@
from .browser_usage import BrowserUsage as BrowserUsage
from .browser_memory import BrowserMemory as BrowserMemory
from .recommendation import Recommendation as Recommendation
+from .strategy_param import StrategyParam as StrategyParam
from .app_list_params import AppListParams as AppListParams
from .audit_log_entry import AuditLogEntry as AuditLogEntry
from .created_api_key import CreatedAPIKey as CreatedAPIKey
@@ -60,6 +66,7 @@
from .browser_proxy_config import BrowserProxyConfig as BrowserProxyConfig
from .browser_repl_content import BrowserReplContent as BrowserReplContent
from .proxy_check_response import ProxyCheckResponse as ProxyCheckResponse
+from .search_create_params import SearchCreateParams as SearchCreateParams
from .api_key_create_params import APIKeyCreateParams as APIKeyCreateParams
from .api_key_rotate_params import APIKeyRotateParams as APIKeyRotateParams
from .api_key_update_params import APIKeyUpdateParams as APIKeyUpdateParams
@@ -73,6 +80,7 @@
from .profile_update_params import ProfileUpdateParams as ProfileUpdateParams
from .project_create_params import ProjectCreateParams as ProjectCreateParams
from .project_update_params import ProjectUpdateParams as ProjectUpdateParams
+from .provider_target_param import ProviderTargetParam as ProviderTargetParam
from .proxy_create_response import ProxyCreateResponse as ProxyCreateResponse
from .proxy_update_response import ProxyUpdateResponse as ProxyUpdateResponse
from .recommendation_result import RecommendationResult as RecommendationResult
diff --git a/src/kernel/types/attempt.py b/src/kernel/types/attempt.py
new file mode 100644
index 00000000..70863016
--- /dev/null
+++ b/src/kernel/types/attempt.py
@@ -0,0 +1,20 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from typing import Optional
+from typing_extensions import Literal
+
+from .._models import BaseModel
+
+__all__ = ["Attempt"]
+
+
+class Attempt(BaseModel):
+ duration_ms: int
+
+ outcome: Literal["success", "empty", "error", "timeout"]
+
+ provider: str
+
+ error_code: Optional[str] = None
+
+ retryable: Optional[bool] = None
diff --git a/src/kernel/types/provider_target_param.py b/src/kernel/types/provider_target_param.py
new file mode 100644
index 00000000..7c3c314f
--- /dev/null
+++ b/src/kernel/types/provider_target_param.py
@@ -0,0 +1,636 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from __future__ import annotations
+
+from typing import List, Union, Optional
+from datetime import date, datetime
+from typing_extensions import Literal, Required, Annotated, TypeAlias, TypedDict
+
+from .._types import SequenceNotStr
+from .._utils import PropertyInfo
+
+__all__ = [
+ "ProviderTargetParam",
+ "SearchBraveTarget",
+ "SearchBraveTargetOptions",
+ "SearchExaTarget",
+ "SearchExaTargetOptions",
+ "SearchExaTargetOptionsContents",
+ "SearchPerplexityTarget",
+ "SearchPerplexityTargetOptions",
+ "SearchContextTarget",
+ "SearchContextTargetOptions",
+ "SearchContextTargetOptionsMarkdownOptions",
+ "SearchContextTargetOptionsMarkdownOptionsPdf",
+ "SearchParallelTarget",
+ "SearchParallelTargetOptions",
+ "SearchParallelTargetOptionsAdvancedSettings",
+ "SearchParallelTargetOptionsAdvancedSettingsExcerptSettings",
+ "SearchParallelTargetOptionsAdvancedSettingsFetchPolicy",
+ "SearchParallelTargetOptionsAdvancedSettingsSourcePolicy",
+ "SearchValyuTarget",
+ "SearchValyuTargetOptions",
+ "SearchOctenTarget",
+ "SearchOctenTargetOptions",
+ "SearchOctenTargetOptionsFullContent",
+ "SearchOctenTargetOptionsHighlight",
+ "SearchYouTarget",
+ "SearchYouTargetOptions",
+ "SearchYouTargetOptionsExtraction",
+ "SearchYouTargetOptionsExtractionFullPage",
+ "SearchTavilyTarget",
+ "SearchTavilyTargetOptions",
+ "SearchSerpAPITarget",
+ "SearchSerpAPITargetOptions",
+]
+
+
+class SearchBraveTargetOptions(TypedDict, total=False):
+ count: int
+ """Provider-native count.
+
+ Lower-only alias for max_results; cannot raise the effective result cap.
+ """
+
+ extra_snippets: bool
+ """Request additional snippets from Brave."""
+
+ goggles: str
+ """Goggles re-ranking definition URL."""
+
+ goggles_id: str
+ """Deprecated Brave Goggle identifier. Prefer goggles."""
+
+ include_fetch_metadata: bool
+ """Include Brave's fetch metadata."""
+
+ offset: int
+ """Page offset supported by Brave."""
+
+ operators: str
+ """Brave search operators."""
+
+ result_filter: str
+ """Comma-separated result types to include, e.g. "web,news"."""
+
+ search_lang: str
+ """Language of the search, e.g. "en"."""
+
+ spellcheck: bool
+ """Apply Brave's query spellcheck."""
+
+ ui_lang: str
+ """Language for UI strings in the response."""
+
+ units: Literal["metric", "imperial"]
+ """Measurement units."""
+
+
+class SearchBraveTarget(TypedDict, total=False):
+ provider: Required[Literal["brave"]]
+
+ options: SearchBraveTargetOptions
+
+
+class SearchExaTargetOptionsContents(TypedDict, total=False):
+ """Provider-native content retrieval.
+
+ Available without requesting Kernel browser retrieval; may incur provider retrieval charges.
+ """
+
+ highlights: bool
+ """Return query-relevant provider excerpts."""
+
+ text: bool
+ """Return provider page text."""
+
+
+class SearchExaTargetOptions(TypedDict, total=False):
+ category: str
+ """Provider data-category hint."""
+
+ compliance: str
+ """Provider-native compliance controls.
+
+ Requires support and authorization on the provider account.
+ """
+
+ contents: SearchExaTargetOptionsContents
+ """Provider-native content retrieval.
+
+ Available without requesting Kernel browser retrieval; may incur provider
+ retrieval charges.
+ """
+
+ max_age_hours: Annotated[float, PropertyInfo(alias="maxAgeHours")]
+ """Provider-native cache-age control.
+
+ Unlike content.max_age_hours, this retains Exa semantics, including any native
+ sentinel values. It does not imply a cross-provider freshness guarantee.
+ """
+
+ num_results: Annotated[int, PropertyInfo(alias="numResults")]
+ """Provider-native count. Lower-only alias for max_results."""
+
+ type: Literal["auto", "fast", "instant"]
+ """Search mode supported by Exa."""
+
+
+class SearchExaTarget(TypedDict, total=False):
+ provider: Required[Literal["exa"]]
+
+ options: SearchExaTargetOptions
+
+
+class SearchPerplexityTargetOptions(TypedDict, total=False):
+ last_updated_after_filter: str
+ """MM/DD/YYYY. Filters by last-updated date, not published date."""
+
+ last_updated_before_filter: str
+ """MM/DD/YYYY upper bound on last-updated date."""
+
+ max_results: int
+ """Provider-native count. Lower-only alias for max_results."""
+
+ max_tokens: int
+ """Values outside the documented range are rejected."""
+
+ max_tokens_per_page: int
+ """Per-page token cap."""
+
+ query: SequenceNotStr[str]
+ """Provider-native multi-query form, applied only to Perplexity.
+
+ Other fallback providers receive the top-level query. Each query may incur a
+ separate provider charge.
+ """
+
+ search_context_size: Literal["low", "medium", "high"]
+ """Provider context size supported by the selected model."""
+
+ search_language_filter: SequenceNotStr[str]
+ """ISO 639-1 language codes, max 20."""
+
+
+class SearchPerplexityTarget(TypedDict, total=False):
+ provider: Required[Literal["perplexity"]]
+
+ options: SearchPerplexityTargetOptions
+
+
+class SearchContextTargetOptionsMarkdownOptionsPdf(TypedDict, total=False):
+ should_parse: Annotated[bool, PropertyInfo(alias="shouldParse")]
+
+
+class SearchContextTargetOptionsMarkdownOptions(TypedDict, total=False):
+ enabled: bool
+
+ include_frames: Annotated[bool, PropertyInfo(alias="includeFrames")]
+
+ include_images: Annotated[bool, PropertyInfo(alias="includeImages")]
+
+ include_links: Annotated[bool, PropertyInfo(alias="includeLinks")]
+
+ max_age_ms: Annotated[int, PropertyInfo(alias="maxAgeMs")]
+
+ pdf: SearchContextTargetOptionsMarkdownOptionsPdf
+
+ shorten_base64_images: Annotated[bool, PropertyInfo(alias="shortenBase64Images")]
+
+ timeout_ms: Annotated[int, PropertyInfo(alias="timeoutMS")]
+
+ use_main_content_only: Annotated[bool, PropertyInfo(alias="useMainContentOnly")]
+
+ wait_for_ms: Annotated[int, PropertyInfo(alias="waitForMs")]
+
+
+class SearchContextTargetOptions(TypedDict, total=False):
+ country: str
+ """ISO 3166-1 alpha-2 country code."""
+
+ exclude_domains: Annotated[SequenceNotStr[str], PropertyInfo(alias="excludeDomains")]
+ """Blocklist of result domains."""
+
+ freshness: Literal["last_24_hours", "last_week", "last_month", "last_year"]
+ """Restrict results to content published within this window."""
+
+ include_domains: Annotated[SequenceNotStr[str], PropertyInfo(alias="includeDomains")]
+ """Allowlist of result domains."""
+
+ markdown_options: Annotated[SearchContextTargetOptionsMarkdownOptions, PropertyInfo(alias="markdownOptions")]
+
+ num_results: Annotated[int, PropertyInfo(alias="numResults")]
+ """Number of results to request from Context.dev."""
+
+ query_fanout: Annotated[bool, PropertyInfo(alias="queryFanout")]
+ """Expand the query into multiple parallel variants."""
+
+ tags: SequenceNotStr[str]
+ """Usage tracking tags."""
+
+ timeout_ms: Annotated[int, PropertyInfo(alias="timeoutMS")]
+ """Context.dev request timeout in milliseconds."""
+
+
+class SearchContextTarget(TypedDict, total=False):
+ provider: Required[Literal["context"]]
+
+ options: SearchContextTargetOptions
+
+
+class SearchParallelTargetOptionsAdvancedSettingsExcerptSettings(TypedDict, total=False):
+ max_chars_per_result: Optional[int]
+
+
+class SearchParallelTargetOptionsAdvancedSettingsFetchPolicy(TypedDict, total=False):
+ disable_cache_fallback: bool
+ """When false, the provider may return cached content after live fetching fails."""
+
+ max_age_seconds: Optional[int]
+ """Native live-fetch trigger; minimum 600 seconds.
+
+ Not the unified hard-freshness control.
+ """
+
+ timeout_seconds: Optional[float]
+ """Native live-fetch timeout, bounded by the remaining overall deadline."""
+
+
+class SearchParallelTargetOptionsAdvancedSettingsSourcePolicy(TypedDict, total=False):
+ after_date: Annotated[Union[str, date, None], PropertyInfo(format="iso8601")]
+ """Native publication-date lower bound."""
+
+ exclude_domains: SequenceNotStr[str]
+ """
+ Native exclusions; the provider ignores these when native include_domains is
+ non-empty.
+ """
+
+ include_domains: SequenceNotStr[str]
+ """Native domain/path restrictions.
+
+ Explicit unified domain parameters take precedence. Combined include/exclude
+ native lists cannot exceed 200 entries.
+ """
+
+
+class SearchParallelTargetOptionsAdvancedSettings(TypedDict, total=False):
+ """Explicit search settings.
+
+ Unified search parameters are re-applied to overlapping settings; native result counts are lower-only.
+ """
+
+ excerpt_settings: SearchParallelTargetOptionsAdvancedSettingsExcerptSettings
+
+ fetch_policy: SearchParallelTargetOptionsAdvancedSettingsFetchPolicy
+
+ location: Optional[str]
+ """Native ISO 3166-1 alpha-2 location preference."""
+
+ max_results: Optional[int]
+ """Native count; lower-only alias for unified max_results."""
+
+ source_policy: SearchParallelTargetOptionsAdvancedSettingsSourcePolicy
+
+
+class SearchParallelTargetOptions(TypedDict, total=False):
+ advanced_settings: SearchParallelTargetOptionsAdvancedSettings
+ """Explicit search settings.
+
+ Unified search parameters are re-applied to overlapping settings; native result
+ counts are lower-only.
+ """
+
+ client_model: str
+ """Client model hint."""
+
+ max_chars_total: int
+ """Cap total characters returned."""
+
+ mode: Literal["turbo", "fast", "basic", "advanced"]
+ """Search mode.
+
+ Basic is used when omitted; each mode can have different latency and charges.
+ """
+
+ objective: str
+ """The goal behind the search, stated separately from the query."""
+
+ search_queries: SequenceNotStr[str]
+ """Provider-native multi-query search. Defaults to [query] for this provider."""
+
+ session_id: str
+ """Group related searches."""
+
+
+class SearchParallelTarget(TypedDict, total=False):
+ provider: Required[Literal["parallel"]]
+
+ options: SearchParallelTargetOptions
+
+
+class SearchValyuTargetOptions(TypedDict, total=False):
+ fast_mode: bool
+ """Trade depth for latency."""
+
+ historical_cache: bool
+ """Allow historical cached results."""
+
+ include_abstracts: bool
+ """Include abstracts for academic sources."""
+
+ instructions: str
+ """Natural-language retrieval guidance."""
+
+ is_tool_call: bool
+ """Mark the search as an agent tool call."""
+
+ max_num_results: int
+ """Provider-native count. Lower-only alias for max_results."""
+
+ max_price: float
+ """Provider-native USD-per-thousand-results price ceiling. Forwarded to Valyu."""
+
+ relevance_threshold: float
+ """Provider-native minimum relevance threshold.
+
+ Not a normalized cross-provider score.
+ """
+
+ response_length: Literal["short", "medium", "large", "max"]
+ """Provider result-content length preset."""
+
+ search_type: str
+ """Corpus selector, including all, web, proprietary, and news.
+
+ Provider corpus choice may change billing; Kernel does not force web-only
+ searches.
+ """
+
+ source_biases: SequenceNotStr[str]
+ """Bias retrieval toward these sources."""
+
+ url_only: bool
+ """Return URLs without content."""
+
+
+class SearchValyuTarget(TypedDict, total=False):
+ provider: Required[Literal["valyu"]]
+
+ options: SearchValyuTargetOptions
+
+
+class SearchOctenTargetOptionsFullContent(TypedDict, total=False):
+ enable: bool
+
+ max_tokens: int
+
+
+class SearchOctenTargetOptionsHighlight(TypedDict, total=False):
+ enable: bool
+
+ max_tokens: int
+
+
+class SearchOctenTargetOptions(TypedDict, total=False):
+ count: int
+
+ end_time: Annotated[Union[str, datetime], PropertyInfo(format="iso8601")]
+
+ exclude_domains: SequenceNotStr[str]
+
+ exclude_text: SequenceNotStr[str]
+
+ format: Literal["markdown", "text"]
+
+ full_content: SearchOctenTargetOptionsFullContent
+
+ highlight: SearchOctenTargetOptionsHighlight
+
+ include_domains: SequenceNotStr[str]
+
+ include_images: bool
+
+ include_text: SequenceNotStr[str]
+
+ language: SequenceNotStr[str]
+
+ safesearch: Literal["off", "strict"]
+
+ start_time: Annotated[Union[str, datetime], PropertyInfo(format="iso8601")]
+
+ time_basis: Literal["auto", "published", "crawled"]
+
+ time_range: Literal["day", "week", "month", "year", "d", "w", "m", "y"]
+
+ topic: Literal["general", "news"]
+
+
+class SearchOctenTarget(TypedDict, total=False):
+ provider: Required[Literal["octen"]]
+
+ options: SearchOctenTargetOptions
+
+
+class SearchYouTargetOptionsExtractionFullPage(TypedDict, total=False):
+ extraction_formats: List[Literal["html", "markdown"]]
+
+
+class SearchYouTargetOptionsExtraction(TypedDict, total=False):
+ """Provider-native page extraction.
+
+ Both modes may incur per-row charges; full_page may retrieve web and news rows.
+ """
+
+ extraction_mode: Required[Literal["highlights", "full_page"]]
+
+ full_page: SearchYouTargetOptionsExtractionFullPage
+
+
+class SearchYouTargetOptions(TypedDict, total=False):
+ boost_domains: SequenceNotStr[str]
+ """Prefer these domains without excluding others.
+
+ Cannot be combined with include_domains if the provider does not accept the
+ combination.
+ """
+
+ count: int
+ """Provider-native per-section count.
+
+ Lower-only alias for max_results. Web and news sections may produce more rows
+ than Kernel returns.
+ """
+
+ crawl_timeout: int
+ """
+ Native extraction timeout in seconds, bounded by the remaining Kernel request
+ deadline.
+ """
+
+ extraction: SearchYouTargetOptionsExtraction
+ """Provider-native page extraction.
+
+ Both modes may incur per-row charges; full_page may retrieve web and news rows.
+ """
+
+ knowledge: Literal["core"]
+ """Request licensed-data output.
+
+ URL-less knowledge entries are not converted into web results; include_raw
+ exposes the full provider response separately.
+ """
+
+ language: str
+ """BCP 47 result language from You.com's 51-value enum, e.g.
+
+ "EN", "JA". Default EN.
+ """
+
+ offset: int
+ """Page offset supported by You.com."""
+
+
+class SearchYouTarget(TypedDict, total=False):
+ provider: Required[Literal["you"]]
+
+ options: SearchYouTargetOptions
+
+
+class SearchTavilyTargetOptions(TypedDict, total=False):
+ auto_parameters: bool
+ """Allow the provider to choose search parameters.
+
+ May select a different billing tier; explicit caller values retain
+ provider-native precedence.
+ """
+
+ chunks_per_source: int
+ """Provider excerpts per source, up to 500 characters each."""
+
+ exact_match: bool
+ """Require the quoted phrases in the query verbatim, bypassing semantic matches."""
+
+ filter_by_language: bool
+ """Strictly filter non-matching languages. Requires `language`."""
+
+ include_answer: Union[bool, Literal["basic", "advanced"]]
+ """Request the provider's generated answer.
+
+ Returned as answer on the search response, independently of include_raw.
+ """
+
+ include_domains_mode: Literal["filter", "boost"]
+ """Native filter versus ranking boost semantics.
+
+ Boost influences ranking rather than restricting results to the listed domains.
+ Requires include_domains.
+ """
+
+ include_favicon: bool
+ """Favicon URL per result."""
+
+ include_image_descriptions: bool
+ """Describe each image. Needs include_images."""
+
+ include_images: bool
+ """Query-related images plus per-result images."""
+
+ include_raw_content: Union[bool, Literal["markdown", "text"]]
+ """Request native full-page content.
+
+ Defaults to markdown when omitted for this provider, False disables that native
+ retrieval; it does not disable explicitly requested Kernel browser retrieval.
+ """
+
+ language: str
+ """ISO 639-1 code or English language name.
+
+ Ranking boost unless filter_by_language.
+ """
+
+ max_results: int
+ """Provider-native count. Lower-only alias for max_results."""
+
+ search_depth: Literal["advanced", "basic", "fast", "ultra-fast"]
+ """Provider relevance and latency tier.
+
+ Some tiers cannot be combined with native safe search; conflicts are described
+ in warnings.
+ """
+
+ topic: Literal["general", "news", "finance"]
+ """Provider corpus selector. Publication metadata depends on the selected topic."""
+
+
+class SearchTavilyTarget(TypedDict, total=False):
+ provider: Required[Literal["tavily"]]
+
+ options: SearchTavilyTargetOptions
+
+
+class SearchSerpAPITargetOptions(TypedDict, total=False):
+ engine: Required[str]
+ """SerpApi engine identifier.
+
+ The Kernel integration currently supports google only.
+ """
+
+ device: Literal["desktop", "mobile", "tablet"]
+ """Device profile used for the search."""
+
+ filter: Literal[0, 1]
+ """Google duplicate-content filter."""
+
+ gl: str
+ """Two-letter Google country code."""
+
+ google_domain: str
+ """Google domain to search when using the google engine."""
+
+ hl: str
+ """Interface language code."""
+
+ location: str
+ """Free-form geographic location used for localized results."""
+
+ nfpr: Literal[0, 1]
+ """Google auto-correction filter."""
+
+ no_cache: bool
+ """When true, bypass SerpApi cached results when supported."""
+
+ num: int
+ """Number of results requested from the search engine."""
+
+ safe: Literal["active", "off"]
+ """Safe-search setting for engines that support it."""
+
+ start: int
+ """Zero-based result offset for pagination."""
+
+ tbm: str
+ """Google vertical search selector, such as images, video, news, or shopping."""
+
+ tbs: str
+ """Google time and search modifiers, including freshness filters."""
+
+
+class SearchSerpAPITarget(TypedDict, total=False):
+ provider: Required[Literal["serpapi"]]
+
+ options: SearchSerpAPITargetOptions
+
+
+ProviderTargetParam: TypeAlias = Union[
+ SearchBraveTarget,
+ SearchExaTarget,
+ SearchPerplexityTarget,
+ SearchContextTarget,
+ SearchParallelTarget,
+ SearchValyuTarget,
+ SearchOctenTarget,
+ SearchYouTarget,
+ SearchTavilyTarget,
+ SearchSerpAPITarget,
+]
diff --git a/src/kernel/types/result.py b/src/kernel/types/result.py
new file mode 100644
index 00000000..23a5d12b
--- /dev/null
+++ b/src/kernel/types/result.py
@@ -0,0 +1,115 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from typing import List, Optional
+from datetime import datetime
+from typing_extensions import Literal
+
+from .._models import BaseModel
+
+__all__ = ["Result", "Content", "ContentError"]
+
+
+class ContentError(BaseModel):
+ code: str
+ """Machine-readable retrieval failure code."""
+
+ message: str
+ """Human-readable failure description."""
+
+ retryable: bool
+
+
+class Content(BaseModel):
+ """Portable retrieval outcome, or native content supplied by the search provider.
+
+ Identity fields remain on the enclosing result. Native excerpts are labeled excerpt rather than full_page. Omission never triggers browser retrieval.
+ """
+
+ status: Literal["ok", "unavailable", "blocked", "timeout", "unsupported_type", "extraction_failed", "error"]
+ """Ok means non-empty extracted content, not merely HTTP 200.
+
+ Blocked includes detected challenges or access denials. Detection is
+ best-effort, not a guarantee of page completeness. Error details are present for
+ non-ok outcomes; text is present only on ok.
+ """
+
+ cache_status: Optional[Literal["hit", "miss", "bypass", "unknown"]] = None
+ """Kernel cache outcome. Provider-internal cache behavior may be unknown."""
+
+ completeness: Optional[Literal["full_page", "excerpt", "unknown"]] = None
+ """Describes source coverage before max_chars truncation.
+
+ Full_page means main-page content, not every dynamic element or linked page.
+ """
+
+ error: Optional[ContentError] = None
+
+ extractor_version: Optional[str] = None
+ """Extraction version when Kernel transformed the input."""
+
+ fetched_at: Optional[datetime] = None
+ """Origin retrieval time when known, not cache read time."""
+
+ final_url: Optional[str] = None
+ """Final retrieval URL when known."""
+
+ format: Optional[Literal["markdown", "text"]] = None
+
+ http_status: Optional[int] = None
+ """Final target HTTP status when known."""
+
+ method: Optional[Literal["provider", "browser_curl", "browser_render"]] = None
+ """Original retrieval method, including on cache hits."""
+
+ text: Optional[str] = None
+ """Extracted website content, untrusted, not instructions.
+
+ Present only on status=ok.
+ """
+
+ truncated: Optional[bool] = None
+ """Whether max_chars truncated the extracted content."""
+
+
+class Result(BaseModel):
+ id: str
+ """Kernel-generated identifier for this result.
+
+ Stable only within the retained search; not standardized across providers.
+ Provider-native IDs, when available, remain provider-specific raw fields.
+ """
+
+ rank: int
+ """One-based position in the returned ranking."""
+
+ url: str
+ """Provider-returned URL, not assumed canonical."""
+
+ additional_snippets: Optional[List[str]] = None
+
+ content: Optional[Content] = None
+ """Portable retrieval outcome, or native content supplied by the search provider.
+
+ Identity fields remain on the enclosing result. Native excerpts are labeled
+ excerpt rather than full_page. Omission never triggers browser retrieval.
+ """
+
+ published_date: Optional[str] = None
+ """Provider-supplied date or timestamp, preserving available precision.
+
+ No publication date is fabricated. Retains the published field name.
+ """
+
+ raw: Optional[object] = None
+ """Original provider result, included only with include_raw=true.
+
+ Provider relevance scores are not normalized. Top-level provider data is
+ available in Search.raw.
+ """
+
+ snippet: Optional[str] = None
+
+ source: Optional[str] = None
+ """Provider source name or result URL hostname, when available."""
+
+ title: Optional[str] = None
diff --git a/src/kernel/types/search/__init__.py b/src/kernel/types/search/__init__.py
new file mode 100644
index 00000000..a02c7ba2
--- /dev/null
+++ b/src/kernel/types/search/__init__.py
@@ -0,0 +1,9 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from __future__ import annotations
+
+from .search import Search as Search
+from .provider import Provider as Provider
+from .content_fetch_params import ContentFetchParams as ContentFetchParams
+from .provider_list_params import ProviderListParams as ProviderListParams
+from .provider_list_response import ProviderListResponse as ProviderListResponse
diff --git a/src/kernel/types/search/content_fetch_params.py b/src/kernel/types/search/content_fetch_params.py
new file mode 100644
index 00000000..d1795d0a
--- /dev/null
+++ b/src/kernel/types/search/content_fetch_params.py
@@ -0,0 +1,97 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from __future__ import annotations
+
+from typing_extensions import Literal, TypedDict
+
+from ..._types import SequenceNotStr
+
+__all__ = ["ContentFetchParams", "Content", "ContentBrowser"]
+
+
+class ContentFetchParams(TypedDict, total=False):
+ content: Content
+ """Defaults to source:auto when omitted."""
+
+ limit: int
+ """
+ Maximum number of search results to fetch when result_ids is omitted, starting
+ from rank 1. Mutually exclusive with result_ids.
+ """
+
+ result_ids: SequenceNotStr[str]
+ """
+ Kernel-generated IDs from the referenced retained search, in desired response
+ order. They are not provider-standard IDs. Mutually exclusive with limit.
+ """
+
+ timeout_ms: int
+ """Overall deadline across all selected results."""
+
+
+class ContentBrowser(TypedDict, total=False):
+ """Invalid with source=provider.
+
+ Supplying browser_id requires
+ source=browser so the chosen identity is not bypassed.
+ """
+
+ browser_id: str
+ """Existing browser session ID authorized for the caller and selected project.
+
+ Reuses its cookies, proxy, and browser configuration. Kernel does not delete a
+ caller-supplied browser. Render mode uses a temporary tab; website activity may
+ still change shared cookies and storage. When omitted, Kernel obtains isolated
+ browser capacity in the caller's account and releases it after retrieval. That
+ capacity is not retained for later interaction. Existing browser quotas apply.
+ """
+
+ mode: Literal["curl", "render"]
+ """Curl uses the browser HTTP stack without navigation or JavaScript execution.
+
+ Render navigates a temporary page and extracts from its DOM. The selected mode
+ is used for the retrieval.
+ """
+
+
+class Content(TypedDict, total=False):
+ """Defaults to source:auto when omitted."""
+
+ browser: ContentBrowser
+ """Invalid with source=provider.
+
+ Supplying browser_id requires source=browser so the chosen identity is not
+ bypassed.
+ """
+
+ format: Literal["markdown", "text"]
+
+ max_age_hours: int
+ """Maximum acceptable age of cached page content, measured from origin retrieval.
+
+ 0 forces a live fetch. Governs the Kernel content cache, which is scoped to the
+ caller organization and project and separated by retrieval context; fetches
+ through a caller-supplied browser_id bypass that cache. Mapped to the provider
+ freshness control when source is provider and the provider supports one;
+ otherwise provider content age is reported as unknown via fetched_at.
+ """
+
+ max_chars: int
+ """Per-result Unicode character limit after extraction."""
+
+ source: Literal["auto", "provider", "browser"]
+ """
+ provider uses the search provider's native content retrieval; browser fetches
+ each URL through a Kernel browser; auto prefers Kernel browser retrieval and
+ falls back to provider-native content when browser retrieval is unavailable or
+ unsuitable. Defaults to auto for both inline and deferred retrieval. Deferred
+ provider retrieval requires post_hoc capability; an explicit provider source
+ without it is a 400. Missing documents produce per-result unavailable outcomes,
+ not request failures.
+ """
+
+ timeout_ms: int
+ """Per-result deadline including capacity acquisition, retrieval, and extraction.
+
+ Also bounded by the overall request deadline.
+ """
diff --git a/src/kernel/types/search/provider.py b/src/kernel/types/search/provider.py
new file mode 100644
index 00000000..fcefff3a
--- /dev/null
+++ b/src/kernel/types/search/provider.py
@@ -0,0 +1,136 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from typing import Dict, List, Optional
+from typing_extensions import Literal
+
+from pydantic import Field as FieldInfo
+
+from ..._models import BaseModel
+
+__all__ = [
+ "Provider",
+ "Content",
+ "Params",
+ "ParamsCountry",
+ "ParamsEndDate",
+ "ParamsExcludeDomains",
+ "ParamsIncludeDomains",
+ "ParamsLanguage",
+ "ParamsRecency",
+ "ParamsSafeSearch",
+ "ParamsStartDate",
+ "ProviderOptions",
+]
+
+
+class Content(BaseModel):
+ freshness_control: bool
+ """Can enforce the requested maximum content age."""
+
+ inline: bool
+ """Supports content retrieval with the search request."""
+
+ post_hoc: bool
+ """Supports content retrieval after the search completes."""
+
+
+class ParamsCountry(BaseModel):
+ support: Literal["native", "emulated", "unsupported"]
+
+ notes: Optional[str] = None
+ """Translation behavior, limitations, and precision."""
+
+
+class ParamsEndDate(BaseModel):
+ support: Literal["native", "emulated", "unsupported"]
+
+ notes: Optional[str] = None
+ """Translation behavior, limitations, and precision."""
+
+
+class ParamsExcludeDomains(BaseModel):
+ support: Literal["native", "emulated", "unsupported"]
+
+ notes: Optional[str] = None
+ """Translation behavior, limitations, and precision."""
+
+
+class ParamsIncludeDomains(BaseModel):
+ support: Literal["native", "emulated", "unsupported"]
+
+ notes: Optional[str] = None
+ """Translation behavior, limitations, and precision."""
+
+
+class ParamsLanguage(BaseModel):
+ support: Literal["native", "emulated", "unsupported"]
+
+ notes: Optional[str] = None
+ """Translation behavior, limitations, and precision."""
+
+
+class ParamsRecency(BaseModel):
+ support: Literal["native", "emulated", "unsupported"]
+
+ notes: Optional[str] = None
+ """Translation behavior, limitations, and precision."""
+
+
+class ParamsSafeSearch(BaseModel):
+ support: Literal["native", "emulated", "unsupported"]
+
+ notes: Optional[str] = None
+ """Translation behavior, limitations, and precision."""
+
+
+class ParamsStartDate(BaseModel):
+ support: Literal["native", "emulated", "unsupported"]
+
+ notes: Optional[str] = None
+ """Translation behavior, limitations, and precision."""
+
+
+class Params(BaseModel):
+ country: ParamsCountry
+
+ end_date: ParamsEndDate
+
+ exclude_domains: ParamsExcludeDomains
+
+ include_domains: ParamsIncludeDomains
+
+ language: ParamsLanguage
+
+ recency: ParamsRecency
+
+ safe_search: ParamsSafeSearch
+
+ start_date: ParamsStartDate
+
+
+class ProviderOptions(BaseModel):
+ schema_: Dict[str, object] = FieldInfo(alias="schema")
+ """JSON Schema for the provider-native options accepted by POST /search."""
+
+ schema_ref: str
+ """OpenAPI component name for the matching typed provider-options schema."""
+
+ examples: Optional[List[Dict[str, object]]] = None
+
+
+class Provider(BaseModel):
+ content: Content
+
+ max_results_cap: int
+
+ params: Params
+
+ provider_options: ProviderOptions
+
+ slug: str
+
+ notes: Optional[List[str]] = None
+ """
+ Provider-specific limitations, conditional filter support, and warnings about
+ search modes.
+ """
diff --git a/src/kernel/types/search/provider_list_params.py b/src/kernel/types/search/provider_list_params.py
new file mode 100644
index 00000000..07ef4659
--- /dev/null
+++ b/src/kernel/types/search/provider_list_params.py
@@ -0,0 +1,12 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from __future__ import annotations
+
+from typing_extensions import Literal, TypedDict
+
+__all__ = ["ProviderListParams"]
+
+
+class ProviderListParams(TypedDict, total=False):
+ slug: Literal["brave", "exa", "perplexity", "context", "parallel", "valyu", "octen", "you", "tavily", "serpapi"]
+ """Optional concrete provider slug filter. Omit to list every provider."""
diff --git a/src/kernel/types/search/provider_list_response.py b/src/kernel/types/search/provider_list_response.py
new file mode 100644
index 00000000..8bafd3c6
--- /dev/null
+++ b/src/kernel/types/search/provider_list_response.py
@@ -0,0 +1,10 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from typing import List
+from typing_extensions import TypeAlias
+
+from .provider import Provider
+
+__all__ = ["ProviderListResponse"]
+
+ProviderListResponse: TypeAlias = List[Provider]
diff --git a/src/kernel/types/search/search.py b/src/kernel/types/search/search.py
new file mode 100644
index 00000000..e02b1dde
--- /dev/null
+++ b/src/kernel/types/search/search.py
@@ -0,0 +1,56 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from typing import List, Optional
+from datetime import datetime
+
+from ..usage import Usage
+from ..result import Result
+from ..attempt import Attempt
+from ..warning import Warning
+from ..._models import BaseModel
+
+__all__ = ["Search"]
+
+
+class Search(BaseModel):
+ """Retained search results and provider attempt history."""
+
+ id: str
+ """Search resource ID. Request tracing uses X-Request-Id."""
+
+ attempts: List[Attempt]
+
+ expires_at: datetime
+ """Expiration of result IDs for deferred retrieval.
+
+ Results expire 24 hours after search completion.
+ """
+
+ provider: str
+ """Concrete serving provider, never auto or fallback."""
+
+ query: str
+ """Echo of the query.
+
+ Native multi-query inputs are visible in the selected strategy target and the
+ optional raw response.
+ """
+
+ results: List[Result]
+
+ usage: Usage
+
+ warnings: List[Warning]
+
+ answer: Optional[str] = None
+ """Provider-generated answer when requested (e.g.
+
+ via Tavily include_answer or Perplexity). Preserved independently of
+ include_raw.
+ """
+
+ raw: Optional[object] = None
+ """
+ Full serving-provider response, including top-level metadata that does not
+ belong to a result. Present only with include_raw=true; untrusted provider data.
+ """
diff --git a/src/kernel/types/search_create_params.py b/src/kernel/types/search_create_params.py
new file mode 100644
index 00000000..da84a3d7
--- /dev/null
+++ b/src/kernel/types/search_create_params.py
@@ -0,0 +1,186 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from __future__ import annotations
+
+from typing import Union
+from datetime import date
+from typing_extensions import Literal, Required, Annotated, TypeAlias, TypedDict
+
+from .._types import SequenceNotStr
+from .._utils import PropertyInfo
+from .strategy_param import StrategyParam
+
+__all__ = ["SearchCreateParams", "Content", "ContentSearchContentOptions", "ContentSearchContentOptionsBrowser"]
+
+
+class SearchCreateParams(TypedDict, total=False):
+ query: Required[str]
+ """Primary search query.
+
+ A provider-native multi-query option applies only to that provider; other
+ providers in a fallback chain receive this query.
+ """
+
+ content: Content
+ """Optional portable content retrieval.
+
+ Pass true for defaults or an options object. Omission never starts Kernel
+ browser work; provider-supplied content is still returned when available,
+ including when requested through native options. Both inline and deferred
+ retrieval use the same options schema.
+ """
+
+ country: str
+ """ISO 3166-1 alpha-2 search locale preference."""
+
+ end_date: Annotated[Union[str, date], PropertyInfo(format="iso8601")]
+ """Inclusive publication-date upper bound; must not precede start_date.
+
+ If recency is also supplied, recency takes precedence with a warning.
+ Unsupported or approximated filtering is reported, or rejected under
+ strict_params.
+ """
+
+ exclude_domains: SequenceNotStr[str]
+ """
+ Hostname exclusions, with the same best-effort/strict behavior as
+ include_domains. Provider-specific combinations that cannot be represented are
+ reported via warnings or rejected in strict mode.
+ """
+
+ include_domains: SequenceNotStr[str]
+ """Hostname inclusion preference, matching a hostname and its subdomains.
+
+ Empty means unrestricted. Translated, emulated, or dropped with a warning
+ according to provider capability unless strict_params is true. Native boost
+ modes remain advisory and are identified in warnings.
+ """
+
+ include_raw: bool
+ """
+ Include untouched per-result payloads and the full serving-provider response in
+ raw fields. Off by default; native top-level outputs such as answer remain
+ available without it.
+ """
+
+ language: str
+ """BCP 47 search language preference."""
+
+ max_results: int
+ """Requested result count from 1 through 100.
+
+ The effective count is clamped to the serving provider's cap with a warning.
+ Effective native counts are the lower of this limit and supplied provider-native
+ count aliases. Strict mode rejects unsupported counts.
+ """
+
+ recency: Literal["hour", "day", "week", "month", "year"]
+ """Relative search window.
+
+ Takes precedence over start_date/end_date with a warning if both are set.
+ Provider-native recency behavior is retained, including documented hour-to-day
+ widening. Unsupported filters are rejected only in strict mode.
+ """
+
+ safe_search: Literal["off", "moderate", "strict"]
+ """Optional safety preference.
+
+ Omit to use provider defaults. Unsupported values are dropped with a warning
+ unless strict_params is true. A search filter is not an authorization boundary.
+ """
+
+ start_date: Annotated[Union[str, date], PropertyInfo(format="iso8601")]
+ """Inclusive publication-date lower bound.
+
+ If recency is also supplied, recency takes precedence with a warning. Provider
+ date semantics, precision, and unsupported filters are reported; unknown source
+ dates are not fabricated or universally post-filtered.
+ """
+
+ strategy: StrategyParam
+ """Omitted strategy defaults to auto."""
+
+ strict_params: bool
+ """
+ When false, unsupported portable parameters are omitted and approximations are
+ described in warnings. When true, every supplied portable parameter must be
+ honored exactly. Requests that cannot be served with those parameters are
+ rejected. This does not guarantee identical rankings or document timestamps
+ across indexes. Authentication and project isolation are always enforced.
+ """
+
+ timeout_ms: int
+ """
+ Overall deadline across search attempts and inline retrieval. No new attempt
+ starts after the deadline. Completed search results survive inline retrieval
+ timeouts.
+ """
+
+
+class ContentSearchContentOptionsBrowser(TypedDict, total=False):
+ """Invalid with source=provider.
+
+ Supplying browser_id requires
+ source=browser so the chosen identity is not bypassed.
+ """
+
+ browser_id: str
+ """Existing browser session ID authorized for the caller and selected project.
+
+ Reuses its cookies, proxy, and browser configuration. Kernel does not delete a
+ caller-supplied browser. Render mode uses a temporary tab; website activity may
+ still change shared cookies and storage. When omitted, Kernel obtains isolated
+ browser capacity in the caller's account and releases it after retrieval. That
+ capacity is not retained for later interaction. Existing browser quotas apply.
+ """
+
+ mode: Literal["curl", "render"]
+ """Curl uses the browser HTTP stack without navigation or JavaScript execution.
+
+ Render navigates a temporary page and extracts from its DOM. The selected mode
+ is used for the retrieval.
+ """
+
+
+class ContentSearchContentOptions(TypedDict, total=False):
+ browser: ContentSearchContentOptionsBrowser
+ """Invalid with source=provider.
+
+ Supplying browser_id requires source=browser so the chosen identity is not
+ bypassed.
+ """
+
+ format: Literal["markdown", "text"]
+
+ max_age_hours: int
+ """Maximum acceptable age of cached page content, measured from origin retrieval.
+
+ 0 forces a live fetch. Governs the Kernel content cache, which is scoped to the
+ caller organization and project and separated by retrieval context; fetches
+ through a caller-supplied browser_id bypass that cache. Mapped to the provider
+ freshness control when source is provider and the provider supports one;
+ otherwise provider content age is reported as unknown via fetched_at.
+ """
+
+ max_chars: int
+ """Per-result Unicode character limit after extraction."""
+
+ source: Literal["auto", "provider", "browser"]
+ """
+ provider uses the search provider's native content retrieval; browser fetches
+ each URL through a Kernel browser; auto prefers Kernel browser retrieval and
+ falls back to provider-native content when browser retrieval is unavailable or
+ unsuitable. Defaults to auto for both inline and deferred retrieval. Deferred
+ provider retrieval requires post_hoc capability; an explicit provider source
+ without it is a 400. Missing documents produce per-result unavailable outcomes,
+ not request failures.
+ """
+
+ timeout_ms: int
+ """Per-result deadline including capacity acquisition, retrieval, and extraction.
+
+ Also bounded by the overall request deadline.
+ """
+
+
+Content: TypeAlias = Union[Literal[True], ContentSearchContentOptions]
diff --git a/src/kernel/types/strategy_param.py b/src/kernel/types/strategy_param.py
new file mode 100644
index 00000000..d755a092
--- /dev/null
+++ b/src/kernel/types/strategy_param.py
@@ -0,0 +1,60 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from __future__ import annotations
+
+from typing import List, Union, Iterable
+from typing_extensions import Literal, Required, TypeAlias, TypedDict
+
+from .provider_target_param import ProviderTargetParam
+
+__all__ = ["StrategyParam", "SearchAutoStrategy", "SearchPinnedStrategy", "SearchFallbackStrategy"]
+
+
+class SearchAutoStrategy(TypedDict, total=False):
+ type: Required[Literal["auto"]]
+ """Let Kernel choose an eligible provider by capability fit."""
+
+ fallback_on: List[Literal["error", "timeout", "empty"]]
+ """
+ Conditions that advance to the next provider under auto routing or an explicit
+ providers chain. Ignored when provider pins a single provider. error means a
+ retryable provider failure, including rate limiting, not invalid caller input or
+ caller quotas. empty means zero results after required filtering. An empty list
+ disables fallback. If every attempt is empty or fails, the response is the first
+ valid empty response with the full attempt trail, or a 502 if none succeeded.
+ """
+
+ provider_options: Iterable[ProviderTargetParam]
+ """
+ Provider targets available to auto routing, each paired with typed native
+ options. Provider names must be unique.
+ """
+
+
+class SearchPinnedStrategy(TypedDict, total=False):
+ provider: Required[ProviderTargetParam]
+ """Provider name paired with its typed native options."""
+
+ type: Required[Literal["pinned"]]
+ """Use exactly the selected provider with no cross-provider fallback."""
+
+
+class SearchFallbackStrategy(TypedDict, total=False):
+ providers: Required[Iterable[ProviderTargetParam]]
+ """Ordered provider targets. Provider names must be unique."""
+
+ type: Required[Literal["fallback"]]
+ """Try providers in order and advance when fallback_on matches the outcome."""
+
+ fallback_on: List[Literal["error", "timeout", "empty"]]
+ """
+ Conditions that advance to the next provider under auto routing or an explicit
+ providers chain. Ignored when provider pins a single provider. error means a
+ retryable provider failure, including rate limiting, not invalid caller input or
+ caller quotas. empty means zero results after required filtering. An empty list
+ disables fallback. If every attempt is empty or fails, the response is the first
+ valid empty response with the full attempt trail, or a 502 if none succeeded.
+ """
+
+
+StrategyParam: TypeAlias = Union[SearchAutoStrategy, SearchPinnedStrategy, SearchFallbackStrategy]
diff --git a/src/kernel/types/usage.py b/src/kernel/types/usage.py
new file mode 100644
index 00000000..76be908c
--- /dev/null
+++ b/src/kernel/types/usage.py
@@ -0,0 +1,24 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from typing import Optional
+
+from .._models import BaseModel
+
+__all__ = ["Usage"]
+
+
+class Usage(BaseModel):
+ content_fetches: int
+ """
+ Number of result URLs for which a Kernel browser retrieval was attempted,
+ excluding cache-only hits.
+ """
+
+ results_count: int
+ """
+ Number of result entries returned, including failed entries on the contents
+ endpoint.
+ """
+
+ cost: Optional[float] = None
+ """Total customer charge in USD when billing data is available."""
diff --git a/src/kernel/types/warning.py b/src/kernel/types/warning.py
new file mode 100644
index 00000000..fc138975
--- /dev/null
+++ b/src/kernel/types/warning.py
@@ -0,0 +1,24 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from typing import Optional
+
+from .._models import BaseModel
+
+__all__ = ["Warning"]
+
+
+class Warning(BaseModel):
+ code: str
+ """
+ Examples: param_unsupported, preference_unsupported, max_results_clamped,
+ domains_truncated, recency_emulated, filter_emulated, date_filter_overridden,
+ provider_ineligible, fallback_failed, content_partial.
+ """
+
+ message: str
+
+ param: Optional[str] = None
+
+ provider: Optional[str] = None
+
+ result_id: Optional[str] = None
diff --git a/tests/api_resources/search/__init__.py b/tests/api_resources/search/__init__.py
new file mode 100644
index 00000000..fd8019a9
--- /dev/null
+++ b/tests/api_resources/search/__init__.py
@@ -0,0 +1 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
diff --git a/tests/api_resources/search/test_contents.py b/tests/api_resources/search/test_contents.py
new file mode 100644
index 00000000..941e6018
--- /dev/null
+++ b/tests/api_resources/search/test_contents.py
@@ -0,0 +1,150 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from __future__ import annotations
+
+import os
+from typing import Any, cast
+
+import pytest
+
+from kernel import Kernel, AsyncKernel
+
+base_url = os.environ.get("TEST_API_BASE_URL", "http://127.0.0.1:4010")
+
+
+class TestContents:
+ parametrize = pytest.mark.parametrize("client", [False, True], indirect=True, ids=["loose", "strict"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ def test_method_fetch(self, client: Kernel) -> None:
+ content = client.search.contents.fetch(
+ id="srch_abc123",
+ )
+ assert content is None
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ def test_method_fetch_with_all_params(self, client: Kernel) -> None:
+ content = client.search.contents.fetch(
+ id="srch_abc123",
+ content={
+ "browser": {
+ "browser_id": "browser_id",
+ "mode": "curl",
+ },
+ "format": "markdown",
+ "max_age_hours": 0,
+ "max_chars": 100,
+ "source": "auto",
+ "timeout_ms": 1000,
+ },
+ limit=1,
+ result_ids=["string"],
+ timeout_ms=1000,
+ )
+ assert content is None
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ def test_raw_response_fetch(self, client: Kernel) -> None:
+ response = client.search.contents.with_raw_response.fetch(
+ id="srch_abc123",
+ )
+
+ assert response.is_closed is True
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+ content = response.parse()
+ assert content is None
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ def test_streaming_response_fetch(self, client: Kernel) -> None:
+ with client.search.contents.with_streaming_response.fetch(
+ id="srch_abc123",
+ ) as response:
+ assert not response.is_closed
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+
+ content = response.parse()
+ assert content is None
+
+ assert cast(Any, response.is_closed) is True
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ def test_path_params_fetch(self, client: Kernel) -> None:
+ with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"):
+ client.search.contents.with_raw_response.fetch(
+ id="",
+ )
+
+
+class TestAsyncContents:
+ parametrize = pytest.mark.parametrize(
+ "async_client", [False, True, {"http_client": "aiohttp"}], indirect=True, ids=["loose", "strict", "aiohttp"]
+ )
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ async def test_method_fetch(self, async_client: AsyncKernel) -> None:
+ content = await async_client.search.contents.fetch(
+ id="srch_abc123",
+ )
+ assert content is None
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ async def test_method_fetch_with_all_params(self, async_client: AsyncKernel) -> None:
+ content = await async_client.search.contents.fetch(
+ id="srch_abc123",
+ content={
+ "browser": {
+ "browser_id": "browser_id",
+ "mode": "curl",
+ },
+ "format": "markdown",
+ "max_age_hours": 0,
+ "max_chars": 100,
+ "source": "auto",
+ "timeout_ms": 1000,
+ },
+ limit=1,
+ result_ids=["string"],
+ timeout_ms=1000,
+ )
+ assert content is None
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ async def test_raw_response_fetch(self, async_client: AsyncKernel) -> None:
+ response = await async_client.search.contents.with_raw_response.fetch(
+ id="srch_abc123",
+ )
+
+ assert response.is_closed is True
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+ content = await response.parse()
+ assert content is None
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ async def test_streaming_response_fetch(self, async_client: AsyncKernel) -> None:
+ async with async_client.search.contents.with_streaming_response.fetch(
+ id="srch_abc123",
+ ) as response:
+ assert not response.is_closed
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+
+ content = await response.parse()
+ assert content is None
+
+ assert cast(Any, response.is_closed) is True
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ async def test_path_params_fetch(self, async_client: AsyncKernel) -> None:
+ with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"):
+ await async_client.search.contents.with_raw_response.fetch(
+ id="",
+ )
diff --git a/tests/api_resources/search/test_providers.py b/tests/api_resources/search/test_providers.py
new file mode 100644
index 00000000..0f511099
--- /dev/null
+++ b/tests/api_resources/search/test_providers.py
@@ -0,0 +1,96 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from __future__ import annotations
+
+import os
+from typing import Any, cast
+
+import pytest
+
+from kernel import Kernel, AsyncKernel
+from tests.utils import assert_matches_type
+from kernel.types.search import ProviderListResponse
+
+base_url = os.environ.get("TEST_API_BASE_URL", "http://127.0.0.1:4010")
+
+
+class TestProviders:
+ parametrize = pytest.mark.parametrize("client", [False, True], indirect=True, ids=["loose", "strict"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ def test_method_list(self, client: Kernel) -> None:
+ provider = client.search.providers.list()
+ assert_matches_type(ProviderListResponse, provider, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ def test_method_list_with_all_params(self, client: Kernel) -> None:
+ provider = client.search.providers.list(
+ slug="brave",
+ )
+ assert_matches_type(ProviderListResponse, provider, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ def test_raw_response_list(self, client: Kernel) -> None:
+ response = client.search.providers.with_raw_response.list()
+
+ assert response.is_closed is True
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+ provider = response.parse()
+ assert_matches_type(ProviderListResponse, provider, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ def test_streaming_response_list(self, client: Kernel) -> None:
+ with client.search.providers.with_streaming_response.list() as response:
+ assert not response.is_closed
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+
+ provider = response.parse()
+ assert_matches_type(ProviderListResponse, provider, path=["response"])
+
+ assert cast(Any, response.is_closed) is True
+
+
+class TestAsyncProviders:
+ parametrize = pytest.mark.parametrize(
+ "async_client", [False, True, {"http_client": "aiohttp"}], indirect=True, ids=["loose", "strict", "aiohttp"]
+ )
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ async def test_method_list(self, async_client: AsyncKernel) -> None:
+ provider = await async_client.search.providers.list()
+ assert_matches_type(ProviderListResponse, provider, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ async def test_method_list_with_all_params(self, async_client: AsyncKernel) -> None:
+ provider = await async_client.search.providers.list(
+ slug="brave",
+ )
+ assert_matches_type(ProviderListResponse, provider, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ async def test_raw_response_list(self, async_client: AsyncKernel) -> None:
+ response = await async_client.search.providers.with_raw_response.list()
+
+ assert response.is_closed is True
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+ provider = await response.parse()
+ assert_matches_type(ProviderListResponse, provider, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ async def test_streaming_response_list(self, async_client: AsyncKernel) -> None:
+ async with async_client.search.providers.with_streaming_response.list() as response:
+ assert not response.is_closed
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+
+ provider = await response.parse()
+ assert_matches_type(ProviderListResponse, provider, path=["response"])
+
+ assert cast(Any, response.is_closed) is True
diff --git a/tests/api_resources/test_search.py b/tests/api_resources/test_search.py
new file mode 100644
index 00000000..ef559856
--- /dev/null
+++ b/tests/api_resources/test_search.py
@@ -0,0 +1,265 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from __future__ import annotations
+
+import os
+from typing import Any, cast
+
+import pytest
+
+from kernel import Kernel, AsyncKernel
+from tests.utils import assert_matches_type
+from kernel._utils import parse_date
+from kernel.types.search import Search
+
+base_url = os.environ.get("TEST_API_BASE_URL", "http://127.0.0.1:4010")
+
+
+class TestSearch:
+ parametrize = pytest.mark.parametrize("client", [False, True], indirect=True, ids=["loose", "strict"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ def test_method_create(self, client: Kernel) -> None:
+ search = client.search.create(
+ query="x",
+ )
+ assert_matches_type(Search, search, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ def test_method_create_with_all_params(self, client: Kernel) -> None:
+ search = client.search.create(
+ query="x",
+ content=True,
+ country="se",
+ end_date=parse_date("2019-12-27"),
+ exclude_domains=["string"],
+ include_domains=["string"],
+ include_raw=True,
+ language="language",
+ max_results=1,
+ recency="hour",
+ safe_search="off",
+ start_date=parse_date("2019-12-27"),
+ strategy={
+ "type": "auto",
+ "fallback_on": ["error"],
+ "provider_options": [
+ {
+ "provider": "brave",
+ "options": {
+ "count": 1,
+ "extra_snippets": True,
+ "goggles": "goggles",
+ "goggles_id": "goggles_id",
+ "include_fetch_metadata": True,
+ "offset": 0,
+ "operators": "operators",
+ "result_filter": "result_filter",
+ "search_lang": "search_lang",
+ "spellcheck": True,
+ "ui_lang": "ui_lang",
+ "units": "metric",
+ },
+ }
+ ],
+ },
+ strict_params=True,
+ timeout_ms=1000,
+ )
+ assert_matches_type(Search, search, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ def test_raw_response_create(self, client: Kernel) -> None:
+ response = client.search.with_raw_response.create(
+ query="x",
+ )
+
+ assert response.is_closed is True
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+ search = response.parse()
+ assert_matches_type(Search, search, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ def test_streaming_response_create(self, client: Kernel) -> None:
+ with client.search.with_streaming_response.create(
+ query="x",
+ ) as response:
+ assert not response.is_closed
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+
+ search = response.parse()
+ assert_matches_type(Search, search, path=["response"])
+
+ assert cast(Any, response.is_closed) is True
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ def test_method_retrieve(self, client: Kernel) -> None:
+ search = client.search.retrieve(
+ "srch_abc123",
+ )
+ assert_matches_type(Search, search, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ def test_raw_response_retrieve(self, client: Kernel) -> None:
+ response = client.search.with_raw_response.retrieve(
+ "srch_abc123",
+ )
+
+ assert response.is_closed is True
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+ search = response.parse()
+ assert_matches_type(Search, search, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ def test_streaming_response_retrieve(self, client: Kernel) -> None:
+ with client.search.with_streaming_response.retrieve(
+ "srch_abc123",
+ ) as response:
+ assert not response.is_closed
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+
+ search = response.parse()
+ assert_matches_type(Search, search, path=["response"])
+
+ assert cast(Any, response.is_closed) is True
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ def test_path_params_retrieve(self, client: Kernel) -> None:
+ with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"):
+ client.search.with_raw_response.retrieve(
+ "",
+ )
+
+
+class TestAsyncSearch:
+ parametrize = pytest.mark.parametrize(
+ "async_client", [False, True, {"http_client": "aiohttp"}], indirect=True, ids=["loose", "strict", "aiohttp"]
+ )
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ async def test_method_create(self, async_client: AsyncKernel) -> None:
+ search = await async_client.search.create(
+ query="x",
+ )
+ assert_matches_type(Search, search, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ async def test_method_create_with_all_params(self, async_client: AsyncKernel) -> None:
+ search = await async_client.search.create(
+ query="x",
+ content=True,
+ country="se",
+ end_date=parse_date("2019-12-27"),
+ exclude_domains=["string"],
+ include_domains=["string"],
+ include_raw=True,
+ language="language",
+ max_results=1,
+ recency="hour",
+ safe_search="off",
+ start_date=parse_date("2019-12-27"),
+ strategy={
+ "type": "auto",
+ "fallback_on": ["error"],
+ "provider_options": [
+ {
+ "provider": "brave",
+ "options": {
+ "count": 1,
+ "extra_snippets": True,
+ "goggles": "goggles",
+ "goggles_id": "goggles_id",
+ "include_fetch_metadata": True,
+ "offset": 0,
+ "operators": "operators",
+ "result_filter": "result_filter",
+ "search_lang": "search_lang",
+ "spellcheck": True,
+ "ui_lang": "ui_lang",
+ "units": "metric",
+ },
+ }
+ ],
+ },
+ strict_params=True,
+ timeout_ms=1000,
+ )
+ assert_matches_type(Search, search, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ async def test_raw_response_create(self, async_client: AsyncKernel) -> None:
+ response = await async_client.search.with_raw_response.create(
+ query="x",
+ )
+
+ assert response.is_closed is True
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+ search = await response.parse()
+ assert_matches_type(Search, search, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ async def test_streaming_response_create(self, async_client: AsyncKernel) -> None:
+ async with async_client.search.with_streaming_response.create(
+ query="x",
+ ) as response:
+ assert not response.is_closed
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+
+ search = await response.parse()
+ assert_matches_type(Search, search, path=["response"])
+
+ assert cast(Any, response.is_closed) is True
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ async def test_method_retrieve(self, async_client: AsyncKernel) -> None:
+ search = await async_client.search.retrieve(
+ "srch_abc123",
+ )
+ assert_matches_type(Search, search, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ async def test_raw_response_retrieve(self, async_client: AsyncKernel) -> None:
+ response = await async_client.search.with_raw_response.retrieve(
+ "srch_abc123",
+ )
+
+ assert response.is_closed is True
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+ search = await response.parse()
+ assert_matches_type(Search, search, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ async def test_streaming_response_retrieve(self, async_client: AsyncKernel) -> None:
+ async with async_client.search.with_streaming_response.retrieve(
+ "srch_abc123",
+ ) as response:
+ assert not response.is_closed
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+
+ search = await response.parse()
+ assert_matches_type(Search, search, 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_retrieve(self, async_client: AsyncKernel) -> None:
+ with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"):
+ await async_client.search.with_raw_response.retrieve(
+ "",
+ )
From 762b97ac2f0e73568cdf0f668a7bca2fde8b7017 Mon Sep 17 00:00:00 2001
From: "kernel-internal[bot]"
<260533166+kernel-internal[bot]@users.noreply.github.com>
Date: Tue, 22 Sep 2026 15:07:57 +0000
Subject: [PATCH 294/366] release: 0.111.0
---
.release-please-manifest.json | 2 +-
CHANGELOG.md | 12 ++++++++++++
pyproject.toml | 2 +-
src/kernel/_version.py | 2 +-
4 files changed, 15 insertions(+), 3 deletions(-)
diff --git a/.release-please-manifest.json b/.release-please-manifest.json
index c37434e9..2969a756 100644
--- a/.release-please-manifest.json
+++ b/.release-please-manifest.json
@@ -1,3 +1,3 @@
{
- ".": "0.110.0"
+ ".": "0.111.0"
}
\ No newline at end of file
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 67530bf8..587846b9 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,17 @@
# Changelog
+## [0.111.0](https://github.com/kernel/kernel-python-sdk/compare/v0.110.0...v0.111.0) (2026-09-22)
+
+
+### Features
+
+* Allow managed auth logins to disable learned skills ([e230ce3](https://github.com/kernel/kernel-python-sdk/commit/e230ce3a684ce7723256170443ba0b84b08310a4))
+* Distinguish optimistic managed auth reauth ([41059ae](https://github.com/kernel/kernel-python-sdk/commit/41059ae6d345b924d7e508e43a8b1d0842c58556))
+* Implement Search API v1 providers ([76b3d88](https://github.com/kernel/kernel-python-sdk/commit/76b3d8871e29d47cfda20f68ab23152daf1790c6))
+* Persist stable managed auth completion timestamps ([17a7960](https://github.com/kernel/kernel-python-sdk/commit/17a7960d9304fdceb54d59b09277651b6166fdb1))
+* Publish restricted_route_unavailable and unknown proxy_error codes ([64805ae](https://github.com/kernel/kernel-python-sdk/commit/64805ae68f0978264fb77801e8e94a470a039e94))
+* Support native prepared Adyen Sessions checkout ([5f2df0a](https://github.com/kernel/kernel-python-sdk/commit/5f2df0a4170aa146eabb23f576195d1a06621925))
+
## [0.110.0](https://github.com/kernel/kernel-python-sdk/compare/v0.109.0...v0.110.0) (2026-09-18)
diff --git a/pyproject.toml b/pyproject.toml
index ef25af35..c8bc1630 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -1,6 +1,6 @@
[project]
name = "kernel"
-version = "0.110.0"
+version = "0.111.0"
description = "The official Python library for the kernel API"
dynamic = ["readme"]
license = "Apache-2.0"
diff --git a/src/kernel/_version.py b/src/kernel/_version.py
index 78e78477..bd2462d5 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.110.0" # x-release-please-version
+__version__ = "0.111.0" # x-release-please-version
From 848805abcfa585e4299742cd8f7a3ff0306febfe Mon Sep 17 00:00:00 2001
From: "kernel-internal[bot]"
<260533166+kernel-internal[bot]@users.noreply.github.com>
Date: Tue, 22 Sep 2026 19:10:24 +0000
Subject: [PATCH 295/366] feat: Publish iframe and worker browser target types
Stainless-Generated-From: a2f1ad1397e88859def542150105c6048160757d
---
src/kernel/types/browsers/browser_event_context.py | 4 +++-
src/kernel/types/browsers/browser_page_crashed_event.py | 2 +-
src/kernel/types/browsers/browser_page_navigation_event.py | 4 +++-
src/kernel/types/browsers/browser_page_tab_opened_event.py | 4 +++-
4 files changed, 10 insertions(+), 4 deletions(-)
diff --git a/src/kernel/types/browsers/browser_event_context.py b/src/kernel/types/browsers/browser_event_context.py
index 6f2e9549..1342799e 100644
--- a/src/kernel/types/browsers/browser_event_context.py
+++ b/src/kernel/types/browsers/browser_event_context.py
@@ -32,7 +32,9 @@ class BrowserEventContext(BaseModel):
target_id: Optional[str] = None
"""Browser target identifier (stable across navigations within a tab)."""
- target_type: Optional[Literal["page", "background_page", "service_worker", "shared_worker", "other"]] = None
+ target_type: Optional[
+ Literal["page", "iframe", "worker", "background_page", "service_worker", "shared_worker", "other"]
+ ] = None
"""CDP target type of the page that produced the event."""
url: Optional[str] = None
diff --git a/src/kernel/types/browsers/browser_page_crashed_event.py b/src/kernel/types/browsers/browser_page_crashed_event.py
index 46e84f32..86c31400 100644
--- a/src/kernel/types/browsers/browser_page_crashed_event.py
+++ b/src/kernel/types/browsers/browser_page_crashed_event.py
@@ -13,7 +13,7 @@ class Data(BaseModel):
target_id: str
"""CDP target identifier of the crashed page."""
- target_type: Literal["page", "background_page", "service_worker", "shared_worker", "other"]
+ target_type: Literal["page", "iframe", "worker", "background_page", "service_worker", "shared_worker", "other"]
"""CDP target type of the page that produced the event."""
url: str
diff --git a/src/kernel/types/browsers/browser_page_navigation_event.py b/src/kernel/types/browsers/browser_page_navigation_event.py
index 5835d6d4..b4504ef5 100644
--- a/src/kernel/types/browsers/browser_page_navigation_event.py
+++ b/src/kernel/types/browsers/browser_page_navigation_event.py
@@ -28,7 +28,9 @@ class Data(BaseModel):
target_id: Optional[str] = None
"""Browser target identifier."""
- target_type: Optional[Literal["page", "background_page", "service_worker", "shared_worker", "other"]] = None
+ target_type: Optional[
+ Literal["page", "iframe", "worker", "background_page", "service_worker", "shared_worker", "other"]
+ ] = None
"""CDP target type of the page that produced the event."""
url: Optional[str] = None
diff --git a/src/kernel/types/browsers/browser_page_tab_opened_event.py b/src/kernel/types/browsers/browser_page_tab_opened_event.py
index 144be476..9fcac2b4 100644
--- a/src/kernel/types/browsers/browser_page_tab_opened_event.py
+++ b/src/kernel/types/browsers/browser_page_tab_opened_event.py
@@ -16,7 +16,9 @@ class Data(BaseModel):
target_id: Optional[str] = None
"""CDP target identifier for the newly opened tab."""
- target_type: Optional[Literal["page", "background_page", "service_worker", "shared_worker", "other"]] = None
+ target_type: Optional[
+ Literal["page", "iframe", "worker", "background_page", "service_worker", "shared_worker", "other"]
+ ] = None
"""CDP target type of the page that produced the event."""
title: Optional[str] = None
From 2b637f562b34ad0963b8ba4d0ecca73fa1dd6349 Mon Sep 17 00:00:00 2001
From: "kernel-internal[bot]"
<260533166+kernel-internal[bot]@users.noreply.github.com>
Date: Tue, 22 Sep 2026 23:05:37 +0000
Subject: [PATCH 296/366] feat: Correct monitor_disconnected description on
computed state
Stainless-Generated-From: d46c162f2516eacd33943461f190394ae3dbac42
---
src/kernel/types/browsers/browser_monitor_disconnected_event.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/kernel/types/browsers/browser_monitor_disconnected_event.py b/src/kernel/types/browsers/browser_monitor_disconnected_event.py
index 2ea3afeb..de4afe5e 100644
--- a/src/kernel/types/browsers/browser_monitor_disconnected_event.py
+++ b/src/kernel/types/browsers/browser_monitor_disconnected_event.py
@@ -17,7 +17,7 @@ class Data(BaseModel):
class BrowserMonitorDisconnectedEvent(BaseModel):
"""The CDP connection to Chrome was lost.
- Telemetry events may be dropped until monitor_reconnected arrives. Treat any in-progress computed state (network_idle, page_layout_settled) as unreliable until then.
+ Telemetry events may be dropped until monitor_reconnected arrives. In-progress computed state is discarded rather than paused, so computed events still pending for the current navigation (network_idle, page_layout_settled, page_navigation_settled) never fire. monitor_reconnected does not restore them. After reattachment a fresh state machine starts, so computed events can resume before the next navigation and carry empty navigation context until one occurs.
"""
category: Literal["monitor"]
From 1fdfce2a3b05c5af330c93cd8e37a217f4d0f99b Mon Sep 17 00:00:00 2001
From: "kernel-internal[bot]"
<260533166+kernel-internal[bot]@users.noreply.github.com>
Date: Wed, 23 Sep 2026 19:10:18 +0000
Subject: [PATCH 297/366] feat: Filter archived telemetry events by type
Stainless-Generated-From: 2a917c5ccb5838e9bdd96775019f433535c381e8
---
src/kernel/resources/browsers/telemetry.py | 32 ++++++++++++-------
.../types/browsers/telemetry_events_params.py | 13 ++++++--
.../api_resources/browsers/test_telemetry.py | 2 ++
3 files changed, 33 insertions(+), 14 deletions(-)
diff --git a/src/kernel/resources/browsers/telemetry.py b/src/kernel/resources/browsers/telemetry.py
index 367ac734..45cc3516 100644
--- a/src/kernel/resources/browsers/telemetry.py
+++ b/src/kernel/resources/browsers/telemetry.py
@@ -7,7 +7,7 @@
import httpx
-from ..._types import Body, Omit, Query, Headers, NotGiven, omit, not_given
+from ..._types import Body, Omit, Query, Headers, NotGiven, SequenceNotStr, omit, not_given
from ..._utils import path_template, maybe_transform, strip_not_given, async_maybe_transform
from ..._compat import cached_property
from ..._resource import SyncAPIResource, AsyncAPIResource
@@ -75,6 +75,7 @@ def events(
offset: int | Omit = omit,
order: str | Omit = omit,
since: str | Omit = omit,
+ type: SequenceNotStr[str] | Omit = omit,
until: str | Omit = omit,
# Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
# The extra values given here take precedence over values defined on the client or passed to this method.
@@ -87,8 +88,9 @@ def events(
To page through
results, pass the X-Next-Offset value from the previous response as offset and
- repeat while X-Has-More is true. Returns an empty list when telemetry data is
- unavailable.
+ repeat while X-Has-More is true. The category and type filters apply within each
+ page, so a filtered page may be empty while X-Has-More is true. Returns an empty
+ list when telemetry data is unavailable.
Args:
category: Restrict results to these event categories. Repeat the parameter for multiple
@@ -104,13 +106,15 @@ def events(
order: Read direction. asc (default) reads oldest first, starting from since or the
offset cursor. desc reads newest first: each request returns one page of up to
limit records ending at the offset cursor (or until, or the newest archived
- event); combining desc with since is rejected with a 400. In either direction
- the category filter applies within the page, so a filtered page may be empty
- while X-Has-More is true.
+ event); combining desc with since is rejected with a 400.
since: Start of the window: an RFC-3339 timestamp, or a duration like 5m meaning that
long ago. Defaults to 5m. Ignored when offset is set.
+ type: Restrict results to these event types, such as page_crashed or
+ captcha_challenge_result. Repeat the parameter for multiple values. Combines
+ with category: when both are set an event must match both.
+
until: End of the window (exclusive): an RFC-3339 timestamp, or a duration like 5m
meaning that long ago.
@@ -139,6 +143,7 @@ def events(
"offset": offset,
"order": order,
"since": since,
+ "type": type,
"until": until,
},
telemetry_events_params.TelemetryEventsParams,
@@ -252,6 +257,7 @@ def events(
offset: int | Omit = omit,
order: str | Omit = omit,
since: str | Omit = omit,
+ type: SequenceNotStr[str] | Omit = omit,
until: str | Omit = omit,
# Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
# The extra values given here take precedence over values defined on the client or passed to this method.
@@ -264,8 +270,9 @@ def events(
To page through
results, pass the X-Next-Offset value from the previous response as offset and
- repeat while X-Has-More is true. Returns an empty list when telemetry data is
- unavailable.
+ repeat while X-Has-More is true. The category and type filters apply within each
+ page, so a filtered page may be empty while X-Has-More is true. Returns an empty
+ list when telemetry data is unavailable.
Args:
category: Restrict results to these event categories. Repeat the parameter for multiple
@@ -281,13 +288,15 @@ def events(
order: Read direction. asc (default) reads oldest first, starting from since or the
offset cursor. desc reads newest first: each request returns one page of up to
limit records ending at the offset cursor (or until, or the newest archived
- event); combining desc with since is rejected with a 400. In either direction
- the category filter applies within the page, so a filtered page may be empty
- while X-Has-More is true.
+ event); combining desc with since is rejected with a 400.
since: Start of the window: an RFC-3339 timestamp, or a duration like 5m meaning that
long ago. Defaults to 5m. Ignored when offset is set.
+ type: Restrict results to these event types, such as page_crashed or
+ captcha_challenge_result. Repeat the parameter for multiple values. Combines
+ with category: when both are set an event must match both.
+
until: End of the window (exclusive): an RFC-3339 timestamp, or a duration like 5m
meaning that long ago.
@@ -316,6 +325,7 @@ def events(
"offset": offset,
"order": order,
"since": since,
+ "type": type,
"until": until,
},
telemetry_events_params.TelemetryEventsParams,
diff --git a/src/kernel/types/browsers/telemetry_events_params.py b/src/kernel/types/browsers/telemetry_events_params.py
index ec385bb5..1253aa1a 100644
--- a/src/kernel/types/browsers/telemetry_events_params.py
+++ b/src/kernel/types/browsers/telemetry_events_params.py
@@ -5,6 +5,8 @@
from typing import List
from typing_extensions import Literal, TypedDict
+from ..._types import SequenceNotStr
+
__all__ = ["TelemetryEventsParams"]
@@ -46,9 +48,7 @@ class TelemetryEventsParams(TypedDict, total=False):
asc (default) reads oldest first, starting from since or the offset cursor. desc
reads newest first: each request returns one page of up to limit records ending
at the offset cursor (or until, or the newest archived event); combining desc
- with since is rejected with a 400. In either direction the category filter
- applies within the page, so a filtered page may be empty while X-Has-More is
- true.
+ with since is rejected with a 400.
"""
since: str
@@ -57,6 +57,13 @@ class TelemetryEventsParams(TypedDict, total=False):
long ago. Defaults to 5m. Ignored when offset is set.
"""
+ type: SequenceNotStr[str]
+ """
+ Restrict results to these event types, such as page_crashed or
+ captcha_challenge_result. Repeat the parameter for multiple values. Combines
+ with category: when both are set an event must match both.
+ """
+
until: str
"""
End of the window (exclusive): an RFC-3339 timestamp, or a duration like 5m
diff --git a/tests/api_resources/browsers/test_telemetry.py b/tests/api_resources/browsers/test_telemetry.py
index b54dfbc1..6bf65cc9 100644
--- a/tests/api_resources/browsers/test_telemetry.py
+++ b/tests/api_resources/browsers/test_telemetry.py
@@ -36,6 +36,7 @@ def test_method_events_with_all_params(self, client: Kernel) -> None:
offset=0,
order="order",
since="since",
+ type=["string"],
until="until",
)
assert_matches_type(SyncOffsetPagination[TelemetryEventsResponse], telemetry, path=["response"])
@@ -149,6 +150,7 @@ async def test_method_events_with_all_params(self, async_client: AsyncKernel) ->
offset=0,
order="order",
since="since",
+ type=["string"],
until="until",
)
assert_matches_type(AsyncOffsetPagination[TelemetryEventsResponse], telemetry, path=["response"])
From f5c540cdd1b0cee422cf47825dbc2212a5b6532f Mon Sep 17 00:00:00 2001
From: "kernel-internal[bot]"
<260533166+kernel-internal[bot]@users.noreply.github.com>
Date: Wed, 23 Sep 2026 22:24:58 +0000
Subject: [PATCH 298/366] feat: Add profiles to browser pool acquire
Stainless-Generated-From: 5176ec1d8f90acc70434a055166d8e9366218443
---
src/kernel/resources/browser_pools.py | 19 +++++++++++++++++--
.../types/browser_pool_acquire_params.py | 8 ++++++++
.../types/browser_pool_release_params.py | 3 ++-
tests/api_resources/test_browser_pools.py | 10 ++++++++++
4 files changed, 37 insertions(+), 3 deletions(-)
diff --git a/src/kernel/resources/browser_pools.py b/src/kernel/resources/browser_pools.py
index 5351d638..13dbceca 100644
--- a/src/kernel/resources/browser_pools.py
+++ b/src/kernel/resources/browser_pools.py
@@ -31,6 +31,7 @@
from ..types.browser_pool import BrowserPool
from ..types.browser_network_config_param import BrowserNetworkConfigParam
from ..types.browser_pool_acquire_response import BrowserPoolAcquireResponse
+from ..types.shared_params.browser_profile import BrowserProfile
from ..types.shared_params.browser_viewport import BrowserViewport
from ..types.shared_params.browser_extension import BrowserExtension
@@ -509,6 +510,7 @@ def acquire(
*,
acquire_timeout_seconds: int | Omit = omit,
name: str | Omit = omit,
+ profile: BrowserProfile | Omit = omit,
start_url: str | Omit = omit,
tags: TagsParam | Omit = omit,
telemetry: Optional[browser_pool_acquire_params.Telemetry] | Omit = omit,
@@ -536,6 +538,10 @@ def acquire(
project. Applies to this lease only and is cleared when the browser is released
back to the pool.
+ profile: Profile selection for the browser session. Provide either id or name. If
+ specified, the matching profile will be loaded into the browser session.
+ Profiles must be created beforehand.
+
start_url: Optional URL to navigate the acquired browser to. Overrides the pool's start_url
for this acquire only. Best-effort: failures to navigate do not fail the
acquire.
@@ -569,6 +575,7 @@ def acquire(
{
"acquire_timeout_seconds": acquire_timeout_seconds,
"name": name,
+ "profile": profile,
"start_url": start_url,
"tags": tags,
"telemetry": telemetry,
@@ -638,7 +645,8 @@ def release(
Defaults to true. A reused browser keeps the configuration it was created with,
so it does not pick up pool configuration changes made while it was in use.
Release with `reuse: false`, or flush the pool afterward, to rebuild it with the
- current configuration.
+ current configuration. Browsers loaded with an acquire-time profile are always
+ destroyed and replaced, even when reuse is true.
extra_headers: Send extra headers
@@ -1139,6 +1147,7 @@ async def acquire(
*,
acquire_timeout_seconds: int | Omit = omit,
name: str | Omit = omit,
+ profile: BrowserProfile | Omit = omit,
start_url: str | Omit = omit,
tags: TagsParam | Omit = omit,
telemetry: Optional[browser_pool_acquire_params.Telemetry] | Omit = omit,
@@ -1166,6 +1175,10 @@ async def acquire(
project. Applies to this lease only and is cleared when the browser is released
back to the pool.
+ profile: Profile selection for the browser session. Provide either id or name. If
+ specified, the matching profile will be loaded into the browser session.
+ Profiles must be created beforehand.
+
start_url: Optional URL to navigate the acquired browser to. Overrides the pool's start_url
for this acquire only. Best-effort: failures to navigate do not fail the
acquire.
@@ -1199,6 +1212,7 @@ async def acquire(
{
"acquire_timeout_seconds": acquire_timeout_seconds,
"name": name,
+ "profile": profile,
"start_url": start_url,
"tags": tags,
"telemetry": telemetry,
@@ -1268,7 +1282,8 @@ async def release(
Defaults to true. A reused browser keeps the configuration it was created with,
so it does not pick up pool configuration changes made while it was in use.
Release with `reuse: false`, or flush the pool afterward, to rebuild it with the
- current configuration.
+ current configuration. Browsers loaded with an acquire-time profile are always
+ destroyed and replaced, even when reuse is true.
extra_headers: Send extra headers
diff --git a/src/kernel/types/browser_pool_acquire_params.py b/src/kernel/types/browser_pool_acquire_params.py
index b5bd708d..823d05b9 100644
--- a/src/kernel/types/browser_pool_acquire_params.py
+++ b/src/kernel/types/browser_pool_acquire_params.py
@@ -6,6 +6,7 @@
from typing_extensions import TypedDict
from .tags_param import TagsParam
+from .shared_params.browser_profile import BrowserProfile
from .browsers.browser_telemetry_categories_config_param import BrowserTelemetryCategoriesConfigParam
__all__ = [
@@ -33,6 +34,13 @@ class BrowserPoolAcquireParams(TypedDict, total=False):
back to the pool.
"""
+ profile: BrowserProfile
+ """Profile selection for the browser session.
+
+ Provide either id or name. If specified, the matching profile will be loaded
+ into the browser session. Profiles must be created beforehand.
+ """
+
start_url: str
"""Optional URL to navigate the acquired browser to.
diff --git a/src/kernel/types/browser_pool_release_params.py b/src/kernel/types/browser_pool_release_params.py
index 944e734b..5d5e6655 100644
--- a/src/kernel/types/browser_pool_release_params.py
+++ b/src/kernel/types/browser_pool_release_params.py
@@ -17,5 +17,6 @@ class BrowserPoolReleaseParams(TypedDict, total=False):
Defaults to true. A reused browser keeps the configuration it was created with,
so it does not pick up pool configuration changes made while it was in use.
Release with `reuse: false`, or flush the pool afterward, to rebuild it with the
- current configuration.
+ current configuration. Browsers loaded with an acquire-time profile are always
+ destroyed and replaced, even when reuse is true.
"""
diff --git a/tests/api_resources/test_browser_pools.py b/tests/api_resources/test_browser_pools.py
index 1f541959..1ba72158 100644
--- a/tests/api_resources/test_browser_pools.py
+++ b/tests/api_resources/test_browser_pools.py
@@ -370,6 +370,11 @@ def test_method_acquire_with_all_params(self, client: Kernel) -> None:
id_or_name="id_or_name",
acquire_timeout_seconds=0,
name="checkout-flow-1",
+ profile={
+ "id": "id",
+ "name": "name",
+ "save_changes": True,
+ },
start_url="https://example.com",
tags={
"team": "backend",
@@ -892,6 +897,11 @@ async def test_method_acquire_with_all_params(self, async_client: AsyncKernel) -
id_or_name="id_or_name",
acquire_timeout_seconds=0,
name="checkout-flow-1",
+ profile={
+ "id": "id",
+ "name": "name",
+ "save_changes": True,
+ },
start_url="https://example.com",
tags={
"team": "backend",
From cbbbe4c865eb72c10be0f95e472921cf61898abc Mon Sep 17 00:00:00 2001
From: "kernel-internal[bot]"
<260533166+kernel-internal[bot]@users.noreply.github.com>
Date: Thu, 24 Sep 2026 15:12:50 +0000
Subject: [PATCH 299/366] feat: Expose Search rollout access in org
entitlements
Stainless-Generated-From: c6d763df764944d0f037891a630cbd37899e4559
---
.../types/organization/org_entitlements.py | 16 ++++++++++++++++
1 file changed, 16 insertions(+)
diff --git a/src/kernel/types/organization/org_entitlements.py b/src/kernel/types/organization/org_entitlements.py
index 614ce2fa..fd871249 100644
--- a/src/kernel/types/organization/org_entitlements.py
+++ b/src/kernel/types/organization/org_entitlements.py
@@ -21,6 +21,7 @@
"FeaturesManagedProxies",
"FeaturesProfiles",
"FeaturesProxyBypassHosts",
+ "FeaturesSearch",
"FeaturesVaults",
"Limits",
"Plan",
@@ -115,6 +116,15 @@ class FeaturesProxyBypassHosts(BaseModel):
"""Whether the organization is entitled to use this feature."""
+class FeaturesSearch(BaseModel):
+ """
+ Whether the Search API is enabled for the organization by its rollout feature flag.
+ """
+
+ enabled: bool
+ """Whether the organization is entitled to use this feature."""
+
+
class FeaturesVaults(BaseModel):
"""
Whether the organization can access vaults, using the same access check as vault API routes.
@@ -149,6 +159,12 @@ class Features(BaseModel):
proxy_bypass_hosts: FeaturesProxyBypassHosts
+ search: FeaturesSearch
+ """
+ Whether the Search API is enabled for the organization by its rollout feature
+ flag.
+ """
+
vaults: FeaturesVaults
"""
Whether the organization can access vaults, using the same access check as vault
From 5a0a461f25cbb59b43b2695fea8294357f3655fb Mon Sep 17 00:00:00 2001
From: "kernel-internal[bot]"
<260533166+kernel-internal[bot]@users.noreply.github.com>
Date: Thu, 24 Sep 2026 18:33:05 +0000
Subject: [PATCH 300/366] feat: Route browser destinations through selected
proxies
Stainless-Generated-From: 63b6ecc138cd3513679f9b07de2e6e8f6e0af813
---
src/kernel/types/browser_network_config.py | 42 ++++++++++++++-
.../types/browser_network_config_param.py | 45 +++++++++++++++-
.../browsers/browser_proxy_error_event.py | 13 ++---
tests/api_resources/test_browser_pools.py | 52 +++++++++++++++++--
tests/api_resources/test_browsers.py | 26 +++++++++-
5 files changed, 163 insertions(+), 15 deletions(-)
diff --git a/src/kernel/types/browser_network_config.py b/src/kernel/types/browser_network_config.py
index e1941816..ec2c7fbf 100644
--- a/src/kernel/types/browser_network_config.py
+++ b/src/kernel/types/browser_network_config.py
@@ -4,7 +4,28 @@
from .._models import BaseModel
-__all__ = ["BrowserNetworkConfig"]
+__all__ = ["BrowserNetworkConfig", "ProxyRoute", "ProxyRouteProxy"]
+
+
+class ProxyRouteProxy(BaseModel):
+ """Select an active non-direct proxy by ID or name. Responses always use ID."""
+
+ id: Optional[str] = None
+
+ name: Optional[str] = None
+
+
+class ProxyRoute(BaseModel):
+ hosts: List[str]
+ """Exact hostnames or leading \\**.
+
+ wildcard patterns (subdomains only); patterns cannot include ports, and matching
+ ignores the destination port. Hosts not matched by any route use the session's
+ top-level proxy (or the browser default when proxy is omitted).
+ """
+
+ proxy: ProxyRouteProxy
+ """Select an active non-direct proxy by ID or name. Responses always use ID."""
class BrowserNetworkConfig(BaseModel):
@@ -35,3 +56,22 @@ class BrowserNetworkConfig(BaseModel):
destinations. Not related to a proxy's bypass_hosts, which selects between
upstream-proxy and Kernel-managed direct egress and cannot reach into a VPN.
"""
+
+ proxy_routes: Optional[List[ProxyRoute]] = None
+ """Per-destination proxy routes for a browser session.
+
+ After setup, a destination hostname is matched against every route's hosts,
+ regardless of port; route order does not matter. An exact hostname beats a
+ wildcard, and a longer wildcard suffix beats a shorter one (for a.b.example.com:
+ "a.b.example.com" > "_.b.example.com" > "_.example.com"). A host pattern may
+ appear in only one route. "\\**.example.com" matches subdomains only, not
+ example.com. A matched request selects the route's proxy instead of the
+ session's top-level proxy (including mode: direct); the route proxy's own
+ bypass_hosts still apply. If the route proxy becomes unavailable, matched
+ requests fail closed without falling back. Requests that match no route use the
+ session's default egress from the top-level proxy field (or the browser default
+ when proxy is omitted: stealth proxy or direct egress). Routes take effect once
+ the session is created; start_url and other traffic during browser setup use the
+ top-level proxy. Setting routes requires proxy v3. Not supported on browser
+ pools.
+ """
diff --git a/src/kernel/types/browser_network_config_param.py b/src/kernel/types/browser_network_config_param.py
index 49f7ea64..298f9d13 100644
--- a/src/kernel/types/browser_network_config_param.py
+++ b/src/kernel/types/browser_network_config_param.py
@@ -2,11 +2,33 @@
from __future__ import annotations
-from typing_extensions import TypedDict
+from typing import Iterable
+from typing_extensions import Required, TypedDict
from .._types import SequenceNotStr
-__all__ = ["BrowserNetworkConfigParam"]
+__all__ = ["BrowserNetworkConfigParam", "ProxyRoute", "ProxyRouteProxy"]
+
+
+class ProxyRouteProxy(TypedDict, total=False):
+ """Select an active non-direct proxy by ID or name. Responses always use ID."""
+
+ id: str
+
+ name: str
+
+
+class ProxyRoute(TypedDict, total=False):
+ hosts: Required[SequenceNotStr[str]]
+ """Exact hostnames or leading \\**.
+
+ wildcard patterns (subdomains only); patterns cannot include ports, and matching
+ ignores the destination port. Hosts not matched by any route use the session's
+ top-level proxy (or the browser default when proxy is omitted).
+ """
+
+ proxy: Required[ProxyRouteProxy]
+ """Select an active non-direct proxy by ID or name. Responses always use ID."""
class BrowserNetworkConfigParam(TypedDict, total=False):
@@ -37,3 +59,22 @@ class BrowserNetworkConfigParam(TypedDict, total=False):
destinations. Not related to a proxy's bypass_hosts, which selects between
upstream-proxy and Kernel-managed direct egress and cannot reach into a VPN.
"""
+
+ proxy_routes: Iterable[ProxyRoute]
+ """Per-destination proxy routes for a browser session.
+
+ After setup, a destination hostname is matched against every route's hosts,
+ regardless of port; route order does not matter. An exact hostname beats a
+ wildcard, and a longer wildcard suffix beats a shorter one (for a.b.example.com:
+ "a.b.example.com" > "_.b.example.com" > "_.example.com"). A host pattern may
+ appear in only one route. "\\**.example.com" matches subdomains only, not
+ example.com. A matched request selects the route's proxy instead of the
+ session's top-level proxy (including mode: direct); the route proxy's own
+ bypass_hosts still apply. If the route proxy becomes unavailable, matched
+ requests fail closed without falling back. Requests that match no route use the
+ session's default egress from the top-level proxy field (or the browser default
+ when proxy is omitted: stealth proxy or direct egress). Routes take effect once
+ the session is created; start_url and other traffic during browser setup use the
+ top-level proxy. Setting routes requires proxy v3. Not supported on browser
+ pools.
+ """
diff --git a/src/kernel/types/browsers/browser_proxy_error_event.py b/src/kernel/types/browsers/browser_proxy_error_event.py
index 5d40f9ed..44961139 100644
--- a/src/kernel/types/browsers/browser_proxy_error_event.py
+++ b/src/kernel/types/browsers/browser_proxy_error_event.py
@@ -18,6 +18,7 @@ class Data(BrowserEventContext):
code: Literal[
"destination_blocked",
+ "destination_route_unavailable",
"provider_blacklisted",
"provider_unreachable",
"provider_rejected",
@@ -33,12 +34,12 @@ class Data(BrowserEventContext):
"""
Proxy-layer error code: the X-Kernel-Proxy-Error response header value from a
branded 5xx error page served by the metro egress host-proxy. Values mirror what
- the proxy emits: destination_blocked, provider_blacklisted,
- provider_unreachable, provider_rejected, origin_tls_timeout,
- origin_response_incomplete, proxy_unavailable, restricted_route_unavailable,
- upstream_timeout, upstream_dns_failure, upstream_connect_failed. A header value
- the browser image does not recognize is reported as unknown, with the header
- value in raw_code.
+ the proxy emits: destination_blocked, destination_route_unavailable,
+ provider_blacklisted, provider_unreachable, provider_rejected,
+ origin_tls_timeout, origin_response_incomplete, proxy_unavailable,
+ restricted_route_unavailable, upstream_timeout, upstream_dns_failure,
+ upstream_connect_failed. A header value the browser image does not recognize is
+ reported as unknown, with the header value in raw_code.
"""
request_id: str
diff --git a/tests/api_resources/test_browser_pools.py b/tests/api_resources/test_browser_pools.py
index 1ba72158..e42664c4 100644
--- a/tests/api_resources/test_browser_pools.py
+++ b/tests/api_resources/test_browser_pools.py
@@ -45,7 +45,18 @@ def test_method_create_with_all_params(self, client: Kernel) -> None:
headless=False,
kiosk_mode=True,
name="my-pool",
- network={"private_hosts": ["*.example.ts.net", "100.64.0.0/10"]},
+ network={
+ "private_hosts": ["*.example.ts.net", "100.64.0.0/10"],
+ "proxy_routes": [
+ {
+ "hosts": ["string"],
+ "proxy": {
+ "id": "x",
+ "name": "x",
+ },
+ }
+ ],
+ },
profile={
"id": "id",
"name": "name",
@@ -184,7 +195,18 @@ def test_method_update_with_all_params(self, client: Kernel) -> None:
headless=False,
kiosk_mode=True,
name="my-pool",
- network={"private_hosts": ["*.example.ts.net", "100.64.0.0/10"]},
+ network={
+ "private_hosts": ["*.example.ts.net", "100.64.0.0/10"],
+ "proxy_routes": [
+ {
+ "hosts": ["string"],
+ "proxy": {
+ "id": "x",
+ "name": "x",
+ },
+ }
+ ],
+ },
profile={
"id": "id",
"name": "name",
@@ -572,7 +594,18 @@ async def test_method_create_with_all_params(self, async_client: AsyncKernel) ->
headless=False,
kiosk_mode=True,
name="my-pool",
- network={"private_hosts": ["*.example.ts.net", "100.64.0.0/10"]},
+ network={
+ "private_hosts": ["*.example.ts.net", "100.64.0.0/10"],
+ "proxy_routes": [
+ {
+ "hosts": ["string"],
+ "proxy": {
+ "id": "x",
+ "name": "x",
+ },
+ }
+ ],
+ },
profile={
"id": "id",
"name": "name",
@@ -711,7 +744,18 @@ async def test_method_update_with_all_params(self, async_client: AsyncKernel) ->
headless=False,
kiosk_mode=True,
name="my-pool",
- network={"private_hosts": ["*.example.ts.net", "100.64.0.0/10"]},
+ network={
+ "private_hosts": ["*.example.ts.net", "100.64.0.0/10"],
+ "proxy_routes": [
+ {
+ "hosts": ["string"],
+ "proxy": {
+ "id": "x",
+ "name": "x",
+ },
+ }
+ ],
+ },
profile={
"id": "id",
"name": "name",
diff --git a/tests/api_resources/test_browsers.py b/tests/api_resources/test_browsers.py
index ffa5f2e9..5caa570e 100644
--- a/tests/api_resources/test_browsers.py
+++ b/tests/api_resources/test_browsers.py
@@ -48,7 +48,18 @@ def test_method_create_with_all_params(self, client: Kernel) -> None:
kiosk_mode=True,
memory="8GiB",
name="checkout-flow-1",
- network={"private_hosts": ["*.example.ts.net", "100.64.0.0/10"]},
+ network={
+ "private_hosts": ["*.example.ts.net", "100.64.0.0/10"],
+ "proxy_routes": [
+ {
+ "hosts": ["string"],
+ "proxy": {
+ "id": "x",
+ "name": "x",
+ },
+ }
+ ],
+ },
profile={
"id": "id",
"name": "name",
@@ -579,7 +590,18 @@ async def test_method_create_with_all_params(self, async_client: AsyncKernel) ->
kiosk_mode=True,
memory="8GiB",
name="checkout-flow-1",
- network={"private_hosts": ["*.example.ts.net", "100.64.0.0/10"]},
+ network={
+ "private_hosts": ["*.example.ts.net", "100.64.0.0/10"],
+ "proxy_routes": [
+ {
+ "hosts": ["string"],
+ "proxy": {
+ "id": "x",
+ "name": "x",
+ },
+ }
+ ],
+ },
profile={
"id": "id",
"name": "name",
From 179dcd9f2c9c27a46bc5f37d241da0f0626ccf82 Mon Sep 17 00:00:00 2001
From: "kernel-internal[bot]"
<260533166+kernel-internal[bot]@users.noreply.github.com>
Date: Thu, 24 Sep 2026 19:14:34 +0000
Subject: [PATCH 301/366] feat: Proxy custom WebMCP tools in the control plane
Stainless-Generated-From: 01e2e8b73c126f2d003902edfdd153c5c249ce6e
---
.stats.yml | 2 +-
api.md | 20 +-
src/kernel/resources/browsers/browsers.py | 16 +-
.../resources/browsers/webmcp/__init__.py | 33 ++
.../resources/browsers/webmcp/custom_tools.py | 401 ++++++++++++++++++
.../resources/browsers/{ => webmcp}/webmcp.py | 104 ++++-
src/kernel/types/browsers/__init__.py | 3 +
.../types/browsers/custom_tool_source.py | 11 +
src/kernel/types/browsers/tool.py | 25 +-
src/kernel/types/browsers/tool_annotations.py | 25 +-
src/kernel/types/browsers/tool_metadata.py | 37 ++
src/kernel/types/browsers/tool_source.py | 9 +
src/kernel/types/browsers/webmcp/__init__.py | 8 +
.../browsers/webmcp/custom_tool_add_params.py | 24 ++
.../browsers/webmcp/custom_tools_response.py | 12 +
.../types/browsers/webmcp/definition.py | 26 ++
src/kernel/types/browsers/webmcp/match.py | 11 +
.../browsers/webmcp_list_tools_params.py | 12 +
tests/api_resources/browsers/test_webmcp.py | 34 +-
.../api_resources/browsers/webmcp/__init__.py | 1 +
.../browsers/webmcp/test_custom_tools.py | 334 +++++++++++++++
21 files changed, 1086 insertions(+), 62 deletions(-)
create mode 100644 src/kernel/resources/browsers/webmcp/__init__.py
create mode 100644 src/kernel/resources/browsers/webmcp/custom_tools.py
rename src/kernel/resources/browsers/{ => webmcp}/webmcp.py (71%)
create mode 100644 src/kernel/types/browsers/custom_tool_source.py
create mode 100644 src/kernel/types/browsers/tool_metadata.py
create mode 100644 src/kernel/types/browsers/webmcp/__init__.py
create mode 100644 src/kernel/types/browsers/webmcp/custom_tool_add_params.py
create mode 100644 src/kernel/types/browsers/webmcp/custom_tools_response.py
create mode 100644 src/kernel/types/browsers/webmcp/definition.py
create mode 100644 src/kernel/types/browsers/webmcp/match.py
create mode 100644 src/kernel/types/browsers/webmcp_list_tools_params.py
create mode 100644 tests/api_resources/browsers/webmcp/__init__.py
create mode 100644 tests/api_resources/browsers/webmcp/test_custom_tools.py
diff --git a/.stats.yml b/.stats.yml
index e8ac4eb6..ec91ee34 100644
--- a/.stats.yml
+++ b/.stats.yml
@@ -1 +1 @@
-configured_endpoints: 169
+configured_endpoints: 172
diff --git a/api.md b/api.md
index 7e82ddfb..1b130bb7 100644
--- a/api.md
+++ b/api.md
@@ -346,12 +346,14 @@ Types:
```python
from kernel.types.browsers import (
+ CustomToolSource,
InvocationFailure,
InvocationResult,
InvokeRequest,
Tool,
ToolAnnotations,
ToolFrame,
+ ToolMetadata,
ToolSource,
ToolsResponse,
)
@@ -359,8 +361,22 @@ from kernel.types.browsers import (
Methods:
-- client.browsers.webmcp.invoke_tool(id_or_name, \*\*params) -> InvocationResult
-- client.browsers.webmcp.list_tools(id_or_name) -> ToolsResponse
+- client.browsers.webmcp.invoke_tool(id_or_name, \*\*params) -> InvocationResult
+- client.browsers.webmcp.list_tools(id_or_name, \*\*params) -> ToolsResponse
+
+### CustomTools
+
+Types:
+
+```python
+from kernel.types.browsers.webmcp import AddRequest, CustomToolsResponse, Definition, Match
+```
+
+Methods:
+
+- client.browsers.webmcp.custom_tools.list(id_or_name) -> CustomToolsResponse
+- client.browsers.webmcp.custom_tools.add(id_or_name, \*\*params) -> CustomToolsResponse
+- client.browsers.webmcp.custom_tools.remove(id, \*, id_or_name) -> None
# Profiles
diff --git a/src/kernel/resources/browsers/browsers.py b/src/kernel/resources/browsers/browsers.py
index d2e519bd..b1cf74d6 100644
--- a/src/kernel/resources/browsers/browsers.py
+++ b/src/kernel/resources/browsers/browsers.py
@@ -24,14 +24,6 @@
FsResourceWithStreamingResponse,
AsyncFsResourceWithStreamingResponse,
)
-from .webmcp import (
- WebmcpResource,
- AsyncWebmcpResource,
- WebmcpResourceWithRawResponse,
- AsyncWebmcpResourceWithRawResponse,
- WebmcpResourceWithStreamingResponse,
- AsyncWebmcpResourceWithStreamingResponse,
-)
from ...types import (
BrowserMemoryRequest,
browser_curl_params,
@@ -94,6 +86,14 @@
async_to_streamed_response_wrapper,
)
from ...pagination import SyncOffsetPagination, AsyncOffsetPagination
+from .webmcp.webmcp import (
+ WebmcpResource,
+ AsyncWebmcpResource,
+ WebmcpResourceWithRawResponse,
+ AsyncWebmcpResourceWithRawResponse,
+ WebmcpResourceWithStreamingResponse,
+ AsyncWebmcpResourceWithStreamingResponse,
+)
from ..._base_client import AsyncPaginator, make_request_options
from ...types.tags_param import TagsParam
from ...types.browser_repl_result import BrowserReplResult
diff --git a/src/kernel/resources/browsers/webmcp/__init__.py b/src/kernel/resources/browsers/webmcp/__init__.py
new file mode 100644
index 00000000..890b007a
--- /dev/null
+++ b/src/kernel/resources/browsers/webmcp/__init__.py
@@ -0,0 +1,33 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from .webmcp import (
+ WebmcpResource,
+ AsyncWebmcpResource,
+ WebmcpResourceWithRawResponse,
+ AsyncWebmcpResourceWithRawResponse,
+ WebmcpResourceWithStreamingResponse,
+ AsyncWebmcpResourceWithStreamingResponse,
+)
+from .custom_tools import (
+ CustomToolsResource,
+ AsyncCustomToolsResource,
+ CustomToolsResourceWithRawResponse,
+ AsyncCustomToolsResourceWithRawResponse,
+ CustomToolsResourceWithStreamingResponse,
+ AsyncCustomToolsResourceWithStreamingResponse,
+)
+
+__all__ = [
+ "CustomToolsResource",
+ "AsyncCustomToolsResource",
+ "CustomToolsResourceWithRawResponse",
+ "AsyncCustomToolsResourceWithRawResponse",
+ "CustomToolsResourceWithStreamingResponse",
+ "AsyncCustomToolsResourceWithStreamingResponse",
+ "WebmcpResource",
+ "AsyncWebmcpResource",
+ "WebmcpResourceWithRawResponse",
+ "AsyncWebmcpResourceWithRawResponse",
+ "WebmcpResourceWithStreamingResponse",
+ "AsyncWebmcpResourceWithStreamingResponse",
+]
diff --git a/src/kernel/resources/browsers/webmcp/custom_tools.py b/src/kernel/resources/browsers/webmcp/custom_tools.py
new file mode 100644
index 00000000..e5126306
--- /dev/null
+++ b/src/kernel/resources/browsers/webmcp/custom_tools.py
@@ -0,0 +1,401 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from __future__ import annotations
+
+import httpx
+
+from ...._types import Body, Omit, Query, Headers, NoneType, NotGiven, omit, not_given
+from ...._utils import path_template, maybe_transform, async_maybe_transform
+from ...._compat import cached_property
+from ...._resource import SyncAPIResource, AsyncAPIResource
+from ...._response import (
+ to_raw_response_wrapper,
+ to_streamed_response_wrapper,
+ async_to_raw_response_wrapper,
+ async_to_streamed_response_wrapper,
+)
+from ...._base_client import make_request_options
+from ....types.browsers.webmcp import custom_tool_add_params
+from ....types.browsers.webmcp.custom_tools_response import CustomToolsResponse
+
+__all__ = ["CustomToolsResource", "AsyncCustomToolsResource"]
+
+
+class CustomToolsResource(SyncAPIResource):
+ """Discover and invoke native page tools across the browser instance."""
+
+ @cached_property
+ def with_raw_response(self) -> CustomToolsResourceWithRawResponse:
+ """
+ This property can be used as a prefix for any HTTP method call to return
+ the raw response object instead of the parsed content.
+
+ For more information, see https://www.github.com/kernel/kernel-python-sdk#accessing-raw-response-data-eg-headers
+ """
+ return CustomToolsResourceWithRawResponse(self)
+
+ @cached_property
+ def with_streaming_response(self) -> CustomToolsResourceWithStreamingResponse:
+ """
+ An alternative to `.with_raw_response` that doesn't eagerly read the response body.
+
+ For more information, see https://www.github.com/kernel/kernel-python-sdk#with_streaming_response
+ """
+ return CustomToolsResourceWithStreamingResponse(self)
+
+ def list(
+ self,
+ id_or_name: str,
+ *,
+ # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
+ # The extra values given here take precedence over values defined on the client or passed to this method.
+ extra_headers: Headers | None = None,
+ extra_query: Query | None = None,
+ extra_body: Body | None = None,
+ timeout: float | httpx.Timeout | None | NotGiven = not_given,
+ ) -> CustomToolsResponse:
+ """
+ Returns every registered custom tool with its generated ID, namespace, matcher,
+ and MCP tool metadata.
+
+ Args:
+ extra_headers: Send extra headers
+
+ extra_query: Add additional query parameters to the request
+
+ extra_body: Add additional JSON properties to the request
+
+ timeout: Override the client-level default timeout for this request, in seconds
+ """
+ if not id_or_name:
+ raise ValueError(f"Expected a non-empty value for `id_or_name` but received {id_or_name!r}")
+ return self._get(
+ path_template("/browsers/{id_or_name}/webmcp/custom-tools", id_or_name=id_or_name),
+ options=make_request_options(
+ extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
+ ),
+ cast_to=CustomToolsResponse,
+ )
+
+ def add(
+ self,
+ id_or_name: str,
+ *,
+ namespace: str,
+ source: str,
+ force_overwrite_namespace: 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,
+ ) -> CustomToolsResponse:
+ """Add a namespaced batch of custom tools.
+
+ A custom tool can be page-backed or
+ CDP-backed. Page-backed tools execute in the page via JavaScript. CDP-backed
+ tools execute via CDP and can use all browser REPL tools (see `/repl`). The
+ source must evaluate to a non-empty array of definitions with URL matchers, tool
+ metadata (including an optional output schema), and execute functions. The batch
+ is added atomically. Matchers apply to top-level documents and nested frames,
+ including out-of-process iframes; each matching tool is exposed once on the
+ tab's top-level document and appears in the WebMCP tool snapshot.
+
+ To update one tool, list the tools, delete its ID, and add its replacement. Set
+ force_overwrite_namespace to replace every existing tool in the namespace
+ atomically; omitted or false adds tools without replacing existing ones.
+ Existing invocations continue.
+
+ Args:
+ source: JavaScript expression that evaluates to a non-empty array of custom tool
+ definitions. Limited to 8,000,000 bytes when UTF-8 encoded, so multi-byte
+ characters reduce the allowed character count.
+
+ force_overwrite_namespace: Atomically replace all existing tools in this namespace with this batch when
+ true.
+
+ extra_headers: Send extra headers
+
+ extra_query: Add additional query parameters to the request
+
+ extra_body: Add additional JSON properties to the request
+
+ timeout: Override the client-level default timeout for this request, in seconds
+ """
+ if not id_or_name:
+ raise ValueError(f"Expected a non-empty value for `id_or_name` but received {id_or_name!r}")
+ return self._post(
+ path_template("/browsers/{id_or_name}/webmcp/custom-tools", id_or_name=id_or_name),
+ body=maybe_transform(
+ {
+ "namespace": namespace,
+ "source": source,
+ "force_overwrite_namespace": force_overwrite_namespace,
+ },
+ custom_tool_add_params.CustomToolAddParams,
+ ),
+ options=make_request_options(
+ extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
+ ),
+ cast_to=CustomToolsResponse,
+ )
+
+ def remove(
+ self,
+ id: str,
+ *,
+ id_or_name: str,
+ # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
+ # The extra values given here take precedence over values defined on the client or passed to this method.
+ extra_headers: Headers | None = None,
+ extra_query: Query | None = None,
+ extra_body: Body | None = None,
+ timeout: float | httpx.Timeout | None | NotGiven = not_given,
+ ) -> None:
+ """Removes one custom tool by generated ID.
+
+ An invocation already in progress is
+ not canceled.
+
+ Args:
+ extra_headers: Send extra headers
+
+ extra_query: Add additional query parameters to the request
+
+ extra_body: Add additional JSON properties to the request
+
+ timeout: Override the client-level default timeout for this request, in seconds
+ """
+ if not id_or_name:
+ raise ValueError(f"Expected a non-empty value for `id_or_name` but received {id_or_name!r}")
+ if not id:
+ raise ValueError(f"Expected a non-empty value for `id` but received {id!r}")
+ extra_headers = {"Accept": "*/*", **(extra_headers or {})}
+ return self._delete(
+ path_template("/browsers/{id_or_name}/webmcp/custom-tools/{id}", id_or_name=id_or_name, id=id),
+ options=make_request_options(
+ extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
+ ),
+ cast_to=NoneType,
+ )
+
+
+class AsyncCustomToolsResource(AsyncAPIResource):
+ """Discover and invoke native page tools across the browser instance."""
+
+ @cached_property
+ def with_raw_response(self) -> AsyncCustomToolsResourceWithRawResponse:
+ """
+ This property can be used as a prefix for any HTTP method call to return
+ the raw response object instead of the parsed content.
+
+ For more information, see https://www.github.com/kernel/kernel-python-sdk#accessing-raw-response-data-eg-headers
+ """
+ return AsyncCustomToolsResourceWithRawResponse(self)
+
+ @cached_property
+ def with_streaming_response(self) -> AsyncCustomToolsResourceWithStreamingResponse:
+ """
+ An alternative to `.with_raw_response` that doesn't eagerly read the response body.
+
+ For more information, see https://www.github.com/kernel/kernel-python-sdk#with_streaming_response
+ """
+ return AsyncCustomToolsResourceWithStreamingResponse(self)
+
+ async def list(
+ self,
+ id_or_name: str,
+ *,
+ # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
+ # The extra values given here take precedence over values defined on the client or passed to this method.
+ extra_headers: Headers | None = None,
+ extra_query: Query | None = None,
+ extra_body: Body | None = None,
+ timeout: float | httpx.Timeout | None | NotGiven = not_given,
+ ) -> CustomToolsResponse:
+ """
+ Returns every registered custom tool with its generated ID, namespace, matcher,
+ and MCP tool metadata.
+
+ Args:
+ extra_headers: Send extra headers
+
+ extra_query: Add additional query parameters to the request
+
+ extra_body: Add additional JSON properties to the request
+
+ timeout: Override the client-level default timeout for this request, in seconds
+ """
+ if not id_or_name:
+ raise ValueError(f"Expected a non-empty value for `id_or_name` but received {id_or_name!r}")
+ return await self._get(
+ path_template("/browsers/{id_or_name}/webmcp/custom-tools", id_or_name=id_or_name),
+ options=make_request_options(
+ extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
+ ),
+ cast_to=CustomToolsResponse,
+ )
+
+ async def add(
+ self,
+ id_or_name: str,
+ *,
+ namespace: str,
+ source: str,
+ force_overwrite_namespace: 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,
+ ) -> CustomToolsResponse:
+ """Add a namespaced batch of custom tools.
+
+ A custom tool can be page-backed or
+ CDP-backed. Page-backed tools execute in the page via JavaScript. CDP-backed
+ tools execute via CDP and can use all browser REPL tools (see `/repl`). The
+ source must evaluate to a non-empty array of definitions with URL matchers, tool
+ metadata (including an optional output schema), and execute functions. The batch
+ is added atomically. Matchers apply to top-level documents and nested frames,
+ including out-of-process iframes; each matching tool is exposed once on the
+ tab's top-level document and appears in the WebMCP tool snapshot.
+
+ To update one tool, list the tools, delete its ID, and add its replacement. Set
+ force_overwrite_namespace to replace every existing tool in the namespace
+ atomically; omitted or false adds tools without replacing existing ones.
+ Existing invocations continue.
+
+ Args:
+ source: JavaScript expression that evaluates to a non-empty array of custom tool
+ definitions. Limited to 8,000,000 bytes when UTF-8 encoded, so multi-byte
+ characters reduce the allowed character count.
+
+ force_overwrite_namespace: Atomically replace all existing tools in this namespace with this batch when
+ true.
+
+ extra_headers: Send extra headers
+
+ extra_query: Add additional query parameters to the request
+
+ extra_body: Add additional JSON properties to the request
+
+ timeout: Override the client-level default timeout for this request, in seconds
+ """
+ if not id_or_name:
+ raise ValueError(f"Expected a non-empty value for `id_or_name` but received {id_or_name!r}")
+ return await self._post(
+ path_template("/browsers/{id_or_name}/webmcp/custom-tools", id_or_name=id_or_name),
+ body=await async_maybe_transform(
+ {
+ "namespace": namespace,
+ "source": source,
+ "force_overwrite_namespace": force_overwrite_namespace,
+ },
+ custom_tool_add_params.CustomToolAddParams,
+ ),
+ options=make_request_options(
+ extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
+ ),
+ cast_to=CustomToolsResponse,
+ )
+
+ async def remove(
+ self,
+ id: str,
+ *,
+ id_or_name: str,
+ # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
+ # The extra values given here take precedence over values defined on the client or passed to this method.
+ extra_headers: Headers | None = None,
+ extra_query: Query | None = None,
+ extra_body: Body | None = None,
+ timeout: float | httpx.Timeout | None | NotGiven = not_given,
+ ) -> None:
+ """Removes one custom tool by generated ID.
+
+ An invocation already in progress is
+ not canceled.
+
+ Args:
+ extra_headers: Send extra headers
+
+ extra_query: Add additional query parameters to the request
+
+ extra_body: Add additional JSON properties to the request
+
+ timeout: Override the client-level default timeout for this request, in seconds
+ """
+ if not id_or_name:
+ raise ValueError(f"Expected a non-empty value for `id_or_name` but received {id_or_name!r}")
+ if not 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(
+ path_template("/browsers/{id_or_name}/webmcp/custom-tools/{id}", id_or_name=id_or_name, id=id),
+ options=make_request_options(
+ extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
+ ),
+ cast_to=NoneType,
+ )
+
+
+class CustomToolsResourceWithRawResponse:
+ def __init__(self, custom_tools: CustomToolsResource) -> None:
+ self._custom_tools = custom_tools
+
+ self.list = to_raw_response_wrapper(
+ custom_tools.list,
+ )
+ self.add = to_raw_response_wrapper(
+ custom_tools.add,
+ )
+ self.remove = to_raw_response_wrapper(
+ custom_tools.remove,
+ )
+
+
+class AsyncCustomToolsResourceWithRawResponse:
+ def __init__(self, custom_tools: AsyncCustomToolsResource) -> None:
+ self._custom_tools = custom_tools
+
+ self.list = async_to_raw_response_wrapper(
+ custom_tools.list,
+ )
+ self.add = async_to_raw_response_wrapper(
+ custom_tools.add,
+ )
+ self.remove = async_to_raw_response_wrapper(
+ custom_tools.remove,
+ )
+
+
+class CustomToolsResourceWithStreamingResponse:
+ def __init__(self, custom_tools: CustomToolsResource) -> None:
+ self._custom_tools = custom_tools
+
+ self.list = to_streamed_response_wrapper(
+ custom_tools.list,
+ )
+ self.add = to_streamed_response_wrapper(
+ custom_tools.add,
+ )
+ self.remove = to_streamed_response_wrapper(
+ custom_tools.remove,
+ )
+
+
+class AsyncCustomToolsResourceWithStreamingResponse:
+ def __init__(self, custom_tools: AsyncCustomToolsResource) -> None:
+ self._custom_tools = custom_tools
+
+ self.list = async_to_streamed_response_wrapper(
+ custom_tools.list,
+ )
+ self.add = async_to_streamed_response_wrapper(
+ custom_tools.add,
+ )
+ self.remove = async_to_streamed_response_wrapper(
+ custom_tools.remove,
+ )
diff --git a/src/kernel/resources/browsers/webmcp.py b/src/kernel/resources/browsers/webmcp/webmcp.py
similarity index 71%
rename from src/kernel/resources/browsers/webmcp.py
rename to src/kernel/resources/browsers/webmcp/webmcp.py
index 450a5c4f..dc030e34 100644
--- a/src/kernel/resources/browsers/webmcp.py
+++ b/src/kernel/resources/browsers/webmcp/webmcp.py
@@ -6,20 +6,28 @@
import httpx
-from ..._types import Body, Omit, Query, Headers, NotGiven, omit, not_given
-from ..._utils import path_template, maybe_transform, async_maybe_transform
-from ..._compat import cached_property
-from ..._resource import SyncAPIResource, AsyncAPIResource
-from ..._response import (
+from ...._types import Body, Omit, Query, Headers, NotGiven, omit, not_given
+from ...._utils import path_template, maybe_transform, async_maybe_transform
+from ...._compat import cached_property
+from ...._resource import SyncAPIResource, AsyncAPIResource
+from ...._response import (
to_raw_response_wrapper,
to_streamed_response_wrapper,
async_to_raw_response_wrapper,
async_to_streamed_response_wrapper,
)
-from ..._base_client import make_request_options
-from ...types.browsers import webmcp_invoke_tool_params
-from ...types.browsers.tools_response import ToolsResponse
-from ...types.browsers.invocation_result import InvocationResult
+from .custom_tools import (
+ CustomToolsResource,
+ AsyncCustomToolsResource,
+ CustomToolsResourceWithRawResponse,
+ AsyncCustomToolsResourceWithRawResponse,
+ CustomToolsResourceWithStreamingResponse,
+ AsyncCustomToolsResourceWithStreamingResponse,
+)
+from ...._base_client import make_request_options
+from ....types.browsers import webmcp_list_tools_params, webmcp_invoke_tool_params
+from ....types.browsers.tools_response import ToolsResponse
+from ....types.browsers.invocation_result import InvocationResult
__all__ = ["WebmcpResource", "AsyncWebmcpResource"]
@@ -27,6 +35,11 @@
class WebmcpResource(SyncAPIResource):
"""Discover and invoke native page tools across the browser instance."""
+ @cached_property
+ def custom_tools(self) -> CustomToolsResource:
+ """Discover and invoke native page tools across the browser instance."""
+ return CustomToolsResource(self._client)
+
@cached_property
def with_raw_response(self) -> WebmcpResourceWithRawResponse:
"""
@@ -69,7 +82,8 @@ def invoke_tool(
then submit through Playwright or computer interaction without invoking the tool
again. If the tab or embedded frame disappears, or the request times out after
invocation begins, the response reports outcome_unknown and the tool is not
- retried.
+ retried. CDP-backed custom tool outputs above 240 KiB return an error rather
+ than a truncated result.
Args:
input: Tool input, limited to 1 MiB after JSON serialization.
@@ -104,6 +118,7 @@ def list_tools(
self,
id_or_name: str,
*,
+ exclude_custom: 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,
@@ -112,12 +127,16 @@ def list_tools(
timeout: float | httpx.Timeout | None | NotGiven = not_given,
) -> ToolsResponse:
"""
- Returns a snapshot of native WebMCP tools available across every open tab and
- embedded frame in the browser. Each tool includes an opaque tool_ref for
- invoking that exact live registration. Tools disappear when their document
- closes or navigates away.
+ Returns a snapshot of native and custom WebMCP tools available across every open
+ tab and embedded frame in the browser. Each tool includes an opaque tool_ref for
+ invoking that exact live registration, nested tool metadata, and source
+ information. Custom tools include their generated ID, namespace, and CDP
+ target_id in source. Tools disappear when their document closes or navigates
+ away. Use exclude_custom to return only page-provided tools.
Args:
+ exclude_custom: Exclude custom tools when true.
+
extra_headers: Send extra headers
extra_query: Add additional query parameters to the request
@@ -131,7 +150,13 @@ def list_tools(
return self._get(
path_template("/browsers/{id_or_name}/webmcp/tools", id_or_name=id_or_name),
options=make_request_options(
- extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
+ extra_headers=extra_headers,
+ extra_query=extra_query,
+ extra_body=extra_body,
+ timeout=timeout,
+ query=maybe_transform(
+ {"exclude_custom": exclude_custom}, webmcp_list_tools_params.WebmcpListToolsParams
+ ),
),
cast_to=ToolsResponse,
)
@@ -140,6 +165,11 @@ def list_tools(
class AsyncWebmcpResource(AsyncAPIResource):
"""Discover and invoke native page tools across the browser instance."""
+ @cached_property
+ def custom_tools(self) -> AsyncCustomToolsResource:
+ """Discover and invoke native page tools across the browser instance."""
+ return AsyncCustomToolsResource(self._client)
+
@cached_property
def with_raw_response(self) -> AsyncWebmcpResourceWithRawResponse:
"""
@@ -182,7 +212,8 @@ async def invoke_tool(
then submit through Playwright or computer interaction without invoking the tool
again. If the tab or embedded frame disappears, or the request times out after
invocation begins, the response reports outcome_unknown and the tool is not
- retried.
+ retried. CDP-backed custom tool outputs above 240 KiB return an error rather
+ than a truncated result.
Args:
input: Tool input, limited to 1 MiB after JSON serialization.
@@ -217,6 +248,7 @@ async def list_tools(
self,
id_or_name: str,
*,
+ exclude_custom: 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,
@@ -225,12 +257,16 @@ async def list_tools(
timeout: float | httpx.Timeout | None | NotGiven = not_given,
) -> ToolsResponse:
"""
- Returns a snapshot of native WebMCP tools available across every open tab and
- embedded frame in the browser. Each tool includes an opaque tool_ref for
- invoking that exact live registration. Tools disappear when their document
- closes or navigates away.
+ Returns a snapshot of native and custom WebMCP tools available across every open
+ tab and embedded frame in the browser. Each tool includes an opaque tool_ref for
+ invoking that exact live registration, nested tool metadata, and source
+ information. Custom tools include their generated ID, namespace, and CDP
+ target_id in source. Tools disappear when their document closes or navigates
+ away. Use exclude_custom to return only page-provided tools.
Args:
+ exclude_custom: Exclude custom tools when true.
+
extra_headers: Send extra headers
extra_query: Add additional query parameters to the request
@@ -244,7 +280,13 @@ async def list_tools(
return await self._get(
path_template("/browsers/{id_or_name}/webmcp/tools", id_or_name=id_or_name),
options=make_request_options(
- extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
+ extra_headers=extra_headers,
+ extra_query=extra_query,
+ extra_body=extra_body,
+ timeout=timeout,
+ query=await async_maybe_transform(
+ {"exclude_custom": exclude_custom}, webmcp_list_tools_params.WebmcpListToolsParams
+ ),
),
cast_to=ToolsResponse,
)
@@ -261,6 +303,11 @@ def __init__(self, webmcp: WebmcpResource) -> None:
webmcp.list_tools,
)
+ @cached_property
+ def custom_tools(self) -> CustomToolsResourceWithRawResponse:
+ """Discover and invoke native page tools across the browser instance."""
+ return CustomToolsResourceWithRawResponse(self._webmcp.custom_tools)
+
class AsyncWebmcpResourceWithRawResponse:
def __init__(self, webmcp: AsyncWebmcpResource) -> None:
@@ -273,6 +320,11 @@ def __init__(self, webmcp: AsyncWebmcpResource) -> None:
webmcp.list_tools,
)
+ @cached_property
+ def custom_tools(self) -> AsyncCustomToolsResourceWithRawResponse:
+ """Discover and invoke native page tools across the browser instance."""
+ return AsyncCustomToolsResourceWithRawResponse(self._webmcp.custom_tools)
+
class WebmcpResourceWithStreamingResponse:
def __init__(self, webmcp: WebmcpResource) -> None:
@@ -285,6 +337,11 @@ def __init__(self, webmcp: WebmcpResource) -> None:
webmcp.list_tools,
)
+ @cached_property
+ def custom_tools(self) -> CustomToolsResourceWithStreamingResponse:
+ """Discover and invoke native page tools across the browser instance."""
+ return CustomToolsResourceWithStreamingResponse(self._webmcp.custom_tools)
+
class AsyncWebmcpResourceWithStreamingResponse:
def __init__(self, webmcp: AsyncWebmcpResource) -> None:
@@ -296,3 +353,8 @@ def __init__(self, webmcp: AsyncWebmcpResource) -> None:
self.list_tools = async_to_streamed_response_wrapper(
webmcp.list_tools,
)
+
+ @cached_property
+ def custom_tools(self) -> AsyncCustomToolsResourceWithStreamingResponse:
+ """Discover and invoke native page tools across the browser instance."""
+ return AsyncCustomToolsResourceWithStreamingResponse(self._webmcp.custom_tools)
diff --git a/src/kernel/types/browsers/__init__.py b/src/kernel/types/browsers/__init__.py
index 34ed9b73..169e6ccf 100644
--- a/src/kernel/types/browsers/__init__.py
+++ b/src/kernel/types/browsers/__init__.py
@@ -6,12 +6,14 @@
from .tool_frame import ToolFrame as ToolFrame
from .tool_source import ToolSource as ToolSource
from .f_move_params import FMoveParams as FMoveParams
+from .tool_metadata import ToolMetadata as ToolMetadata
from .tools_response import ToolsResponse as ToolsResponse
from .f_upload_params import FUploadParams as FUploadParams
from .tool_annotations import ToolAnnotations as ToolAnnotations
from .invocation_result import InvocationResult as InvocationResult
from .log_stream_params import LogStreamParams as LogStreamParams
from .browser_call_stack import BrowserCallStack as BrowserCallStack
+from .custom_tool_source import CustomToolSource as CustomToolSource
from .f_file_info_params import FFileInfoParams as FFileInfoParams
from .f_read_file_params import FReadFileParams as FReadFileParams
from .f_list_files_params import FListFilesParams as FListFilesParams
@@ -46,6 +48,7 @@
from .telemetry_events_params import TelemetryEventsParams as TelemetryEventsParams
from .telemetry_stream_params import TelemetryStreamParams as TelemetryStreamParams
from .browser_telemetry_config import BrowserTelemetryConfig as BrowserTelemetryConfig
+from .webmcp_list_tools_params import WebmcpListToolsParams as WebmcpListToolsParams
from .browser_cdp_command_event import BrowserCdpCommandEvent as BrowserCdpCommandEvent
from .browser_cdp_connect_event import BrowserCdpConnectEvent as BrowserCdpConnectEvent
from .browser_console_log_event import BrowserConsoleLogEvent as BrowserConsoleLogEvent
diff --git a/src/kernel/types/browsers/custom_tool_source.py b/src/kernel/types/browsers/custom_tool_source.py
new file mode 100644
index 00000000..0ab59ab7
--- /dev/null
+++ b/src/kernel/types/browsers/custom_tool_source.py
@@ -0,0 +1,11 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from ..._models import BaseModel
+
+__all__ = ["CustomToolSource"]
+
+
+class CustomToolSource(BaseModel):
+ id: str
+
+ namespace: str
diff --git a/src/kernel/types/browsers/tool.py b/src/kernel/types/browsers/tool.py
index 12e63767..5bde27d0 100644
--- a/src/kernel/types/browsers/tool.py
+++ b/src/kernel/types/browsers/tool.py
@@ -1,31 +1,26 @@
# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
-from typing import Dict, Optional
-
from ..._models import BaseModel
from .tool_source import ToolSource
-from .tool_annotations import ToolAnnotations
+from .tool_metadata import ToolMetadata
__all__ = ["Tool"]
class Tool(BaseModel):
- description: str
-
- input_schema: Dict[str, object]
-
- name: str
-
source: ToolSource
+ tool: ToolMetadata
+ """
+ Tool metadata follows the
+ [MCP Tool definition](https://modelcontextprotocol.io/specification/2025-11-25/server/tools#tool)
+ and the
+ [WebMCP RegisteredTool definition](https://webmachinelearning.github.io/webmcp/#dictdef-registeredtool).
+ outputSchema is optional for page and custom tools.
+ """
+
tool_ref: str
"""Opaque reference for invoking this exact live registration.
It becomes invalid when its document or browser process is replaced.
"""
-
- annotations: Optional[ToolAnnotations] = None
- """Page-provided behavioral hints.
-
- These values are untrusted and are not enforced by Kernel.
- """
diff --git a/src/kernel/types/browsers/tool_annotations.py b/src/kernel/types/browsers/tool_annotations.py
index 9f9b097d..7c12d425 100644
--- a/src/kernel/types/browsers/tool_annotations.py
+++ b/src/kernel/types/browsers/tool_annotations.py
@@ -1,20 +1,31 @@
# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+from typing import Optional
+
+from pydantic import Field as FieldInfo
+
from ..._models import BaseModel
__all__ = ["ToolAnnotations"]
class ToolAnnotations(BaseModel):
- """Page-provided behavioral hints.
-
- These values are untrusted and are not enforced by Kernel.
"""
+ Tool-provided behavioral hints from the [MCP tool specification](https://modelcontextprotocol.io/specification/2025-11-25/server/tools#tool)
+ and the [WebMCP ToolAnnotations definition](https://webmachinelearning.github.io/webmcp/#dictdef-toolannotations).
+ These hints are untrusted and are not enforced by Kernel.
+ """
+
+ autosubmit: Optional[bool] = None
+
+ consequential_hint: Optional[bool] = FieldInfo(alias="consequentialHint", default=None)
+
+ destructive_hint: Optional[bool] = FieldInfo(alias="destructiveHint", default=None)
- autosubmit: bool
+ idempotent_hint: Optional[bool] = FieldInfo(alias="idempotentHint", default=None)
- consequential: bool
+ open_world_hint: Optional[bool] = FieldInfo(alias="openWorldHint", default=None)
- read_only: bool
+ read_only_hint: Optional[bool] = FieldInfo(alias="readOnlyHint", default=None)
- untrusted_content: bool
+ untrusted_content_hint: Optional[bool] = FieldInfo(alias="untrustedContentHint", default=None)
diff --git a/src/kernel/types/browsers/tool_metadata.py b/src/kernel/types/browsers/tool_metadata.py
new file mode 100644
index 00000000..747f461e
--- /dev/null
+++ b/src/kernel/types/browsers/tool_metadata.py
@@ -0,0 +1,37 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from typing import Dict, Optional
+
+from pydantic import Field as FieldInfo
+
+from ..._models import BaseModel
+from .tool_annotations import ToolAnnotations
+
+__all__ = ["ToolMetadata"]
+
+
+class ToolMetadata(BaseModel):
+ """
+ Tool metadata follows the [MCP Tool definition](https://modelcontextprotocol.io/specification/2025-11-25/server/tools#tool)
+ and the [WebMCP RegisteredTool definition](https://webmachinelearning.github.io/webmcp/#dictdef-registeredtool).
+ outputSchema is optional for page and custom tools.
+ """
+
+ description: str
+
+ input_schema: Dict[str, object] = FieldInfo(alias="inputSchema")
+
+ name: str
+
+ annotations: Optional[ToolAnnotations] = None
+ """
+ Tool-provided behavioral hints from the
+ [MCP tool specification](https://modelcontextprotocol.io/specification/2025-11-25/server/tools#tool)
+ and the
+ [WebMCP ToolAnnotations definition](https://webmachinelearning.github.io/webmcp/#dictdef-toolannotations).
+ These hints are untrusted and are not enforced by Kernel.
+ """
+
+ output_schema: Optional[Dict[str, object]] = FieldInfo(alias="outputSchema", default=None)
+
+ title: Optional[str] = None
diff --git a/src/kernel/types/browsers/tool_source.py b/src/kernel/types/browsers/tool_source.py
index ed95f856..65b57412 100644
--- a/src/kernel/types/browsers/tool_source.py
+++ b/src/kernel/types/browsers/tool_source.py
@@ -4,6 +4,7 @@
from ..._models import BaseModel
from .tool_frame import ToolFrame
+from .custom_tool_source import CustomToolSource
__all__ = ["ToolSource"]
@@ -32,3 +33,11 @@ class ToolSource(BaseModel):
Monotonically increasing identifier for the browser window during the current
browser process.
"""
+
+ custom: Optional[CustomToolSource] = None
+
+ target_id: Optional[str] = None
+ """
+ CDP target ID for a custom tool's registration tab; omitted for page-provided
+ tools.
+ """
diff --git a/src/kernel/types/browsers/webmcp/__init__.py b/src/kernel/types/browsers/webmcp/__init__.py
new file mode 100644
index 00000000..cbad2766
--- /dev/null
+++ b/src/kernel/types/browsers/webmcp/__init__.py
@@ -0,0 +1,8 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from __future__ import annotations
+
+from .match import Match as Match
+from .definition import Definition as Definition
+from .custom_tools_response import CustomToolsResponse as CustomToolsResponse
+from .custom_tool_add_params import CustomToolAddParams as CustomToolAddParams
diff --git a/src/kernel/types/browsers/webmcp/custom_tool_add_params.py b/src/kernel/types/browsers/webmcp/custom_tool_add_params.py
new file mode 100644
index 00000000..3fc195e2
--- /dev/null
+++ b/src/kernel/types/browsers/webmcp/custom_tool_add_params.py
@@ -0,0 +1,24 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from __future__ import annotations
+
+from typing_extensions import Required, TypedDict
+
+__all__ = ["CustomToolAddParams"]
+
+
+class CustomToolAddParams(TypedDict, total=False):
+ namespace: Required[str]
+
+ source: Required[str]
+ """
+ JavaScript expression that evaluates to a non-empty array of custom tool
+ definitions. Limited to 8,000,000 bytes when UTF-8 encoded, so multi-byte
+ characters reduce the allowed character count.
+ """
+
+ force_overwrite_namespace: bool
+ """
+ Atomically replace all existing tools in this namespace with this batch when
+ true.
+ """
diff --git a/src/kernel/types/browsers/webmcp/custom_tools_response.py b/src/kernel/types/browsers/webmcp/custom_tools_response.py
new file mode 100644
index 00000000..4b5df6bd
--- /dev/null
+++ b/src/kernel/types/browsers/webmcp/custom_tools_response.py
@@ -0,0 +1,12 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from typing import List
+
+from ...._models import BaseModel
+from .definition import Definition
+
+__all__ = ["CustomToolsResponse"]
+
+
+class CustomToolsResponse(BaseModel):
+ tools: List[Definition]
diff --git a/src/kernel/types/browsers/webmcp/definition.py b/src/kernel/types/browsers/webmcp/definition.py
new file mode 100644
index 00000000..3a5cbeaa
--- /dev/null
+++ b/src/kernel/types/browsers/webmcp/definition.py
@@ -0,0 +1,26 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from .match import Match
+from ...._models import BaseModel
+from ..tool_metadata import ToolMetadata
+
+__all__ = ["Definition"]
+
+
+class Definition(BaseModel):
+ id: str
+
+ kind: str
+
+ match: Match
+
+ namespace: str
+
+ tool: ToolMetadata
+ """
+ Tool metadata follows the
+ [MCP Tool definition](https://modelcontextprotocol.io/specification/2025-11-25/server/tools#tool)
+ and the
+ [WebMCP RegisteredTool definition](https://webmachinelearning.github.io/webmcp/#dictdef-registeredtool).
+ outputSchema is optional for page and custom tools.
+ """
diff --git a/src/kernel/types/browsers/webmcp/match.py b/src/kernel/types/browsers/webmcp/match.py
new file mode 100644
index 00000000..51523b24
--- /dev/null
+++ b/src/kernel/types/browsers/webmcp/match.py
@@ -0,0 +1,11 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from typing import List
+
+from ...._models import BaseModel
+
+__all__ = ["Match"]
+
+
+class Match(BaseModel):
+ url_patterns: List[str]
diff --git a/src/kernel/types/browsers/webmcp_list_tools_params.py b/src/kernel/types/browsers/webmcp_list_tools_params.py
new file mode 100644
index 00000000..f499c9ca
--- /dev/null
+++ b/src/kernel/types/browsers/webmcp_list_tools_params.py
@@ -0,0 +1,12 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from __future__ import annotations
+
+from typing_extensions import TypedDict
+
+__all__ = ["WebmcpListToolsParams"]
+
+
+class WebmcpListToolsParams(TypedDict, total=False):
+ exclude_custom: bool
+ """Exclude custom tools when true."""
diff --git a/tests/api_resources/browsers/test_webmcp.py b/tests/api_resources/browsers/test_webmcp.py
index a55d692c..ac354bca 100644
--- a/tests/api_resources/browsers/test_webmcp.py
+++ b/tests/api_resources/browsers/test_webmcp.py
@@ -82,7 +82,16 @@ def test_path_params_invoke_tool(self, client: Kernel) -> None:
@parametrize
def test_method_list_tools(self, client: Kernel) -> None:
webmcp = client.browsers.webmcp.list_tools(
- "htzv5orfit78e1m2biiifpbv",
+ id_or_name="htzv5orfit78e1m2biiifpbv",
+ )
+ assert_matches_type(ToolsResponse, webmcp, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ def test_method_list_tools_with_all_params(self, client: Kernel) -> None:
+ webmcp = client.browsers.webmcp.list_tools(
+ id_or_name="htzv5orfit78e1m2biiifpbv",
+ exclude_custom=True,
)
assert_matches_type(ToolsResponse, webmcp, path=["response"])
@@ -90,7 +99,7 @@ def test_method_list_tools(self, client: Kernel) -> None:
@parametrize
def test_raw_response_list_tools(self, client: Kernel) -> None:
response = client.browsers.webmcp.with_raw_response.list_tools(
- "htzv5orfit78e1m2biiifpbv",
+ id_or_name="htzv5orfit78e1m2biiifpbv",
)
assert response.is_closed is True
@@ -102,7 +111,7 @@ def test_raw_response_list_tools(self, client: Kernel) -> None:
@parametrize
def test_streaming_response_list_tools(self, client: Kernel) -> None:
with client.browsers.webmcp.with_streaming_response.list_tools(
- "htzv5orfit78e1m2biiifpbv",
+ id_or_name="htzv5orfit78e1m2biiifpbv",
) as response:
assert not response.is_closed
assert response.http_request.headers.get("X-Stainless-Lang") == "python"
@@ -117,7 +126,7 @@ def test_streaming_response_list_tools(self, client: Kernel) -> None:
def test_path_params_list_tools(self, client: Kernel) -> None:
with pytest.raises(ValueError, match=r"Expected a non-empty value for `id_or_name` but received ''"):
client.browsers.webmcp.with_raw_response.list_tools(
- "",
+ id_or_name="",
)
@@ -191,7 +200,16 @@ async def test_path_params_invoke_tool(self, async_client: AsyncKernel) -> None:
@parametrize
async def test_method_list_tools(self, async_client: AsyncKernel) -> None:
webmcp = await async_client.browsers.webmcp.list_tools(
- "htzv5orfit78e1m2biiifpbv",
+ id_or_name="htzv5orfit78e1m2biiifpbv",
+ )
+ assert_matches_type(ToolsResponse, webmcp, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ async def test_method_list_tools_with_all_params(self, async_client: AsyncKernel) -> None:
+ webmcp = await async_client.browsers.webmcp.list_tools(
+ id_or_name="htzv5orfit78e1m2biiifpbv",
+ exclude_custom=True,
)
assert_matches_type(ToolsResponse, webmcp, path=["response"])
@@ -199,7 +217,7 @@ async def test_method_list_tools(self, async_client: AsyncKernel) -> None:
@parametrize
async def test_raw_response_list_tools(self, async_client: AsyncKernel) -> None:
response = await async_client.browsers.webmcp.with_raw_response.list_tools(
- "htzv5orfit78e1m2biiifpbv",
+ id_or_name="htzv5orfit78e1m2biiifpbv",
)
assert response.is_closed is True
@@ -211,7 +229,7 @@ async def test_raw_response_list_tools(self, async_client: AsyncKernel) -> None:
@parametrize
async def test_streaming_response_list_tools(self, async_client: AsyncKernel) -> None:
async with async_client.browsers.webmcp.with_streaming_response.list_tools(
- "htzv5orfit78e1m2biiifpbv",
+ id_or_name="htzv5orfit78e1m2biiifpbv",
) as response:
assert not response.is_closed
assert response.http_request.headers.get("X-Stainless-Lang") == "python"
@@ -226,5 +244,5 @@ async def test_streaming_response_list_tools(self, async_client: AsyncKernel) ->
async def test_path_params_list_tools(self, async_client: AsyncKernel) -> None:
with pytest.raises(ValueError, match=r"Expected a non-empty value for `id_or_name` but received ''"):
await async_client.browsers.webmcp.with_raw_response.list_tools(
- "",
+ id_or_name="",
)
diff --git a/tests/api_resources/browsers/webmcp/__init__.py b/tests/api_resources/browsers/webmcp/__init__.py
new file mode 100644
index 00000000..fd8019a9
--- /dev/null
+++ b/tests/api_resources/browsers/webmcp/__init__.py
@@ -0,0 +1 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
diff --git a/tests/api_resources/browsers/webmcp/test_custom_tools.py b/tests/api_resources/browsers/webmcp/test_custom_tools.py
new file mode 100644
index 00000000..49b3b4ee
--- /dev/null
+++ b/tests/api_resources/browsers/webmcp/test_custom_tools.py
@@ -0,0 +1,334 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from __future__ import annotations
+
+import os
+from typing import Any, cast
+
+import pytest
+
+from kernel import Kernel, AsyncKernel
+from tests.utils import assert_matches_type
+from kernel.types.browsers.webmcp import CustomToolsResponse
+
+base_url = os.environ.get("TEST_API_BASE_URL", "http://127.0.0.1:4010")
+
+
+class TestCustomTools:
+ parametrize = pytest.mark.parametrize("client", [False, True], indirect=True, ids=["loose", "strict"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ def test_method_list(self, client: Kernel) -> None:
+ custom_tool = client.browsers.webmcp.custom_tools.list(
+ "id_or_name",
+ )
+ assert_matches_type(CustomToolsResponse, custom_tool, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ def test_raw_response_list(self, client: Kernel) -> None:
+ response = client.browsers.webmcp.custom_tools.with_raw_response.list(
+ "id_or_name",
+ )
+
+ assert response.is_closed is True
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+ custom_tool = response.parse()
+ assert_matches_type(CustomToolsResponse, custom_tool, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ def test_streaming_response_list(self, client: Kernel) -> None:
+ with client.browsers.webmcp.custom_tools.with_streaming_response.list(
+ "id_or_name",
+ ) as response:
+ assert not response.is_closed
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+
+ custom_tool = response.parse()
+ assert_matches_type(CustomToolsResponse, custom_tool, path=["response"])
+
+ assert cast(Any, response.is_closed) is True
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ def test_path_params_list(self, client: Kernel) -> None:
+ with pytest.raises(ValueError, match=r"Expected a non-empty value for `id_or_name` but received ''"):
+ client.browsers.webmcp.custom_tools.with_raw_response.list(
+ "",
+ )
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ def test_method_add(self, client: Kernel) -> None:
+ custom_tool = client.browsers.webmcp.custom_tools.add(
+ id_or_name="id_or_name",
+ namespace="namespace",
+ source="source",
+ )
+ assert_matches_type(CustomToolsResponse, custom_tool, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ def test_method_add_with_all_params(self, client: Kernel) -> None:
+ custom_tool = client.browsers.webmcp.custom_tools.add(
+ id_or_name="id_or_name",
+ namespace="namespace",
+ source="source",
+ force_overwrite_namespace=True,
+ )
+ assert_matches_type(CustomToolsResponse, custom_tool, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ def test_raw_response_add(self, client: Kernel) -> None:
+ response = client.browsers.webmcp.custom_tools.with_raw_response.add(
+ id_or_name="id_or_name",
+ namespace="namespace",
+ source="source",
+ )
+
+ assert response.is_closed is True
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+ custom_tool = response.parse()
+ assert_matches_type(CustomToolsResponse, custom_tool, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ def test_streaming_response_add(self, client: Kernel) -> None:
+ with client.browsers.webmcp.custom_tools.with_streaming_response.add(
+ id_or_name="id_or_name",
+ namespace="namespace",
+ source="source",
+ ) as response:
+ assert not response.is_closed
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+
+ custom_tool = response.parse()
+ assert_matches_type(CustomToolsResponse, custom_tool, path=["response"])
+
+ assert cast(Any, response.is_closed) is True
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ def test_path_params_add(self, client: Kernel) -> None:
+ with pytest.raises(ValueError, match=r"Expected a non-empty value for `id_or_name` but received ''"):
+ client.browsers.webmcp.custom_tools.with_raw_response.add(
+ id_or_name="",
+ namespace="namespace",
+ source="source",
+ )
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ def test_method_remove(self, client: Kernel) -> None:
+ custom_tool = client.browsers.webmcp.custom_tools.remove(
+ id="ct_n10b9798ad53ecc4y69z31e1",
+ id_or_name="id_or_name",
+ )
+ assert custom_tool is None
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ def test_raw_response_remove(self, client: Kernel) -> None:
+ response = client.browsers.webmcp.custom_tools.with_raw_response.remove(
+ id="ct_n10b9798ad53ecc4y69z31e1",
+ id_or_name="id_or_name",
+ )
+
+ assert response.is_closed is True
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+ custom_tool = response.parse()
+ assert custom_tool is None
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ def test_streaming_response_remove(self, client: Kernel) -> None:
+ with client.browsers.webmcp.custom_tools.with_streaming_response.remove(
+ id="ct_n10b9798ad53ecc4y69z31e1",
+ id_or_name="id_or_name",
+ ) as response:
+ assert not response.is_closed
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+
+ custom_tool = response.parse()
+ assert custom_tool is None
+
+ assert cast(Any, response.is_closed) is True
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ def test_path_params_remove(self, client: Kernel) -> None:
+ with pytest.raises(ValueError, match=r"Expected a non-empty value for `id_or_name` but received ''"):
+ client.browsers.webmcp.custom_tools.with_raw_response.remove(
+ id="ct_n10b9798ad53ecc4y69z31e1",
+ id_or_name="",
+ )
+
+ with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"):
+ client.browsers.webmcp.custom_tools.with_raw_response.remove(
+ id="",
+ id_or_name="id_or_name",
+ )
+
+
+class TestAsyncCustomTools:
+ parametrize = pytest.mark.parametrize(
+ "async_client", [False, True, {"http_client": "aiohttp"}], indirect=True, ids=["loose", "strict", "aiohttp"]
+ )
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ async def test_method_list(self, async_client: AsyncKernel) -> None:
+ custom_tool = await async_client.browsers.webmcp.custom_tools.list(
+ "id_or_name",
+ )
+ assert_matches_type(CustomToolsResponse, custom_tool, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ async def test_raw_response_list(self, async_client: AsyncKernel) -> None:
+ response = await async_client.browsers.webmcp.custom_tools.with_raw_response.list(
+ "id_or_name",
+ )
+
+ assert response.is_closed is True
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+ custom_tool = await response.parse()
+ assert_matches_type(CustomToolsResponse, custom_tool, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ async def test_streaming_response_list(self, async_client: AsyncKernel) -> None:
+ async with async_client.browsers.webmcp.custom_tools.with_streaming_response.list(
+ "id_or_name",
+ ) as response:
+ assert not response.is_closed
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+
+ custom_tool = await response.parse()
+ assert_matches_type(CustomToolsResponse, custom_tool, 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_list(self, async_client: AsyncKernel) -> None:
+ with pytest.raises(ValueError, match=r"Expected a non-empty value for `id_or_name` but received ''"):
+ await async_client.browsers.webmcp.custom_tools.with_raw_response.list(
+ "",
+ )
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ async def test_method_add(self, async_client: AsyncKernel) -> None:
+ custom_tool = await async_client.browsers.webmcp.custom_tools.add(
+ id_or_name="id_or_name",
+ namespace="namespace",
+ source="source",
+ )
+ assert_matches_type(CustomToolsResponse, custom_tool, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ async def test_method_add_with_all_params(self, async_client: AsyncKernel) -> None:
+ custom_tool = await async_client.browsers.webmcp.custom_tools.add(
+ id_or_name="id_or_name",
+ namespace="namespace",
+ source="source",
+ force_overwrite_namespace=True,
+ )
+ assert_matches_type(CustomToolsResponse, custom_tool, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ async def test_raw_response_add(self, async_client: AsyncKernel) -> None:
+ response = await async_client.browsers.webmcp.custom_tools.with_raw_response.add(
+ id_or_name="id_or_name",
+ namespace="namespace",
+ source="source",
+ )
+
+ assert response.is_closed is True
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+ custom_tool = await response.parse()
+ assert_matches_type(CustomToolsResponse, custom_tool, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ async def test_streaming_response_add(self, async_client: AsyncKernel) -> None:
+ async with async_client.browsers.webmcp.custom_tools.with_streaming_response.add(
+ id_or_name="id_or_name",
+ namespace="namespace",
+ source="source",
+ ) as response:
+ assert not response.is_closed
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+
+ custom_tool = await response.parse()
+ assert_matches_type(CustomToolsResponse, custom_tool, 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_add(self, async_client: AsyncKernel) -> None:
+ with pytest.raises(ValueError, match=r"Expected a non-empty value for `id_or_name` but received ''"):
+ await async_client.browsers.webmcp.custom_tools.with_raw_response.add(
+ id_or_name="",
+ namespace="namespace",
+ source="source",
+ )
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ async def test_method_remove(self, async_client: AsyncKernel) -> None:
+ custom_tool = await async_client.browsers.webmcp.custom_tools.remove(
+ id="ct_n10b9798ad53ecc4y69z31e1",
+ id_or_name="id_or_name",
+ )
+ assert custom_tool is None
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ async def test_raw_response_remove(self, async_client: AsyncKernel) -> None:
+ response = await async_client.browsers.webmcp.custom_tools.with_raw_response.remove(
+ id="ct_n10b9798ad53ecc4y69z31e1",
+ id_or_name="id_or_name",
+ )
+
+ assert response.is_closed is True
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+ custom_tool = await response.parse()
+ assert custom_tool is None
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ async def test_streaming_response_remove(self, async_client: AsyncKernel) -> None:
+ async with async_client.browsers.webmcp.custom_tools.with_streaming_response.remove(
+ id="ct_n10b9798ad53ecc4y69z31e1",
+ id_or_name="id_or_name",
+ ) as response:
+ assert not response.is_closed
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+
+ custom_tool = await response.parse()
+ assert custom_tool is None
+
+ assert cast(Any, response.is_closed) is True
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ async def test_path_params_remove(self, async_client: AsyncKernel) -> None:
+ with pytest.raises(ValueError, match=r"Expected a non-empty value for `id_or_name` but received ''"):
+ await async_client.browsers.webmcp.custom_tools.with_raw_response.remove(
+ id="ct_n10b9798ad53ecc4y69z31e1",
+ id_or_name="",
+ )
+
+ with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"):
+ await async_client.browsers.webmcp.custom_tools.with_raw_response.remove(
+ id="",
+ id_or_name="id_or_name",
+ )
From bbb14a623a138b53548df67327d848a41c778855 Mon Sep 17 00:00:00 2001
From: "kernel-internal[bot]"
<260533166+kernel-internal[bot]@users.noreply.github.com>
Date: Thu, 24 Sep 2026 19:19:30 +0000
Subject: [PATCH 302/366] release: 0.112.0
---
.release-please-manifest.json | 2 +-
CHANGELOG.md | 13 +++++++++++++
pyproject.toml | 2 +-
src/kernel/_version.py | 2 +-
4 files changed, 16 insertions(+), 3 deletions(-)
diff --git a/.release-please-manifest.json b/.release-please-manifest.json
index 2969a756..820b5c1a 100644
--- a/.release-please-manifest.json
+++ b/.release-please-manifest.json
@@ -1,3 +1,3 @@
{
- ".": "0.111.0"
+ ".": "0.112.0"
}
\ No newline at end of file
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 587846b9..284e73ea 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,18 @@
# Changelog
+## [0.112.0](https://github.com/kernel/kernel-python-sdk/compare/v0.111.0...v0.112.0) (2026-09-24)
+
+
+### Features
+
+* Add profiles to browser pool acquire ([f5c540c](https://github.com/kernel/kernel-python-sdk/commit/f5c540cdd1b0cee422cf47825dbc2212a5b6532f))
+* Correct monitor_disconnected description on computed state ([2b637f5](https://github.com/kernel/kernel-python-sdk/commit/2b637f562b34ad0963b8ba4d0ecca73fa1dd6349))
+* Expose Search rollout access in org entitlements ([cbbbe4c](https://github.com/kernel/kernel-python-sdk/commit/cbbbe4c865eb72c10be0f95e472921cf61898abc))
+* Filter archived telemetry events by type ([1fdfce2](https://github.com/kernel/kernel-python-sdk/commit/1fdfce2a3b05c5af330c93cd8e37a217f4d0f99b))
+* Proxy custom WebMCP tools in the control plane ([179dcd9](https://github.com/kernel/kernel-python-sdk/commit/179dcd9f2c9c27a46bc5f37d241da0f0626ccf82))
+* Publish iframe and worker browser target types ([848805a](https://github.com/kernel/kernel-python-sdk/commit/848805abcfa585e4299742cd8f7a3ff0306febfe))
+* Route browser destinations through selected proxies ([5a0a461](https://github.com/kernel/kernel-python-sdk/commit/5a0a461f25cbb59b43b2695fea8294357f3655fb))
+
## [0.111.0](https://github.com/kernel/kernel-python-sdk/compare/v0.110.0...v0.111.0) (2026-09-22)
diff --git a/pyproject.toml b/pyproject.toml
index c8bc1630..55d2a163 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -1,6 +1,6 @@
[project]
name = "kernel"
-version = "0.111.0"
+version = "0.112.0"
description = "The official Python library for the kernel API"
dynamic = ["readme"]
license = "Apache-2.0"
diff --git a/src/kernel/_version.py b/src/kernel/_version.py
index bd2462d5..8211aa6c 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.111.0" # x-release-please-version
+__version__ = "0.112.0" # x-release-please-version
From 1da4b212644549666e19f23146b8d1e2ab9511d9 Mon Sep 17 00:00:00 2001
From: "kernel-internal[bot]"
<260533166+kernel-internal[bot]@users.noreply.github.com>
Date: Sat, 26 Sep 2026 03:06:04 +0000
Subject: [PATCH 303/366] feat: Randomize automatic search provider routing
Stainless-Generated-From: 3a5146ad5bd2c016799d76d0361f72c61bfce90a
---
src/kernel/resources/search/providers.py | 6 ++++--
src/kernel/types/search/provider_list_params.py | 5 ++++-
2 files changed, 8 insertions(+), 3 deletions(-)
diff --git a/src/kernel/resources/search/providers.py b/src/kernel/resources/search/providers.py
index f913f1cd..c2b3b31e 100644
--- a/src/kernel/resources/search/providers.py
+++ b/src/kernel/resources/search/providers.py
@@ -64,7 +64,8 @@ def list(
contains no latency benchmarks. X-Request-Id identifies the request.
Args:
- slug: Optional concrete provider slug filter. Omit to list every provider.
+ slug: Optional concrete provider slug filter. Omit to list every provider. A slug that
+ is not listed returns an empty array.
extra_headers: Send extra headers
@@ -128,7 +129,8 @@ async def list(
contains no latency benchmarks. X-Request-Id identifies the request.
Args:
- slug: Optional concrete provider slug filter. Omit to list every provider.
+ slug: Optional concrete provider slug filter. Omit to list every provider. A slug that
+ is not listed returns an empty array.
extra_headers: Send extra headers
diff --git a/src/kernel/types/search/provider_list_params.py b/src/kernel/types/search/provider_list_params.py
index 07ef4659..908a1b17 100644
--- a/src/kernel/types/search/provider_list_params.py
+++ b/src/kernel/types/search/provider_list_params.py
@@ -9,4 +9,7 @@
class ProviderListParams(TypedDict, total=False):
slug: Literal["brave", "exa", "perplexity", "context", "parallel", "valyu", "octen", "you", "tavily", "serpapi"]
- """Optional concrete provider slug filter. Omit to list every provider."""
+ """Optional concrete provider slug filter.
+
+ Omit to list every provider. A slug that is not listed returns an empty array.
+ """
From 7d004def693a57505b4294307b272ba1fc4f78b2 Mon Sep 17 00:00:00 2001
From: "kernel-internal[bot]"
<260533166+kernel-internal[bot]@users.noreply.github.com>
Date: Sun, 27 Sep 2026 21:44:26 +0000
Subject: [PATCH 304/366] feat: Support 16GiB memory in browser pools
Stainless-Generated-From: 491b38f4736aca671f22d24be01e6d55fec9058b
---
src/kernel/resources/browser_pools.py | 22 +++++++++++++++++++
src/kernel/types/browser_pool.py | 4 ++++
.../types/browser_pool_create_params.py | 4 ++++
.../types/browser_pool_update_params.py | 9 +++++++-
tests/api_resources/test_browser_pools.py | 4 ++++
5 files changed, 42 insertions(+), 1 deletion(-)
diff --git a/src/kernel/resources/browser_pools.py b/src/kernel/resources/browser_pools.py
index 13dbceca..20fdc7fa 100644
--- a/src/kernel/resources/browser_pools.py
+++ b/src/kernel/resources/browser_pools.py
@@ -8,6 +8,7 @@
import httpx
from ..types import (
+ BrowserMemoryRequest,
browser_pool_list_params,
browser_pool_create_params,
browser_pool_delete_params,
@@ -29,6 +30,7 @@
from .._base_client import AsyncPaginator, make_request_options
from ..types.tags_param import TagsParam
from ..types.browser_pool import BrowserPool
+from ..types.browser_memory_request import BrowserMemoryRequest
from ..types.browser_network_config_param import BrowserNetworkConfigParam
from ..types.browser_pool_acquire_response import BrowserPoolAcquireResponse
from ..types.shared_params.browser_profile import BrowserProfile
@@ -69,6 +71,7 @@ def create(
fill_rate_per_minute: int | Omit = omit,
headless: bool | Omit = omit,
kiosk_mode: bool | Omit = omit,
+ memory: BrowserMemoryRequest | Omit = omit,
name: str | Omit = omit,
network: BrowserNetworkConfigParam | Omit = omit,
profile: browser_pool_create_params.Profile | Omit = omit,
@@ -116,6 +119,8 @@ def create(
kiosk_mode: If true, launches the browser in kiosk mode to hide address bar and tabs in live
view. Defaults to false.
+ memory: Memory requested for headful browsers in this pool.
+
name: Optional name for the browser pool. Must be unique within the project.
network: Network configuration applied to browsers in this pool.
@@ -187,6 +192,7 @@ def create(
"fill_rate_per_minute": fill_rate_per_minute,
"headless": headless,
"kiosk_mode": kiosk_mode,
+ "memory": memory,
"name": name,
"network": network,
"profile": profile,
@@ -250,6 +256,7 @@ def update(
fill_rate_per_minute: int | Omit = omit,
headless: bool | Omit = omit,
kiosk_mode: bool | Omit = omit,
+ memory: Literal["8GiB", "16GiB"] | Omit = omit,
name: str | Omit = omit,
network: BrowserNetworkConfigParam | Omit = omit,
profile: browser_pool_update_params.Profile | Omit = omit,
@@ -302,6 +309,10 @@ def update(
kiosk_mode: If provided, replaces whether browsers launch in kiosk mode.
+ memory: Memory requested for newly-warmed headful browsers in this pool. Existing
+ browsers retain their original allocation. Use discard_all_idle to replace idle
+ browsers.
+
name: If provided, replaces the pool name. Empty string is a no-op; the pool name
cannot be cleared or reset to empty once assigned.
@@ -380,6 +391,7 @@ def update(
"fill_rate_per_minute": fill_rate_per_minute,
"headless": headless,
"kiosk_mode": kiosk_mode,
+ "memory": memory,
"name": name,
"network": network,
"profile": profile,
@@ -706,6 +718,7 @@ async def create(
fill_rate_per_minute: int | Omit = omit,
headless: bool | Omit = omit,
kiosk_mode: bool | Omit = omit,
+ memory: BrowserMemoryRequest | Omit = omit,
name: str | Omit = omit,
network: BrowserNetworkConfigParam | Omit = omit,
profile: browser_pool_create_params.Profile | Omit = omit,
@@ -753,6 +766,8 @@ async def create(
kiosk_mode: If true, launches the browser in kiosk mode to hide address bar and tabs in live
view. Defaults to false.
+ memory: Memory requested for headful browsers in this pool.
+
name: Optional name for the browser pool. Must be unique within the project.
network: Network configuration applied to browsers in this pool.
@@ -824,6 +839,7 @@ async def create(
"fill_rate_per_minute": fill_rate_per_minute,
"headless": headless,
"kiosk_mode": kiosk_mode,
+ "memory": memory,
"name": name,
"network": network,
"profile": profile,
@@ -887,6 +903,7 @@ async def update(
fill_rate_per_minute: int | Omit = omit,
headless: bool | Omit = omit,
kiosk_mode: bool | Omit = omit,
+ memory: Literal["8GiB", "16GiB"] | Omit = omit,
name: str | Omit = omit,
network: BrowserNetworkConfigParam | Omit = omit,
profile: browser_pool_update_params.Profile | Omit = omit,
@@ -939,6 +956,10 @@ async def update(
kiosk_mode: If provided, replaces whether browsers launch in kiosk mode.
+ memory: Memory requested for newly-warmed headful browsers in this pool. Existing
+ browsers retain their original allocation. Use discard_all_idle to replace idle
+ browsers.
+
name: If provided, replaces the pool name. Empty string is a no-op; the pool name
cannot be cleared or reset to empty once assigned.
@@ -1017,6 +1038,7 @@ async def update(
"fill_rate_per_minute": fill_rate_per_minute,
"headless": headless,
"kiosk_mode": kiosk_mode,
+ "memory": memory,
"name": name,
"network": network,
"profile": profile,
diff --git a/src/kernel/types/browser_pool.py b/src/kernel/types/browser_pool.py
index 68f96450..58a1a9cd 100644
--- a/src/kernel/types/browser_pool.py
+++ b/src/kernel/types/browser_pool.py
@@ -5,6 +5,7 @@
from typing_extensions import Literal
from .._models import BaseModel
+from .browser_memory import BrowserMemory
from .browser_network_config import BrowserNetworkConfig
from .shared.browser_viewport import BrowserViewport
from .shared.browser_extension import BrowserExtension
@@ -73,6 +74,9 @@ class BrowserPoolConfig(BaseModel):
view.
"""
+ memory: Optional[BrowserMemory] = None
+ """Memory allocated to the browser session."""
+
name: Optional[str] = None
"""Optional name for the browser pool. Must be unique within the project."""
diff --git a/src/kernel/types/browser_pool_create_params.py b/src/kernel/types/browser_pool_create_params.py
index 33cff052..99fb61d2 100644
--- a/src/kernel/types/browser_pool_create_params.py
+++ b/src/kernel/types/browser_pool_create_params.py
@@ -5,6 +5,7 @@
from typing import Dict, Iterable, Optional
from typing_extensions import Literal, Required, TypedDict
+from .browser_memory_request import BrowserMemoryRequest
from .browser_network_config_param import BrowserNetworkConfigParam
from .shared_params.browser_viewport import BrowserViewport
from .shared_params.browser_extension import BrowserExtension
@@ -59,6 +60,9 @@ class BrowserPoolCreateParams(TypedDict, total=False):
view. Defaults to false.
"""
+ memory: BrowserMemoryRequest
+ """Memory requested for headful browsers in this pool."""
+
name: str
"""Optional name for the browser pool. Must be unique within the project."""
diff --git a/src/kernel/types/browser_pool_update_params.py b/src/kernel/types/browser_pool_update_params.py
index 9bf898f2..2ccfa9b9 100644
--- a/src/kernel/types/browser_pool_update_params.py
+++ b/src/kernel/types/browser_pool_update_params.py
@@ -3,7 +3,7 @@
from __future__ import annotations
from typing import Dict, Iterable, Optional
-from typing_extensions import TypedDict
+from typing_extensions import Literal, TypedDict
from .browser_network_config_param import BrowserNetworkConfigParam
from .shared_params.browser_viewport import BrowserViewport
@@ -61,6 +61,13 @@ class BrowserPoolUpdateParams(TypedDict, total=False):
kiosk_mode: bool
"""If provided, replaces whether browsers launch in kiosk mode."""
+ memory: Literal["8GiB", "16GiB"]
+ """Memory requested for newly-warmed headful browsers in this pool.
+
+ Existing browsers retain their original allocation. Use discard_all_idle to
+ replace idle browsers.
+ """
+
name: str
"""If provided, replaces the pool name.
diff --git a/tests/api_resources/test_browser_pools.py b/tests/api_resources/test_browser_pools.py
index e42664c4..d97b0704 100644
--- a/tests/api_resources/test_browser_pools.py
+++ b/tests/api_resources/test_browser_pools.py
@@ -44,6 +44,7 @@ def test_method_create_with_all_params(self, client: Kernel) -> None:
fill_rate_per_minute=0,
headless=False,
kiosk_mode=True,
+ memory="8GiB",
name="my-pool",
network={
"private_hosts": ["*.example.ts.net", "100.64.0.0/10"],
@@ -194,6 +195,7 @@ def test_method_update_with_all_params(self, client: Kernel) -> None:
fill_rate_per_minute=0,
headless=False,
kiosk_mode=True,
+ memory="8GiB",
name="my-pool",
network={
"private_hosts": ["*.example.ts.net", "100.64.0.0/10"],
@@ -593,6 +595,7 @@ async def test_method_create_with_all_params(self, async_client: AsyncKernel) ->
fill_rate_per_minute=0,
headless=False,
kiosk_mode=True,
+ memory="8GiB",
name="my-pool",
network={
"private_hosts": ["*.example.ts.net", "100.64.0.0/10"],
@@ -743,6 +746,7 @@ async def test_method_update_with_all_params(self, async_client: AsyncKernel) ->
fill_rate_per_minute=0,
headless=False,
kiosk_mode=True,
+ memory="8GiB",
name="my-pool",
network={
"private_hosts": ["*.example.ts.net", "100.64.0.0/10"],
From 2eabd23ab1b3ba810532869b1103f2a89b492bf9 Mon Sep 17 00:00:00 2001
From: "kernel-internal[bot]"
<260533166+kernel-internal[bot]@users.noreply.github.com>
Date: Sun, 27 Sep 2026 21:48:55 +0000
Subject: [PATCH 305/366] release: 0.113.0
---
.release-please-manifest.json | 2 +-
CHANGELOG.md | 8 ++++++++
pyproject.toml | 2 +-
src/kernel/_version.py | 2 +-
4 files changed, 11 insertions(+), 3 deletions(-)
diff --git a/.release-please-manifest.json b/.release-please-manifest.json
index 820b5c1a..939bb175 100644
--- a/.release-please-manifest.json
+++ b/.release-please-manifest.json
@@ -1,3 +1,3 @@
{
- ".": "0.112.0"
+ ".": "0.113.0"
}
\ No newline at end of file
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 284e73ea..347647b8 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,13 @@
# Changelog
+## [0.113.0](https://github.com/kernel/kernel-python-sdk/compare/v0.112.0...v0.113.0) (2026-09-27)
+
+
+### Features
+
+* Randomize automatic search provider routing ([1da4b21](https://github.com/kernel/kernel-python-sdk/commit/1da4b212644549666e19f23146b8d1e2ab9511d9))
+* Support 16GiB memory in browser pools ([7d004de](https://github.com/kernel/kernel-python-sdk/commit/7d004def693a57505b4294307b272ba1fc4f78b2))
+
## [0.112.0](https://github.com/kernel/kernel-python-sdk/compare/v0.111.0...v0.112.0) (2026-09-24)
diff --git a/pyproject.toml b/pyproject.toml
index 55d2a163..82dfdf81 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -1,6 +1,6 @@
[project]
name = "kernel"
-version = "0.112.0"
+version = "0.113.0"
description = "The official Python library for the kernel API"
dynamic = ["readme"]
license = "Apache-2.0"
diff --git a/src/kernel/_version.py b/src/kernel/_version.py
index 8211aa6c..3350781a 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.112.0" # x-release-please-version
+__version__ = "0.113.0" # x-release-please-version
From 28fb2d3c8fd83090d83404a0b407557585bf93f6 Mon Sep 17 00:00:00 2001
From: "kernel-internal[bot]"
<260533166+kernel-internal[bot]@users.noreply.github.com>
Date: Mon, 28 Sep 2026 14:20:00 +0000
Subject: [PATCH 306/366] feat: Accept telemetry.storage and settle it at
browser create
Stainless-Generated-From: 8f39036711533f5cf7d1dcae85d98c367dbf9d74
---
api.md | 1 +
.../types/auth/connection_create_params.py | 14 ++++++++++++++
.../types/auth/connection_login_params.py | 14 ++++++++++++++
.../types/auth/connection_update_params.py | 14 ++++++++++++++
src/kernel/types/auth/managed_auth.py | 14 ++++++++++++++
.../types/auth/managed_auth_browser_config.py | 14 ++++++++++++++
.../auth/managed_auth_browser_config_param.py | 14 ++++++++++++++
src/kernel/types/browser_create_params.py | 14 ++++++++++++++
.../types/browser_pool_acquire_params.py | 14 ++++++++++++++
src/kernel/types/browser_pool_create_params.py | 14 ++++++++++++++
src/kernel/types/browser_pool_update_params.py | 14 ++++++++++++++
src/kernel/types/browser_update_params.py | 14 ++++++++++++++
src/kernel/types/browsers/__init__.py | 1 +
.../types/browsers/browser_telemetry_config.py | 7 +++++++
.../browser_telemetry_storage_config.py | 18 ++++++++++++++++++
tests/api_resources/auth/test_connections.py | 12 ++++++++++++
tests/api_resources/test_browser_pools.py | 6 ++++++
tests/api_resources/test_browsers.py | 4 ++++
18 files changed, 203 insertions(+)
create mode 100644 src/kernel/types/browsers/browser_telemetry_storage_config.py
diff --git a/api.md b/api.md
index 1b130bb7..f8529996 100644
--- a/api.md
+++ b/api.md
@@ -207,6 +207,7 @@ from kernel.types.browsers import (
BrowserTelemetryEvent,
BrowserTelemetryExportConfig,
BrowserTelemetryOtlpExportConfig,
+ BrowserTelemetryStorageConfig,
TelemetryEventsResponse,
TelemetryStreamResponse,
)
diff --git a/src/kernel/types/auth/connection_create_params.py b/src/kernel/types/auth/connection_create_params.py
index 0ea02cbd..03dfc9c1 100644
--- a/src/kernel/types/auth/connection_create_params.py
+++ b/src/kernel/types/auth/connection_create_params.py
@@ -15,6 +15,7 @@
"BrowserTelemetryExport",
"BrowserTelemetryExportOtlp",
"BrowserTelemetryExportOtlpDestination",
+ "BrowserTelemetryStorage",
"Credential",
"Proxy",
]
@@ -166,6 +167,16 @@ class BrowserTelemetryExport(TypedDict, total=False):
"""
+class BrowserTelemetryStorage(TypedDict, total=False):
+ """Whether to persist this session's captured telemetry to Kernel storage."""
+
+ enabled: bool
+ """Whether captured telemetry is persisted to Kernel storage.
+
+ Defaults to true. Setting false is not supported yet and is rejected.
+ """
+
+
class BrowserTelemetry(TypedDict, total=False):
"""Deprecated.
@@ -205,6 +216,9 @@ class BrowserTelemetry(TypedDict, total=False):
Omit to capture without exporting.
"""
+ storage: BrowserTelemetryStorage
+ """Whether to persist this session's captured telemetry to Kernel storage."""
+
class Credential(TypedDict, total=False):
"""Reference to credentials for the auth connection.
diff --git a/src/kernel/types/auth/connection_login_params.py b/src/kernel/types/auth/connection_login_params.py
index 0d62506d..3c456c99 100644
--- a/src/kernel/types/auth/connection_login_params.py
+++ b/src/kernel/types/auth/connection_login_params.py
@@ -14,6 +14,7 @@
"BrowserTelemetryExport",
"BrowserTelemetryExportOtlp",
"BrowserTelemetryExportOtlpDestination",
+ "BrowserTelemetryStorage",
"Proxy",
]
@@ -92,6 +93,16 @@ class BrowserTelemetryExport(TypedDict, total=False):
"""
+class BrowserTelemetryStorage(TypedDict, total=False):
+ """Whether to persist this session's captured telemetry to Kernel storage."""
+
+ enabled: bool
+ """Whether captured telemetry is persisted to Kernel storage.
+
+ Defaults to true. Setting false is not supported yet and is rejected.
+ """
+
+
class BrowserTelemetry(TypedDict, total=False):
"""Deprecated.
@@ -131,6 +142,9 @@ class BrowserTelemetry(TypedDict, total=False):
Omit to capture without exporting.
"""
+ storage: BrowserTelemetryStorage
+ """Whether to persist this session's captured telemetry to Kernel storage."""
+
class Proxy(TypedDict, total=False):
"""Deprecated. Use browser.proxy. Retained during migration for existing clients."""
diff --git a/src/kernel/types/auth/connection_update_params.py b/src/kernel/types/auth/connection_update_params.py
index 21977396..db776ec9 100644
--- a/src/kernel/types/auth/connection_update_params.py
+++ b/src/kernel/types/auth/connection_update_params.py
@@ -15,6 +15,7 @@
"BrowserTelemetryExport",
"BrowserTelemetryExportOtlp",
"BrowserTelemetryExportOtlpDestination",
+ "BrowserTelemetryStorage",
"Credential",
"Proxy",
]
@@ -129,6 +130,16 @@ class BrowserTelemetryExport(TypedDict, total=False):
"""
+class BrowserTelemetryStorage(TypedDict, total=False):
+ """Whether to persist this session's captured telemetry to Kernel storage."""
+
+ enabled: bool
+ """Whether captured telemetry is persisted to Kernel storage.
+
+ Defaults to true. Setting false is not supported yet and is rejected.
+ """
+
+
class BrowserTelemetry(TypedDict, total=False):
"""Deprecated.
@@ -168,6 +179,9 @@ class BrowserTelemetry(TypedDict, total=False):
Omit to capture without exporting.
"""
+ storage: BrowserTelemetryStorage
+ """Whether to persist this session's captured telemetry to Kernel storage."""
+
class Credential(TypedDict, total=False):
"""Reference to credentials for the auth connection.
diff --git a/src/kernel/types/auth/managed_auth.py b/src/kernel/types/auth/managed_auth.py
index be202e61..aadb0718 100644
--- a/src/kernel/types/auth/managed_auth.py
+++ b/src/kernel/types/auth/managed_auth.py
@@ -14,6 +14,7 @@
"BrowserTelemetryExport",
"BrowserTelemetryExportOtlp",
"BrowserTelemetryExportOtlpDestination",
+ "BrowserTelemetryStorage",
"Choice",
"Credential",
"DiscoveredField",
@@ -68,6 +69,16 @@ class BrowserTelemetryExport(BaseModel):
"""
+class BrowserTelemetryStorage(BaseModel):
+ """Whether to persist this session's captured telemetry to Kernel storage."""
+
+ enabled: Optional[bool] = None
+ """Whether captured telemetry is persisted to Kernel storage.
+
+ Defaults to true. Setting false is not supported yet and is rejected.
+ """
+
+
class BrowserTelemetry(BaseModel):
"""Deprecated.
@@ -107,6 +118,9 @@ class BrowserTelemetry(BaseModel):
Omit to capture without exporting.
"""
+ storage: Optional[BrowserTelemetryStorage] = None
+ """Whether to persist this session's captured telemetry to Kernel storage."""
+
class Choice(BaseModel):
"""Canonical auth-flow choice awaiting user selection."""
diff --git a/src/kernel/types/auth/managed_auth_browser_config.py b/src/kernel/types/auth/managed_auth_browser_config.py
index 3309904e..363af851 100644
--- a/src/kernel/types/auth/managed_auth_browser_config.py
+++ b/src/kernel/types/auth/managed_auth_browser_config.py
@@ -13,6 +13,7 @@
"TelemetryExport",
"TelemetryExportOtlp",
"TelemetryExportOtlpDestination",
+ "TelemetryStorage",
]
@@ -60,6 +61,16 @@ class TelemetryExport(BaseModel):
"""
+class TelemetryStorage(BaseModel):
+ """Whether to persist this session's captured telemetry to Kernel storage."""
+
+ enabled: Optional[bool] = None
+ """Whether captured telemetry is persisted to Kernel storage.
+
+ Defaults to true. Setting false is not supported yet and is rejected.
+ """
+
+
class Telemetry(BaseModel):
"""Browser telemetry configuration using the same semantics as browser create."""
@@ -96,6 +107,9 @@ class Telemetry(BaseModel):
Omit to capture without exporting.
"""
+ storage: Optional[TelemetryStorage] = None
+ """Whether to persist this session's captured telemetry to Kernel storage."""
+
class ManagedAuthBrowserConfig(BaseModel):
"""
diff --git a/src/kernel/types/auth/managed_auth_browser_config_param.py b/src/kernel/types/auth/managed_auth_browser_config_param.py
index 6f8eab6d..ae9c171d 100644
--- a/src/kernel/types/auth/managed_auth_browser_config_param.py
+++ b/src/kernel/types/auth/managed_auth_browser_config_param.py
@@ -14,6 +14,7 @@
"TelemetryExport",
"TelemetryExportOtlp",
"TelemetryExportOtlpDestination",
+ "TelemetryStorage",
]
@@ -61,6 +62,16 @@ class TelemetryExport(TypedDict, total=False):
"""
+class TelemetryStorage(TypedDict, total=False):
+ """Whether to persist this session's captured telemetry to Kernel storage."""
+
+ enabled: bool
+ """Whether captured telemetry is persisted to Kernel storage.
+
+ Defaults to true. Setting false is not supported yet and is rejected.
+ """
+
+
class Telemetry(TypedDict, total=False):
"""Browser telemetry configuration using the same semantics as browser create."""
@@ -97,6 +108,9 @@ class Telemetry(TypedDict, total=False):
Omit to capture without exporting.
"""
+ storage: TelemetryStorage
+ """Whether to persist this session's captured telemetry to Kernel storage."""
+
class ManagedAuthBrowserConfigParam(TypedDict, total=False):
"""
diff --git a/src/kernel/types/browser_create_params.py b/src/kernel/types/browser_create_params.py
index ab486672..bcc71dc9 100644
--- a/src/kernel/types/browser_create_params.py
+++ b/src/kernel/types/browser_create_params.py
@@ -21,6 +21,7 @@
"TelemetryExport",
"TelemetryExportOtlp",
"TelemetryExportOtlpDestination",
+ "TelemetryStorage",
]
@@ -217,6 +218,16 @@ class TelemetryExport(TypedDict, total=False):
"""
+class TelemetryStorage(TypedDict, total=False):
+ """Whether to persist this session's captured telemetry to Kernel storage."""
+
+ enabled: bool
+ """Whether captured telemetry is persisted to Kernel storage.
+
+ Defaults to true. Setting false is not supported yet and is rejected.
+ """
+
+
class Telemetry(TypedDict, total=False):
"""Telemetry configuration for the browser session.
@@ -255,3 +266,6 @@ class Telemetry(TypedDict, total=False):
Omit to capture without exporting.
"""
+
+ storage: TelemetryStorage
+ """Whether to persist this session's captured telemetry to Kernel storage."""
diff --git a/src/kernel/types/browser_pool_acquire_params.py b/src/kernel/types/browser_pool_acquire_params.py
index 823d05b9..30a355e2 100644
--- a/src/kernel/types/browser_pool_acquire_params.py
+++ b/src/kernel/types/browser_pool_acquire_params.py
@@ -15,6 +15,7 @@
"TelemetryExport",
"TelemetryExportOtlp",
"TelemetryExportOtlpDestination",
+ "TelemetryStorage",
]
@@ -112,6 +113,16 @@ class TelemetryExport(TypedDict, total=False):
"""
+class TelemetryStorage(TypedDict, total=False):
+ """Whether to persist this session's captured telemetry to Kernel storage."""
+
+ enabled: bool
+ """Whether captured telemetry is persisted to Kernel storage.
+
+ Defaults to true. Setting false is not supported yet and is rejected.
+ """
+
+
class Telemetry(TypedDict, total=False):
"""Telemetry override for the acquired browser, applied to this lease only.
@@ -150,3 +161,6 @@ class Telemetry(TypedDict, total=False):
Omit to capture without exporting.
"""
+
+ storage: TelemetryStorage
+ """Whether to persist this session's captured telemetry to Kernel storage."""
diff --git a/src/kernel/types/browser_pool_create_params.py b/src/kernel/types/browser_pool_create_params.py
index 99fb61d2..ea734521 100644
--- a/src/kernel/types/browser_pool_create_params.py
+++ b/src/kernel/types/browser_pool_create_params.py
@@ -18,6 +18,7 @@
"TelemetryExport",
"TelemetryExportOtlp",
"TelemetryExportOtlpDestination",
+ "TelemetryStorage",
]
@@ -209,6 +210,16 @@ class TelemetryExport(TypedDict, total=False):
"""
+class TelemetryStorage(TypedDict, total=False):
+ """Whether to persist this session's captured telemetry to Kernel storage."""
+
+ enabled: bool
+ """Whether captured telemetry is persisted to Kernel storage.
+
+ Defaults to true. Setting false is not supported yet and is rejected.
+ """
+
+
class Telemetry(TypedDict, total=False):
"""Telemetry configuration applied to browsers warmed into this pool.
@@ -247,3 +258,6 @@ class Telemetry(TypedDict, total=False):
Omit to capture without exporting.
"""
+
+ storage: TelemetryStorage
+ """Whether to persist this session's captured telemetry to Kernel storage."""
diff --git a/src/kernel/types/browser_pool_update_params.py b/src/kernel/types/browser_pool_update_params.py
index 2ccfa9b9..e2190dc6 100644
--- a/src/kernel/types/browser_pool_update_params.py
+++ b/src/kernel/types/browser_pool_update_params.py
@@ -17,6 +17,7 @@
"TelemetryExport",
"TelemetryExportOtlp",
"TelemetryExportOtlpDestination",
+ "TelemetryStorage",
]
@@ -222,6 +223,16 @@ class TelemetryExport(TypedDict, total=False):
"""
+class TelemetryStorage(TypedDict, total=False):
+ """Whether to persist this session's captured telemetry to Kernel storage."""
+
+ enabled: bool
+ """Whether captured telemetry is persisted to Kernel storage.
+
+ Defaults to true. Setting false is not supported yet and is rejected.
+ """
+
+
class Telemetry(TypedDict, total=False):
"""If provided, updates the pool's telemetry configuration.
@@ -260,3 +271,6 @@ class Telemetry(TypedDict, total=False):
Omit to capture without exporting.
"""
+
+ storage: TelemetryStorage
+ """Whether to persist this session's captured telemetry to Kernel storage."""
diff --git a/src/kernel/types/browser_update_params.py b/src/kernel/types/browser_update_params.py
index c0592e28..19e7e592 100644
--- a/src/kernel/types/browser_update_params.py
+++ b/src/kernel/types/browser_update_params.py
@@ -17,6 +17,7 @@
"TelemetryExport",
"TelemetryExportOtlp",
"TelemetryExportOtlpDestination",
+ "TelemetryStorage",
"Viewport",
]
@@ -133,6 +134,16 @@ class TelemetryExport(TypedDict, total=False):
"""
+class TelemetryStorage(TypedDict, total=False):
+ """Whether to persist this session's captured telemetry to Kernel storage."""
+
+ enabled: bool
+ """Whether captured telemetry is persisted to Kernel storage.
+
+ Defaults to true. Setting false is not supported yet and is rejected.
+ """
+
+
class Telemetry(TypedDict, total=False):
"""Telemetry configuration.
@@ -172,6 +183,9 @@ class Telemetry(TypedDict, total=False):
Omit to capture without exporting.
"""
+ storage: TelemetryStorage
+ """Whether to persist this session's captured telemetry to Kernel storage."""
+
class Viewport(BrowserViewport, total=False):
"""Viewport configuration to apply to the browser session."""
diff --git a/src/kernel/types/browsers/__init__.py b/src/kernel/types/browsers/__init__.py
index 169e6ccf..2decde3a 100644
--- a/src/kernel/types/browsers/__init__.py
+++ b/src/kernel/types/browsers/__init__.py
@@ -88,6 +88,7 @@
from .computer_write_clipboard_params import ComputerWriteClipboardParams as ComputerWriteClipboardParams
from .browser_monitor_screenshot_event import BrowserMonitorScreenshotEvent as BrowserMonitorScreenshotEvent
from .browser_telemetry_control_config import BrowserTelemetryControlConfig as BrowserTelemetryControlConfig
+from .browser_telemetry_storage_config import BrowserTelemetryStorageConfig as BrowserTelemetryStorageConfig
from .computer_read_clipboard_response import ComputerReadClipboardResponse as ComputerReadClipboardResponse
from .browser_monitor_init_failed_event import BrowserMonitorInitFailedEvent as BrowserMonitorInitFailedEvent
from .browser_monitor_reconnected_event import BrowserMonitorReconnectedEvent as BrowserMonitorReconnectedEvent
diff --git a/src/kernel/types/browsers/browser_telemetry_config.py b/src/kernel/types/browsers/browser_telemetry_config.py
index 8d24e6bf..6eec504f 100644
--- a/src/kernel/types/browsers/browser_telemetry_config.py
+++ b/src/kernel/types/browsers/browser_telemetry_config.py
@@ -4,6 +4,7 @@
from ..._models import BaseModel
from .browser_telemetry_export_config import BrowserTelemetryExportConfig
+from .browser_telemetry_storage_config import BrowserTelemetryStorageConfig
from .browser_telemetry_categories_config import BrowserTelemetryCategoriesConfig
__all__ = ["BrowserTelemetryConfig"]
@@ -20,3 +21,9 @@ class BrowserTelemetryConfig(BaseModel):
Omitted when the export state is unknown.
"""
+
+ storage: Optional[BrowserTelemetryStorageConfig] = None
+ """Whether the session's captured telemetry is persisted to Kernel storage.
+
+ Omitted for browsers created before this setting existed, which persist it.
+ """
diff --git a/src/kernel/types/browsers/browser_telemetry_storage_config.py b/src/kernel/types/browsers/browser_telemetry_storage_config.py
new file mode 100644
index 00000000..f79f307e
--- /dev/null
+++ b/src/kernel/types/browsers/browser_telemetry_storage_config.py
@@ -0,0 +1,18 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from typing import Optional
+
+from ..._models import BaseModel
+
+__all__ = ["BrowserTelemetryStorageConfig"]
+
+
+class BrowserTelemetryStorageConfig(BaseModel):
+ """Kernel storage state for a session's captured telemetry."""
+
+ enabled: Optional[bool] = None
+ """Whether captured telemetry is persisted to Kernel storage.
+
+ When off, the session's events are only available on the live stream and through
+ any configured export.
+ """
diff --git a/tests/api_resources/auth/test_connections.py b/tests/api_resources/auth/test_connections.py
index 311b9971..f4bf875f 100644
--- a/tests/api_resources/auth/test_connections.py
+++ b/tests/api_resources/auth/test_connections.py
@@ -74,6 +74,7 @@ def test_method_create_with_all_params(self, client: Kernel) -> None:
"enabled": True,
}
},
+ "storage": {"enabled": True},
},
},
browser_telemetry={
@@ -102,6 +103,7 @@ def test_method_create_with_all_params(self, client: Kernel) -> None:
"enabled": True,
}
},
+ "storage": {"enabled": True},
},
credential={
"auto": True,
@@ -240,6 +242,7 @@ def test_method_update_with_all_params(self, client: Kernel) -> None:
"enabled": True,
}
},
+ "storage": {"enabled": True},
},
},
browser_telemetry={
@@ -268,6 +271,7 @@ def test_method_update_with_all_params(self, client: Kernel) -> None:
"enabled": True,
}
},
+ "storage": {"enabled": True},
},
credential={
"auto": True,
@@ -491,6 +495,7 @@ def test_method_login_with_all_params(self, client: Kernel) -> None:
"enabled": True,
}
},
+ "storage": {"enabled": True},
},
},
browser_telemetry={
@@ -519,6 +524,7 @@ def test_method_login_with_all_params(self, client: Kernel) -> None:
"enabled": True,
}
},
+ "storage": {"enabled": True},
},
proxy={
"id": "id",
@@ -737,6 +743,7 @@ async def test_method_create_with_all_params(self, async_client: AsyncKernel) ->
"enabled": True,
}
},
+ "storage": {"enabled": True},
},
},
browser_telemetry={
@@ -765,6 +772,7 @@ async def test_method_create_with_all_params(self, async_client: AsyncKernel) ->
"enabled": True,
}
},
+ "storage": {"enabled": True},
},
credential={
"auto": True,
@@ -903,6 +911,7 @@ async def test_method_update_with_all_params(self, async_client: AsyncKernel) ->
"enabled": True,
}
},
+ "storage": {"enabled": True},
},
},
browser_telemetry={
@@ -931,6 +940,7 @@ async def test_method_update_with_all_params(self, async_client: AsyncKernel) ->
"enabled": True,
}
},
+ "storage": {"enabled": True},
},
credential={
"auto": True,
@@ -1154,6 +1164,7 @@ async def test_method_login_with_all_params(self, async_client: AsyncKernel) ->
"enabled": True,
}
},
+ "storage": {"enabled": True},
},
},
browser_telemetry={
@@ -1182,6 +1193,7 @@ async def test_method_login_with_all_params(self, async_client: AsyncKernel) ->
"enabled": True,
}
},
+ "storage": {"enabled": True},
},
proxy={
"id": "id",
diff --git a/tests/api_resources/test_browser_pools.py b/tests/api_resources/test_browser_pools.py
index d97b0704..cd0c1b98 100644
--- a/tests/api_resources/test_browser_pools.py
+++ b/tests/api_resources/test_browser_pools.py
@@ -93,6 +93,7 @@ def test_method_create_with_all_params(self, client: Kernel) -> None:
"enabled": True,
}
},
+ "storage": {"enabled": True},
},
timeout_seconds=10,
viewport={
@@ -244,6 +245,7 @@ def test_method_update_with_all_params(self, client: Kernel) -> None:
"enabled": True,
}
},
+ "storage": {"enabled": True},
},
timeout_seconds=10,
viewport={
@@ -430,6 +432,7 @@ def test_method_acquire_with_all_params(self, client: Kernel) -> None:
"enabled": True,
}
},
+ "storage": {"enabled": True},
},
)
assert_matches_type(BrowserPoolAcquireResponse, browser_pool, path=["response"])
@@ -644,6 +647,7 @@ async def test_method_create_with_all_params(self, async_client: AsyncKernel) ->
"enabled": True,
}
},
+ "storage": {"enabled": True},
},
timeout_seconds=10,
viewport={
@@ -795,6 +799,7 @@ async def test_method_update_with_all_params(self, async_client: AsyncKernel) ->
"enabled": True,
}
},
+ "storage": {"enabled": True},
},
timeout_seconds=10,
viewport={
@@ -981,6 +986,7 @@ async def test_method_acquire_with_all_params(self, async_client: AsyncKernel) -
"enabled": True,
}
},
+ "storage": {"enabled": True},
},
)
assert_matches_type(BrowserPoolAcquireResponse, browser_pool, path=["response"])
diff --git a/tests/api_resources/test_browsers.py b/tests/api_resources/test_browsers.py
index 5caa570e..a9f0c46b 100644
--- a/tests/api_resources/test_browsers.py
+++ b/tests/api_resources/test_browsers.py
@@ -104,6 +104,7 @@ def test_method_create_with_all_params(self, client: Kernel) -> None:
"enabled": True,
}
},
+ "storage": {"enabled": True},
},
timeout_seconds=10,
vaults=[
@@ -250,6 +251,7 @@ def test_method_update_with_all_params(self, client: Kernel) -> None:
"enabled": True,
}
},
+ "storage": {"enabled": True},
},
viewport={
"height": 800,
@@ -646,6 +648,7 @@ async def test_method_create_with_all_params(self, async_client: AsyncKernel) ->
"enabled": True,
}
},
+ "storage": {"enabled": True},
},
timeout_seconds=10,
vaults=[
@@ -792,6 +795,7 @@ async def test_method_update_with_all_params(self, async_client: AsyncKernel) ->
"enabled": True,
}
},
+ "storage": {"enabled": True},
},
viewport={
"height": 800,
From 2ff8d4acacb1b8152b9c7d1b38067c1b02167f40 Mon Sep 17 00:00:00 2001
From: "kernel-internal[bot]"
<260533166+kernel-internal[bot]@users.noreply.github.com>
Date: Mon, 28 Sep 2026 16:59:35 +0000
Subject: [PATCH 307/366] feat: Allow export-only network and console telemetry
for BAA orgs
Stainless-Generated-From: 1ea489e4871d8446f69ea3f39d82ec4856d6491b
---
src/kernel/types/auth/connection_create_params.py | 3 ++-
src/kernel/types/auth/connection_login_params.py | 3 ++-
src/kernel/types/auth/connection_update_params.py | 3 ++-
src/kernel/types/auth/managed_auth.py | 3 ++-
src/kernel/types/auth/managed_auth_browser_config.py | 3 ++-
src/kernel/types/auth/managed_auth_browser_config_param.py | 3 ++-
src/kernel/types/browser_create_params.py | 3 ++-
src/kernel/types/browser_pool_acquire_params.py | 3 ++-
src/kernel/types/browser_pool_create_params.py | 3 ++-
src/kernel/types/browser_pool_update_params.py | 3 ++-
src/kernel/types/browser_update_params.py | 3 ++-
11 files changed, 22 insertions(+), 11 deletions(-)
diff --git a/src/kernel/types/auth/connection_create_params.py b/src/kernel/types/auth/connection_create_params.py
index 03dfc9c1..bf0ac55b 100644
--- a/src/kernel/types/auth/connection_create_params.py
+++ b/src/kernel/types/auth/connection_create_params.py
@@ -173,7 +173,8 @@ class BrowserTelemetryStorage(TypedDict, total=False):
enabled: bool
"""Whether captured telemetry is persisted to Kernel storage.
- Defaults to true. Setting false is not supported yet and is rejected.
+ Defaults to true. Setting false requires an OTLP destination and cannot be
+ changed after the browser is created.
"""
diff --git a/src/kernel/types/auth/connection_login_params.py b/src/kernel/types/auth/connection_login_params.py
index 3c456c99..19e452c9 100644
--- a/src/kernel/types/auth/connection_login_params.py
+++ b/src/kernel/types/auth/connection_login_params.py
@@ -99,7 +99,8 @@ class BrowserTelemetryStorage(TypedDict, total=False):
enabled: bool
"""Whether captured telemetry is persisted to Kernel storage.
- Defaults to true. Setting false is not supported yet and is rejected.
+ Defaults to true. Setting false requires an OTLP destination and cannot be
+ changed after the browser is created.
"""
diff --git a/src/kernel/types/auth/connection_update_params.py b/src/kernel/types/auth/connection_update_params.py
index db776ec9..36a9a9f8 100644
--- a/src/kernel/types/auth/connection_update_params.py
+++ b/src/kernel/types/auth/connection_update_params.py
@@ -136,7 +136,8 @@ class BrowserTelemetryStorage(TypedDict, total=False):
enabled: bool
"""Whether captured telemetry is persisted to Kernel storage.
- Defaults to true. Setting false is not supported yet and is rejected.
+ Defaults to true. Setting false requires an OTLP destination and cannot be
+ changed after the browser is created.
"""
diff --git a/src/kernel/types/auth/managed_auth.py b/src/kernel/types/auth/managed_auth.py
index aadb0718..eb5dda03 100644
--- a/src/kernel/types/auth/managed_auth.py
+++ b/src/kernel/types/auth/managed_auth.py
@@ -75,7 +75,8 @@ class BrowserTelemetryStorage(BaseModel):
enabled: Optional[bool] = None
"""Whether captured telemetry is persisted to Kernel storage.
- Defaults to true. Setting false is not supported yet and is rejected.
+ Defaults to true. Setting false requires an OTLP destination and cannot be
+ changed after the browser is created.
"""
diff --git a/src/kernel/types/auth/managed_auth_browser_config.py b/src/kernel/types/auth/managed_auth_browser_config.py
index 363af851..1cbab85a 100644
--- a/src/kernel/types/auth/managed_auth_browser_config.py
+++ b/src/kernel/types/auth/managed_auth_browser_config.py
@@ -67,7 +67,8 @@ class TelemetryStorage(BaseModel):
enabled: Optional[bool] = None
"""Whether captured telemetry is persisted to Kernel storage.
- Defaults to true. Setting false is not supported yet and is rejected.
+ Defaults to true. Setting false requires an OTLP destination and cannot be
+ changed after the browser is created.
"""
diff --git a/src/kernel/types/auth/managed_auth_browser_config_param.py b/src/kernel/types/auth/managed_auth_browser_config_param.py
index ae9c171d..47651227 100644
--- a/src/kernel/types/auth/managed_auth_browser_config_param.py
+++ b/src/kernel/types/auth/managed_auth_browser_config_param.py
@@ -68,7 +68,8 @@ class TelemetryStorage(TypedDict, total=False):
enabled: bool
"""Whether captured telemetry is persisted to Kernel storage.
- Defaults to true. Setting false is not supported yet and is rejected.
+ Defaults to true. Setting false requires an OTLP destination and cannot be
+ changed after the browser is created.
"""
diff --git a/src/kernel/types/browser_create_params.py b/src/kernel/types/browser_create_params.py
index bcc71dc9..fd33cee5 100644
--- a/src/kernel/types/browser_create_params.py
+++ b/src/kernel/types/browser_create_params.py
@@ -224,7 +224,8 @@ class TelemetryStorage(TypedDict, total=False):
enabled: bool
"""Whether captured telemetry is persisted to Kernel storage.
- Defaults to true. Setting false is not supported yet and is rejected.
+ Defaults to true. Setting false requires an OTLP destination and cannot be
+ changed after the browser is created.
"""
diff --git a/src/kernel/types/browser_pool_acquire_params.py b/src/kernel/types/browser_pool_acquire_params.py
index 30a355e2..0a74c176 100644
--- a/src/kernel/types/browser_pool_acquire_params.py
+++ b/src/kernel/types/browser_pool_acquire_params.py
@@ -119,7 +119,8 @@ class TelemetryStorage(TypedDict, total=False):
enabled: bool
"""Whether captured telemetry is persisted to Kernel storage.
- Defaults to true. Setting false is not supported yet and is rejected.
+ Defaults to true. Setting false requires an OTLP destination and cannot be
+ changed after the browser is created.
"""
diff --git a/src/kernel/types/browser_pool_create_params.py b/src/kernel/types/browser_pool_create_params.py
index ea734521..fa1f5f89 100644
--- a/src/kernel/types/browser_pool_create_params.py
+++ b/src/kernel/types/browser_pool_create_params.py
@@ -216,7 +216,8 @@ class TelemetryStorage(TypedDict, total=False):
enabled: bool
"""Whether captured telemetry is persisted to Kernel storage.
- Defaults to true. Setting false is not supported yet and is rejected.
+ Defaults to true. Setting false requires an OTLP destination and cannot be
+ changed after the browser is created.
"""
diff --git a/src/kernel/types/browser_pool_update_params.py b/src/kernel/types/browser_pool_update_params.py
index e2190dc6..ebf5cd35 100644
--- a/src/kernel/types/browser_pool_update_params.py
+++ b/src/kernel/types/browser_pool_update_params.py
@@ -229,7 +229,8 @@ class TelemetryStorage(TypedDict, total=False):
enabled: bool
"""Whether captured telemetry is persisted to Kernel storage.
- Defaults to true. Setting false is not supported yet and is rejected.
+ Defaults to true. Setting false requires an OTLP destination and cannot be
+ changed after the browser is created.
"""
diff --git a/src/kernel/types/browser_update_params.py b/src/kernel/types/browser_update_params.py
index 19e7e592..08cd43c2 100644
--- a/src/kernel/types/browser_update_params.py
+++ b/src/kernel/types/browser_update_params.py
@@ -140,7 +140,8 @@ class TelemetryStorage(TypedDict, total=False):
enabled: bool
"""Whether captured telemetry is persisted to Kernel storage.
- Defaults to true. Setting false is not supported yet and is rejected.
+ Defaults to true. Setting false requires an OTLP destination and cannot be
+ changed after the browser is created.
"""
From 74dbad8f0cdf5b414022ad907f6e20ee54893c59 Mon Sep 17 00:00:00 2001
From: "kernel-internal[bot]"
<260533166+kernel-internal[bot]@users.noreply.github.com>
Date: Mon, 28 Sep 2026 17:19:53 +0000
Subject: [PATCH 308/366] feat: Expose missing managed auth check URL as
verification unavailable
Stainless-Generated-From: 1995c3fd399a0dc1dbc1a88983b6bb0d0673d0f8
---
src/kernel/types/auth/managed_auth.py | 13 ++++++++++++-
1 file changed, 12 insertions(+), 1 deletion(-)
diff --git a/src/kernel/types/auth/managed_auth.py b/src/kernel/types/auth/managed_auth.py
index eb5dda03..aab06668 100644
--- a/src/kernel/types/auth/managed_auth.py
+++ b/src/kernel/types/auth/managed_auth.py
@@ -324,7 +324,11 @@ class ManagedAuth(BaseModel):
"""
status: Literal["AUTHENTICATED", "NEEDS_AUTH"]
- """Current authentication status of the managed profile"""
+ """Last known authentication status of the managed profile.
+
+ An inconclusive health check preserves this status and does not verify the
+ current session.
+ """
allowed_domains: Optional[List[str]] = None
"""Additional hostname roots valid for this auth flow, besides the primary domain.
@@ -519,6 +523,13 @@ class ManagedAuth(BaseModel):
3600 (1 hour), Free: 21600 (6 hours).
"""
+ health_check_unavailable_reason: Optional[Literal["no_auth_check_url"]] = None
+ """Why health checks cannot verify this connection.
+
+ Present when health checks are enabled but no auth check URL is available; a
+ recent last_auth_check_at is not evidence of a valid session.
+ """
+
health_checks: Optional[bool] = None
"""Whether periodic health checks are enabled for this connection.
From 2937b10cce135282b6badc7e5bf71d339500ffdb Mon Sep 17 00:00:00 2001
From: "kernel-internal[bot]"
<260533166+kernel-internal[bot]@users.noreply.github.com>
Date: Tue, 29 Sep 2026 20:17:01 +0000
Subject: [PATCH 309/366] feat: Let Vaults fill credentials from 1Password
Stainless-Generated-From: 6d937aebb18d99636c515de11d6958083e7ca24c
---
api.md | 15 +
src/kernel/resources/vaults/items.py | 846 ++++++-
src/kernel/types/vaults/__init__.py | 38 +
.../vaults/credential_account_vault_item.py | 73 +
...ential_account_vault_item_request_param.py | 15 +
.../types/vaults/credential_vault_item.py | 59 +-
.../credential_vault_item_request_param.py | 25 +-
.../vaults/credential_vault_item_spec.py | 24 +-
.../credential_vault_item_spec_input_param.py | 31 +-
.../vaults/credential_vault_item_state.py | 22 +-
.../vaults/item_perform_operation_params.py | 80 +-
src/kernel/types/vaults/item_upsert_params.py | 18 +-
.../kernel_credential_vault_item_spec.py | 25 +
..._credential_vault_item_spec_input_param.py | 34 +
.../kernel_credential_vault_item_state.py | 22 +
.../one_password_credential_account_spec.py | 23 +
..._password_credential_account_spec_param.py | 23 +
.../one_password_credential_account_state.py | 16 +
...one_password_credential_vault_item_spec.py | 57 +
..._credential_vault_item_spec_input_param.py | 84 +
...ne_password_credential_vault_item_state.py | 125 +
...fill_vault_item_operation_request_param.py | 33 +
...ssword_fill_vault_item_operation_result.py | 36 +
.../types/vaults/one_password_oauth_action.py | 14 +
...over_vault_item_operation_request_param.py | 16 +
...cess_vault_item_operation_request_param.py | 26 +
src/kernel/types/vaults/vault_item.py | 28 +-
src/kernel/types/vaults/vault_item_action.py | 2 +
.../vaults/vault_item_operation_response.py | 33 +-
.../types/vaults/wallet_vault_item_spec.py | 4 +-
tests/api_resources/vaults/test_items.py | 2081 ++++++++++++-----
31 files changed, 3164 insertions(+), 764 deletions(-)
create mode 100644 src/kernel/types/vaults/credential_account_vault_item.py
create mode 100644 src/kernel/types/vaults/credential_account_vault_item_request_param.py
create mode 100644 src/kernel/types/vaults/kernel_credential_vault_item_spec.py
create mode 100644 src/kernel/types/vaults/kernel_credential_vault_item_spec_input_param.py
create mode 100644 src/kernel/types/vaults/kernel_credential_vault_item_state.py
create mode 100644 src/kernel/types/vaults/one_password_credential_account_spec.py
create mode 100644 src/kernel/types/vaults/one_password_credential_account_spec_param.py
create mode 100644 src/kernel/types/vaults/one_password_credential_account_state.py
create mode 100644 src/kernel/types/vaults/one_password_credential_vault_item_spec.py
create mode 100644 src/kernel/types/vaults/one_password_credential_vault_item_spec_input_param.py
create mode 100644 src/kernel/types/vaults/one_password_credential_vault_item_state.py
create mode 100644 src/kernel/types/vaults/one_password_fill_vault_item_operation_request_param.py
create mode 100644 src/kernel/types/vaults/one_password_fill_vault_item_operation_result.py
create mode 100644 src/kernel/types/vaults/one_password_oauth_action.py
create mode 100644 src/kernel/types/vaults/one_password_recover_vault_item_operation_request_param.py
create mode 100644 src/kernel/types/vaults/one_password_request_access_vault_item_operation_request_param.py
diff --git a/api.md b/api.md
index f8529996..c15b15db 100644
--- a/api.md
+++ b/api.md
@@ -555,6 +555,8 @@ from kernel.types.vaults import (
CardVaultItemSpec,
CardVaultItemState,
CollectVaultItemOperationRequest,
+ CredentialAccountVaultItem,
+ CredentialAccountVaultItemRequest,
CredentialCollectionAction,
CredentialVaultFieldDefinition,
CredentialVaultFieldInput,
@@ -570,6 +572,19 @@ from kernel.types.vaults import (
CredentialVaultItemUpdateRequest,
FillVaultItemOperationRequest,
FillVaultItemOperationResult,
+ KernelCredentialVaultItemSpec,
+ KernelCredentialVaultItemSpecInput,
+ KernelCredentialVaultItemState,
+ OnePasswordCredentialAccountSpec,
+ OnePasswordCredentialAccountState,
+ OnePasswordCredentialVaultItemSpec,
+ OnePasswordCredentialVaultItemSpecInput,
+ OnePasswordCredentialVaultItemState,
+ OnePasswordFillVaultItemOperationRequest,
+ OnePasswordFillVaultItemOperationResult,
+ OnePasswordOAuthAction,
+ OnePasswordRecoverVaultItemOperationRequest,
+ OnePasswordRequestAccessVaultItemOperationRequest,
PrepareCheckoutVaultItemOperationRequest,
VaultCardAliases,
VaultCardFillField,
diff --git a/src/kernel/resources/vaults/items.py b/src/kernel/resources/vaults/items.py
index acaf0315..31aefa82 100644
--- a/src/kernel/resources/vaults/items.py
+++ b/src/kernel/resources/vaults/items.py
@@ -2,12 +2,13 @@
from __future__ import annotations
-from typing import Any, List, Iterable, cast
+from typing import Any, List, Union, Iterable, cast
+from datetime import datetime
from typing_extensions import Literal, overload
import httpx
-from ..._types import Body, Omit, Query, Headers, NoneType, NotGiven, omit, not_given
+from ..._types import Body, Omit, Query, Headers, NoneType, NotGiven, SequenceNotStr, omit, not_given
from ..._utils import path_template, required_args, maybe_transform, async_maybe_transform
from ..._compat import cached_property
from ..._resource import SyncAPIResource, AsyncAPIResource
@@ -34,6 +35,7 @@
from ...types.vaults.vault_item_operation_response import VaultItemOperationResponse
from ...types.vaults.credential_vault_item_spec_input_param import CredentialVaultItemSpecInputParam
from ...types.vaults.credential_vault_item_spec_update_param import CredentialVaultItemSpecUpdateParam
+from ...types.vaults.one_password_credential_account_spec_param import OnePasswordCredentialAccountSpecParam
__all__ = ["ItemsResource", "AsyncItemsResource"]
@@ -596,20 +598,299 @@ def perform_operation(
"""
...
+ @overload
+ def perform_operation(
+ self,
+ key: str,
+ *,
+ id_or_name: str,
+ browser_id: str,
+ type: Literal["1pw_create_access_request"],
+ goal: str | Omit = omit,
+ keywords: SequenceNotStr[str] | Omit = omit,
+ reason: str | Omit = omit,
+ # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
+ # The extra values given here take precedence over values defined on the client or passed to this method.
+ extra_headers: Headers | None = None,
+ extra_query: Query | None = None,
+ extra_body: Body | None = None,
+ timeout: float | httpx.Timeout | None | NotGiven = not_given,
+ ) -> VaultItemOperationResponse:
+ """
+ Retrieve the item first and invoke only an operation listed in
+ `available_operations`, following its natural-language description. Availability
+ is rechecked at execution time; unavailable operations return 409. Authorization
+ and preparation may call an external provider and return updated state. Link
+ cards advertise authorize without checkout context. Eligible unused AgentCard
+ cards advertise prepare_checkout, which requires checkout context and obtains
+ device approval before native Square Pay. Keep the returned approval page open,
+ poll until ready_to_submit, then submit before preparation.expires_at. Unused
+ preparations expire automatically and cannot be reused. If spend-request
+ creation is rejected with a non-retryable provider error, the card item is
+ deleted and the provider's error code and message are returned. Rate limits
+ return HTTP 429 and retain the card item; stop, back off, and retry the same
+ authorize operation.
+
+ Fill returns a value-free execution result. Validation failures before writing
+ return 400 (invalid request or targets), 403 (access or destination denied), 404
+ (resource not found), or 409 (item or browser not ready). Once writing starts,
+ known partial failures and indeterminate field outcomes return 200 with status
+ `failed` or `unknown`, not an automatic-retry signal. A transport error may
+ leave the outcome unknown; do not automatically retry.
+
+ Args:
+ browser_id: Kernel browser session used to invoke the extension.
+
+ 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
+ """
+ ...
+
+ @overload
+ def perform_operation(
+ self,
+ key: str,
+ *,
+ id_or_name: str,
+ browser_id: str,
+ type: Literal["1pw_access_request_status"],
+ timeout_seconds: int | Omit = omit,
+ # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
+ # The extra values given here take precedence over values defined on the client or passed to this method.
+ extra_headers: Headers | None = None,
+ extra_query: Query | None = None,
+ extra_body: Body | None = None,
+ timeout: float | httpx.Timeout | None | NotGiven = not_given,
+ ) -> VaultItemOperationResponse:
+ """
+ Retrieve the item first and invoke only an operation listed in
+ `available_operations`, following its natural-language description. Availability
+ is rechecked at execution time; unavailable operations return 409. Authorization
+ and preparation may call an external provider and return updated state. Link
+ cards advertise authorize without checkout context. Eligible unused AgentCard
+ cards advertise prepare_checkout, which requires checkout context and obtains
+ device approval before native Square Pay. Keep the returned approval page open,
+ poll until ready_to_submit, then submit before preparation.expires_at. Unused
+ preparations expire automatically and cannot be reused. If spend-request
+ creation is rejected with a non-retryable provider error, the card item is
+ deleted and the provider's error code and message are returned. Rate limits
+ return HTTP 429 and retain the card item; stop, back off, and retry the same
+ authorize operation.
+
+ Fill returns a value-free execution result. Validation failures before writing
+ return 400 (invalid request or targets), 403 (access or destination denied), 404
+ (resource not found), or 409 (item or browser not ready). Once writing starts,
+ known partial failures and indeterminate field outcomes return 200 with status
+ `failed` or `unknown`, not an automatic-retry signal. A transport error may
+ leave the outcome unknown; do not automatically retry.
+
+ Args:
+ extra_headers: Send extra headers
+
+ extra_query: Add additional query parameters to the request
+
+ extra_body: Add additional JSON properties to the request
+
+ timeout: Override the client-level default timeout for this request, in seconds
+ """
+ ...
+
+ @overload
+ def perform_operation(
+ self,
+ key: str,
+ *,
+ id_or_name: str,
+ browser_id: str,
+ page_url: str,
+ type: Literal["1pw_fill"],
+ entry_id: str | Omit = omit,
+ timeout_ms: int | Omit = omit,
+ # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
+ # The extra values given here take precedence over values defined on the client or passed to this method.
+ extra_headers: Headers | None = None,
+ extra_query: Query | None = None,
+ extra_body: Body | None = None,
+ timeout: float | httpx.Timeout | None | NotGiven = not_given,
+ ) -> VaultItemOperationResponse:
+ """
+ Retrieve the item first and invoke only an operation listed in
+ `available_operations`, following its natural-language description. Availability
+ is rechecked at execution time; unavailable operations return 409. Authorization
+ and preparation may call an external provider and return updated state. Link
+ cards advertise authorize without checkout context. Eligible unused AgentCard
+ cards advertise prepare_checkout, which requires checkout context and obtains
+ device approval before native Square Pay. Keep the returned approval page open,
+ poll until ready_to_submit, then submit before preparation.expires_at. Unused
+ preparations expire automatically and cannot be reused. If spend-request
+ creation is rejected with a non-retryable provider error, the card item is
+ deleted and the provider's error code and message are returned. Rate limits
+ return HTTP 429 and retain the card item; stop, back off, and retry the same
+ authorize operation.
+
+ Fill returns a value-free execution result. Validation failures before writing
+ return 400 (invalid request or targets), 403 (access or destination denied), 404
+ (resource not found), or 409 (item or browser not ready). Once writing starts,
+ known partial failures and indeterminate field outcomes return 200 with status
+ `failed` or `unknown`, not an automatic-retry signal. A transport error may
+ leave the outcome unknown; do not automatically retry.
+
+ Args:
+ browser_id: Browser session ID, not a reusable browser name.
+
+ page_url: Exact current top-level page URL. Must match exactly one open page in the
+ browser.
+
+ entry_id: ID of an approved request entry. Required when several approved entries have the
+ page's origin.
+
+ 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
+ """
+ ...
+
+ @overload
+ def perform_operation(
+ self,
+ key: str,
+ *,
+ id_or_name: str,
+ type: Literal["1pw_recover"],
+ # 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,
+ ) -> VaultItemOperationResponse:
+ """
+ Retrieve the item first and invoke only an operation listed in
+ `available_operations`, following its natural-language description. Availability
+ is rechecked at execution time; unavailable operations return 409. Authorization
+ and preparation may call an external provider and return updated state. Link
+ cards advertise authorize without checkout context. Eligible unused AgentCard
+ cards advertise prepare_checkout, which requires checkout context and obtains
+ device approval before native Square Pay. Keep the returned approval page open,
+ poll until ready_to_submit, then submit before preparation.expires_at. Unused
+ preparations expire automatically and cannot be reused. If spend-request
+ creation is rejected with a non-retryable provider error, the card item is
+ deleted and the provider's error code and message are returned. Rate limits
+ return HTTP 429 and retain the card item; stop, back off, and retry the same
+ authorize operation.
+
+ Fill returns a value-free execution result. Validation failures before writing
+ return 400 (invalid request or targets), 403 (access or destination denied), 404
+ (resource not found), or 409 (item or browser not ready). Once writing starts,
+ known partial failures and indeterminate field outcomes return 200 with status
+ `failed` or `unknown`, not an automatic-retry signal. A transport error may
+ leave the outcome unknown; do not automatically retry.
+
+ Args:
+ extra_headers: Send extra headers
+
+ extra_query: Add additional query parameters to the request
+
+ extra_body: Add additional JSON properties to the request
+
+ timeout: Override the client-level default timeout for this request, in seconds
+ """
+ ...
+
+ @overload
+ def perform_operation(
+ self,
+ key: str,
+ *,
+ id_or_name: str,
+ access_token: str,
+ type: Literal["1pw_update_access_token"],
+ access_token_expires_at: Union[str, datetime] | 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,
+ ) -> VaultItemOperationResponse:
+ """
+ Retrieve the item first and invoke only an operation listed in
+ `available_operations`, following its natural-language description. Availability
+ is rechecked at execution time; unavailable operations return 409. Authorization
+ and preparation may call an external provider and return updated state. Link
+ cards advertise authorize without checkout context. Eligible unused AgentCard
+ cards advertise prepare_checkout, which requires checkout context and obtains
+ device approval before native Square Pay. Keep the returned approval page open,
+ poll until ready_to_submit, then submit before preparation.expires_at. Unused
+ preparations expire automatically and cannot be reused. If spend-request
+ creation is rejected with a non-retryable provider error, the card item is
+ deleted and the provider's error code and message are returned. Rate limits
+ return HTTP 429 and retain the card item; stop, back off, and retry the same
+ authorize operation.
+
+ Fill returns a value-free execution result. Validation failures before writing
+ return 400 (invalid request or targets), 403 (access or destination denied), 404
+ (resource not found), or 409 (item or browser not ready). Once writing starts,
+ known partial failures and indeterminate field outcomes return 200 with status
+ `failed` or `unknown`, not an automatic-retry signal. A transport error may
+ leave the outcome unknown; do not automatically retry.
+
+ Args:
+ access_token_expires_at: Optional supplied expiry. Omit to clear the old expiry.
+
+ 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
+ """
+ ...
+
@required_args(
- ["id_or_name", "type"], ["id_or_name", "checkout", "type"], ["id_or_name", "browser_id", "fields", "type"]
+ ["id_or_name", "type"],
+ ["id_or_name", "checkout", "type"],
+ ["id_or_name", "browser_id", "fields", "type"],
+ ["id_or_name", "browser_id", "type"],
+ ["id_or_name", "browser_id", "page_url", "type"],
+ ["id_or_name", "access_token", "type"],
)
def perform_operation(
self,
key: str,
*,
id_or_name: str,
- type: Literal["authorize"] | Literal["collect"] | Literal["prepare_checkout"] | Literal["fill"],
+ type: Literal["authorize"]
+ | Literal["collect"]
+ | Literal["prepare_checkout"]
+ | Literal["fill"]
+ | Literal["1pw_create_access_request"]
+ | Literal["1pw_access_request_status"]
+ | Literal["1pw_fill"]
+ | Literal["1pw_recover"]
+ | Literal["1pw_update_access_token"],
checkout: VaultCheckoutContextParam | Omit = omit,
browser_id: str | Omit = omit,
fields: Iterable[VaultFillFieldParam] | Omit = omit,
page_url: str | Omit = omit,
timeout_ms: int | Omit = omit,
+ goal: str | Omit = omit,
+ keywords: SequenceNotStr[str] | Omit = omit,
+ reason: str | Omit = omit,
+ timeout_seconds: int | Omit = omit,
+ entry_id: str | Omit = omit,
+ access_token: str | Omit = omit,
+ access_token_expires_at: Union[str, datetime] | 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,
@@ -633,6 +914,13 @@ def perform_operation(
"fields": fields,
"page_url": page_url,
"timeout_ms": timeout_ms,
+ "goal": goal,
+ "keywords": keywords,
+ "reason": reason,
+ "timeout_seconds": timeout_seconds,
+ "entry_id": entry_id,
+ "access_token": access_token,
+ "access_token_expires_at": access_token_expires_at,
},
item_perform_operation_params.ItemPerformOperationParams,
),
@@ -728,6 +1016,43 @@ def upsert(
"""
...
+ @overload
+ def upsert(
+ self,
+ key: str,
+ *,
+ id_or_name: str,
+ spec: OnePasswordCredentialAccountSpecParam,
+ type: Literal["credential_account"],
+ # 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,
+ ) -> VaultItem:
+ """
+ Create an item under a key unique within its vault, or retrieve the existing
+ item when its specification matches. An identical card PUT returns the existing
+ card in any lifecycle state without polling the provider, reauthorizing,
+ replacing aliases, or resetting recovery. Conflicting specifications return 409.
+ Provider-specific authorization requirements and retry behavior are described in
+ the item's request schema. Do not use credential items to store, collect, or
+ fill credit card data, including card numbers (PANs), security codes (CVV/CVC),
+ or expiration dates. Use wallet and card item types for credit cards and payment
+ checkout instead.
+
+ Args:
+ extra_headers: Send extra headers
+
+ extra_query: Add additional query parameters to the request
+
+ extra_body: Add additional JSON properties to the request
+
+ timeout: Override the client-level default timeout for this request, in seconds
+ """
+ ...
+
@overload
def upsert(
self,
@@ -779,8 +1104,9 @@ def upsert(
id_or_name: str,
spec: item_upsert_params.WalletVaultItemRequestSpec
| CardVaultItemSpecParam
+ | OnePasswordCredentialAccountSpecParam
| CredentialVaultItemSpecInputParam,
- type: Literal["wallet"] | Literal["card"] | Literal["credential"],
+ type: Literal["wallet"] | Literal["card"] | Literal["credential_account"] | Literal["credential"],
# 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,
@@ -1029,24 +1355,273 @@ async def update(
),
)
- async def list(
+ async def list(
+ self,
+ id_or_name: str,
+ *,
+ # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
+ # The extra values given here take precedence over values defined on the client or passed to this method.
+ extra_headers: Headers | None = None,
+ extra_query: Query | None = None,
+ extra_body: Body | None = None,
+ timeout: float | httpx.Timeout | None | NotGiven = not_given,
+ ) -> ItemListResponse:
+ """Credential entries include safe field metadata and non-sensitive values.
+
+ Listing
+ never creates or renews collection sessions; only an existing unexpired active
+ session is included. Use single-item GET or collect to obtain a fresh link.
+
+ Args:
+ extra_headers: Send extra headers
+
+ extra_query: Add additional query parameters to the request
+
+ extra_body: Add additional JSON properties to the request
+
+ timeout: Override the client-level default timeout for this request, in seconds
+ """
+ if not id_or_name:
+ raise ValueError(f"Expected a non-empty value for `id_or_name` but received {id_or_name!r}")
+ return await self._get(
+ path_template("/vaults/{id_or_name}/items", id_or_name=id_or_name),
+ options=make_request_options(
+ extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
+ ),
+ cast_to=ItemListResponse,
+ )
+
+ async def delete(
+ self,
+ key: str,
+ *,
+ id_or_name: str,
+ # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
+ # The extra values given here take precedence over values defined on the client or passed to this method.
+ extra_headers: Headers | None = None,
+ extra_query: Query | None = None,
+ extra_body: Body | None = None,
+ timeout: float | httpx.Timeout | None | NotGiven = not_given,
+ ) -> None:
+ """
+ Unresolved payment operations normally block deletion, including operations on
+ child cards of a wallet. An AgentCard card in recovery_required whose checkout
+ create response returned no authorization ID may be explicitly abandoned by
+ deleting that card directly; deleting its wallet or vault remains blocked.
+ Deleting or recreating an item is not proof that a payment did not occur.
+
+ Args:
+ extra_headers: Send extra headers
+
+ extra_query: Add additional query parameters to the request
+
+ extra_body: Add additional JSON properties to the request
+
+ timeout: Override the client-level default timeout for this request, in seconds
+ """
+ if not id_or_name:
+ raise ValueError(f"Expected a non-empty value for `id_or_name` but received {id_or_name!r}")
+ if not key:
+ raise ValueError(f"Expected a non-empty value for `key` but received {key!r}")
+ extra_headers = {"Accept": "*/*", **(extra_headers or {})}
+ return await self._delete(
+ path_template("/vaults/{id_or_name}/items/{key}", id_or_name=id_or_name, key=key),
+ options=make_request_options(
+ extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
+ ),
+ cast_to=NoneType,
+ )
+
+ async def events(
+ self,
+ key: str,
+ *,
+ id_or_name: str,
+ after: str | Omit = omit,
+ wait: int | Omit = omit,
+ # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
+ # The extra values given here take precedence over values defined on the client or passed to this method.
+ extra_headers: Headers | None = None,
+ extra_query: Query | None = None,
+ extra_body: Body | None = None,
+ timeout: float | httpx.Timeout | None | NotGiven = not_given,
+ ) -> ItemEventsResponse:
+ """
+ List immutable audit events for a vault item
+
+ Args:
+ after: Return events after this event ID.
+
+ wait: Long-poll for new events for up to this many seconds.
+
+ extra_headers: Send extra headers
+
+ extra_query: Add additional query parameters to the request
+
+ extra_body: Add additional JSON properties to the request
+
+ timeout: Override the client-level default timeout for this request, in seconds
+ """
+ if not id_or_name:
+ raise ValueError(f"Expected a non-empty value for `id_or_name` but received {id_or_name!r}")
+ if not key:
+ raise ValueError(f"Expected a non-empty value for `key` but received {key!r}")
+ return await self._get(
+ path_template("/vaults/{id_or_name}/items/{key}/events", id_or_name=id_or_name, key=key),
+ options=make_request_options(
+ extra_headers=extra_headers,
+ extra_query=extra_query,
+ extra_body=extra_body,
+ timeout=timeout,
+ query=await async_maybe_transform(
+ {
+ "after": after,
+ "wait": wait,
+ },
+ item_events_params.ItemEventsParams,
+ ),
+ ),
+ cast_to=ItemEventsResponse,
+ )
+
+ @overload
+ async def perform_operation(
+ self,
+ key: str,
+ *,
+ id_or_name: str,
+ type: Literal["authorize"],
+ # 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,
+ ) -> VaultItemOperationResponse:
+ """
+ Retrieve the item first and invoke only an operation listed in
+ `available_operations`, following its natural-language description. Availability
+ is rechecked at execution time; unavailable operations return 409. Authorization
+ and preparation may call an external provider and return updated state. Link
+ cards advertise authorize without checkout context. Eligible unused AgentCard
+ cards advertise prepare_checkout, which requires checkout context and obtains
+ device approval before native Square Pay. Keep the returned approval page open,
+ poll until ready_to_submit, then submit before preparation.expires_at. Unused
+ preparations expire automatically and cannot be reused. If spend-request
+ creation is rejected with a non-retryable provider error, the card item is
+ deleted and the provider's error code and message are returned. Rate limits
+ return HTTP 429 and retain the card item; stop, back off, and retry the same
+ authorize operation.
+
+ Fill returns a value-free execution result. Validation failures before writing
+ return 400 (invalid request or targets), 403 (access or destination denied), 404
+ (resource not found), or 409 (item or browser not ready). Once writing starts,
+ known partial failures and indeterminate field outcomes return 200 with status
+ `failed` or `unknown`, not an automatic-retry signal. A transport error may
+ leave the outcome unknown; do not automatically retry.
+
+ Args:
+ extra_headers: Send extra headers
+
+ extra_query: Add additional query parameters to the request
+
+ extra_body: Add additional JSON properties to the request
+
+ timeout: Override the client-level default timeout for this request, in seconds
+ """
+ ...
+
+ @overload
+ async def perform_operation(
+ self,
+ key: str,
+ *,
+ id_or_name: str,
+ type: Literal["collect"],
+ # 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,
+ ) -> VaultItemOperationResponse:
+ """
+ Retrieve the item first and invoke only an operation listed in
+ `available_operations`, following its natural-language description. Availability
+ is rechecked at execution time; unavailable operations return 409. Authorization
+ and preparation may call an external provider and return updated state. Link
+ cards advertise authorize without checkout context. Eligible unused AgentCard
+ cards advertise prepare_checkout, which requires checkout context and obtains
+ device approval before native Square Pay. Keep the returned approval page open,
+ poll until ready_to_submit, then submit before preparation.expires_at. Unused
+ preparations expire automatically and cannot be reused. If spend-request
+ creation is rejected with a non-retryable provider error, the card item is
+ deleted and the provider's error code and message are returned. Rate limits
+ return HTTP 429 and retain the card item; stop, back off, and retry the same
+ authorize operation.
+
+ Fill returns a value-free execution result. Validation failures before writing
+ return 400 (invalid request or targets), 403 (access or destination denied), 404
+ (resource not found), or 409 (item or browser not ready). Once writing starts,
+ known partial failures and indeterminate field outcomes return 200 with status
+ `failed` or `unknown`, not an automatic-retry signal. A transport error may
+ leave the outcome unknown; do not automatically retry.
+
+ Args:
+ extra_headers: Send extra headers
+
+ extra_query: Add additional query parameters to the request
+
+ extra_body: Add additional JSON properties to the request
+
+ timeout: Override the client-level default timeout for this request, in seconds
+ """
+ ...
+
+ @overload
+ async def perform_operation(
self,
- id_or_name: str,
+ key: str,
*,
+ id_or_name: str,
+ checkout: VaultCheckoutContextParam,
+ type: Literal["prepare_checkout"],
# 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,
- ) -> ItemListResponse:
- """Credential entries include safe field metadata and non-sensitive values.
+ ) -> VaultItemOperationResponse:
+ """
+ Retrieve the item first and invoke only an operation listed in
+ `available_operations`, following its natural-language description. Availability
+ is rechecked at execution time; unavailable operations return 409. Authorization
+ and preparation may call an external provider and return updated state. Link
+ cards advertise authorize without checkout context. Eligible unused AgentCard
+ cards advertise prepare_checkout, which requires checkout context and obtains
+ device approval before native Square Pay. Keep the returned approval page open,
+ poll until ready_to_submit, then submit before preparation.expires_at. Unused
+ preparations expire automatically and cannot be reused. If spend-request
+ creation is rejected with a non-retryable provider error, the card item is
+ deleted and the provider's error code and message are returned. Rate limits
+ return HTTP 429 and retain the card item; stop, back off, and retry the same
+ authorize operation.
- Listing
- never creates or renews collection sessions; only an existing unexpired active
- session is included. Use single-item GET or collect to obtain a fresh link.
+ Fill returns a value-free execution result. Validation failures before writing
+ return 400 (invalid request or targets), 403 (access or destination denied), 404
+ (resource not found), or 409 (item or browser not ready). Once writing starts,
+ known partial failures and indeterminate field outcomes return 200 with status
+ `failed` or `unknown`, not an automatic-retry signal. A transport error may
+ leave the outcome unknown; do not automatically retry.
Args:
+ checkout: Required when preparing an unused AgentCard card for a supported checkout
+ processor. Consent is bound to this browser and declared merchant origin, not a
+ tab. Wait for the item's ready_to_submit status before native Pay and submit
+ within its readiness deadline. Unused preparations expire automatically; every
+ preparation is single-use, including after failure or expiry.
+
extra_headers: Send extra headers
extra_query: Add additional query parameters to the request
@@ -1055,36 +1630,61 @@ async def list(
timeout: Override the client-level default timeout for this request, in seconds
"""
- if not id_or_name:
- raise ValueError(f"Expected a non-empty value for `id_or_name` but received {id_or_name!r}")
- return await self._get(
- path_template("/vaults/{id_or_name}/items", id_or_name=id_or_name),
- options=make_request_options(
- extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
- ),
- cast_to=ItemListResponse,
- )
+ ...
- async def delete(
+ @overload
+ async def perform_operation(
self,
key: str,
*,
id_or_name: str,
+ browser_id: str,
+ fields: Iterable[VaultFillFieldParam],
+ type: Literal["fill"],
+ page_url: str | Omit = omit,
+ timeout_ms: int | Omit = omit,
# Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
# The extra values given here take precedence over values defined on the client or passed to this method.
extra_headers: Headers | None = None,
extra_query: Query | None = None,
extra_body: Body | None = None,
timeout: float | httpx.Timeout | None | NotGiven = not_given,
- ) -> None:
+ ) -> VaultItemOperationResponse:
"""
- Unresolved payment operations normally block deletion, including operations on
- child cards of a wallet. An AgentCard card in recovery_required whose checkout
- create response returned no authorization ID may be explicitly abandoned by
- deleting that card directly; deleting its wallet or vault remains blocked.
- Deleting or recreating an item is not proof that a payment did not occur.
+ Retrieve the item first and invoke only an operation listed in
+ `available_operations`, following its natural-language description. Availability
+ is rechecked at execution time; unavailable operations return 409. Authorization
+ and preparation may call an external provider and return updated state. Link
+ cards advertise authorize without checkout context. Eligible unused AgentCard
+ cards advertise prepare_checkout, which requires checkout context and obtains
+ device approval before native Square Pay. Keep the returned approval page open,
+ poll until ready_to_submit, then submit before preparation.expires_at. Unused
+ preparations expire automatically and cannot be reused. If spend-request
+ creation is rejected with a non-retryable provider error, the card item is
+ deleted and the provider's error code and message are returned. Rate limits
+ return HTTP 429 and retain the card item; stop, back off, and retry the same
+ authorize operation.
+
+ Fill returns a value-free execution result. Validation failures before writing
+ return 400 (invalid request or targets), 403 (access or destination denied), 404
+ (resource not found), or 409 (item or browser not ready). Once writing starts,
+ known partial failures and indeterminate field outcomes return 200 with status
+ `failed` or `unknown`, not an automatic-retry signal. A transport error may
+ leave the outcome unknown; do not automatically retry.
Args:
+ browser_id: Browser session ID, not a reusable browser name.
+
+ fields: Field bindings for this step. No two bindings may resolve to the same element.
+
+ page_url: Exact current top-level page URL, including path, query, and fragment. Must
+ match exactly one open page in the browser; zero or multiple matches fail. No
+ prefix or glob matching. Required for cards, which must use HTTPS without
+ embedded credentials. Optional for credentials, where omission requires exactly
+ one open page.
+
+ timeout_ms: Total operation deadline in milliseconds, not a per-field timeout.
+
extra_headers: Send extra headers
extra_query: Add additional query parameters to the request
@@ -1093,40 +1693,50 @@ async def delete(
timeout: Override the client-level default timeout for this request, in seconds
"""
- if not id_or_name:
- raise ValueError(f"Expected a non-empty value for `id_or_name` but received {id_or_name!r}")
- if not key:
- raise ValueError(f"Expected a non-empty value for `key` but received {key!r}")
- extra_headers = {"Accept": "*/*", **(extra_headers or {})}
- return await self._delete(
- path_template("/vaults/{id_or_name}/items/{key}", id_or_name=id_or_name, key=key),
- options=make_request_options(
- extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
- ),
- cast_to=NoneType,
- )
+ ...
- async def events(
+ @overload
+ async def perform_operation(
self,
key: str,
*,
id_or_name: str,
- after: str | Omit = omit,
- wait: int | Omit = omit,
+ browser_id: str,
+ type: Literal["1pw_create_access_request"],
+ goal: str | Omit = omit,
+ keywords: SequenceNotStr[str] | Omit = omit,
+ reason: str | Omit = omit,
# Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
# The extra values given here take precedence over values defined on the client or passed to this method.
extra_headers: Headers | None = None,
extra_query: Query | None = None,
extra_body: Body | None = None,
timeout: float | httpx.Timeout | None | NotGiven = not_given,
- ) -> ItemEventsResponse:
+ ) -> VaultItemOperationResponse:
"""
- List immutable audit events for a vault item
+ Retrieve the item first and invoke only an operation listed in
+ `available_operations`, following its natural-language description. Availability
+ is rechecked at execution time; unavailable operations return 409. Authorization
+ and preparation may call an external provider and return updated state. Link
+ cards advertise authorize without checkout context. Eligible unused AgentCard
+ cards advertise prepare_checkout, which requires checkout context and obtains
+ device approval before native Square Pay. Keep the returned approval page open,
+ poll until ready_to_submit, then submit before preparation.expires_at. Unused
+ preparations expire automatically and cannot be reused. If spend-request
+ creation is rejected with a non-retryable provider error, the card item is
+ deleted and the provider's error code and message are returned. Rate limits
+ return HTTP 429 and retain the card item; stop, back off, and retry the same
+ authorize operation.
- Args:
- after: Return events after this event ID.
+ Fill returns a value-free execution result. Validation failures before writing
+ return 400 (invalid request or targets), 403 (access or destination denied), 404
+ (resource not found), or 409 (item or browser not ready). Once writing starts,
+ known partial failures and indeterminate field outcomes return 200 with status
+ `failed` or `unknown`, not an automatic-retry signal. A transport error may
+ leave the outcome unknown; do not automatically retry.
- wait: Long-poll for new events for up to this many seconds.
+ Args:
+ browser_id: Kernel browser session used to invoke the extension.
extra_headers: Send extra headers
@@ -1136,27 +1746,7 @@ async def events(
timeout: Override the client-level default timeout for this request, in seconds
"""
- if not id_or_name:
- raise ValueError(f"Expected a non-empty value for `id_or_name` but received {id_or_name!r}")
- if not key:
- raise ValueError(f"Expected a non-empty value for `key` but received {key!r}")
- return await self._get(
- path_template("/vaults/{id_or_name}/items/{key}/events", id_or_name=id_or_name, key=key),
- options=make_request_options(
- extra_headers=extra_headers,
- extra_query=extra_query,
- extra_body=extra_body,
- timeout=timeout,
- query=await async_maybe_transform(
- {
- "after": after,
- "wait": wait,
- },
- item_events_params.ItemEventsParams,
- ),
- ),
- cast_to=ItemEventsResponse,
- )
+ ...
@overload
async def perform_operation(
@@ -1164,7 +1754,9 @@ async def perform_operation(
key: str,
*,
id_or_name: str,
- type: Literal["authorize"],
+ browser_id: str,
+ type: Literal["1pw_access_request_status"],
+ timeout_seconds: int | Omit = omit,
# Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
# The extra values given here take precedence over values defined on the client or passed to this method.
extra_headers: Headers | None = None,
@@ -1211,7 +1803,11 @@ async def perform_operation(
key: str,
*,
id_or_name: str,
- type: Literal["collect"],
+ browser_id: str,
+ page_url: str,
+ type: Literal["1pw_fill"],
+ entry_id: str | Omit = omit,
+ timeout_ms: int | Omit = omit,
# Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
# The extra values given here take precedence over values defined on the client or passed to this method.
extra_headers: Headers | None = None,
@@ -1242,6 +1838,14 @@ async def perform_operation(
leave the outcome unknown; do not automatically retry.
Args:
+ browser_id: Browser session ID, not a reusable browser name.
+
+ page_url: Exact current top-level page URL. Must match exactly one open page in the
+ browser.
+
+ entry_id: ID of an approved request entry. Required when several approved entries have the
+ page's origin.
+
extra_headers: Send extra headers
extra_query: Add additional query parameters to the request
@@ -1258,8 +1862,7 @@ async def perform_operation(
key: str,
*,
id_or_name: str,
- checkout: VaultCheckoutContextParam,
- type: Literal["prepare_checkout"],
+ type: Literal["1pw_recover"],
# 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,
@@ -1290,12 +1893,6 @@ async def perform_operation(
leave the outcome unknown; do not automatically retry.
Args:
- checkout: Required when preparing an unused AgentCard card for a supported checkout
- processor. Consent is bound to this browser and declared merchant origin, not a
- tab. Wait for the item's ready_to_submit status before native Pay and submit
- within its readiness deadline. Unused preparations expire automatically; every
- preparation is single-use, including after failure or expiry.
-
extra_headers: Send extra headers
extra_query: Add additional query parameters to the request
@@ -1312,11 +1909,9 @@ async def perform_operation(
key: str,
*,
id_or_name: str,
- browser_id: str,
- fields: Iterable[VaultFillFieldParam],
- type: Literal["fill"],
- page_url: str | Omit = omit,
- timeout_ms: int | Omit = omit,
+ access_token: str,
+ type: Literal["1pw_update_access_token"],
+ access_token_expires_at: Union[str, datetime] | 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,
@@ -1347,17 +1942,7 @@ async def perform_operation(
leave the outcome unknown; do not automatically retry.
Args:
- browser_id: Browser session ID, not a reusable browser name.
-
- fields: Field bindings for this step. No two bindings may resolve to the same element.
-
- page_url: Exact current top-level page URL, including path, query, and fragment. Must
- match exactly one open page in the browser; zero or multiple matches fail. No
- prefix or glob matching. Required for cards, which must use HTTPS without
- embedded credentials. Optional for credentials, where omission requires exactly
- one open page.
-
- timeout_ms: Total operation deadline in milliseconds, not a per-field timeout.
+ access_token_expires_at: Optional supplied expiry. Omit to clear the old expiry.
extra_headers: Send extra headers
@@ -1370,19 +1955,39 @@ async def perform_operation(
...
@required_args(
- ["id_or_name", "type"], ["id_or_name", "checkout", "type"], ["id_or_name", "browser_id", "fields", "type"]
+ ["id_or_name", "type"],
+ ["id_or_name", "checkout", "type"],
+ ["id_or_name", "browser_id", "fields", "type"],
+ ["id_or_name", "browser_id", "type"],
+ ["id_or_name", "browser_id", "page_url", "type"],
+ ["id_or_name", "access_token", "type"],
)
async def perform_operation(
self,
key: str,
*,
id_or_name: str,
- type: Literal["authorize"] | Literal["collect"] | Literal["prepare_checkout"] | Literal["fill"],
+ type: Literal["authorize"]
+ | Literal["collect"]
+ | Literal["prepare_checkout"]
+ | Literal["fill"]
+ | Literal["1pw_create_access_request"]
+ | Literal["1pw_access_request_status"]
+ | Literal["1pw_fill"]
+ | Literal["1pw_recover"]
+ | Literal["1pw_update_access_token"],
checkout: VaultCheckoutContextParam | Omit = omit,
browser_id: str | Omit = omit,
fields: Iterable[VaultFillFieldParam] | Omit = omit,
page_url: str | Omit = omit,
timeout_ms: int | Omit = omit,
+ goal: str | Omit = omit,
+ keywords: SequenceNotStr[str] | Omit = omit,
+ reason: str | Omit = omit,
+ timeout_seconds: int | Omit = omit,
+ entry_id: str | Omit = omit,
+ access_token: str | Omit = omit,
+ access_token_expires_at: Union[str, datetime] | 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,
@@ -1406,6 +2011,13 @@ async def perform_operation(
"fields": fields,
"page_url": page_url,
"timeout_ms": timeout_ms,
+ "goal": goal,
+ "keywords": keywords,
+ "reason": reason,
+ "timeout_seconds": timeout_seconds,
+ "entry_id": entry_id,
+ "access_token": access_token,
+ "access_token_expires_at": access_token_expires_at,
},
item_perform_operation_params.ItemPerformOperationParams,
),
@@ -1501,6 +2113,43 @@ async def upsert(
"""
...
+ @overload
+ async def upsert(
+ self,
+ key: str,
+ *,
+ id_or_name: str,
+ spec: OnePasswordCredentialAccountSpecParam,
+ type: Literal["credential_account"],
+ # 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,
+ ) -> VaultItem:
+ """
+ Create an item under a key unique within its vault, or retrieve the existing
+ item when its specification matches. An identical card PUT returns the existing
+ card in any lifecycle state without polling the provider, reauthorizing,
+ replacing aliases, or resetting recovery. Conflicting specifications return 409.
+ Provider-specific authorization requirements and retry behavior are described in
+ the item's request schema. Do not use credential items to store, collect, or
+ fill credit card data, including card numbers (PANs), security codes (CVV/CVC),
+ or expiration dates. Use wallet and card item types for credit cards and payment
+ checkout instead.
+
+ Args:
+ extra_headers: Send extra headers
+
+ extra_query: Add additional query parameters to the request
+
+ extra_body: Add additional JSON properties to the request
+
+ timeout: Override the client-level default timeout for this request, in seconds
+ """
+ ...
+
@overload
async def upsert(
self,
@@ -1552,8 +2201,9 @@ async def upsert(
id_or_name: str,
spec: item_upsert_params.WalletVaultItemRequestSpec
| CardVaultItemSpecParam
+ | OnePasswordCredentialAccountSpecParam
| CredentialVaultItemSpecInputParam,
- type: Literal["wallet"] | Literal["card"] | Literal["credential"],
+ type: Literal["wallet"] | Literal["card"] | Literal["credential_account"] | Literal["credential"],
# 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,
diff --git a/src/kernel/types/vaults/__init__.py b/src/kernel/types/vaults/__init__.py
index e2dd28eb..220eafc2 100644
--- a/src/kernel/types/vaults/__init__.py
+++ b/src/kernel/types/vaults/__init__.py
@@ -20,6 +20,7 @@
from .wallet_vault_item_spec import WalletVaultItemSpec as WalletVaultItemSpec
from .vault_fill_field_result import VaultFillFieldResult as VaultFillFieldResult
from .wallet_vault_item_state import WalletVaultItemState as WalletVaultItemState
+from .one_password_oauth_action import OnePasswordOAuthAction as OnePasswordOAuthAction
from .card_vault_item_spec_param import CardVaultItemSpecParam as CardVaultItemSpecParam
from .credential_vault_item_spec import CredentialVaultItemSpec as CredentialVaultItemSpec
from .credential_vault_field_type import CredentialVaultFieldType as CredentialVaultFieldType
@@ -28,15 +29,22 @@
from .credential_collection_action import CredentialCollectionAction as CredentialCollectionAction
from .credential_vault_field_state import CredentialVaultFieldState as CredentialVaultFieldState
from .vault_checkout_context_param import VaultCheckoutContextParam as VaultCheckoutContextParam
+from .credential_account_vault_item import CredentialAccountVaultItem as CredentialAccountVaultItem
from .item_perform_operation_params import ItemPerformOperationParams as ItemPerformOperationParams
from .vault_item_operation_response import VaultItemOperationResponse as VaultItemOperationResponse
from .agentcard_checkout_preparation import AgentcardCheckoutPreparation as AgentcardCheckoutPreparation
from .agentcard_checkout_authorization import AgentcardCheckoutAuthorization as AgentcardCheckoutAuthorization
from .fill_vault_item_operation_result import FillVaultItemOperationResult as FillVaultItemOperationResult
from .credential_vault_field_definition import CredentialVaultFieldDefinition as CredentialVaultFieldDefinition
+from .kernel_credential_vault_item_spec import KernelCredentialVaultItemSpec as KernelCredentialVaultItemSpec
from .credential_vault_field_input_param import CredentialVaultFieldInputParam as CredentialVaultFieldInputParam
+from .kernel_credential_vault_item_state import KernelCredentialVaultItemState as KernelCredentialVaultItemState
from .credential_vault_field_update_param import CredentialVaultFieldUpdateParam as CredentialVaultFieldUpdateParam
from .credential_vault_item_request_param import CredentialVaultItemRequestParam as CredentialVaultItemRequestParam
+from .one_password_credential_account_spec import OnePasswordCredentialAccountSpec as OnePasswordCredentialAccountSpec
+from .one_password_credential_account_state import (
+ OnePasswordCredentialAccountState as OnePasswordCredentialAccountState,
+)
from .credential_vault_item_spec_input_param import (
CredentialVaultItemSpecInputParam as CredentialVaultItemSpecInputParam,
)
@@ -46,15 +54,45 @@
from .fill_vault_item_operation_request_param import (
FillVaultItemOperationRequestParam as FillVaultItemOperationRequestParam,
)
+from .one_password_credential_vault_item_spec import (
+ OnePasswordCredentialVaultItemSpec as OnePasswordCredentialVaultItemSpec,
+)
+from .one_password_credential_vault_item_state import (
+ OnePasswordCredentialVaultItemState as OnePasswordCredentialVaultItemState,
+)
from .collect_vault_item_operation_request_param import (
CollectVaultItemOperationRequestParam as CollectVaultItemOperationRequestParam,
)
from .credential_vault_item_update_request_param import (
CredentialVaultItemUpdateRequestParam as CredentialVaultItemUpdateRequestParam,
)
+from .one_password_credential_account_spec_param import (
+ OnePasswordCredentialAccountSpecParam as OnePasswordCredentialAccountSpecParam,
+)
+from .credential_account_vault_item_request_param import (
+ CredentialAccountVaultItemRequestParam as CredentialAccountVaultItemRequestParam,
+)
from .authorize_vault_item_operation_request_param import (
AuthorizeVaultItemOperationRequestParam as AuthorizeVaultItemOperationRequestParam,
)
+from .kernel_credential_vault_item_spec_input_param import (
+ KernelCredentialVaultItemSpecInputParam as KernelCredentialVaultItemSpecInputParam,
+)
+from .one_password_fill_vault_item_operation_result import (
+ OnePasswordFillVaultItemOperationResult as OnePasswordFillVaultItemOperationResult,
+)
+from .one_password_credential_vault_item_spec_input_param import (
+ OnePasswordCredentialVaultItemSpecInputParam as OnePasswordCredentialVaultItemSpecInputParam,
+)
from .prepare_checkout_vault_item_operation_request_param import (
PrepareCheckoutVaultItemOperationRequestParam as PrepareCheckoutVaultItemOperationRequestParam,
)
+from .one_password_fill_vault_item_operation_request_param import (
+ OnePasswordFillVaultItemOperationRequestParam as OnePasswordFillVaultItemOperationRequestParam,
+)
+from .one_password_recover_vault_item_operation_request_param import (
+ OnePasswordRecoverVaultItemOperationRequestParam as OnePasswordRecoverVaultItemOperationRequestParam,
+)
+from .one_password_request_access_vault_item_operation_request_param import (
+ OnePasswordRequestAccessVaultItemOperationRequestParam as OnePasswordRequestAccessVaultItemOperationRequestParam,
+)
diff --git a/src/kernel/types/vaults/credential_account_vault_item.py b/src/kernel/types/vaults/credential_account_vault_item.py
new file mode 100644
index 00000000..ef7555cb
--- /dev/null
+++ b/src/kernel/types/vaults/credential_account_vault_item.py
@@ -0,0 +1,73 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from typing import List, Optional
+from datetime import datetime
+from typing_extensions import Literal
+
+from ..._models import BaseModel
+from .one_password_oauth_action import OnePasswordOAuthAction
+from .one_password_credential_account_spec import OnePasswordCredentialAccountSpec
+from .one_password_credential_account_state import OnePasswordCredentialAccountState
+
+__all__ = ["CredentialAccountVaultItem", "AvailableExpansion", "AvailableOperation"]
+
+
+class AvailableExpansion(BaseModel):
+ """
+ Live data that can currently be requested by passing its type to the item GET expand parameter.
+ """
+
+ description: str
+
+ type: Literal["payment_methods"]
+
+
+class AvailableOperation(BaseModel):
+ """An operation that is currently valid for this item.
+
+ Read the description before invoking it through the item operations endpoint.
+ """
+
+ description: str
+
+ type: Literal[
+ "authorize",
+ "collect",
+ "prepare_checkout",
+ "fill",
+ "1pw_create_access_request",
+ "1pw_access_request_status",
+ "1pw_fill",
+ "1pw_recover",
+ "1pw_update_access_token",
+ ]
+
+
+class CredentialAccountVaultItem(BaseModel):
+ id: str
+
+ available_expansions: List[AvailableExpansion]
+
+ available_operations: List[AvailableOperation]
+ """Advertises 1pw_recover when Kernel can recover a failed account link.
+
+ Recovery is unavailable while authorization is pending or after the connection
+ has already been reset.
+ """
+
+ created_at: datetime
+
+ key: str
+ """Immutable item key assigned when the item is created."""
+
+ spec: OnePasswordCredentialAccountSpec
+
+ state: OnePasswordCredentialAccountState
+
+ type: Literal["credential_account"]
+
+ updated_at: datetime
+
+ action: Optional[OnePasswordOAuthAction] = None
+
+ expires_at: Optional[datetime] = None
diff --git a/src/kernel/types/vaults/credential_account_vault_item_request_param.py b/src/kernel/types/vaults/credential_account_vault_item_request_param.py
new file mode 100644
index 00000000..187b25f2
--- /dev/null
+++ b/src/kernel/types/vaults/credential_account_vault_item_request_param.py
@@ -0,0 +1,15 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from __future__ import annotations
+
+from typing_extensions import Literal, Required, TypedDict
+
+from .one_password_credential_account_spec_param import OnePasswordCredentialAccountSpecParam
+
+__all__ = ["CredentialAccountVaultItemRequestParam"]
+
+
+class CredentialAccountVaultItemRequestParam(TypedDict, total=False):
+ spec: Required[OnePasswordCredentialAccountSpecParam]
+
+ type: Required[Literal["credential_account"]]
diff --git a/src/kernel/types/vaults/credential_vault_item.py b/src/kernel/types/vaults/credential_vault_item.py
index 79c12abc..3cee5030 100644
--- a/src/kernel/types/vaults/credential_vault_item.py
+++ b/src/kernel/types/vaults/credential_vault_item.py
@@ -1,15 +1,22 @@
# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
-from typing import List, Optional
+from typing import List, Union, Optional
from datetime import datetime
-from typing_extensions import Literal
+from typing_extensions import Literal, Annotated, TypeAlias
+from ..._utils import PropertyInfo
from ..._models import BaseModel
from .credential_vault_item_spec import CredentialVaultItemSpec
from .credential_vault_item_state import CredentialVaultItemState
from .credential_collection_action import CredentialCollectionAction
-__all__ = ["CredentialVaultItem", "AvailableExpansion", "AvailableOperation"]
+__all__ = [
+ "CredentialVaultItem",
+ "AvailableExpansion",
+ "AvailableOperation",
+ "Action",
+ "ActionOnePasswordAccessApprovalAction",
+]
class AvailableExpansion(BaseModel):
@@ -30,7 +37,39 @@ class AvailableOperation(BaseModel):
description: str
- type: Literal["authorize", "collect", "prepare_checkout", "fill"]
+ type: Literal[
+ "authorize",
+ "collect",
+ "prepare_checkout",
+ "fill",
+ "1pw_create_access_request",
+ "1pw_access_request_status",
+ "1pw_fill",
+ "1pw_recover",
+ "1pw_update_access_token",
+ ]
+
+
+class ActionOnePasswordAccessApprovalAction(BaseModel):
+ instructions: str
+ """
+ Steps for the agent to hand approval to the human and poll the resulting
+ decision.
+ """
+
+ name: Literal["1password_access_approval"]
+
+ url: str
+ """Native 1Password approval link.
+
+ Present it to the account owner without modifying it; it does not grant access
+ until they approve in their app.
+ """
+
+
+Action: TypeAlias = Annotated[
+ Union[CredentialCollectionAction, ActionOnePasswordAccessApprovalAction], PropertyInfo(discriminator="name")
+]
class CredentialVaultItem(BaseModel):
@@ -39,9 +78,11 @@ class CredentialVaultItem(BaseModel):
available_expansions: List[AvailableExpansion]
available_operations: List[AvailableOperation]
- """Advertises collect for ready and pending_collection items.
+ """Kernel credentials advertise collect and fill when eligible.
- Browser fill is advertised only when separately implemented and eligible.
+ 1Password credentials advertise 1pw_create_access_request until a request is
+ made, 1pw_access_request_status while its approval is pending, and 1pw_fill
+ after access is granted.
"""
created_at: datetime
@@ -50,6 +91,10 @@ class CredentialVaultItem(BaseModel):
"""Immutable item key assigned when the item is created."""
spec: CredentialVaultItemSpec
+ """
+ Stored-token credentials omit account and never return access_token or
+ integration_key.
+ """
state: CredentialVaultItemState
@@ -63,7 +108,7 @@ class CredentialVaultItem(BaseModel):
collection-link renewal.
"""
- action: Optional[CredentialCollectionAction] = None
+ action: Optional[Action] = None
"""
One schema-derived form for the item, available in ready or pending_collection
state. Render every form-supported field as editable; omit totp fields and
diff --git a/src/kernel/types/vaults/credential_vault_item_request_param.py b/src/kernel/types/vaults/credential_vault_item_request_param.py
index 169af036..84d03bf5 100644
--- a/src/kernel/types/vaults/credential_vault_item_request_param.py
+++ b/src/kernel/types/vaults/credential_vault_item_request_param.py
@@ -11,19 +11,18 @@
class CredentialVaultItemRequestParam(TypedDict, total=False):
"""
- Create a credential item without a wallet or external provider.
- Do not use credential items to store, collect, or fill credit card data,
- including card numbers (PANs), security codes (CVV/CVC), or expiration dates.
- Use wallet and card item types for credit cards and payment checkout instead.
- If all required fields have values, return ready without a collection action;
- collect can still open its form. Otherwise return pending_collection with
- a time-scoped Kernel-hosted collection action. Missing
- optional fields alone do not trigger collection. Repeating the original
- creation request returns the current item without overwriting later edits;
- a different request at the same key returns 409. Use PATCH for updates.
- Required totp fields must include a valid seed on creation; otherwise return
- 400 rather than opening a form that cannot collect it. Optional totp fields
- may be unset and populated later through PATCH.
+ Ask the end-user whether to link their site credential through 1Password.
+ If they choose 1Password, connect their account and request access to a login
+ in their own non-shared vault; passkeys are not supported. If they decline
+ or that path fails, collect a Kernel-hosted credential item instead. Never
+ automatically retry an uncertain 1Password request or fill.
+ Do not use credential items for credit card data. Use wallet and card item types instead.
+ Kernel credentials declare fields and may enter pending_collection.
+ 1Password credentials either reference a connected credential_account or
+ store a supplied access token and integration key encrypted on the item.
+ They store no login values or selectors. Repeating the original creation
+ request returns the current item without overwriting later state. A
+ different request at the same key returns 409.
"""
spec: Required[CredentialVaultItemSpecInputParam]
diff --git a/src/kernel/types/vaults/credential_vault_item_spec.py b/src/kernel/types/vaults/credential_vault_item_spec.py
index 307d2b25..fe452071 100644
--- a/src/kernel/types/vaults/credential_vault_item_spec.py
+++ b/src/kernel/types/vaults/credential_vault_item_spec.py
@@ -1,22 +1,14 @@
# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
-from typing import List, Optional
+from typing import Union
+from typing_extensions import Annotated, TypeAlias
-from ..._models import BaseModel
-from .credential_vault_field_definition import CredentialVaultFieldDefinition
+from ..._utils import PropertyInfo
+from .kernel_credential_vault_item_spec import KernelCredentialVaultItemSpec
+from .one_password_credential_vault_item_spec import OnePasswordCredentialVaultItemSpec
__all__ = ["CredentialVaultItemSpec"]
-
-class CredentialVaultItemSpec(BaseModel):
- fields: List[CredentialVaultFieldDefinition]
- """
- Ordered field definitions rendered in this order by credential collection forms.
- """
-
- description: Optional[str] = None
- """
- Recognizable site or service name displayed verbatim as the form title, without
- suffixes such as sign-in credentials. Display text only, not an enforced
- destination policy.
- """
+CredentialVaultItemSpec: TypeAlias = Annotated[
+ Union[KernelCredentialVaultItemSpec, OnePasswordCredentialVaultItemSpec], PropertyInfo(discriminator="provider")
+]
diff --git a/src/kernel/types/vaults/credential_vault_item_spec_input_param.py b/src/kernel/types/vaults/credential_vault_item_spec_input_param.py
index ab4eaee8..3b36cbeb 100644
--- a/src/kernel/types/vaults/credential_vault_item_spec_input_param.py
+++ b/src/kernel/types/vaults/credential_vault_item_spec_input_param.py
@@ -2,31 +2,14 @@
from __future__ import annotations
-from typing import Iterable
-from typing_extensions import Required, TypedDict
+from typing import Union
+from typing_extensions import TypeAlias
-from .credential_vault_field_input_param import CredentialVaultFieldInputParam
+from .kernel_credential_vault_item_spec_input_param import KernelCredentialVaultItemSpecInputParam
+from .one_password_credential_vault_item_spec_input_param import OnePasswordCredentialVaultItemSpecInputParam
__all__ = ["CredentialVaultItemSpecInputParam"]
-
-class CredentialVaultItemSpecInputParam(TypedDict, total=False):
- """Credential fields are for login and other non-payment credentials.
-
- Do not store, collect, or fill credit card data in credential items. Use wallet and card item types for credit cards and payment checkout instead. Field order is preserved in the user-facing collection form, so list fields in the same top-to-bottom order as the website.
- """
-
- fields: Required[Iterable[CredentialVaultFieldInputParam]]
- """Ordered field definitions.
-
- Use the website's top-to-bottom field order; the collection form renders this
- order unchanged.
- """
-
- description: str
- """
- The site's recognizable display name, used verbatim as the user-facing form
- title (for example, Hacker News). Use only the site or service name; do not
- append sign-in, login, credentials, or task instructions. This is display text,
- not an enforced destination policy. At most 16 KiB in UTF-8 bytes.
- """
+CredentialVaultItemSpecInputParam: TypeAlias = Union[
+ KernelCredentialVaultItemSpecInputParam, OnePasswordCredentialVaultItemSpecInputParam
+]
diff --git a/src/kernel/types/vaults/credential_vault_item_state.py b/src/kernel/types/vaults/credential_vault_item_state.py
index 7d64ad95..8c0077e0 100644
--- a/src/kernel/types/vaults/credential_vault_item_state.py
+++ b/src/kernel/types/vaults/credential_vault_item_state.py
@@ -1,20 +1,14 @@
# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
-from typing import Dict
-from typing_extensions import Literal
+from typing import Union
+from typing_extensions import Annotated, TypeAlias
-from ..._models import BaseModel
-from .credential_vault_field_state import CredentialVaultFieldState
+from ..._utils import PropertyInfo
+from .kernel_credential_vault_item_state import KernelCredentialVaultItemState
+from .one_password_credential_vault_item_state import OnePasswordCredentialVaultItemState
__all__ = ["CredentialVaultItemState"]
-
-class CredentialVaultItemState(BaseModel):
- fields: Dict[str, CredentialVaultFieldState]
- """Exactly one entry for each declared field."""
-
- status: Literal["pending_collection", "ready"]
- """Ready means all required fields have values, not that a login succeeded.
-
- Optional fields may remain unset.
- """
+CredentialVaultItemState: TypeAlias = Annotated[
+ Union[KernelCredentialVaultItemState, OnePasswordCredentialVaultItemState], PropertyInfo(discriminator="provider")
+]
diff --git a/src/kernel/types/vaults/item_perform_operation_params.py b/src/kernel/types/vaults/item_perform_operation_params.py
index 7e866ff3..8d1490b5 100644
--- a/src/kernel/types/vaults/item_perform_operation_params.py
+++ b/src/kernel/types/vaults/item_perform_operation_params.py
@@ -3,8 +3,11 @@
from __future__ import annotations
from typing import Union, Iterable
-from typing_extensions import Literal, Required, TypeAlias, TypedDict
+from datetime import datetime
+from typing_extensions import Literal, Required, Annotated, TypeAlias, TypedDict
+from ..._types import SequenceNotStr
+from ..._utils import PropertyInfo
from .vault_fill_field_param import VaultFillFieldParam
from .vault_checkout_context_param import VaultCheckoutContextParam
@@ -14,6 +17,11 @@
"CollectVaultItemOperationRequest",
"PrepareCheckoutVaultItemOperationRequest",
"FillVaultItemOperationRequest",
+ "OnePasswordRequestAccessVaultItemOperationRequest",
+ "OnePasswordPollAccessVaultItemOperationRequest",
+ "OnePasswordFillVaultItemOperationRequest",
+ "OnePasswordRecoverVaultItemOperationRequest",
+ "OnePasswordUpdateAccessTokenVaultItemOperationRequest",
]
@@ -68,9 +76,79 @@ class FillVaultItemOperationRequest(TypedDict, total=False):
"""Total operation deadline in milliseconds, not a per-field timeout."""
+class OnePasswordRequestAccessVaultItemOperationRequest(TypedDict, total=False):
+ id_or_name: Required[str]
+
+ browser_id: Required[str]
+ """Kernel browser session used to invoke the extension."""
+
+ type: Required[Literal["1pw_create_access_request"]]
+
+ goal: str
+
+ keywords: SequenceNotStr[str]
+
+ reason: str
+
+
+class OnePasswordPollAccessVaultItemOperationRequest(TypedDict, total=False):
+ id_or_name: Required[str]
+
+ browser_id: Required[str]
+
+ type: Required[Literal["1pw_access_request_status"]]
+
+ timeout_seconds: int
+
+
+class OnePasswordFillVaultItemOperationRequest(TypedDict, total=False):
+ id_or_name: Required[str]
+
+ browser_id: Required[str]
+ """Browser session ID, not a reusable browser name."""
+
+ page_url: Required[str]
+ """Exact current top-level page URL.
+
+ Must match exactly one open page in the browser.
+ """
+
+ type: Required[Literal["1pw_fill"]]
+
+ entry_id: str
+ """ID of an approved request entry.
+
+ Required when several approved entries have the page's origin.
+ """
+
+ timeout_ms: int
+
+
+class OnePasswordRecoverVaultItemOperationRequest(TypedDict, total=False):
+ id_or_name: Required[str]
+
+ type: Required[Literal["1pw_recover"]]
+
+
+class OnePasswordUpdateAccessTokenVaultItemOperationRequest(TypedDict, total=False):
+ id_or_name: Required[str]
+
+ access_token: Required[str]
+
+ type: Required[Literal["1pw_update_access_token"]]
+
+ access_token_expires_at: Annotated[Union[str, datetime], PropertyInfo(format="iso8601")]
+ """Optional supplied expiry. Omit to clear the old expiry."""
+
+
ItemPerformOperationParams: TypeAlias = Union[
AuthorizeVaultItemOperationRequest,
CollectVaultItemOperationRequest,
PrepareCheckoutVaultItemOperationRequest,
FillVaultItemOperationRequest,
+ OnePasswordRequestAccessVaultItemOperationRequest,
+ OnePasswordPollAccessVaultItemOperationRequest,
+ OnePasswordFillVaultItemOperationRequest,
+ OnePasswordRecoverVaultItemOperationRequest,
+ OnePasswordUpdateAccessTokenVaultItemOperationRequest,
]
diff --git a/src/kernel/types/vaults/item_upsert_params.py b/src/kernel/types/vaults/item_upsert_params.py
index 4e4799dd..fc9acbdf 100644
--- a/src/kernel/types/vaults/item_upsert_params.py
+++ b/src/kernel/types/vaults/item_upsert_params.py
@@ -7,6 +7,7 @@
from .card_vault_item_spec_param import CardVaultItemSpecParam
from .credential_vault_item_spec_input_param import CredentialVaultItemSpecInputParam
+from .one_password_credential_account_spec_param import OnePasswordCredentialAccountSpecParam
__all__ = [
"ItemUpsertParams",
@@ -23,6 +24,7 @@
"WalletVaultItemRequestSpecAgentCardWalletVaultItemSpec",
"WalletVaultItemRequestSpecAgentCardWalletVaultItemSpecProviderConfig",
"CardVaultItemRequest",
+ "CredentialAccountVaultItemRequest",
"CredentialVaultItemRequest",
]
@@ -67,7 +69,7 @@ class WalletVaultItemRequestSpecLinkWalletVaultItemRequestSpecAuthorizationImpor
):
"""Select a provider config by ID or name.
- Responses return the ID. Renaming a config does not change existing wallet bindings; a wallet cannot switch to a different config after creation.
+ Responses return the ID. Renaming a config does not change existing wallet bindings; an item cannot switch to a different config after creation.
"""
id: str
@@ -84,7 +86,7 @@ class WalletVaultItemRequestSpecLinkWalletVaultItemRequestSpecAuthorizationImpor
"""Select a provider config by ID or name.
Responses return the ID. Renaming a config does not change existing wallet
- bindings; a wallet cannot switch to a different config after creation.
+ bindings; an item cannot switch to a different config after creation.
"""
type: Required[Literal["customer_managed"]]
@@ -187,6 +189,14 @@ class CardVaultItemRequest(TypedDict, total=False):
type: Required[Literal["card"]]
+class CredentialAccountVaultItemRequest(TypedDict, total=False):
+ id_or_name: Required[str]
+
+ spec: Required[OnePasswordCredentialAccountSpecParam]
+
+ type: Required[Literal["credential_account"]]
+
+
class CredentialVaultItemRequest(TypedDict, total=False):
id_or_name: Required[str]
@@ -202,4 +212,6 @@ class CredentialVaultItemRequest(TypedDict, total=False):
type: Required[Literal["credential"]]
-ItemUpsertParams: TypeAlias = Union[WalletVaultItemRequest, CardVaultItemRequest, CredentialVaultItemRequest]
+ItemUpsertParams: TypeAlias = Union[
+ WalletVaultItemRequest, CardVaultItemRequest, CredentialAccountVaultItemRequest, CredentialVaultItemRequest
+]
diff --git a/src/kernel/types/vaults/kernel_credential_vault_item_spec.py b/src/kernel/types/vaults/kernel_credential_vault_item_spec.py
new file mode 100644
index 00000000..a58a11d6
--- /dev/null
+++ b/src/kernel/types/vaults/kernel_credential_vault_item_spec.py
@@ -0,0 +1,25 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from typing import List, Optional
+from typing_extensions import Literal
+
+from ..._models import BaseModel
+from .credential_vault_field_definition import CredentialVaultFieldDefinition
+
+__all__ = ["KernelCredentialVaultItemSpec"]
+
+
+class KernelCredentialVaultItemSpec(BaseModel):
+ fields: List[CredentialVaultFieldDefinition]
+ """
+ Ordered field definitions rendered in this order by credential collection forms.
+ """
+
+ provider: Literal["kernel"]
+
+ description: Optional[str] = None
+ """
+ Recognizable site or service name displayed verbatim as the form title, without
+ suffixes such as sign-in credentials. Display text only, not an enforced
+ destination policy.
+ """
diff --git a/src/kernel/types/vaults/kernel_credential_vault_item_spec_input_param.py b/src/kernel/types/vaults/kernel_credential_vault_item_spec_input_param.py
new file mode 100644
index 00000000..d8ae9257
--- /dev/null
+++ b/src/kernel/types/vaults/kernel_credential_vault_item_spec_input_param.py
@@ -0,0 +1,34 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from __future__ import annotations
+
+from typing import Iterable
+from typing_extensions import Literal, Required, TypedDict
+
+from .credential_vault_field_input_param import CredentialVaultFieldInputParam
+
+__all__ = ["KernelCredentialVaultItemSpecInputParam"]
+
+
+class KernelCredentialVaultItemSpecInputParam(TypedDict, total=False):
+ """Credential fields are for login and other non-payment credentials.
+
+ Do not store, collect, or fill credit card data in credential items. Use wallet and card item types for credit cards and payment checkout instead. Field order is preserved in the user-facing collection form, so list fields in the same top-to-bottom order as the website.
+ """
+
+ fields: Required[Iterable[CredentialVaultFieldInputParam]]
+ """Ordered field definitions.
+
+ Use the website's top-to-bottom field order; the collection form renders this
+ order unchanged.
+ """
+
+ provider: Required[Literal["kernel"]]
+
+ description: str
+ """
+ The site's recognizable display name, used verbatim as the user-facing form
+ title (for example, Hacker News). Use only the site or service name; do not
+ append sign-in, login, credentials, or task instructions. This is display text,
+ not an enforced destination policy. At most 16 KiB in UTF-8 bytes.
+ """
diff --git a/src/kernel/types/vaults/kernel_credential_vault_item_state.py b/src/kernel/types/vaults/kernel_credential_vault_item_state.py
new file mode 100644
index 00000000..48a85af0
--- /dev/null
+++ b/src/kernel/types/vaults/kernel_credential_vault_item_state.py
@@ -0,0 +1,22 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from typing import Dict
+from typing_extensions import Literal
+
+from ..._models import BaseModel
+from .credential_vault_field_state import CredentialVaultFieldState
+
+__all__ = ["KernelCredentialVaultItemState"]
+
+
+class KernelCredentialVaultItemState(BaseModel):
+ fields: Dict[str, CredentialVaultFieldState]
+ """Exactly one entry for each declared field."""
+
+ provider: Literal["kernel"]
+
+ status: Literal["pending_collection", "ready"]
+ """Ready means all required fields have values, not that a login succeeded.
+
+ Optional fields may remain unset.
+ """
diff --git a/src/kernel/types/vaults/one_password_credential_account_spec.py b/src/kernel/types/vaults/one_password_credential_account_spec.py
new file mode 100644
index 00000000..cea6ee4d
--- /dev/null
+++ b/src/kernel/types/vaults/one_password_credential_account_spec.py
@@ -0,0 +1,23 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from typing_extensions import Literal
+
+from ..._models import BaseModel
+
+__all__ = ["OnePasswordCredentialAccountSpec", "Authorization", "AuthorizationClient"]
+
+
+class AuthorizationClient(BaseModel):
+ type: Literal["kernel_managed"]
+
+
+class Authorization(BaseModel):
+ client: AuthorizationClient
+
+ method: Literal["oauth"]
+
+
+class OnePasswordCredentialAccountSpec(BaseModel):
+ authorization: Authorization
+
+ provider: Literal["1password"]
diff --git a/src/kernel/types/vaults/one_password_credential_account_spec_param.py b/src/kernel/types/vaults/one_password_credential_account_spec_param.py
new file mode 100644
index 00000000..12bf6023
--- /dev/null
+++ b/src/kernel/types/vaults/one_password_credential_account_spec_param.py
@@ -0,0 +1,23 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from __future__ import annotations
+
+from typing_extensions import Literal, Required, TypedDict
+
+__all__ = ["OnePasswordCredentialAccountSpecParam", "Authorization", "AuthorizationClient"]
+
+
+class AuthorizationClient(TypedDict, total=False):
+ type: Required[Literal["kernel_managed"]]
+
+
+class Authorization(TypedDict, total=False):
+ client: Required[AuthorizationClient]
+
+ method: Required[Literal["oauth"]]
+
+
+class OnePasswordCredentialAccountSpecParam(TypedDict, total=False):
+ authorization: Required[Authorization]
+
+ provider: Required[Literal["1password"]]
diff --git a/src/kernel/types/vaults/one_password_credential_account_state.py b/src/kernel/types/vaults/one_password_credential_account_state.py
new file mode 100644
index 00000000..07eab56b
--- /dev/null
+++ b/src/kernel/types/vaults/one_password_credential_account_state.py
@@ -0,0 +1,16 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from typing import Optional
+from typing_extensions import Literal
+
+from ..._models import BaseModel
+
+__all__ = ["OnePasswordCredentialAccountState"]
+
+
+class OnePasswordCredentialAccountState(BaseModel):
+ provider: Literal["1password"]
+
+ status: Literal["pending_authorization", "connected", "reconnect_required", "declined"]
+
+ status_reason: Optional[str] = None
diff --git a/src/kernel/types/vaults/one_password_credential_vault_item_spec.py b/src/kernel/types/vaults/one_password_credential_vault_item_spec.py
new file mode 100644
index 00000000..84811174
--- /dev/null
+++ b/src/kernel/types/vaults/one_password_credential_vault_item_spec.py
@@ -0,0 +1,57 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from typing import List, Optional
+from datetime import datetime
+from typing_extensions import Literal
+
+from ..._models import BaseModel
+
+__all__ = ["OnePasswordCredentialVaultItemSpec", "Requests", "RequestsEntry", "RequestsEntryParameters"]
+
+
+class RequestsEntryParameters(BaseModel):
+ website: str
+
+
+class RequestsEntry(BaseModel):
+ parameters: RequestsEntryParameters
+
+ type: str
+ """Must be login."""
+
+ keywords: Optional[List[str]] = None
+
+ reason: Optional[str] = None
+
+
+class Requests(BaseModel):
+ """Credential Request v2 input sent to the extension.
+
+ A credential item may request up to five login entries.
+ """
+
+ entries: List[RequestsEntry]
+
+ version: int
+ """Must be 2."""
+
+ goal: Optional[str] = None
+
+
+class OnePasswordCredentialVaultItemSpec(BaseModel):
+ """
+ Stored-token credentials omit account and never return access_token or integration_key.
+ """
+
+ provider: Literal["1password"]
+
+ requests: Requests
+ """Credential Request v2 input sent to the extension.
+
+ A credential item may request up to five login entries.
+ """
+
+ access_token_expires_at: Optional[datetime] = None
+ """Customer-supplied expiry metadata, if provided."""
+
+ account: Optional[str] = None
diff --git a/src/kernel/types/vaults/one_password_credential_vault_item_spec_input_param.py b/src/kernel/types/vaults/one_password_credential_vault_item_spec_input_param.py
new file mode 100644
index 00000000..3a2b4c19
--- /dev/null
+++ b/src/kernel/types/vaults/one_password_credential_vault_item_spec_input_param.py
@@ -0,0 +1,84 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from __future__ import annotations
+
+from typing import Union, Iterable
+from datetime import datetime
+from typing_extensions import Literal, Required, Annotated, TypedDict
+
+from ..._types import SequenceNotStr
+from ..._utils import PropertyInfo
+
+__all__ = ["OnePasswordCredentialVaultItemSpecInputParam", "Requests", "RequestsEntry", "RequestsEntryParameters"]
+
+
+class RequestsEntryParameters(TypedDict, total=False):
+ website: Required[str]
+
+
+class RequestsEntry(TypedDict, total=False):
+ parameters: Required[RequestsEntryParameters]
+
+ type: Required[str]
+ """Must be login."""
+
+ keywords: SequenceNotStr[str]
+
+ reason: str
+
+
+class Requests(TypedDict, total=False):
+ """Credential Request v2 input sent to the extension.
+
+ A credential item may request up to five login entries.
+ """
+
+ entries: Required[Iterable[RequestsEntry]]
+
+ version: Required[int]
+ """Must be 2."""
+
+ goal: str
+
+
+class OnePasswordCredentialVaultItemSpecInputParam(TypedDict, total=False):
+ """
+ A login request backed by a connected 1Password account or by a customer-supplied access token and matching integration key. Supply either account or both secrets, never both. Supplied secrets are write-only and never returned. Supply requests for new items; website remains supported for existing account-backed callers.
+ """
+
+ provider: Required[Literal["1password"]]
+
+ access_token: str
+ """Customer-supplied 1Password broker token.
+
+ Requires integration_key; stored encrypted on this item. Omit if providing a
+ connected credential_account item via the account field.
+ """
+
+ access_token_expires_at: Annotated[Union[str, datetime], PropertyInfo(format="iso8601")]
+ """Optional supplied token expiry metadata for stored-token credentials.
+
+ Omit if providing a connected credential_account item via the account field.
+ """
+
+ account: str
+ """Key of a connected credential_account item in the same vault.
+
+ Omit for stored-token credentials.
+ """
+
+ integration_key: str
+ """Matching customer-supplied integration key.
+
+ Requires access_token; stored encrypted on this item. Omit if providing a
+ connected credential_account item via the account field.
+ """
+
+ requests: Requests
+ """Credential Request v2 input sent to the extension.
+
+ A credential item may request up to five login entries.
+ """
+
+ website: str
+ """Legacy single-login shorthand. Supply requests instead."""
diff --git a/src/kernel/types/vaults/one_password_credential_vault_item_state.py b/src/kernel/types/vaults/one_password_credential_vault_item_state.py
new file mode 100644
index 00000000..5cf9b390
--- /dev/null
+++ b/src/kernel/types/vaults/one_password_credential_vault_item_state.py
@@ -0,0 +1,125 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from typing import List, Optional
+from typing_extensions import Literal
+
+from pydantic import Field as FieldInfo
+
+from ..._models import BaseModel
+
+__all__ = [
+ "OnePasswordCredentialVaultItemState",
+ "AccessRequest",
+ "AccessRequestEntry",
+ "AccessRequestEntryParameters",
+ "AccessRequestRequest",
+ "AccessRequestRequestEntry",
+ "AccessRequestRequestEntryParameters",
+]
+
+
+class AccessRequestEntryParameters(BaseModel):
+ website: Optional[str] = None
+
+
+class AccessRequestEntry(BaseModel):
+ id: Optional[str] = None
+
+ keywords: Optional[List[str]] = None
+
+ parameters: Optional[AccessRequestEntryParameters] = None
+
+ reason: Optional[str] = None
+
+ type: Optional[str] = None
+
+
+class AccessRequestRequestEntryParameters(BaseModel):
+ website: Optional[str] = None
+
+
+class AccessRequestRequestEntry(BaseModel):
+ id: Optional[str] = None
+
+ keywords: Optional[List[str]] = None
+
+ parameters: Optional[AccessRequestRequestEntryParameters] = None
+
+ reason: Optional[str] = None
+
+ type: Optional[str] = None
+
+
+class AccessRequestRequest(BaseModel):
+ """The request object if returned by the extension.
+
+ The observed create response may omit entries; no entry IDs are invented.
+ """
+
+ entries: Optional[List[AccessRequestRequestEntry]] = None
+
+ goal: Optional[str] = None
+
+ version: Optional[int] = None
+
+
+class AccessRequest(BaseModel):
+ """Non-secret broker state.
+
+ Granted credential references stay encrypted server-side and can only be used by the fill operation.
+ """
+
+ id: str
+
+ has_autofill_token: bool
+
+ state: str
+ """One of pending, resolved, denied, or failed."""
+
+ created_at: Optional[str] = FieldInfo(alias="createdAt", default=None)
+ """Provider-created timestamp as returned by the broker."""
+
+ entries: Optional[List[AccessRequestEntry]] = None
+ """Login entries returned directly on accessRequest by the observed extension
+ build.
+
+ Omitted when the provider does not supply them.
+ """
+
+ goal: Optional[str] = None
+ """Goal echoed by the observed createAccessRequest response when present."""
+
+ granted_count: Optional[int] = None
+
+ identity: Optional[str] = None
+ """Opaque provider identity returned by the broker."""
+
+ path: Optional[str] = None
+ """Provider path if supplied in the broker response."""
+
+ request: Optional[AccessRequestRequest] = None
+ """The request object if returned by the extension.
+
+ The observed create response may omit entries; no entry IDs are invented.
+ """
+
+
+class OnePasswordCredentialVaultItemState(BaseModel):
+ provider: Literal["1password"]
+
+ status: Literal["pending_authorization", "ready", "declined", "failed"]
+
+ access_request: Optional[AccessRequest] = None
+ """Non-secret broker state.
+
+ Granted credential references stay encrypted server-side and can only be used by
+ the fill operation.
+ """
+
+ access_request_id: Optional[str] = None
+ """
+ Opaque request ID returned by the 1Password broker after a successful
+ createAccessRequest call.
+ """
+
+ status_reason: Optional[str] = None
diff --git a/src/kernel/types/vaults/one_password_fill_vault_item_operation_request_param.py b/src/kernel/types/vaults/one_password_fill_vault_item_operation_request_param.py
new file mode 100644
index 00000000..f36a456c
--- /dev/null
+++ b/src/kernel/types/vaults/one_password_fill_vault_item_operation_request_param.py
@@ -0,0 +1,33 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from __future__ import annotations
+
+from typing_extensions import Literal, Required, TypedDict
+
+__all__ = ["OnePasswordFillVaultItemOperationRequestParam"]
+
+
+class OnePasswordFillVaultItemOperationRequestParam(TypedDict, total=False):
+ """Fill and submit an approved 1Password login in the selected browser page.
+
+ The page must share the selected entry's login origin. Supply entry_id when more than one approved entry matches the page origin. The extension selects fields; callers cannot supply selectors or secret values. Submission does not confirm website authentication.
+ """
+
+ browser_id: Required[str]
+ """Browser session ID, not a reusable browser name."""
+
+ page_url: Required[str]
+ """Exact current top-level page URL.
+
+ Must match exactly one open page in the browser.
+ """
+
+ type: Required[Literal["1pw_fill"]]
+
+ entry_id: str
+ """ID of an approved request entry.
+
+ Required when several approved entries have the page's origin.
+ """
+
+ timeout_ms: int
diff --git a/src/kernel/types/vaults/one_password_fill_vault_item_operation_result.py b/src/kernel/types/vaults/one_password_fill_vault_item_operation_result.py
new file mode 100644
index 00000000..b1586f5b
--- /dev/null
+++ b/src/kernel/types/vaults/one_password_fill_vault_item_operation_result.py
@@ -0,0 +1,36 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from typing import Optional
+from typing_extensions import Literal
+
+from ..._models import BaseModel
+
+__all__ = ["OnePasswordFillVaultItemOperationResult"]
+
+
+class OnePasswordFillVaultItemOperationResult(BaseModel):
+ """The submission result reported by the 1Password extension when available.
+
+ Kernel returns fill_unknown if the extension call has no conclusive result. Inspect the page to determine successful authentication on the website.
+ """
+
+ status: Literal["fill_submitted", "fill_failed", "fill_unknown"]
+ """Kernel's outcome of the extension call.
+
+ fill_submitted means the extension reported submission, not website
+ authentication. fill_failed means the extension returned a known failure and may
+ include error_code. fill_unknown means submission may have happened without a
+ conclusive response; it has no error_code and must not be retried in the same
+ browser.
+ """
+
+ type: Literal["1pw_fill"]
+
+ error_code: Optional[Literal["fillFailed", "autosubmitFailed", "noExistingCredentials", "authenticationFailed"]] = (
+ None
+ )
+ """Present only for a conclusive fill_failed response.
+
+ These are allowlisted 1Password extension codes, never raw errors, secrets, or
+ page content.
+ """
diff --git a/src/kernel/types/vaults/one_password_oauth_action.py b/src/kernel/types/vaults/one_password_oauth_action.py
new file mode 100644
index 00000000..b2e4d377
--- /dev/null
+++ b/src/kernel/types/vaults/one_password_oauth_action.py
@@ -0,0 +1,14 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from typing_extensions import Literal
+
+from ..._models import BaseModel
+
+__all__ = ["OnePasswordOAuthAction"]
+
+
+class OnePasswordOAuthAction(BaseModel):
+ name: Literal["1password_oauth"]
+
+ url: str
+ """1Password-hosted OAuth authorization URL for the human to open."""
diff --git a/src/kernel/types/vaults/one_password_recover_vault_item_operation_request_param.py b/src/kernel/types/vaults/one_password_recover_vault_item_operation_request_param.py
new file mode 100644
index 00000000..5913ab40
--- /dev/null
+++ b/src/kernel/types/vaults/one_password_recover_vault_item_operation_request_param.py
@@ -0,0 +1,16 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from __future__ import annotations
+
+from typing_extensions import Literal, Required, TypedDict
+
+__all__ = ["OnePasswordRecoverVaultItemOperationRequestParam"]
+
+
+class OnePasswordRecoverVaultItemOperationRequestParam(TypedDict, total=False):
+ """Kernel encountered a recoverable error while linking this 1Password account.
+
+ Use this action to get a new link to recover the connection. After recovery completes, start a new authorization on the same item.
+ """
+
+ type: Required[Literal["1pw_recover"]]
diff --git a/src/kernel/types/vaults/one_password_request_access_vault_item_operation_request_param.py b/src/kernel/types/vaults/one_password_request_access_vault_item_operation_request_param.py
new file mode 100644
index 00000000..259a31b4
--- /dev/null
+++ b/src/kernel/types/vaults/one_password_request_access_vault_item_operation_request_param.py
@@ -0,0 +1,26 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from __future__ import annotations
+
+from typing_extensions import Literal, Required, TypedDict
+
+from ..._types import SequenceNotStr
+
+__all__ = ["OnePasswordRequestAccessVaultItemOperationRequestParam"]
+
+
+class OnePasswordRequestAccessVaultItemOperationRequestParam(TypedDict, total=False):
+ """
+ Request access to login entries in the end-user's own, non-shared 1Password vault through the browser extension, auto-loaded into the browser. The end-user approves access in the 1Password app. Shared-vault items and passkeys are not supported. Per-entry reason and keywords overrides are only supported for a single login entry.
+ """
+
+ browser_id: Required[str]
+ """Kernel browser session used to invoke the extension."""
+
+ type: Required[Literal["1pw_create_access_request"]]
+
+ goal: str
+
+ keywords: SequenceNotStr[str]
+
+ reason: str
diff --git a/src/kernel/types/vaults/vault_item.py b/src/kernel/types/vaults/vault_item.py
index b9723f4a..7fcdfc5d 100644
--- a/src/kernel/types/vaults/vault_item.py
+++ b/src/kernel/types/vaults/vault_item.py
@@ -13,6 +13,7 @@
from .credential_vault_item import CredentialVaultItem
from .wallet_vault_item_spec import WalletVaultItemSpec
from .wallet_vault_item_state import WalletVaultItemState
+from .credential_account_vault_item import CredentialAccountVaultItem
__all__ = [
"VaultItem",
@@ -44,7 +45,17 @@ class WalletVaultItemAvailableOperation(BaseModel):
description: str
- type: Literal["authorize", "collect", "prepare_checkout", "fill"]
+ type: Literal[
+ "authorize",
+ "collect",
+ "prepare_checkout",
+ "fill",
+ "1pw_create_access_request",
+ "1pw_access_request_status",
+ "1pw_fill",
+ "1pw_recover",
+ "1pw_update_access_token",
+ ]
class WalletVaultItemExpanded(BaseModel):
@@ -108,7 +119,17 @@ class CardVaultItemAvailableOperation(BaseModel):
description: str
- type: Literal["authorize", "collect", "prepare_checkout", "fill"]
+ type: Literal[
+ "authorize",
+ "collect",
+ "prepare_checkout",
+ "fill",
+ "1pw_create_access_request",
+ "1pw_access_request_status",
+ "1pw_fill",
+ "1pw_recover",
+ "1pw_update_access_token",
+ ]
class CardVaultItem(BaseModel):
@@ -142,5 +163,6 @@ class CardVaultItem(BaseModel):
VaultItem: TypeAlias = Annotated[
- Union[WalletVaultItem, CardVaultItem, CredentialVaultItem], PropertyInfo(discriminator="type")
+ Union[WalletVaultItem, CardVaultItem, CredentialAccountVaultItem, CredentialVaultItem],
+ PropertyInfo(discriminator="type"),
]
diff --git a/src/kernel/types/vaults/vault_item_action.py b/src/kernel/types/vaults/vault_item_action.py
index 9054447e..e3816895 100644
--- a/src/kernel/types/vaults/vault_item_action.py
+++ b/src/kernel/types/vaults/vault_item_action.py
@@ -5,6 +5,7 @@
from ..._utils import PropertyInfo
from ..._models import BaseModel
+from .one_password_oauth_action import OnePasswordOAuthAction
__all__ = [
"VaultItemAction",
@@ -55,6 +56,7 @@ class CardEnrollmentAction(BaseModel):
VaultItemAction: TypeAlias = Annotated[
Union[
LinkOAuthAction,
+ OnePasswordOAuthAction,
SpendApprovalAction,
PushApprovalAction,
CollectAction,
diff --git a/src/kernel/types/vaults/vault_item_operation_response.py b/src/kernel/types/vaults/vault_item_operation_response.py
index 33dcd8b3..fd858295 100644
--- a/src/kernel/types/vaults/vault_item_operation_response.py
+++ b/src/kernel/types/vaults/vault_item_operation_response.py
@@ -12,7 +12,9 @@
from .credential_vault_item import CredentialVaultItem
from .wallet_vault_item_spec import WalletVaultItemSpec
from .wallet_vault_item_state import WalletVaultItemState
+from .credential_account_vault_item import CredentialAccountVaultItem
from .fill_vault_item_operation_result import FillVaultItemOperationResult
+from .one_password_fill_vault_item_operation_result import OnePasswordFillVaultItemOperationResult
__all__ = [
"VaultItemOperationResponse",
@@ -44,7 +46,17 @@ class WalletVaultItemAvailableOperation(BaseModel):
description: str
- type: Literal["authorize", "collect", "prepare_checkout", "fill"]
+ type: Literal[
+ "authorize",
+ "collect",
+ "prepare_checkout",
+ "fill",
+ "1pw_create_access_request",
+ "1pw_access_request_status",
+ "1pw_fill",
+ "1pw_recover",
+ "1pw_update_access_token",
+ ]
class WalletVaultItemExpanded(BaseModel):
@@ -108,7 +120,17 @@ class CardVaultItemAvailableOperation(BaseModel):
description: str
- type: Literal["authorize", "collect", "prepare_checkout", "fill"]
+ type: Literal[
+ "authorize",
+ "collect",
+ "prepare_checkout",
+ "fill",
+ "1pw_create_access_request",
+ "1pw_access_request_status",
+ "1pw_fill",
+ "1pw_recover",
+ "1pw_update_access_token",
+ ]
class CardVaultItem(BaseModel):
@@ -142,5 +164,10 @@ class CardVaultItem(BaseModel):
VaultItemOperationResponse: TypeAlias = Union[
- WalletVaultItem, CardVaultItem, CredentialVaultItem, FillVaultItemOperationResult
+ WalletVaultItem,
+ CardVaultItem,
+ CredentialAccountVaultItem,
+ CredentialVaultItem,
+ FillVaultItemOperationResult,
+ OnePasswordFillVaultItemOperationResult,
]
diff --git a/src/kernel/types/vaults/wallet_vault_item_spec.py b/src/kernel/types/vaults/wallet_vault_item_spec.py
index 2c17933d..a09456a0 100644
--- a/src/kernel/types/vaults/wallet_vault_item_spec.py
+++ b/src/kernel/types/vaults/wallet_vault_item_spec.py
@@ -26,7 +26,7 @@ class LinkWalletVaultItemSpecAuthorizationClientKernelManagedOAuthClient(BaseMod
class LinkWalletVaultItemSpecAuthorizationClientCustomerManagedOAuthClientProviderConfig(BaseModel):
"""Select a provider config by ID or name.
- Responses return the ID. Renaming a config does not change existing wallet bindings; a wallet cannot switch to a different config after creation.
+ Responses return the ID. Renaming a config does not change existing wallet bindings; an item cannot switch to a different config after creation.
"""
id: Optional[str] = None
@@ -39,7 +39,7 @@ class LinkWalletVaultItemSpecAuthorizationClientCustomerManagedOAuthClient(BaseM
"""Select a provider config by ID or name.
Responses return the ID. Renaming a config does not change existing wallet
- bindings; a wallet cannot switch to a different config after creation.
+ bindings; an item cannot switch to a different config after creation.
"""
type: Literal["customer_managed"]
diff --git a/tests/api_resources/vaults/test_items.py b/tests/api_resources/vaults/test_items.py
index 61f350e4..bbe7a151 100644
--- a/tests/api_resources/vaults/test_items.py
+++ b/tests/api_resources/vaults/test_items.py
@@ -9,6 +9,7 @@
from kernel import Kernel, AsyncKernel
from tests.utils import assert_matches_type
+from kernel._utils import parse_datetime
from kernel.types.vaults import (
VaultItem,
ItemListResponse,
@@ -876,461 +877,479 @@ def test_path_params_perform_operation_overload_4(self, client: Kernel) -> None:
@pytest.mark.skip(reason="Mock server tests are disabled")
@parametrize
- def test_method_upsert_overload_1(self, client: Kernel) -> None:
- item = client.vaults.items.upsert(
- key="x",
+ def test_method_perform_operation_overload_5(self, client: Kernel) -> None:
+ item = client.vaults.items.perform_operation(
+ key="key",
id_or_name="id_or_name",
- spec={
- "authorization": {
- "client": {"type": "kernel_managed"},
- "method": "oauth",
- },
- "provider": "link",
- },
- type="wallet",
+ browser_id="x",
+ type="1pw_create_access_request",
)
- assert_matches_type(VaultItem, item, path=["response"])
+ assert_matches_type(VaultItemOperationResponse, item, path=["response"])
@pytest.mark.skip(reason="Mock server tests are disabled")
@parametrize
- def test_method_upsert_with_all_params_overload_1(self, client: Kernel) -> None:
- item = client.vaults.items.upsert(
- key="x",
+ def test_method_perform_operation_with_all_params_overload_5(self, client: Kernel) -> None:
+ item = client.vaults.items.perform_operation(
+ key="key",
id_or_name="id_or_name",
- spec={
- "authorization": {
- "client": {"type": "kernel_managed"},
- "method": "oauth",
- },
- "provider": "link",
- },
- type="wallet",
+ browser_id="x",
+ type="1pw_create_access_request",
+ goal="goal",
+ keywords=["x"],
+ reason="reason",
)
- assert_matches_type(VaultItem, item, path=["response"])
+ assert_matches_type(VaultItemOperationResponse, item, path=["response"])
@pytest.mark.skip(reason="Mock server tests are disabled")
@parametrize
- def test_raw_response_upsert_overload_1(self, client: Kernel) -> None:
- response = client.vaults.items.with_raw_response.upsert(
- key="x",
+ def test_raw_response_perform_operation_overload_5(self, client: Kernel) -> None:
+ response = client.vaults.items.with_raw_response.perform_operation(
+ key="key",
id_or_name="id_or_name",
- spec={
- "authorization": {
- "client": {"type": "kernel_managed"},
- "method": "oauth",
- },
- "provider": "link",
- },
- type="wallet",
+ browser_id="x",
+ type="1pw_create_access_request",
)
assert response.is_closed is True
assert response.http_request.headers.get("X-Stainless-Lang") == "python"
item = response.parse()
- assert_matches_type(VaultItem, item, path=["response"])
+ assert_matches_type(VaultItemOperationResponse, item, path=["response"])
@pytest.mark.skip(reason="Mock server tests are disabled")
@parametrize
- def test_streaming_response_upsert_overload_1(self, client: Kernel) -> None:
- with client.vaults.items.with_streaming_response.upsert(
- key="x",
+ def test_streaming_response_perform_operation_overload_5(self, client: Kernel) -> None:
+ with client.vaults.items.with_streaming_response.perform_operation(
+ key="key",
id_or_name="id_or_name",
- spec={
- "authorization": {
- "client": {"type": "kernel_managed"},
- "method": "oauth",
- },
- "provider": "link",
- },
- type="wallet",
+ browser_id="x",
+ type="1pw_create_access_request",
) as response:
assert not response.is_closed
assert response.http_request.headers.get("X-Stainless-Lang") == "python"
item = response.parse()
- assert_matches_type(VaultItem, item, path=["response"])
+ assert_matches_type(VaultItemOperationResponse, item, path=["response"])
assert cast(Any, response.is_closed) is True
@pytest.mark.skip(reason="Mock server tests are disabled")
@parametrize
- def test_path_params_upsert_overload_1(self, client: Kernel) -> None:
+ def test_path_params_perform_operation_overload_5(self, client: Kernel) -> None:
with pytest.raises(ValueError, match=r"Expected a non-empty value for `id_or_name` but received ''"):
- client.vaults.items.with_raw_response.upsert(
- key="x",
+ client.vaults.items.with_raw_response.perform_operation(
+ key="key",
id_or_name="",
- spec={
- "authorization": {
- "client": {"type": "kernel_managed"},
- "method": "oauth",
- },
- "provider": "link",
- },
- type="wallet",
+ browser_id="x",
+ type="1pw_create_access_request",
)
with pytest.raises(ValueError, match=r"Expected a non-empty value for `key` but received ''"):
- client.vaults.items.with_raw_response.upsert(
+ client.vaults.items.with_raw_response.perform_operation(
key="",
id_or_name="id_or_name",
- spec={
- "authorization": {
- "client": {"type": "kernel_managed"},
- "method": "oauth",
- },
- "provider": "link",
- },
- type="wallet",
+ browser_id="x",
+ type="1pw_create_access_request",
)
@pytest.mark.skip(reason="Mock server tests are disabled")
@parametrize
- def test_method_upsert_overload_2(self, client: Kernel) -> None:
- item = client.vaults.items.upsert(
- key="x",
+ def test_method_perform_operation_overload_6(self, client: Kernel) -> None:
+ item = client.vaults.items.perform_operation(
+ key="key",
id_or_name="id_or_name",
- spec={
- "amount": 1,
- "context": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
- "currency": "bFx",
- "merchant_name": "x",
- "merchant_url": "https://example.com",
- "payment_method_id": "x",
- "provider": "link",
- "wallet": "wallet",
- },
- type="card",
+ browser_id="x",
+ type="1pw_access_request_status",
)
- assert_matches_type(VaultItem, item, path=["response"])
+ assert_matches_type(VaultItemOperationResponse, item, path=["response"])
@pytest.mark.skip(reason="Mock server tests are disabled")
@parametrize
- def test_method_upsert_with_all_params_overload_2(self, client: Kernel) -> None:
- item = client.vaults.items.upsert(
- key="x",
+ def test_method_perform_operation_with_all_params_overload_6(self, client: Kernel) -> None:
+ item = client.vaults.items.perform_operation(
+ key="key",
id_or_name="id_or_name",
- spec={
- "amount": 1,
- "context": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
- "currency": "bFx",
- "merchant_name": "x",
- "merchant_url": "https://example.com",
- "payment_method_id": "x",
- "provider": "link",
- "wallet": "wallet",
- "expires_at": 0,
- "line_items": [
- {
- "name": "name",
- "description": "description",
- "image_url": "image_url",
- "product_url": "product_url",
- "quantity": 1,
- "sku": "sku",
- "totals": [
- {
- "amount": 0,
- "display_text": "display_text",
- "type": "type",
- }
- ],
- "unit_amount": 0,
- "url": "url",
- }
- ],
- "metadata": {"foo": "string"},
- "totals": [
- {
- "amount": 0,
- "display_text": "display_text",
- "type": "type",
- }
- ],
- },
- type="card",
+ browser_id="x",
+ type="1pw_access_request_status",
+ timeout_seconds=0,
)
- assert_matches_type(VaultItem, item, path=["response"])
+ assert_matches_type(VaultItemOperationResponse, item, path=["response"])
@pytest.mark.skip(reason="Mock server tests are disabled")
@parametrize
- def test_raw_response_upsert_overload_2(self, client: Kernel) -> None:
- response = client.vaults.items.with_raw_response.upsert(
- key="x",
+ def test_raw_response_perform_operation_overload_6(self, client: Kernel) -> None:
+ response = client.vaults.items.with_raw_response.perform_operation(
+ key="key",
id_or_name="id_or_name",
- spec={
- "amount": 1,
- "context": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
- "currency": "bFx",
- "merchant_name": "x",
- "merchant_url": "https://example.com",
- "payment_method_id": "x",
- "provider": "link",
- "wallet": "wallet",
- },
- type="card",
+ browser_id="x",
+ type="1pw_access_request_status",
)
assert response.is_closed is True
assert response.http_request.headers.get("X-Stainless-Lang") == "python"
item = response.parse()
- assert_matches_type(VaultItem, item, path=["response"])
+ assert_matches_type(VaultItemOperationResponse, item, path=["response"])
@pytest.mark.skip(reason="Mock server tests are disabled")
@parametrize
- def test_streaming_response_upsert_overload_2(self, client: Kernel) -> None:
- with client.vaults.items.with_streaming_response.upsert(
- key="x",
+ def test_streaming_response_perform_operation_overload_6(self, client: Kernel) -> None:
+ with client.vaults.items.with_streaming_response.perform_operation(
+ key="key",
id_or_name="id_or_name",
- spec={
- "amount": 1,
- "context": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
- "currency": "bFx",
- "merchant_name": "x",
- "merchant_url": "https://example.com",
- "payment_method_id": "x",
- "provider": "link",
- "wallet": "wallet",
- },
- type="card",
+ browser_id="x",
+ type="1pw_access_request_status",
) as response:
assert not response.is_closed
assert response.http_request.headers.get("X-Stainless-Lang") == "python"
item = response.parse()
- assert_matches_type(VaultItem, item, path=["response"])
+ assert_matches_type(VaultItemOperationResponse, item, path=["response"])
assert cast(Any, response.is_closed) is True
@pytest.mark.skip(reason="Mock server tests are disabled")
@parametrize
- def test_path_params_upsert_overload_2(self, client: Kernel) -> None:
+ def test_path_params_perform_operation_overload_6(self, client: Kernel) -> None:
with pytest.raises(ValueError, match=r"Expected a non-empty value for `id_or_name` but received ''"):
- client.vaults.items.with_raw_response.upsert(
- key="x",
+ client.vaults.items.with_raw_response.perform_operation(
+ key="key",
id_or_name="",
- spec={
- "amount": 1,
- "context": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
- "currency": "bFx",
- "merchant_name": "x",
- "merchant_url": "https://example.com",
- "payment_method_id": "x",
- "provider": "link",
- "wallet": "wallet",
- },
- type="card",
+ browser_id="x",
+ type="1pw_access_request_status",
)
with pytest.raises(ValueError, match=r"Expected a non-empty value for `key` but received ''"):
- client.vaults.items.with_raw_response.upsert(
+ client.vaults.items.with_raw_response.perform_operation(
key="",
id_or_name="id_or_name",
- spec={
- "amount": 1,
- "context": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
- "currency": "bFx",
- "merchant_name": "x",
- "merchant_url": "https://example.com",
- "payment_method_id": "x",
- "provider": "link",
- "wallet": "wallet",
- },
- type="card",
+ browser_id="x",
+ type="1pw_access_request_status",
)
@pytest.mark.skip(reason="Mock server tests are disabled")
@parametrize
- def test_method_upsert_overload_3(self, client: Kernel) -> None:
- item = client.vaults.items.upsert(
- key="x",
+ def test_method_perform_operation_overload_7(self, client: Kernel) -> None:
+ item = client.vaults.items.perform_operation(
+ key="key",
id_or_name="id_or_name",
- spec={
- "fields": [
- {
- "name": "name",
- "type": "text",
- }
- ]
- },
- type="credential",
+ browser_id="x",
+ page_url="https://example.com",
+ type="1pw_fill",
)
- assert_matches_type(VaultItem, item, path=["response"])
+ assert_matches_type(VaultItemOperationResponse, item, path=["response"])
@pytest.mark.skip(reason="Mock server tests are disabled")
@parametrize
- def test_method_upsert_with_all_params_overload_3(self, client: Kernel) -> None:
- item = client.vaults.items.upsert(
- key="x",
+ def test_method_perform_operation_with_all_params_overload_7(self, client: Kernel) -> None:
+ item = client.vaults.items.perform_operation(
+ key="key",
id_or_name="id_or_name",
- spec={
- "fields": [
- {
- "name": "name",
- "type": "text",
- "label": "label",
- "required": True,
- "sensitive": True,
- "value": "x",
- }
- ],
- "description": "description",
- },
- type="credential",
+ browser_id="x",
+ page_url="https://example.com",
+ type="1pw_fill",
+ entry_id="x",
+ timeout_ms=1,
)
- assert_matches_type(VaultItem, item, path=["response"])
+ assert_matches_type(VaultItemOperationResponse, item, path=["response"])
@pytest.mark.skip(reason="Mock server tests are disabled")
@parametrize
- def test_raw_response_upsert_overload_3(self, client: Kernel) -> None:
- response = client.vaults.items.with_raw_response.upsert(
- key="x",
+ def test_raw_response_perform_operation_overload_7(self, client: Kernel) -> None:
+ response = client.vaults.items.with_raw_response.perform_operation(
+ key="key",
id_or_name="id_or_name",
- spec={
- "fields": [
- {
- "name": "name",
- "type": "text",
- }
- ]
- },
- type="credential",
+ browser_id="x",
+ page_url="https://example.com",
+ type="1pw_fill",
)
assert response.is_closed is True
assert response.http_request.headers.get("X-Stainless-Lang") == "python"
item = response.parse()
- assert_matches_type(VaultItem, item, path=["response"])
+ assert_matches_type(VaultItemOperationResponse, item, path=["response"])
@pytest.mark.skip(reason="Mock server tests are disabled")
@parametrize
- def test_streaming_response_upsert_overload_3(self, client: Kernel) -> None:
- with client.vaults.items.with_streaming_response.upsert(
- key="x",
+ def test_streaming_response_perform_operation_overload_7(self, client: Kernel) -> None:
+ with client.vaults.items.with_streaming_response.perform_operation(
+ key="key",
id_or_name="id_or_name",
- spec={
- "fields": [
- {
- "name": "name",
- "type": "text",
- }
- ]
- },
- type="credential",
+ browser_id="x",
+ page_url="https://example.com",
+ type="1pw_fill",
) as response:
assert not response.is_closed
assert response.http_request.headers.get("X-Stainless-Lang") == "python"
item = response.parse()
- assert_matches_type(VaultItem, item, path=["response"])
+ assert_matches_type(VaultItemOperationResponse, item, path=["response"])
assert cast(Any, response.is_closed) is True
@pytest.mark.skip(reason="Mock server tests are disabled")
@parametrize
- def test_path_params_upsert_overload_3(self, client: Kernel) -> None:
+ def test_path_params_perform_operation_overload_7(self, client: Kernel) -> None:
with pytest.raises(ValueError, match=r"Expected a non-empty value for `id_or_name` but received ''"):
- client.vaults.items.with_raw_response.upsert(
- key="x",
+ client.vaults.items.with_raw_response.perform_operation(
+ key="key",
id_or_name="",
- spec={
- "fields": [
- {
- "name": "name",
- "type": "text",
- }
- ]
- },
- type="credential",
+ browser_id="x",
+ page_url="https://example.com",
+ type="1pw_fill",
)
with pytest.raises(ValueError, match=r"Expected a non-empty value for `key` but received ''"):
- client.vaults.items.with_raw_response.upsert(
+ client.vaults.items.with_raw_response.perform_operation(
key="",
id_or_name="id_or_name",
- spec={
- "fields": [
- {
- "name": "name",
- "type": "text",
- }
- ]
- },
- type="credential",
+ browser_id="x",
+ page_url="https://example.com",
+ type="1pw_fill",
)
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ def test_method_perform_operation_overload_8(self, client: Kernel) -> None:
+ item = client.vaults.items.perform_operation(
+ key="key",
+ id_or_name="id_or_name",
+ type="1pw_recover",
+ )
+ assert_matches_type(VaultItemOperationResponse, item, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ def test_raw_response_perform_operation_overload_8(self, client: Kernel) -> None:
+ response = client.vaults.items.with_raw_response.perform_operation(
+ key="key",
+ id_or_name="id_or_name",
+ type="1pw_recover",
+ )
-class TestAsyncItems:
- parametrize = pytest.mark.parametrize(
- "async_client", [False, True, {"http_client": "aiohttp"}], indirect=True, ids=["loose", "strict", "aiohttp"]
- )
+ assert response.is_closed is True
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+ item = response.parse()
+ assert_matches_type(VaultItemOperationResponse, item, path=["response"])
@pytest.mark.skip(reason="Mock server tests are disabled")
@parametrize
- async def test_method_retrieve(self, async_client: AsyncKernel) -> None:
- item = await async_client.vaults.items.retrieve(
+ def test_streaming_response_perform_operation_overload_8(self, client: Kernel) -> None:
+ with client.vaults.items.with_streaming_response.perform_operation(
+ key="key",
+ id_or_name="id_or_name",
+ type="1pw_recover",
+ ) as response:
+ assert not response.is_closed
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+
+ item = response.parse()
+ assert_matches_type(VaultItemOperationResponse, item, path=["response"])
+
+ assert cast(Any, response.is_closed) is True
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ def test_path_params_perform_operation_overload_8(self, client: Kernel) -> None:
+ with pytest.raises(ValueError, match=r"Expected a non-empty value for `id_or_name` but received ''"):
+ client.vaults.items.with_raw_response.perform_operation(
+ key="key",
+ id_or_name="",
+ type="1pw_recover",
+ )
+
+ with pytest.raises(ValueError, match=r"Expected a non-empty value for `key` but received ''"):
+ client.vaults.items.with_raw_response.perform_operation(
+ key="",
+ id_or_name="id_or_name",
+ type="1pw_recover",
+ )
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ def test_method_perform_operation_overload_9(self, client: Kernel) -> None:
+ item = client.vaults.items.perform_operation(
+ key="key",
+ id_or_name="id_or_name",
+ access_token="x",
+ type="1pw_update_access_token",
+ )
+ assert_matches_type(VaultItemOperationResponse, item, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ def test_method_perform_operation_with_all_params_overload_9(self, client: Kernel) -> None:
+ item = client.vaults.items.perform_operation(
+ key="key",
+ id_or_name="id_or_name",
+ access_token="x",
+ type="1pw_update_access_token",
+ access_token_expires_at=parse_datetime("2019-12-27T18:11:19.117Z"),
+ )
+ assert_matches_type(VaultItemOperationResponse, item, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ def test_raw_response_perform_operation_overload_9(self, client: Kernel) -> None:
+ response = client.vaults.items.with_raw_response.perform_operation(
+ key="key",
+ id_or_name="id_or_name",
+ access_token="x",
+ type="1pw_update_access_token",
+ )
+
+ assert response.is_closed is True
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+ item = response.parse()
+ assert_matches_type(VaultItemOperationResponse, item, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ def test_streaming_response_perform_operation_overload_9(self, client: Kernel) -> None:
+ with client.vaults.items.with_streaming_response.perform_operation(
+ key="key",
+ id_or_name="id_or_name",
+ access_token="x",
+ type="1pw_update_access_token",
+ ) as response:
+ assert not response.is_closed
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+
+ item = response.parse()
+ assert_matches_type(VaultItemOperationResponse, item, path=["response"])
+
+ assert cast(Any, response.is_closed) is True
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ def test_path_params_perform_operation_overload_9(self, client: Kernel) -> None:
+ with pytest.raises(ValueError, match=r"Expected a non-empty value for `id_or_name` but received ''"):
+ client.vaults.items.with_raw_response.perform_operation(
+ key="key",
+ id_or_name="",
+ access_token="x",
+ type="1pw_update_access_token",
+ )
+
+ with pytest.raises(ValueError, match=r"Expected a non-empty value for `key` but received ''"):
+ client.vaults.items.with_raw_response.perform_operation(
+ key="",
+ id_or_name="id_or_name",
+ access_token="x",
+ type="1pw_update_access_token",
+ )
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ def test_method_upsert_overload_1(self, client: Kernel) -> None:
+ item = client.vaults.items.upsert(
key="x",
id_or_name="id_or_name",
+ spec={
+ "authorization": {
+ "client": {"type": "kernel_managed"},
+ "method": "oauth",
+ },
+ "provider": "link",
+ },
+ type="wallet",
)
assert_matches_type(VaultItem, item, path=["response"])
@pytest.mark.skip(reason="Mock server tests are disabled")
@parametrize
- async def test_method_retrieve_with_all_params(self, async_client: AsyncKernel) -> None:
- item = await async_client.vaults.items.retrieve(
+ def test_method_upsert_with_all_params_overload_1(self, client: Kernel) -> None:
+ item = client.vaults.items.upsert(
key="x",
id_or_name="id_or_name",
- expand=["payment_methods"],
- wait=0,
+ spec={
+ "authorization": {
+ "client": {"type": "kernel_managed"},
+ "method": "oauth",
+ },
+ "provider": "link",
+ },
+ type="wallet",
)
assert_matches_type(VaultItem, item, path=["response"])
@pytest.mark.skip(reason="Mock server tests are disabled")
@parametrize
- async def test_raw_response_retrieve(self, async_client: AsyncKernel) -> None:
- response = await async_client.vaults.items.with_raw_response.retrieve(
+ def test_raw_response_upsert_overload_1(self, client: Kernel) -> None:
+ response = client.vaults.items.with_raw_response.upsert(
key="x",
id_or_name="id_or_name",
+ spec={
+ "authorization": {
+ "client": {"type": "kernel_managed"},
+ "method": "oauth",
+ },
+ "provider": "link",
+ },
+ type="wallet",
)
assert response.is_closed is True
assert response.http_request.headers.get("X-Stainless-Lang") == "python"
- item = await response.parse()
+ item = response.parse()
assert_matches_type(VaultItem, item, path=["response"])
@pytest.mark.skip(reason="Mock server tests are disabled")
@parametrize
- async def test_streaming_response_retrieve(self, async_client: AsyncKernel) -> None:
- async with async_client.vaults.items.with_streaming_response.retrieve(
+ def test_streaming_response_upsert_overload_1(self, client: Kernel) -> None:
+ with client.vaults.items.with_streaming_response.upsert(
key="x",
id_or_name="id_or_name",
+ spec={
+ "authorization": {
+ "client": {"type": "kernel_managed"},
+ "method": "oauth",
+ },
+ "provider": "link",
+ },
+ type="wallet",
) as response:
assert not response.is_closed
assert response.http_request.headers.get("X-Stainless-Lang") == "python"
- item = await response.parse()
+ item = response.parse()
assert_matches_type(VaultItem, item, 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_retrieve(self, async_client: AsyncKernel) -> None:
+ def test_path_params_upsert_overload_1(self, client: Kernel) -> None:
with pytest.raises(ValueError, match=r"Expected a non-empty value for `id_or_name` but received ''"):
- await async_client.vaults.items.with_raw_response.retrieve(
+ client.vaults.items.with_raw_response.upsert(
key="x",
id_or_name="",
+ spec={
+ "authorization": {
+ "client": {"type": "kernel_managed"},
+ "method": "oauth",
+ },
+ "provider": "link",
+ },
+ type="wallet",
)
with pytest.raises(ValueError, match=r"Expected a non-empty value for `key` but received ''"):
- await async_client.vaults.items.with_raw_response.retrieve(
+ client.vaults.items.with_raw_response.upsert(
key="",
id_or_name="id_or_name",
+ spec={
+ "authorization": {
+ "client": {"type": "kernel_managed"},
+ "method": "oauth",
+ },
+ "provider": "link",
+ },
+ type="wallet",
)
@pytest.mark.skip(reason="Mock server tests are disabled")
@parametrize
- async def test_method_update_overload_1(self, async_client: AsyncKernel) -> None:
- item = await async_client.vaults.items.update(
+ def test_method_upsert_overload_2(self, client: Kernel) -> None:
+ item = client.vaults.items.upsert(
key="x",
id_or_name="id_or_name",
spec={
@@ -1343,13 +1362,14 @@ async def test_method_update_overload_1(self, async_client: AsyncKernel) -> None
"provider": "link",
"wallet": "wallet",
},
+ type="card",
)
assert_matches_type(VaultItem, item, path=["response"])
@pytest.mark.skip(reason="Mock server tests are disabled")
@parametrize
- async def test_method_update_with_all_params_overload_1(self, async_client: AsyncKernel) -> None:
- item = await async_client.vaults.items.update(
+ def test_method_upsert_with_all_params_overload_2(self, client: Kernel) -> None:
+ item = client.vaults.items.upsert(
key="x",
id_or_name="id_or_name",
spec={
@@ -1396,8 +1416,8 @@ async def test_method_update_with_all_params_overload_1(self, async_client: Asyn
@pytest.mark.skip(reason="Mock server tests are disabled")
@parametrize
- async def test_raw_response_update_overload_1(self, async_client: AsyncKernel) -> None:
- response = await async_client.vaults.items.with_raw_response.update(
+ def test_raw_response_upsert_overload_2(self, client: Kernel) -> None:
+ response = client.vaults.items.with_raw_response.upsert(
key="x",
id_or_name="id_or_name",
spec={
@@ -1410,17 +1430,18 @@ async def test_raw_response_update_overload_1(self, async_client: AsyncKernel) -
"provider": "link",
"wallet": "wallet",
},
+ type="card",
)
assert response.is_closed is True
assert response.http_request.headers.get("X-Stainless-Lang") == "python"
- item = await response.parse()
+ item = response.parse()
assert_matches_type(VaultItem, item, path=["response"])
@pytest.mark.skip(reason="Mock server tests are disabled")
@parametrize
- async def test_streaming_response_update_overload_1(self, async_client: AsyncKernel) -> None:
- async with async_client.vaults.items.with_streaming_response.update(
+ def test_streaming_response_upsert_overload_2(self, client: Kernel) -> None:
+ with client.vaults.items.with_streaming_response.upsert(
key="x",
id_or_name="id_or_name",
spec={
@@ -1433,20 +1454,21 @@ async def test_streaming_response_update_overload_1(self, async_client: AsyncKer
"provider": "link",
"wallet": "wallet",
},
+ type="card",
) as response:
assert not response.is_closed
assert response.http_request.headers.get("X-Stainless-Lang") == "python"
- item = await response.parse()
+ item = response.parse()
assert_matches_type(VaultItem, item, 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_overload_1(self, async_client: AsyncKernel) -> None:
+ def test_path_params_upsert_overload_2(self, client: Kernel) -> None:
with pytest.raises(ValueError, match=r"Expected a non-empty value for `id_or_name` but received ''"):
- await async_client.vaults.items.with_raw_response.update(
+ client.vaults.items.with_raw_response.upsert(
key="x",
id_or_name="",
spec={
@@ -1459,10 +1481,11 @@ async def test_path_params_update_overload_1(self, async_client: AsyncKernel) ->
"provider": "link",
"wallet": "wallet",
},
+ type="card",
)
with pytest.raises(ValueError, match=r"Expected a non-empty value for `key` but received ''"):
- await async_client.vaults.items.with_raw_response.update(
+ client.vaults.items.with_raw_response.upsert(
key="",
id_or_name="id_or_name",
spec={
@@ -1475,265 +1498,1192 @@ async def test_path_params_update_overload_1(self, async_client: AsyncKernel) ->
"provider": "link",
"wallet": "wallet",
},
+ type="card",
)
@pytest.mark.skip(reason="Mock server tests are disabled")
@parametrize
- async def test_method_update_overload_2(self, async_client: AsyncKernel) -> None:
- item = await async_client.vaults.items.update(
+ def test_method_upsert_overload_3(self, client: Kernel) -> None:
+ item = client.vaults.items.upsert(
key="x",
id_or_name="id_or_name",
- spec={},
+ spec={
+ "authorization": {
+ "client": {"type": "kernel_managed"},
+ "method": "oauth",
+ },
+ "provider": "1password",
+ },
+ type="credential_account",
+ )
+ assert_matches_type(VaultItem, item, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ def test_raw_response_upsert_overload_3(self, client: Kernel) -> None:
+ response = client.vaults.items.with_raw_response.upsert(
+ key="x",
+ id_or_name="id_or_name",
+ spec={
+ "authorization": {
+ "client": {"type": "kernel_managed"},
+ "method": "oauth",
+ },
+ "provider": "1password",
+ },
+ type="credential_account",
+ )
+
+ assert response.is_closed is True
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+ item = response.parse()
+ assert_matches_type(VaultItem, item, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ def test_streaming_response_upsert_overload_3(self, client: Kernel) -> None:
+ with client.vaults.items.with_streaming_response.upsert(
+ key="x",
+ id_or_name="id_or_name",
+ spec={
+ "authorization": {
+ "client": {"type": "kernel_managed"},
+ "method": "oauth",
+ },
+ "provider": "1password",
+ },
+ type="credential_account",
+ ) as response:
+ assert not response.is_closed
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+
+ item = response.parse()
+ assert_matches_type(VaultItem, item, path=["response"])
+
+ assert cast(Any, response.is_closed) is True
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ def test_path_params_upsert_overload_3(self, client: Kernel) -> None:
+ with pytest.raises(ValueError, match=r"Expected a non-empty value for `id_or_name` but received ''"):
+ client.vaults.items.with_raw_response.upsert(
+ key="x",
+ id_or_name="",
+ spec={
+ "authorization": {
+ "client": {"type": "kernel_managed"},
+ "method": "oauth",
+ },
+ "provider": "1password",
+ },
+ type="credential_account",
+ )
+
+ with pytest.raises(ValueError, match=r"Expected a non-empty value for `key` but received ''"):
+ client.vaults.items.with_raw_response.upsert(
+ key="",
+ id_or_name="id_or_name",
+ spec={
+ "authorization": {
+ "client": {"type": "kernel_managed"},
+ "method": "oauth",
+ },
+ "provider": "1password",
+ },
+ type="credential_account",
+ )
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ def test_method_upsert_overload_4(self, client: Kernel) -> None:
+ item = client.vaults.items.upsert(
+ key="x",
+ id_or_name="id_or_name",
+ spec={
+ "fields": [
+ {
+ "name": "name",
+ "type": "text",
+ }
+ ],
+ "provider": "kernel",
+ },
type="credential",
- version=1,
)
assert_matches_type(VaultItem, item, path=["response"])
@pytest.mark.skip(reason="Mock server tests are disabled")
@parametrize
- async def test_method_update_with_all_params_overload_2(self, async_client: AsyncKernel) -> None:
- item = await async_client.vaults.items.update(
+ def test_method_upsert_with_all_params_overload_4(self, client: Kernel) -> None:
+ item = client.vaults.items.upsert(
key="x",
id_or_name="id_or_name",
spec={
+ "fields": [
+ {
+ "name": "name",
+ "type": "text",
+ "label": "label",
+ "required": True,
+ "sensitive": True,
+ "value": "x",
+ }
+ ],
+ "provider": "kernel",
"description": "description",
- "fields": {"foo": {"value": "value"}},
},
type="credential",
- version=1,
- expected_item_id="x",
)
assert_matches_type(VaultItem, item, path=["response"])
@pytest.mark.skip(reason="Mock server tests are disabled")
@parametrize
- async def test_raw_response_update_overload_2(self, async_client: AsyncKernel) -> None:
- response = await async_client.vaults.items.with_raw_response.update(
+ def test_raw_response_upsert_overload_4(self, client: Kernel) -> None:
+ response = client.vaults.items.with_raw_response.upsert(
key="x",
id_or_name="id_or_name",
- spec={},
+ spec={
+ "fields": [
+ {
+ "name": "name",
+ "type": "text",
+ }
+ ],
+ "provider": "kernel",
+ },
type="credential",
- version=1,
)
assert response.is_closed is True
assert response.http_request.headers.get("X-Stainless-Lang") == "python"
- item = await response.parse()
+ item = response.parse()
assert_matches_type(VaultItem, item, path=["response"])
@pytest.mark.skip(reason="Mock server tests are disabled")
@parametrize
- async def test_streaming_response_update_overload_2(self, async_client: AsyncKernel) -> None:
- async with async_client.vaults.items.with_streaming_response.update(
+ def test_streaming_response_upsert_overload_4(self, client: Kernel) -> None:
+ with client.vaults.items.with_streaming_response.upsert(
key="x",
id_or_name="id_or_name",
- spec={},
+ spec={
+ "fields": [
+ {
+ "name": "name",
+ "type": "text",
+ }
+ ],
+ "provider": "kernel",
+ },
type="credential",
- version=1,
) as response:
assert not response.is_closed
assert response.http_request.headers.get("X-Stainless-Lang") == "python"
- item = await response.parse()
+ item = response.parse()
assert_matches_type(VaultItem, item, 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_overload_2(self, async_client: AsyncKernel) -> None:
+ def test_path_params_upsert_overload_4(self, client: Kernel) -> None:
with pytest.raises(ValueError, match=r"Expected a non-empty value for `id_or_name` but received ''"):
- await async_client.vaults.items.with_raw_response.update(
+ client.vaults.items.with_raw_response.upsert(
key="x",
id_or_name="",
- spec={},
+ spec={
+ "fields": [
+ {
+ "name": "name",
+ "type": "text",
+ }
+ ],
+ "provider": "kernel",
+ },
type="credential",
- version=1,
)
with pytest.raises(ValueError, match=r"Expected a non-empty value for `key` but received ''"):
- await async_client.vaults.items.with_raw_response.update(
+ client.vaults.items.with_raw_response.upsert(
key="",
id_or_name="id_or_name",
- spec={},
+ spec={
+ "fields": [
+ {
+ "name": "name",
+ "type": "text",
+ }
+ ],
+ "provider": "kernel",
+ },
type="credential",
- version=1,
)
+
+class TestAsyncItems:
+ parametrize = pytest.mark.parametrize(
+ "async_client", [False, True, {"http_client": "aiohttp"}], indirect=True, ids=["loose", "strict", "aiohttp"]
+ )
+
@pytest.mark.skip(reason="Mock server tests are disabled")
@parametrize
- async def test_method_list(self, async_client: AsyncKernel) -> None:
- item = await async_client.vaults.items.list(
- "id_or_name",
+ async def test_method_retrieve(self, async_client: AsyncKernel) -> None:
+ item = await async_client.vaults.items.retrieve(
+ key="x",
+ id_or_name="id_or_name",
)
- assert_matches_type(ItemListResponse, item, path=["response"])
+ assert_matches_type(VaultItem, item, path=["response"])
@pytest.mark.skip(reason="Mock server tests are disabled")
@parametrize
- async def test_raw_response_list(self, async_client: AsyncKernel) -> None:
- response = await async_client.vaults.items.with_raw_response.list(
- "id_or_name",
+ async def test_method_retrieve_with_all_params(self, async_client: AsyncKernel) -> None:
+ item = await async_client.vaults.items.retrieve(
+ key="x",
+ id_or_name="id_or_name",
+ expand=["payment_methods"],
+ wait=0,
+ )
+ assert_matches_type(VaultItem, item, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ async def test_raw_response_retrieve(self, async_client: AsyncKernel) -> None:
+ response = await async_client.vaults.items.with_raw_response.retrieve(
+ key="x",
+ id_or_name="id_or_name",
)
assert response.is_closed is True
assert response.http_request.headers.get("X-Stainless-Lang") == "python"
item = await response.parse()
- assert_matches_type(ItemListResponse, item, path=["response"])
+ assert_matches_type(VaultItem, item, path=["response"])
@pytest.mark.skip(reason="Mock server tests are disabled")
@parametrize
- async def test_streaming_response_list(self, async_client: AsyncKernel) -> None:
- async with async_client.vaults.items.with_streaming_response.list(
- "id_or_name",
+ async def test_streaming_response_retrieve(self, async_client: AsyncKernel) -> None:
+ async with async_client.vaults.items.with_streaming_response.retrieve(
+ key="x",
+ id_or_name="id_or_name",
) as response:
assert not response.is_closed
assert response.http_request.headers.get("X-Stainless-Lang") == "python"
item = await response.parse()
- assert_matches_type(ItemListResponse, item, path=["response"])
+ assert_matches_type(VaultItem, item, 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_list(self, async_client: AsyncKernel) -> None:
- with pytest.raises(ValueError, match=r"Expected a non-empty value for `id_or_name` but received ''"):
- await async_client.vaults.items.with_raw_response.list(
- "",
- )
+ async def test_path_params_retrieve(self, async_client: AsyncKernel) -> None:
+ with pytest.raises(ValueError, match=r"Expected a non-empty value for `id_or_name` but received ''"):
+ await async_client.vaults.items.with_raw_response.retrieve(
+ key="x",
+ id_or_name="",
+ )
+
+ with pytest.raises(ValueError, match=r"Expected a non-empty value for `key` but received ''"):
+ await async_client.vaults.items.with_raw_response.retrieve(
+ key="",
+ id_or_name="id_or_name",
+ )
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ async def test_method_update_overload_1(self, async_client: AsyncKernel) -> None:
+ item = await async_client.vaults.items.update(
+ key="x",
+ id_or_name="id_or_name",
+ spec={
+ "amount": 1,
+ "context": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
+ "currency": "bFx",
+ "merchant_name": "x",
+ "merchant_url": "https://example.com",
+ "payment_method_id": "x",
+ "provider": "link",
+ "wallet": "wallet",
+ },
+ )
+ assert_matches_type(VaultItem, item, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ async def test_method_update_with_all_params_overload_1(self, async_client: AsyncKernel) -> None:
+ item = await async_client.vaults.items.update(
+ key="x",
+ id_or_name="id_or_name",
+ spec={
+ "amount": 1,
+ "context": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
+ "currency": "bFx",
+ "merchant_name": "x",
+ "merchant_url": "https://example.com",
+ "payment_method_id": "x",
+ "provider": "link",
+ "wallet": "wallet",
+ "expires_at": 0,
+ "line_items": [
+ {
+ "name": "name",
+ "description": "description",
+ "image_url": "image_url",
+ "product_url": "product_url",
+ "quantity": 1,
+ "sku": "sku",
+ "totals": [
+ {
+ "amount": 0,
+ "display_text": "display_text",
+ "type": "type",
+ }
+ ],
+ "unit_amount": 0,
+ "url": "url",
+ }
+ ],
+ "metadata": {"foo": "string"},
+ "totals": [
+ {
+ "amount": 0,
+ "display_text": "display_text",
+ "type": "type",
+ }
+ ],
+ },
+ type="card",
+ )
+ assert_matches_type(VaultItem, item, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ async def test_raw_response_update_overload_1(self, async_client: AsyncKernel) -> None:
+ response = await async_client.vaults.items.with_raw_response.update(
+ key="x",
+ id_or_name="id_or_name",
+ spec={
+ "amount": 1,
+ "context": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
+ "currency": "bFx",
+ "merchant_name": "x",
+ "merchant_url": "https://example.com",
+ "payment_method_id": "x",
+ "provider": "link",
+ "wallet": "wallet",
+ },
+ )
+
+ assert response.is_closed is True
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+ item = await response.parse()
+ assert_matches_type(VaultItem, item, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ async def test_streaming_response_update_overload_1(self, async_client: AsyncKernel) -> None:
+ async with async_client.vaults.items.with_streaming_response.update(
+ key="x",
+ id_or_name="id_or_name",
+ spec={
+ "amount": 1,
+ "context": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
+ "currency": "bFx",
+ "merchant_name": "x",
+ "merchant_url": "https://example.com",
+ "payment_method_id": "x",
+ "provider": "link",
+ "wallet": "wallet",
+ },
+ ) as response:
+ assert not response.is_closed
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+
+ item = await response.parse()
+ assert_matches_type(VaultItem, item, 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_overload_1(self, async_client: AsyncKernel) -> None:
+ with pytest.raises(ValueError, match=r"Expected a non-empty value for `id_or_name` but received ''"):
+ await async_client.vaults.items.with_raw_response.update(
+ key="x",
+ id_or_name="",
+ spec={
+ "amount": 1,
+ "context": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
+ "currency": "bFx",
+ "merchant_name": "x",
+ "merchant_url": "https://example.com",
+ "payment_method_id": "x",
+ "provider": "link",
+ "wallet": "wallet",
+ },
+ )
+
+ with pytest.raises(ValueError, match=r"Expected a non-empty value for `key` but received ''"):
+ await async_client.vaults.items.with_raw_response.update(
+ key="",
+ id_or_name="id_or_name",
+ spec={
+ "amount": 1,
+ "context": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
+ "currency": "bFx",
+ "merchant_name": "x",
+ "merchant_url": "https://example.com",
+ "payment_method_id": "x",
+ "provider": "link",
+ "wallet": "wallet",
+ },
+ )
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ async def test_method_update_overload_2(self, async_client: AsyncKernel) -> None:
+ item = await async_client.vaults.items.update(
+ key="x",
+ id_or_name="id_or_name",
+ spec={},
+ type="credential",
+ version=1,
+ )
+ assert_matches_type(VaultItem, item, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ async def test_method_update_with_all_params_overload_2(self, async_client: AsyncKernel) -> None:
+ item = await async_client.vaults.items.update(
+ key="x",
+ id_or_name="id_or_name",
+ spec={
+ "description": "description",
+ "fields": {"foo": {"value": "value"}},
+ },
+ type="credential",
+ version=1,
+ expected_item_id="x",
+ )
+ assert_matches_type(VaultItem, item, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ async def test_raw_response_update_overload_2(self, async_client: AsyncKernel) -> None:
+ response = await async_client.vaults.items.with_raw_response.update(
+ key="x",
+ id_or_name="id_or_name",
+ spec={},
+ type="credential",
+ version=1,
+ )
+
+ assert response.is_closed is True
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+ item = await response.parse()
+ assert_matches_type(VaultItem, item, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ async def test_streaming_response_update_overload_2(self, async_client: AsyncKernel) -> None:
+ async with async_client.vaults.items.with_streaming_response.update(
+ key="x",
+ id_or_name="id_or_name",
+ spec={},
+ type="credential",
+ version=1,
+ ) as response:
+ assert not response.is_closed
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+
+ item = await response.parse()
+ assert_matches_type(VaultItem, item, 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_overload_2(self, async_client: AsyncKernel) -> None:
+ with pytest.raises(ValueError, match=r"Expected a non-empty value for `id_or_name` but received ''"):
+ await async_client.vaults.items.with_raw_response.update(
+ key="x",
+ id_or_name="",
+ spec={},
+ type="credential",
+ version=1,
+ )
+
+ with pytest.raises(ValueError, match=r"Expected a non-empty value for `key` but received ''"):
+ await async_client.vaults.items.with_raw_response.update(
+ key="",
+ id_or_name="id_or_name",
+ spec={},
+ type="credential",
+ version=1,
+ )
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ async def test_method_list(self, async_client: AsyncKernel) -> None:
+ item = await async_client.vaults.items.list(
+ "id_or_name",
+ )
+ assert_matches_type(ItemListResponse, item, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ async def test_raw_response_list(self, async_client: AsyncKernel) -> None:
+ response = await async_client.vaults.items.with_raw_response.list(
+ "id_or_name",
+ )
+
+ assert response.is_closed is True
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+ item = await response.parse()
+ assert_matches_type(ItemListResponse, item, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ async def test_streaming_response_list(self, async_client: AsyncKernel) -> None:
+ async with async_client.vaults.items.with_streaming_response.list(
+ "id_or_name",
+ ) as response:
+ assert not response.is_closed
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+
+ item = await response.parse()
+ assert_matches_type(ItemListResponse, item, 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_list(self, async_client: AsyncKernel) -> None:
+ with pytest.raises(ValueError, match=r"Expected a non-empty value for `id_or_name` but received ''"):
+ await async_client.vaults.items.with_raw_response.list(
+ "",
+ )
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ async def test_method_delete(self, async_client: AsyncKernel) -> None:
+ item = await async_client.vaults.items.delete(
+ key="x",
+ id_or_name="id_or_name",
+ )
+ assert item is None
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ async def test_raw_response_delete(self, async_client: AsyncKernel) -> None:
+ response = await async_client.vaults.items.with_raw_response.delete(
+ key="x",
+ id_or_name="id_or_name",
+ )
+
+ assert response.is_closed is True
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+ item = await response.parse()
+ assert item is None
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ async def test_streaming_response_delete(self, async_client: AsyncKernel) -> None:
+ async with async_client.vaults.items.with_streaming_response.delete(
+ key="x",
+ id_or_name="id_or_name",
+ ) as response:
+ assert not response.is_closed
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+
+ item = await response.parse()
+ assert item is None
+
+ assert cast(Any, response.is_closed) is True
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ async def test_path_params_delete(self, async_client: AsyncKernel) -> None:
+ with pytest.raises(ValueError, match=r"Expected a non-empty value for `id_or_name` but received ''"):
+ await async_client.vaults.items.with_raw_response.delete(
+ key="x",
+ id_or_name="",
+ )
+
+ with pytest.raises(ValueError, match=r"Expected a non-empty value for `key` but received ''"):
+ await async_client.vaults.items.with_raw_response.delete(
+ key="",
+ id_or_name="id_or_name",
+ )
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ async def test_method_events(self, async_client: AsyncKernel) -> None:
+ item = await async_client.vaults.items.events(
+ key="key",
+ id_or_name="id_or_name",
+ )
+ assert_matches_type(ItemEventsResponse, item, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ async def test_method_events_with_all_params(self, async_client: AsyncKernel) -> None:
+ item = await async_client.vaults.items.events(
+ key="key",
+ id_or_name="id_or_name",
+ after="after",
+ wait=0,
+ )
+ assert_matches_type(ItemEventsResponse, item, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ async def test_raw_response_events(self, async_client: AsyncKernel) -> None:
+ response = await async_client.vaults.items.with_raw_response.events(
+ key="key",
+ id_or_name="id_or_name",
+ )
+
+ assert response.is_closed is True
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+ item = await response.parse()
+ assert_matches_type(ItemEventsResponse, item, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ async def test_streaming_response_events(self, async_client: AsyncKernel) -> None:
+ async with async_client.vaults.items.with_streaming_response.events(
+ key="key",
+ id_or_name="id_or_name",
+ ) as response:
+ assert not response.is_closed
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+
+ item = await response.parse()
+ assert_matches_type(ItemEventsResponse, item, path=["response"])
+
+ assert cast(Any, response.is_closed) is True
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ async def test_path_params_events(self, async_client: AsyncKernel) -> None:
+ with pytest.raises(ValueError, match=r"Expected a non-empty value for `id_or_name` but received ''"):
+ await async_client.vaults.items.with_raw_response.events(
+ key="key",
+ id_or_name="",
+ )
+
+ with pytest.raises(ValueError, match=r"Expected a non-empty value for `key` but received ''"):
+ await async_client.vaults.items.with_raw_response.events(
+ key="",
+ id_or_name="id_or_name",
+ )
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ async def test_method_perform_operation_overload_1(self, async_client: AsyncKernel) -> None:
+ item = await async_client.vaults.items.perform_operation(
+ key="key",
+ id_or_name="id_or_name",
+ type="authorize",
+ )
+ assert_matches_type(VaultItemOperationResponse, item, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ async def test_raw_response_perform_operation_overload_1(self, async_client: AsyncKernel) -> None:
+ response = await async_client.vaults.items.with_raw_response.perform_operation(
+ key="key",
+ id_or_name="id_or_name",
+ type="authorize",
+ )
+
+ assert response.is_closed is True
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+ item = await response.parse()
+ assert_matches_type(VaultItemOperationResponse, item, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ async def test_streaming_response_perform_operation_overload_1(self, async_client: AsyncKernel) -> None:
+ async with async_client.vaults.items.with_streaming_response.perform_operation(
+ key="key",
+ id_or_name="id_or_name",
+ type="authorize",
+ ) as response:
+ assert not response.is_closed
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+
+ item = await response.parse()
+ assert_matches_type(VaultItemOperationResponse, item, 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_perform_operation_overload_1(self, async_client: AsyncKernel) -> None:
+ with pytest.raises(ValueError, match=r"Expected a non-empty value for `id_or_name` but received ''"):
+ await async_client.vaults.items.with_raw_response.perform_operation(
+ key="key",
+ id_or_name="",
+ type="authorize",
+ )
+
+ with pytest.raises(ValueError, match=r"Expected a non-empty value for `key` but received ''"):
+ await async_client.vaults.items.with_raw_response.perform_operation(
+ key="",
+ id_or_name="id_or_name",
+ type="authorize",
+ )
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ async def test_method_perform_operation_overload_2(self, async_client: AsyncKernel) -> None:
+ item = await async_client.vaults.items.perform_operation(
+ key="key",
+ id_or_name="id_or_name",
+ type="collect",
+ )
+ assert_matches_type(VaultItemOperationResponse, item, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ async def test_raw_response_perform_operation_overload_2(self, async_client: AsyncKernel) -> None:
+ response = await async_client.vaults.items.with_raw_response.perform_operation(
+ key="key",
+ id_or_name="id_or_name",
+ type="collect",
+ )
+
+ assert response.is_closed is True
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+ item = await response.parse()
+ assert_matches_type(VaultItemOperationResponse, item, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ async def test_streaming_response_perform_operation_overload_2(self, async_client: AsyncKernel) -> None:
+ async with async_client.vaults.items.with_streaming_response.perform_operation(
+ key="key",
+ id_or_name="id_or_name",
+ type="collect",
+ ) as response:
+ assert not response.is_closed
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+
+ item = await response.parse()
+ assert_matches_type(VaultItemOperationResponse, item, 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_perform_operation_overload_2(self, async_client: AsyncKernel) -> None:
+ with pytest.raises(ValueError, match=r"Expected a non-empty value for `id_or_name` but received ''"):
+ await async_client.vaults.items.with_raw_response.perform_operation(
+ key="key",
+ id_or_name="",
+ type="collect",
+ )
+
+ with pytest.raises(ValueError, match=r"Expected a non-empty value for `key` but received ''"):
+ await async_client.vaults.items.with_raw_response.perform_operation(
+ key="",
+ id_or_name="id_or_name",
+ type="collect",
+ )
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ async def test_method_perform_operation_overload_3(self, async_client: AsyncKernel) -> None:
+ item = await async_client.vaults.items.perform_operation(
+ key="key",
+ id_or_name="id_or_name",
+ checkout={
+ "browser_id": "browser_id",
+ "environment": "production",
+ "merchant_origin": "merchant_origin",
+ },
+ type="prepare_checkout",
+ )
+ assert_matches_type(VaultItemOperationResponse, item, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ async def test_method_perform_operation_with_all_params_overload_3(self, async_client: AsyncKernel) -> None:
+ item = await async_client.vaults.items.perform_operation(
+ key="key",
+ id_or_name="id_or_name",
+ checkout={
+ "browser_id": "browser_id",
+ "environment": "production",
+ "merchant_origin": "merchant_origin",
+ "psp": "square",
+ },
+ type="prepare_checkout",
+ )
+ assert_matches_type(VaultItemOperationResponse, item, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ async def test_raw_response_perform_operation_overload_3(self, async_client: AsyncKernel) -> None:
+ response = await async_client.vaults.items.with_raw_response.perform_operation(
+ key="key",
+ id_or_name="id_or_name",
+ checkout={
+ "browser_id": "browser_id",
+ "environment": "production",
+ "merchant_origin": "merchant_origin",
+ },
+ type="prepare_checkout",
+ )
+
+ assert response.is_closed is True
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+ item = await response.parse()
+ assert_matches_type(VaultItemOperationResponse, item, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ async def test_streaming_response_perform_operation_overload_3(self, async_client: AsyncKernel) -> None:
+ async with async_client.vaults.items.with_streaming_response.perform_operation(
+ key="key",
+ id_or_name="id_or_name",
+ checkout={
+ "browser_id": "browser_id",
+ "environment": "production",
+ "merchant_origin": "merchant_origin",
+ },
+ type="prepare_checkout",
+ ) as response:
+ assert not response.is_closed
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+
+ item = await response.parse()
+ assert_matches_type(VaultItemOperationResponse, item, 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_perform_operation_overload_3(self, async_client: AsyncKernel) -> None:
+ with pytest.raises(ValueError, match=r"Expected a non-empty value for `id_or_name` but received ''"):
+ await async_client.vaults.items.with_raw_response.perform_operation(
+ key="key",
+ id_or_name="",
+ checkout={
+ "browser_id": "browser_id",
+ "environment": "production",
+ "merchant_origin": "merchant_origin",
+ },
+ type="prepare_checkout",
+ )
+
+ with pytest.raises(ValueError, match=r"Expected a non-empty value for `key` but received ''"):
+ await async_client.vaults.items.with_raw_response.perform_operation(
+ key="",
+ id_or_name="id_or_name",
+ checkout={
+ "browser_id": "browser_id",
+ "environment": "production",
+ "merchant_origin": "merchant_origin",
+ },
+ type="prepare_checkout",
+ )
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ async def test_method_perform_operation_overload_4(self, async_client: AsyncKernel) -> None:
+ item = await async_client.vaults.items.perform_operation(
+ key="key",
+ id_or_name="id_or_name",
+ browser_id="browser-session-id",
+ fields=[
+ {
+ "field": "number",
+ "selector": "#card-number",
+ },
+ {
+ "field": "exp_month",
+ "selector": "#expiry-month",
+ },
+ {
+ "field": "exp_year",
+ "selector": "#expiry-year",
+ },
+ {
+ "field": "cvc",
+ "selector": "#security-code",
+ },
+ ],
+ type="fill",
+ )
+ assert_matches_type(VaultItemOperationResponse, item, path=["response"])
@pytest.mark.skip(reason="Mock server tests are disabled")
@parametrize
- async def test_method_delete(self, async_client: AsyncKernel) -> None:
- item = await async_client.vaults.items.delete(
- key="x",
+ async def test_method_perform_operation_with_all_params_overload_4(self, async_client: AsyncKernel) -> None:
+ item = await async_client.vaults.items.perform_operation(
+ key="key",
id_or_name="id_or_name",
+ browser_id="browser-session-id",
+ fields=[
+ {
+ "field": "number",
+ "selector": "#card-number",
+ "format": "MM/YY",
+ },
+ {
+ "field": "exp_month",
+ "selector": "#expiry-month",
+ "format": "MM/YY",
+ },
+ {
+ "field": "exp_year",
+ "selector": "#expiry-year",
+ "format": "MM/YY",
+ },
+ {
+ "field": "cvc",
+ "selector": "#security-code",
+ "format": "MM/YY",
+ },
+ ],
+ type="fill",
+ page_url="https://shop.example/checkout",
+ timeout_ms=1,
)
- assert item is None
+ assert_matches_type(VaultItemOperationResponse, item, path=["response"])
@pytest.mark.skip(reason="Mock server tests are disabled")
@parametrize
- async def test_raw_response_delete(self, async_client: AsyncKernel) -> None:
- response = await async_client.vaults.items.with_raw_response.delete(
- key="x",
+ async def test_raw_response_perform_operation_overload_4(self, async_client: AsyncKernel) -> None:
+ response = await async_client.vaults.items.with_raw_response.perform_operation(
+ key="key",
id_or_name="id_or_name",
+ browser_id="browser-session-id",
+ fields=[
+ {
+ "field": "number",
+ "selector": "#card-number",
+ },
+ {
+ "field": "exp_month",
+ "selector": "#expiry-month",
+ },
+ {
+ "field": "exp_year",
+ "selector": "#expiry-year",
+ },
+ {
+ "field": "cvc",
+ "selector": "#security-code",
+ },
+ ],
+ type="fill",
)
assert response.is_closed is True
assert response.http_request.headers.get("X-Stainless-Lang") == "python"
item = await response.parse()
- assert item is None
+ assert_matches_type(VaultItemOperationResponse, item, path=["response"])
@pytest.mark.skip(reason="Mock server tests are disabled")
@parametrize
- async def test_streaming_response_delete(self, async_client: AsyncKernel) -> None:
- async with async_client.vaults.items.with_streaming_response.delete(
- key="x",
+ async def test_streaming_response_perform_operation_overload_4(self, async_client: AsyncKernel) -> None:
+ async with async_client.vaults.items.with_streaming_response.perform_operation(
+ key="key",
id_or_name="id_or_name",
+ browser_id="browser-session-id",
+ fields=[
+ {
+ "field": "number",
+ "selector": "#card-number",
+ },
+ {
+ "field": "exp_month",
+ "selector": "#expiry-month",
+ },
+ {
+ "field": "exp_year",
+ "selector": "#expiry-year",
+ },
+ {
+ "field": "cvc",
+ "selector": "#security-code",
+ },
+ ],
+ type="fill",
) as response:
assert not response.is_closed
assert response.http_request.headers.get("X-Stainless-Lang") == "python"
item = await response.parse()
- assert item is None
+ assert_matches_type(VaultItemOperationResponse, item, 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_delete(self, async_client: AsyncKernel) -> None:
+ async def test_path_params_perform_operation_overload_4(self, async_client: AsyncKernel) -> None:
with pytest.raises(ValueError, match=r"Expected a non-empty value for `id_or_name` but received ''"):
- await async_client.vaults.items.with_raw_response.delete(
- key="x",
+ await async_client.vaults.items.with_raw_response.perform_operation(
+ key="key",
id_or_name="",
+ browser_id="browser-session-id",
+ fields=[
+ {
+ "field": "number",
+ "selector": "#card-number",
+ },
+ {
+ "field": "exp_month",
+ "selector": "#expiry-month",
+ },
+ {
+ "field": "exp_year",
+ "selector": "#expiry-year",
+ },
+ {
+ "field": "cvc",
+ "selector": "#security-code",
+ },
+ ],
+ type="fill",
)
with pytest.raises(ValueError, match=r"Expected a non-empty value for `key` but received ''"):
- await async_client.vaults.items.with_raw_response.delete(
+ await async_client.vaults.items.with_raw_response.perform_operation(
key="",
id_or_name="id_or_name",
+ browser_id="browser-session-id",
+ fields=[
+ {
+ "field": "number",
+ "selector": "#card-number",
+ },
+ {
+ "field": "exp_month",
+ "selector": "#expiry-month",
+ },
+ {
+ "field": "exp_year",
+ "selector": "#expiry-year",
+ },
+ {
+ "field": "cvc",
+ "selector": "#security-code",
+ },
+ ],
+ type="fill",
)
@pytest.mark.skip(reason="Mock server tests are disabled")
@parametrize
- async def test_method_events(self, async_client: AsyncKernel) -> None:
- item = await async_client.vaults.items.events(
+ async def test_method_perform_operation_overload_5(self, async_client: AsyncKernel) -> None:
+ item = await async_client.vaults.items.perform_operation(
key="key",
id_or_name="id_or_name",
+ browser_id="x",
+ type="1pw_create_access_request",
)
- assert_matches_type(ItemEventsResponse, item, path=["response"])
+ assert_matches_type(VaultItemOperationResponse, item, path=["response"])
@pytest.mark.skip(reason="Mock server tests are disabled")
@parametrize
- async def test_method_events_with_all_params(self, async_client: AsyncKernel) -> None:
- item = await async_client.vaults.items.events(
+ async def test_method_perform_operation_with_all_params_overload_5(self, async_client: AsyncKernel) -> None:
+ item = await async_client.vaults.items.perform_operation(
key="key",
id_or_name="id_or_name",
- after="after",
- wait=0,
+ browser_id="x",
+ type="1pw_create_access_request",
+ goal="goal",
+ keywords=["x"],
+ reason="reason",
)
- assert_matches_type(ItemEventsResponse, item, path=["response"])
+ assert_matches_type(VaultItemOperationResponse, item, path=["response"])
@pytest.mark.skip(reason="Mock server tests are disabled")
@parametrize
- async def test_raw_response_events(self, async_client: AsyncKernel) -> None:
- response = await async_client.vaults.items.with_raw_response.events(
+ async def test_raw_response_perform_operation_overload_5(self, async_client: AsyncKernel) -> None:
+ response = await async_client.vaults.items.with_raw_response.perform_operation(
key="key",
id_or_name="id_or_name",
+ browser_id="x",
+ type="1pw_create_access_request",
)
assert response.is_closed is True
assert response.http_request.headers.get("X-Stainless-Lang") == "python"
item = await response.parse()
- assert_matches_type(ItemEventsResponse, item, path=["response"])
+ assert_matches_type(VaultItemOperationResponse, item, path=["response"])
@pytest.mark.skip(reason="Mock server tests are disabled")
@parametrize
- async def test_streaming_response_events(self, async_client: AsyncKernel) -> None:
- async with async_client.vaults.items.with_streaming_response.events(
+ async def test_streaming_response_perform_operation_overload_5(self, async_client: AsyncKernel) -> None:
+ async with async_client.vaults.items.with_streaming_response.perform_operation(
key="key",
id_or_name="id_or_name",
+ browser_id="x",
+ type="1pw_create_access_request",
) as response:
assert not response.is_closed
assert response.http_request.headers.get("X-Stainless-Lang") == "python"
item = await response.parse()
- assert_matches_type(ItemEventsResponse, item, path=["response"])
+ assert_matches_type(VaultItemOperationResponse, item, path=["response"])
assert cast(Any, response.is_closed) is True
@pytest.mark.skip(reason="Mock server tests are disabled")
@parametrize
- async def test_path_params_events(self, async_client: AsyncKernel) -> None:
+ async def test_path_params_perform_operation_overload_5(self, async_client: AsyncKernel) -> None:
with pytest.raises(ValueError, match=r"Expected a non-empty value for `id_or_name` but received ''"):
- await async_client.vaults.items.with_raw_response.events(
+ await async_client.vaults.items.with_raw_response.perform_operation(
key="key",
id_or_name="",
+ browser_id="x",
+ type="1pw_create_access_request",
)
with pytest.raises(ValueError, match=r"Expected a non-empty value for `key` but received ''"):
- await async_client.vaults.items.with_raw_response.events(
+ await async_client.vaults.items.with_raw_response.perform_operation(
key="",
id_or_name="id_or_name",
+ browser_id="x",
+ type="1pw_create_access_request",
)
@pytest.mark.skip(reason="Mock server tests are disabled")
@parametrize
- async def test_method_perform_operation_overload_1(self, async_client: AsyncKernel) -> None:
+ async def test_method_perform_operation_overload_6(self, async_client: AsyncKernel) -> None:
item = await async_client.vaults.items.perform_operation(
key="key",
id_or_name="id_or_name",
- type="authorize",
+ browser_id="x",
+ type="1pw_access_request_status",
)
assert_matches_type(VaultItemOperationResponse, item, path=["response"])
@pytest.mark.skip(reason="Mock server tests are disabled")
@parametrize
- async def test_raw_response_perform_operation_overload_1(self, async_client: AsyncKernel) -> None:
+ async def test_method_perform_operation_with_all_params_overload_6(self, async_client: AsyncKernel) -> None:
+ item = await async_client.vaults.items.perform_operation(
+ key="key",
+ id_or_name="id_or_name",
+ browser_id="x",
+ type="1pw_access_request_status",
+ timeout_seconds=0,
+ )
+ assert_matches_type(VaultItemOperationResponse, item, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ async def test_raw_response_perform_operation_overload_6(self, async_client: AsyncKernel) -> None:
response = await async_client.vaults.items.with_raw_response.perform_operation(
key="key",
id_or_name="id_or_name",
- type="authorize",
+ browser_id="x",
+ type="1pw_access_request_status",
)
assert response.is_closed is True
@@ -1743,11 +2693,12 @@ async def test_raw_response_perform_operation_overload_1(self, async_client: Asy
@pytest.mark.skip(reason="Mock server tests are disabled")
@parametrize
- async def test_streaming_response_perform_operation_overload_1(self, async_client: AsyncKernel) -> None:
+ async def test_streaming_response_perform_operation_overload_6(self, async_client: AsyncKernel) -> None:
async with async_client.vaults.items.with_streaming_response.perform_operation(
key="key",
id_or_name="id_or_name",
- type="authorize",
+ browser_id="x",
+ type="1pw_access_request_status",
) as response:
assert not response.is_closed
assert response.http_request.headers.get("X-Stainless-Lang") == "python"
@@ -1759,38 +2710,58 @@ async def test_streaming_response_perform_operation_overload_1(self, async_clien
@pytest.mark.skip(reason="Mock server tests are disabled")
@parametrize
- async def test_path_params_perform_operation_overload_1(self, async_client: AsyncKernel) -> None:
+ async def test_path_params_perform_operation_overload_6(self, async_client: AsyncKernel) -> None:
with pytest.raises(ValueError, match=r"Expected a non-empty value for `id_or_name` but received ''"):
await async_client.vaults.items.with_raw_response.perform_operation(
key="key",
id_or_name="",
- type="authorize",
+ browser_id="x",
+ type="1pw_access_request_status",
)
with pytest.raises(ValueError, match=r"Expected a non-empty value for `key` but received ''"):
await async_client.vaults.items.with_raw_response.perform_operation(
key="",
id_or_name="id_or_name",
- type="authorize",
+ browser_id="x",
+ type="1pw_access_request_status",
)
@pytest.mark.skip(reason="Mock server tests are disabled")
@parametrize
- async def test_method_perform_operation_overload_2(self, async_client: AsyncKernel) -> None:
+ async def test_method_perform_operation_overload_7(self, async_client: AsyncKernel) -> None:
item = await async_client.vaults.items.perform_operation(
key="key",
id_or_name="id_or_name",
- type="collect",
+ browser_id="x",
+ page_url="https://example.com",
+ type="1pw_fill",
)
assert_matches_type(VaultItemOperationResponse, item, path=["response"])
@pytest.mark.skip(reason="Mock server tests are disabled")
@parametrize
- async def test_raw_response_perform_operation_overload_2(self, async_client: AsyncKernel) -> None:
+ async def test_method_perform_operation_with_all_params_overload_7(self, async_client: AsyncKernel) -> None:
+ item = await async_client.vaults.items.perform_operation(
+ key="key",
+ id_or_name="id_or_name",
+ browser_id="x",
+ page_url="https://example.com",
+ type="1pw_fill",
+ entry_id="x",
+ timeout_ms=1,
+ )
+ assert_matches_type(VaultItemOperationResponse, item, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ async def test_raw_response_perform_operation_overload_7(self, async_client: AsyncKernel) -> None:
response = await async_client.vaults.items.with_raw_response.perform_operation(
key="key",
id_or_name="id_or_name",
- type="collect",
+ browser_id="x",
+ page_url="https://example.com",
+ type="1pw_fill",
)
assert response.is_closed is True
@@ -1800,11 +2771,13 @@ async def test_raw_response_perform_operation_overload_2(self, async_client: Asy
@pytest.mark.skip(reason="Mock server tests are disabled")
@parametrize
- async def test_streaming_response_perform_operation_overload_2(self, async_client: AsyncKernel) -> None:
+ async def test_streaming_response_perform_operation_overload_7(self, async_client: AsyncKernel) -> None:
async with async_client.vaults.items.with_streaming_response.perform_operation(
key="key",
id_or_name="id_or_name",
- type="collect",
+ browser_id="x",
+ page_url="https://example.com",
+ type="1pw_fill",
) as response:
assert not response.is_closed
assert response.http_request.headers.get("X-Stainless-Lang") == "python"
@@ -1816,64 +2789,42 @@ async def test_streaming_response_perform_operation_overload_2(self, async_clien
@pytest.mark.skip(reason="Mock server tests are disabled")
@parametrize
- async def test_path_params_perform_operation_overload_2(self, async_client: AsyncKernel) -> None:
+ async def test_path_params_perform_operation_overload_7(self, async_client: AsyncKernel) -> None:
with pytest.raises(ValueError, match=r"Expected a non-empty value for `id_or_name` but received ''"):
await async_client.vaults.items.with_raw_response.perform_operation(
key="key",
id_or_name="",
- type="collect",
+ browser_id="x",
+ page_url="https://example.com",
+ type="1pw_fill",
)
with pytest.raises(ValueError, match=r"Expected a non-empty value for `key` but received ''"):
await async_client.vaults.items.with_raw_response.perform_operation(
- key="",
- id_or_name="id_or_name",
- type="collect",
- )
-
- @pytest.mark.skip(reason="Mock server tests are disabled")
- @parametrize
- async def test_method_perform_operation_overload_3(self, async_client: AsyncKernel) -> None:
- item = await async_client.vaults.items.perform_operation(
- key="key",
- id_or_name="id_or_name",
- checkout={
- "browser_id": "browser_id",
- "environment": "production",
- "merchant_origin": "merchant_origin",
- },
- type="prepare_checkout",
- )
- assert_matches_type(VaultItemOperationResponse, item, path=["response"])
+ key="",
+ id_or_name="id_or_name",
+ browser_id="x",
+ page_url="https://example.com",
+ type="1pw_fill",
+ )
@pytest.mark.skip(reason="Mock server tests are disabled")
@parametrize
- async def test_method_perform_operation_with_all_params_overload_3(self, async_client: AsyncKernel) -> None:
+ async def test_method_perform_operation_overload_8(self, async_client: AsyncKernel) -> None:
item = await async_client.vaults.items.perform_operation(
key="key",
id_or_name="id_or_name",
- checkout={
- "browser_id": "browser_id",
- "environment": "production",
- "merchant_origin": "merchant_origin",
- "psp": "square",
- },
- type="prepare_checkout",
+ type="1pw_recover",
)
assert_matches_type(VaultItemOperationResponse, item, path=["response"])
@pytest.mark.skip(reason="Mock server tests are disabled")
@parametrize
- async def test_raw_response_perform_operation_overload_3(self, async_client: AsyncKernel) -> None:
+ async def test_raw_response_perform_operation_overload_8(self, async_client: AsyncKernel) -> None:
response = await async_client.vaults.items.with_raw_response.perform_operation(
key="key",
id_or_name="id_or_name",
- checkout={
- "browser_id": "browser_id",
- "environment": "production",
- "merchant_origin": "merchant_origin",
- },
- type="prepare_checkout",
+ type="1pw_recover",
)
assert response.is_closed is True
@@ -1883,16 +2834,11 @@ async def test_raw_response_perform_operation_overload_3(self, async_client: Asy
@pytest.mark.skip(reason="Mock server tests are disabled")
@parametrize
- async def test_streaming_response_perform_operation_overload_3(self, async_client: AsyncKernel) -> None:
+ async def test_streaming_response_perform_operation_overload_8(self, async_client: AsyncKernel) -> None:
async with async_client.vaults.items.with_streaming_response.perform_operation(
key="key",
id_or_name="id_or_name",
- checkout={
- "browser_id": "browser_id",
- "environment": "production",
- "merchant_origin": "merchant_origin",
- },
- type="prepare_checkout",
+ type="1pw_recover",
) as response:
assert not response.is_closed
assert response.http_request.headers.get("X-Stainless-Lang") == "python"
@@ -1904,121 +2850,52 @@ async def test_streaming_response_perform_operation_overload_3(self, async_clien
@pytest.mark.skip(reason="Mock server tests are disabled")
@parametrize
- async def test_path_params_perform_operation_overload_3(self, async_client: AsyncKernel) -> None:
+ async def test_path_params_perform_operation_overload_8(self, async_client: AsyncKernel) -> None:
with pytest.raises(ValueError, match=r"Expected a non-empty value for `id_or_name` but received ''"):
await async_client.vaults.items.with_raw_response.perform_operation(
key="key",
id_or_name="",
- checkout={
- "browser_id": "browser_id",
- "environment": "production",
- "merchant_origin": "merchant_origin",
- },
- type="prepare_checkout",
+ type="1pw_recover",
)
with pytest.raises(ValueError, match=r"Expected a non-empty value for `key` but received ''"):
await async_client.vaults.items.with_raw_response.perform_operation(
key="",
id_or_name="id_or_name",
- checkout={
- "browser_id": "browser_id",
- "environment": "production",
- "merchant_origin": "merchant_origin",
- },
- type="prepare_checkout",
+ type="1pw_recover",
)
@pytest.mark.skip(reason="Mock server tests are disabled")
@parametrize
- async def test_method_perform_operation_overload_4(self, async_client: AsyncKernel) -> None:
+ async def test_method_perform_operation_overload_9(self, async_client: AsyncKernel) -> None:
item = await async_client.vaults.items.perform_operation(
key="key",
id_or_name="id_or_name",
- browser_id="browser-session-id",
- fields=[
- {
- "field": "number",
- "selector": "#card-number",
- },
- {
- "field": "exp_month",
- "selector": "#expiry-month",
- },
- {
- "field": "exp_year",
- "selector": "#expiry-year",
- },
- {
- "field": "cvc",
- "selector": "#security-code",
- },
- ],
- type="fill",
+ access_token="x",
+ type="1pw_update_access_token",
)
assert_matches_type(VaultItemOperationResponse, item, path=["response"])
@pytest.mark.skip(reason="Mock server tests are disabled")
@parametrize
- async def test_method_perform_operation_with_all_params_overload_4(self, async_client: AsyncKernel) -> None:
+ async def test_method_perform_operation_with_all_params_overload_9(self, async_client: AsyncKernel) -> None:
item = await async_client.vaults.items.perform_operation(
key="key",
id_or_name="id_or_name",
- browser_id="browser-session-id",
- fields=[
- {
- "field": "number",
- "selector": "#card-number",
- "format": "MM/YY",
- },
- {
- "field": "exp_month",
- "selector": "#expiry-month",
- "format": "MM/YY",
- },
- {
- "field": "exp_year",
- "selector": "#expiry-year",
- "format": "MM/YY",
- },
- {
- "field": "cvc",
- "selector": "#security-code",
- "format": "MM/YY",
- },
- ],
- type="fill",
- page_url="https://shop.example/checkout",
- timeout_ms=1,
+ access_token="x",
+ type="1pw_update_access_token",
+ access_token_expires_at=parse_datetime("2019-12-27T18:11:19.117Z"),
)
assert_matches_type(VaultItemOperationResponse, item, path=["response"])
@pytest.mark.skip(reason="Mock server tests are disabled")
@parametrize
- async def test_raw_response_perform_operation_overload_4(self, async_client: AsyncKernel) -> None:
+ async def test_raw_response_perform_operation_overload_9(self, async_client: AsyncKernel) -> None:
response = await async_client.vaults.items.with_raw_response.perform_operation(
key="key",
id_or_name="id_or_name",
- browser_id="browser-session-id",
- fields=[
- {
- "field": "number",
- "selector": "#card-number",
- },
- {
- "field": "exp_month",
- "selector": "#expiry-month",
- },
- {
- "field": "exp_year",
- "selector": "#expiry-year",
- },
- {
- "field": "cvc",
- "selector": "#security-code",
- },
- ],
- type="fill",
+ access_token="x",
+ type="1pw_update_access_token",
)
assert response.is_closed is True
@@ -2028,30 +2905,12 @@ async def test_raw_response_perform_operation_overload_4(self, async_client: Asy
@pytest.mark.skip(reason="Mock server tests are disabled")
@parametrize
- async def test_streaming_response_perform_operation_overload_4(self, async_client: AsyncKernel) -> None:
+ async def test_streaming_response_perform_operation_overload_9(self, async_client: AsyncKernel) -> None:
async with async_client.vaults.items.with_streaming_response.perform_operation(
key="key",
id_or_name="id_or_name",
- browser_id="browser-session-id",
- fields=[
- {
- "field": "number",
- "selector": "#card-number",
- },
- {
- "field": "exp_month",
- "selector": "#expiry-month",
- },
- {
- "field": "exp_year",
- "selector": "#expiry-year",
- },
- {
- "field": "cvc",
- "selector": "#security-code",
- },
- ],
- type="fill",
+ access_token="x",
+ type="1pw_update_access_token",
) as response:
assert not response.is_closed
assert response.http_request.headers.get("X-Stainless-Lang") == "python"
@@ -2063,57 +2922,21 @@ async def test_streaming_response_perform_operation_overload_4(self, async_clien
@pytest.mark.skip(reason="Mock server tests are disabled")
@parametrize
- async def test_path_params_perform_operation_overload_4(self, async_client: AsyncKernel) -> None:
+ async def test_path_params_perform_operation_overload_9(self, async_client: AsyncKernel) -> None:
with pytest.raises(ValueError, match=r"Expected a non-empty value for `id_or_name` but received ''"):
await async_client.vaults.items.with_raw_response.perform_operation(
key="key",
id_or_name="",
- browser_id="browser-session-id",
- fields=[
- {
- "field": "number",
- "selector": "#card-number",
- },
- {
- "field": "exp_month",
- "selector": "#expiry-month",
- },
- {
- "field": "exp_year",
- "selector": "#expiry-year",
- },
- {
- "field": "cvc",
- "selector": "#security-code",
- },
- ],
- type="fill",
+ access_token="x",
+ type="1pw_update_access_token",
)
with pytest.raises(ValueError, match=r"Expected a non-empty value for `key` but received ''"):
await async_client.vaults.items.with_raw_response.perform_operation(
key="",
id_or_name="id_or_name",
- browser_id="browser-session-id",
- fields=[
- {
- "field": "number",
- "selector": "#card-number",
- },
- {
- "field": "exp_month",
- "selector": "#expiry-month",
- },
- {
- "field": "exp_year",
- "selector": "#expiry-year",
- },
- {
- "field": "cvc",
- "selector": "#security-code",
- },
- ],
- type="fill",
+ access_token="x",
+ type="1pw_update_access_token",
)
@pytest.mark.skip(reason="Mock server tests are disabled")
@@ -2383,6 +3206,98 @@ async def test_path_params_upsert_overload_2(self, async_client: AsyncKernel) ->
@pytest.mark.skip(reason="Mock server tests are disabled")
@parametrize
async def test_method_upsert_overload_3(self, async_client: AsyncKernel) -> None:
+ item = await async_client.vaults.items.upsert(
+ key="x",
+ id_or_name="id_or_name",
+ spec={
+ "authorization": {
+ "client": {"type": "kernel_managed"},
+ "method": "oauth",
+ },
+ "provider": "1password",
+ },
+ type="credential_account",
+ )
+ assert_matches_type(VaultItem, item, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ async def test_raw_response_upsert_overload_3(self, async_client: AsyncKernel) -> None:
+ response = await async_client.vaults.items.with_raw_response.upsert(
+ key="x",
+ id_or_name="id_or_name",
+ spec={
+ "authorization": {
+ "client": {"type": "kernel_managed"},
+ "method": "oauth",
+ },
+ "provider": "1password",
+ },
+ type="credential_account",
+ )
+
+ assert response.is_closed is True
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+ item = await response.parse()
+ assert_matches_type(VaultItem, item, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ async def test_streaming_response_upsert_overload_3(self, async_client: AsyncKernel) -> None:
+ async with async_client.vaults.items.with_streaming_response.upsert(
+ key="x",
+ id_or_name="id_or_name",
+ spec={
+ "authorization": {
+ "client": {"type": "kernel_managed"},
+ "method": "oauth",
+ },
+ "provider": "1password",
+ },
+ type="credential_account",
+ ) as response:
+ assert not response.is_closed
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+
+ item = await response.parse()
+ assert_matches_type(VaultItem, item, 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_upsert_overload_3(self, async_client: AsyncKernel) -> None:
+ with pytest.raises(ValueError, match=r"Expected a non-empty value for `id_or_name` but received ''"):
+ await async_client.vaults.items.with_raw_response.upsert(
+ key="x",
+ id_or_name="",
+ spec={
+ "authorization": {
+ "client": {"type": "kernel_managed"},
+ "method": "oauth",
+ },
+ "provider": "1password",
+ },
+ type="credential_account",
+ )
+
+ with pytest.raises(ValueError, match=r"Expected a non-empty value for `key` but received ''"):
+ await async_client.vaults.items.with_raw_response.upsert(
+ key="",
+ id_or_name="id_or_name",
+ spec={
+ "authorization": {
+ "client": {"type": "kernel_managed"},
+ "method": "oauth",
+ },
+ "provider": "1password",
+ },
+ type="credential_account",
+ )
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ async def test_method_upsert_overload_4(self, async_client: AsyncKernel) -> None:
item = await async_client.vaults.items.upsert(
key="x",
id_or_name="id_or_name",
@@ -2392,7 +3307,8 @@ async def test_method_upsert_overload_3(self, async_client: AsyncKernel) -> None
"name": "name",
"type": "text",
}
- ]
+ ],
+ "provider": "kernel",
},
type="credential",
)
@@ -2400,7 +3316,7 @@ async def test_method_upsert_overload_3(self, async_client: AsyncKernel) -> None
@pytest.mark.skip(reason="Mock server tests are disabled")
@parametrize
- async def test_method_upsert_with_all_params_overload_3(self, async_client: AsyncKernel) -> None:
+ async def test_method_upsert_with_all_params_overload_4(self, async_client: AsyncKernel) -> None:
item = await async_client.vaults.items.upsert(
key="x",
id_or_name="id_or_name",
@@ -2415,6 +3331,7 @@ async def test_method_upsert_with_all_params_overload_3(self, async_client: Asyn
"value": "x",
}
],
+ "provider": "kernel",
"description": "description",
},
type="credential",
@@ -2423,7 +3340,7 @@ async def test_method_upsert_with_all_params_overload_3(self, async_client: Asyn
@pytest.mark.skip(reason="Mock server tests are disabled")
@parametrize
- async def test_raw_response_upsert_overload_3(self, async_client: AsyncKernel) -> None:
+ async def test_raw_response_upsert_overload_4(self, async_client: AsyncKernel) -> None:
response = await async_client.vaults.items.with_raw_response.upsert(
key="x",
id_or_name="id_or_name",
@@ -2433,7 +3350,8 @@ async def test_raw_response_upsert_overload_3(self, async_client: AsyncKernel) -
"name": "name",
"type": "text",
}
- ]
+ ],
+ "provider": "kernel",
},
type="credential",
)
@@ -2445,7 +3363,7 @@ async def test_raw_response_upsert_overload_3(self, async_client: AsyncKernel) -
@pytest.mark.skip(reason="Mock server tests are disabled")
@parametrize
- async def test_streaming_response_upsert_overload_3(self, async_client: AsyncKernel) -> None:
+ async def test_streaming_response_upsert_overload_4(self, async_client: AsyncKernel) -> None:
async with async_client.vaults.items.with_streaming_response.upsert(
key="x",
id_or_name="id_or_name",
@@ -2455,7 +3373,8 @@ async def test_streaming_response_upsert_overload_3(self, async_client: AsyncKer
"name": "name",
"type": "text",
}
- ]
+ ],
+ "provider": "kernel",
},
type="credential",
) as response:
@@ -2469,7 +3388,7 @@ async def test_streaming_response_upsert_overload_3(self, async_client: AsyncKer
@pytest.mark.skip(reason="Mock server tests are disabled")
@parametrize
- async def test_path_params_upsert_overload_3(self, async_client: AsyncKernel) -> None:
+ async def test_path_params_upsert_overload_4(self, async_client: AsyncKernel) -> None:
with pytest.raises(ValueError, match=r"Expected a non-empty value for `id_or_name` but received ''"):
await async_client.vaults.items.with_raw_response.upsert(
key="x",
@@ -2480,7 +3399,8 @@ async def test_path_params_upsert_overload_3(self, async_client: AsyncKernel) ->
"name": "name",
"type": "text",
}
- ]
+ ],
+ "provider": "kernel",
},
type="credential",
)
@@ -2495,7 +3415,8 @@ async def test_path_params_upsert_overload_3(self, async_client: AsyncKernel) ->
"name": "name",
"type": "text",
}
- ]
+ ],
+ "provider": "kernel",
},
type="credential",
)
From 5c1bed353eb421bff589bcc86b2940ca750e3999 Mon Sep 17 00:00:00 2001
From: "kernel-internal[bot]"
<260533166+kernel-internal[bot]@users.noreply.github.com>
Date: Wed, 30 Sep 2026 13:29:13 +0000
Subject: [PATCH 310/366] feat: chore(stlc): seal custom-code tracking files
Stainless-Generated-From: 8ac1635568224ca17c04e5c6f15ac6e66e31b932
---
src/kernel/types/vault_provider_config.py | 8 +++-
.../vault_provider_config_create_params.py | 8 ++++
.../vault_provider_config_update_params.py | 6 +++
.../test_vault_provider_configs.py | 38 ++++++++++++++++++-
4 files changed, 57 insertions(+), 3 deletions(-)
diff --git a/src/kernel/types/vault_provider_config.py b/src/kernel/types/vault_provider_config.py
index 7ab9a049..03b0a427 100644
--- a/src/kernel/types/vault_provider_config.py
+++ b/src/kernel/types/vault_provider_config.py
@@ -1,6 +1,6 @@
# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
-from typing import Union
+from typing import Union, Optional
from datetime import datetime
from typing_extensions import Literal, Annotated, TypeAlias
@@ -30,6 +30,12 @@ class VaultLinkProviderConfig(BaseModel):
updated_at: datetime
+ publishable_key: Optional[str] = None
+ """Stripe publishable key sent to Link when refreshing and revoking wallet grants.
+
+ Omitted when not configured.
+ """
+
class VaultAgentCardProviderConfig(BaseModel):
"""Response schema for an AgentCard configuration, without secret credentials.
diff --git a/src/kernel/types/vault_provider_config_create_params.py b/src/kernel/types/vault_provider_config_create_params.py
index 185f06a6..bbef1ed7 100644
--- a/src/kernel/types/vault_provider_config_create_params.py
+++ b/src/kernel/types/vault_provider_config_create_params.py
@@ -28,6 +28,14 @@ class VaultLinkProviderConfigRequestCredentials(TypedDict, total=False):
client_secret: Required[str]
+ publishable_key: str
+ """Stripe publishable key for the account that owns the Link OAuth client.
+
+ Link requires it as the bearer credential when Kernel refreshes or revokes
+ imported wallet grants; without it, those wallets stop working when the imported
+ access token expires.
+ """
+
class VaultAgentCardProviderConfigRequest(TypedDict, total=False):
credentials: Required[VaultAgentCardProviderConfigRequestCredentials]
diff --git a/src/kernel/types/vault_provider_config_update_params.py b/src/kernel/types/vault_provider_config_update_params.py
index 49d1be8a..b4c18d20 100644
--- a/src/kernel/types/vault_provider_config_update_params.py
+++ b/src/kernel/types/vault_provider_config_update_params.py
@@ -26,3 +26,9 @@ class Credentials(TypedDict, total=False):
"""
client_secret: str
+
+ publishable_key: str
+ """Link configurations only.
+
+ Stripe publishable key sent to Link when refreshing and revoking wallet grants.
+ """
diff --git a/tests/api_resources/test_vault_provider_configs.py b/tests/api_resources/test_vault_provider_configs.py
index 7ab21cd5..7fe512e3 100644
--- a/tests/api_resources/test_vault_provider_configs.py
+++ b/tests/api_resources/test_vault_provider_configs.py
@@ -33,6 +33,20 @@ def test_method_create_overload_1(self, client: Kernel) -> None:
)
assert_matches_type(VaultProviderConfig, vault_provider_config, path=["response"])
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ def test_method_create_with_all_params_overload_1(self, client: Kernel) -> None:
+ vault_provider_config = client.vault_provider_configs.create(
+ credentials={
+ "client_id": "x",
+ "client_secret": "x",
+ "publishable_key": "pk_test_lK9w2kI5J1",
+ },
+ name="name",
+ provider="link",
+ )
+ assert_matches_type(VaultProviderConfig, vault_provider_config, path=["response"])
+
@pytest.mark.skip(reason="Mock server tests are disabled")
@parametrize
def test_raw_response_create_overload_1(self, client: Kernel) -> None:
@@ -173,7 +187,10 @@ def test_method_update(self, client: Kernel) -> None:
def test_method_update_with_all_params(self, client: Kernel) -> None:
vault_provider_config = client.vault_provider_configs.update(
id_or_name="id_or_name",
- credentials={"client_secret": "x"},
+ credentials={
+ "client_secret": "x",
+ "publishable_key": "pk_test_lK9w2kI5J1",
+ },
name="renamed-link-client",
)
assert_matches_type(VaultProviderConfig, vault_provider_config, path=["response"])
@@ -310,6 +327,20 @@ async def test_method_create_overload_1(self, async_client: AsyncKernel) -> None
)
assert_matches_type(VaultProviderConfig, vault_provider_config, path=["response"])
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ async def test_method_create_with_all_params_overload_1(self, async_client: AsyncKernel) -> None:
+ vault_provider_config = await async_client.vault_provider_configs.create(
+ credentials={
+ "client_id": "x",
+ "client_secret": "x",
+ "publishable_key": "pk_test_lK9w2kI5J1",
+ },
+ name="name",
+ provider="link",
+ )
+ assert_matches_type(VaultProviderConfig, vault_provider_config, path=["response"])
+
@pytest.mark.skip(reason="Mock server tests are disabled")
@parametrize
async def test_raw_response_create_overload_1(self, async_client: AsyncKernel) -> None:
@@ -450,7 +481,10 @@ async def test_method_update(self, async_client: AsyncKernel) -> None:
async def test_method_update_with_all_params(self, async_client: AsyncKernel) -> None:
vault_provider_config = await async_client.vault_provider_configs.update(
id_or_name="id_or_name",
- credentials={"client_secret": "x"},
+ credentials={
+ "client_secret": "x",
+ "publishable_key": "pk_test_lK9w2kI5J1",
+ },
name="renamed-link-client",
)
assert_matches_type(VaultProviderConfig, vault_provider_config, path=["response"])
From f6e1c01ac7e1cd1a06142292a59228603b78379c Mon Sep 17 00:00:00 2001
From: "kernel-internal[bot]"
<260533166+kernel-internal[bot]@users.noreply.github.com>
Date: Wed, 30 Sep 2026 13:34:38 +0000
Subject: [PATCH 311/366] release: 0.114.0
---
.release-please-manifest.json | 2 +-
CHANGELOG.md | 11 +++++++++++
pyproject.toml | 2 +-
src/kernel/_version.py | 2 +-
4 files changed, 14 insertions(+), 3 deletions(-)
diff --git a/.release-please-manifest.json b/.release-please-manifest.json
index 939bb175..d61779f3 100644
--- a/.release-please-manifest.json
+++ b/.release-please-manifest.json
@@ -1,3 +1,3 @@
{
- ".": "0.113.0"
+ ".": "0.114.0"
}
\ No newline at end of file
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 347647b8..c1ad420f 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,16 @@
# Changelog
+## [0.114.0](https://github.com/kernel/kernel-python-sdk/compare/v0.113.0...v0.114.0) (2026-09-30)
+
+
+### Features
+
+* Accept telemetry.storage and settle it at browser create ([28fb2d3](https://github.com/kernel/kernel-python-sdk/commit/28fb2d3c8fd83090d83404a0b407557585bf93f6))
+* Allow export-only network and console telemetry for BAA orgs ([2ff8d4a](https://github.com/kernel/kernel-python-sdk/commit/2ff8d4acacb1b8152b9c7d1b38067c1b02167f40))
+* chore(stlc): seal custom-code tracking files ([5c1bed3](https://github.com/kernel/kernel-python-sdk/commit/5c1bed353eb421bff589bcc86b2940ca750e3999))
+* Expose missing managed auth check URL as verification unavailable ([74dbad8](https://github.com/kernel/kernel-python-sdk/commit/74dbad8f0cdf5b414022ad907f6e20ee54893c59))
+* Let Vaults fill credentials from 1Password ([2937b10](https://github.com/kernel/kernel-python-sdk/commit/2937b10cce135282b6badc7e5bf71d339500ffdb))
+
## [0.113.0](https://github.com/kernel/kernel-python-sdk/compare/v0.112.0...v0.113.0) (2026-09-27)
diff --git a/pyproject.toml b/pyproject.toml
index 82dfdf81..7c2a12fe 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -1,6 +1,6 @@
[project]
name = "kernel"
-version = "0.113.0"
+version = "0.114.0"
description = "The official Python library for the kernel API"
dynamic = ["readme"]
license = "Apache-2.0"
diff --git a/src/kernel/_version.py b/src/kernel/_version.py
index 3350781a..f1d9dca2 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.113.0" # x-release-please-version
+__version__ = "0.114.0" # x-release-please-version
From 1f0aabc15ab7f7d943074978d8ba6839560f2ebe Mon Sep 17 00:00:00 2001
From: rgarcia <72655+rgarcia@users.noreply.github.com>
Date: Wed, 30 Sep 2026 13:38:57 +0000
Subject: [PATCH 312/366] docs: correct 0.114.0 changelog entry for Link
publishable key
---
CHANGELOG.md | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index c1ad420f..762517f3 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -7,9 +7,9 @@
* Accept telemetry.storage and settle it at browser create ([28fb2d3](https://github.com/kernel/kernel-python-sdk/commit/28fb2d3c8fd83090d83404a0b407557585bf93f6))
* Allow export-only network and console telemetry for BAA orgs ([2ff8d4a](https://github.com/kernel/kernel-python-sdk/commit/2ff8d4acacb1b8152b9c7d1b38067c1b02167f40))
-* chore(stlc): seal custom-code tracking files ([5c1bed3](https://github.com/kernel/kernel-python-sdk/commit/5c1bed353eb421bff589bcc86b2940ca750e3999))
* Expose missing managed auth check URL as verification unavailable ([74dbad8](https://github.com/kernel/kernel-python-sdk/commit/74dbad8f0cdf5b414022ad907f6e20ee54893c59))
* Let Vaults fill credentials from 1Password ([2937b10](https://github.com/kernel/kernel-python-sdk/commit/2937b10cce135282b6badc7e5bf71d339500ffdb))
+* Send Stripe publishable key when refreshing customer-owned Link grants ([5c1bed3](https://github.com/kernel/kernel-python-sdk/commit/5c1bed353eb421bff589bcc86b2940ca750e3999))
## [0.113.0](https://github.com/kernel/kernel-python-sdk/compare/v0.112.0...v0.113.0) (2026-09-27)
From 56fcf273db57e5bf4808687d7f5d05ea42db7e42 Mon Sep 17 00:00:00 2001
From: "kernel-internal[bot]"
<260533166+kernel-internal[bot]@users.noreply.github.com>
Date: Wed, 30 Sep 2026 17:42:42 +0000
Subject: [PATCH 313/366] feat: chore(stlc): seal custom-code tracking files
Stainless-Generated-From: 56c68476ea52d859cac195cc12cb9248e0de4860
---
api.md | 2 +-
src/kernel/resources/search/contents.py | 43 +++++---
src/kernel/types/result.py | 10 +-
src/kernel/types/search/__init__.py | 1 +
.../types/search/content_fetch_params.py | 62 +++++------
src/kernel/types/search/response.py | 101 ++++++++++++++++++
src/kernel/types/search_create_params.py | 62 +++++------
src/kernel/types/usage.py | 4 +-
tests/api_resources/search/test_contents.py | 18 ++--
9 files changed, 214 insertions(+), 89 deletions(-)
create mode 100644 src/kernel/types/search/response.py
diff --git a/api.md b/api.md
index c15b15db..ecc5c578 100644
--- a/api.md
+++ b/api.md
@@ -791,7 +791,7 @@ from kernel.types.search import FetchRequest, Response
Methods:
-- client.search.contents.fetch(id, \*\*params) -> None
+- client.search.contents.fetch(id, \*\*params) -> Response
## Providers
diff --git a/src/kernel/resources/search/contents.py b/src/kernel/resources/search/contents.py
index f41983c0..4704eabd 100644
--- a/src/kernel/resources/search/contents.py
+++ b/src/kernel/resources/search/contents.py
@@ -4,7 +4,7 @@
import httpx
-from ..._types import Body, Omit, Query, Headers, NoneType, NotGiven, SequenceNotStr, omit, not_given
+from ..._types import Body, Omit, Query, Headers, NotGiven, SequenceNotStr, omit, not_given
from ..._utils import path_template, maybe_transform, async_maybe_transform
from ..._compat import cached_property
from ..._resource import SyncAPIResource, AsyncAPIResource
@@ -16,6 +16,7 @@
)
from ..._base_client import make_request_options
from ...types.search import content_fetch_params
+from ...types.search.response import Response
__all__ = ["ContentsResource", "AsyncContentsResource"]
@@ -56,11 +57,18 @@ def fetch(
extra_query: Query | None = None,
extra_body: Body | None = None,
timeout: float | httpx.Timeout | None | NotGiven = not_given,
- ) -> None:
- """
- Deferred result-content retrieval is reserved but not available in this release.
- Requests return 404 until the retrieval implementation is shipped. X-Request-Id
- identifies this request separately from the search resource.
+ ) -> Response:
+ """Retrieves selected results from a retained search.
+
+ Provide exactly one of
+ result_ids or limit; the latter fetches the top results. Content defaults to
+ source:auto. Responses preserve result_ids order. Unknown result IDs are
+ rejected before retrieval starts. Missing, expired, or inaccessible searches
+ return 404. Once retrieval begins, return one outcome per selected result,
+ including timeout entries for work unfinished at the overall deadline. Browser
+ retrievals run sequentially in result order, so later results may time out when
+ earlier pages are slow. X-Request-Id identifies this request separately from the
+ search resource.
Args:
content: Defaults to source:auto when omitted.
@@ -83,7 +91,6 @@ def fetch(
"""
if not id:
raise ValueError(f"Expected a non-empty value for `id` but received {id!r}")
- extra_headers = {"Accept": "*/*", **(extra_headers or {})}
return self._post(
path_template("/search/{id}/contents", id=id),
body=maybe_transform(
@@ -98,7 +105,7 @@ def fetch(
options=make_request_options(
extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
),
- cast_to=NoneType,
+ cast_to=Response,
)
@@ -138,11 +145,18 @@ async def fetch(
extra_query: Query | None = None,
extra_body: Body | None = None,
timeout: float | httpx.Timeout | None | NotGiven = not_given,
- ) -> None:
- """
- Deferred result-content retrieval is reserved but not available in this release.
- Requests return 404 until the retrieval implementation is shipped. X-Request-Id
- identifies this request separately from the search resource.
+ ) -> Response:
+ """Retrieves selected results from a retained search.
+
+ Provide exactly one of
+ result_ids or limit; the latter fetches the top results. Content defaults to
+ source:auto. Responses preserve result_ids order. Unknown result IDs are
+ rejected before retrieval starts. Missing, expired, or inaccessible searches
+ return 404. Once retrieval begins, return one outcome per selected result,
+ including timeout entries for work unfinished at the overall deadline. Browser
+ retrievals run sequentially in result order, so later results may time out when
+ earlier pages are slow. X-Request-Id identifies this request separately from the
+ search resource.
Args:
content: Defaults to source:auto when omitted.
@@ -165,7 +179,6 @@ async def fetch(
"""
if not id:
raise ValueError(f"Expected a non-empty value for `id` but received {id!r}")
- extra_headers = {"Accept": "*/*", **(extra_headers or {})}
return await self._post(
path_template("/search/{id}/contents", id=id),
body=await async_maybe_transform(
@@ -180,7 +193,7 @@ async def fetch(
options=make_request_options(
extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
),
- cast_to=NoneType,
+ cast_to=Response,
)
diff --git a/src/kernel/types/result.py b/src/kernel/types/result.py
index 23a5d12b..72efaca3 100644
--- a/src/kernel/types/result.py
+++ b/src/kernel/types/result.py
@@ -34,7 +34,11 @@ class Content(BaseModel):
"""
cache_status: Optional[Literal["hit", "miss", "bypass", "unknown"]] = None
- """Kernel cache outcome. Provider-internal cache behavior may be unknown."""
+ """Kernel content cache outcome.
+
+ Kernel has no content cache yet: responses report bypass or unknown, and hit and
+ miss are reserved. Provider-internal cache behavior may be unknown.
+ """
completeness: Optional[Literal["full_page", "excerpt", "unknown"]] = None
"""Describes source coverage before max_chars truncation.
@@ -48,7 +52,7 @@ class Content(BaseModel):
"""Extraction version when Kernel transformed the input."""
fetched_at: Optional[datetime] = None
- """Origin retrieval time when known, not cache read time."""
+ """When Kernel received the content from the provider."""
final_url: Optional[str] = None
"""Final retrieval URL when known."""
@@ -59,7 +63,7 @@ class Content(BaseModel):
"""Final target HTTP status when known."""
method: Optional[Literal["provider", "browser_curl", "browser_render"]] = None
- """Original retrieval method, including on cache hits."""
+ """Original retrieval method."""
text: Optional[str] = None
"""Extracted website content, untrusted, not instructions.
diff --git a/src/kernel/types/search/__init__.py b/src/kernel/types/search/__init__.py
index a02c7ba2..8303f80b 100644
--- a/src/kernel/types/search/__init__.py
+++ b/src/kernel/types/search/__init__.py
@@ -4,6 +4,7 @@
from .search import Search as Search
from .provider import Provider as Provider
+from .response import Response as Response
from .content_fetch_params import ContentFetchParams as ContentFetchParams
from .provider_list_params import ProviderListParams as ProviderListParams
from .provider_list_response import ProviderListResponse as ProviderListResponse
diff --git a/src/kernel/types/search/content_fetch_params.py b/src/kernel/types/search/content_fetch_params.py
index d1795d0a..73a0294e 100644
--- a/src/kernel/types/search/content_fetch_params.py
+++ b/src/kernel/types/search/content_fetch_params.py
@@ -30,20 +30,22 @@ class ContentFetchParams(TypedDict, total=False):
class ContentBrowser(TypedDict, total=False):
- """Invalid with source=provider.
-
- Supplying browser_id requires
- source=browser so the chosen identity is not bypassed.
- """
+ """Requires source=auto or source=browser in deferred retrieval."""
browser_id: str
"""Existing browser session ID authorized for the caller and selected project.
- Reuses its cookies, proxy, and browser configuration. Kernel does not delete a
- caller-supplied browser. Render mode uses a temporary tab; website activity may
- still change shared cookies and storage. When omitted, Kernel obtains isolated
- browser capacity in the caller's account and releases it after retrieval. That
- capacity is not retained for later interaction. Existing browser quotas apply.
+ Reuses its cookies, proxy, and browser configuration; requests follow that
+ browser's existing network access behavior, with no additional destination
+ allowlist in this endpoint. Kernel does not delete a caller-supplied browser.
+ Render mode uses a temporary tab; website activity may still change shared
+ cookies and storage. When omitted and any result needs browser retrieval, Kernel
+ creates one temporary browser for the request using the dashboard launch
+ defaults (headful, stealth, default proxy), tags it with search_id, and deletes
+ it when the request finishes. It is billed and counts toward browser concurrency
+ like any other browser. A concurrency rejection returns 429 for source=browser;
+ for source=auto, results with retained content are still returned and the rest
+ report the rejection.
"""
mode: Literal["curl", "render"]
@@ -58,36 +60,36 @@ class Content(TypedDict, total=False):
"""Defaults to source:auto when omitted."""
browser: ContentBrowser
- """Invalid with source=provider.
-
- Supplying browser_id requires source=browser so the chosen identity is not
- bypassed.
- """
+ """Requires source=auto or source=browser in deferred retrieval."""
format: Literal["markdown", "text"]
max_age_hours: int
- """Maximum acceptable age of cached page content, measured from origin retrieval.
-
- 0 forces a live fetch. Governs the Kernel content cache, which is scoped to the
- caller organization and project and separated by retrieval context; fetches
- through a caller-supplied browser_id bypass that cache. Mapped to the provider
- freshness control when source is provider and the provider supports one;
- otherwise provider content age is reported as unknown via fetched_at.
+ """
+ For source=auto, maximum acceptable age of retained provider content, measured
+ from when the search received it from the provider. A value of 0 disables reuse
+ of retained content, so every result is fetched through a browser.
+ source=provider reuses retained provider content without freshness validation.
+ source=browser always fetches through a browser and does not use this age limit.
"""
max_chars: int
- """Per-result Unicode character limit after extraction."""
+ """Per-result Unicode character limit after extraction.
+
+ Retained provider content cannot exceed what was stored at search time; such
+ results report truncated when the stored text was already truncated.
+ """
source: Literal["auto", "provider", "browser"]
"""
- provider uses the search provider's native content retrieval; browser fetches
- each URL through a Kernel browser; auto prefers Kernel browser retrieval and
- falls back to provider-native content when browser retrieval is unavailable or
- unsuitable. Defaults to auto for both inline and deferred retrieval. Deferred
- provider retrieval requires post_hoc capability; an explicit provider source
- without it is a 400. Missing documents produce per-result unavailable outcomes,
- not request failures.
+ auto uses retained provider content within max_age_hours; for deferred retrieval
+ it falls back to a Kernel browser (caller-supplied or temporary) for results
+ without it. Inline retrieval never uses a browser. provider reuses retained
+ provider content when available, without freshness validation, and never
+ provisions a browser. browser fetches each URL through a Kernel browser, either
+ caller-supplied or temporary. No option makes a new provider request. Defaults
+ to auto for both inline and deferred retrieval. Missing documents produce
+ per-result unavailable outcomes, not request failures.
"""
timeout_ms: int
diff --git a/src/kernel/types/search/response.py b/src/kernel/types/search/response.py
new file mode 100644
index 00000000..444e40e3
--- /dev/null
+++ b/src/kernel/types/search/response.py
@@ -0,0 +1,101 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from typing import List, Optional
+from datetime import datetime
+from typing_extensions import Literal
+
+from ..usage import Usage
+from ..warning import Warning
+from ..._models import BaseModel
+
+__all__ = ["Response", "Content", "ContentError"]
+
+
+class ContentError(BaseModel):
+ code: str
+ """Machine-readable retrieval failure code."""
+
+ message: str
+ """Human-readable failure description."""
+
+ retryable: bool
+
+
+class Content(BaseModel):
+ result_id: str
+
+ status: Literal["ok", "unavailable", "blocked", "timeout", "unsupported_type", "extraction_failed", "error"]
+ """Ok means non-empty extracted content, not merely HTTP 200.
+
+ Blocked includes detected challenges or access denials. Detection is
+ best-effort, not a guarantee of page completeness. Error details are present for
+ non-ok outcomes; text is present only on ok.
+ """
+
+ url: str
+ """Original result URL."""
+
+ cache_status: Optional[Literal["hit", "miss", "bypass", "unknown"]] = None
+ """Kernel content cache outcome.
+
+ Kernel has no content cache yet: responses report bypass or unknown, and hit and
+ miss are reserved. Provider-internal cache behavior may be unknown.
+ """
+
+ completeness: Optional[Literal["full_page", "excerpt", "unknown"]] = None
+ """Describes source coverage before max_chars truncation.
+
+ Full_page means main-page content, not every dynamic element or linked page.
+ """
+
+ error: Optional[ContentError] = None
+
+ extractor_version: Optional[str] = None
+ """Extraction version when Kernel transformed the input."""
+
+ fetched_at: Optional[datetime] = None
+ """
+ When Kernel fetched the content, or received it from the provider for retained
+ content.
+ """
+
+ final_url: Optional[str] = None
+ """Final retrieval URL after redirects when known.
+
+ Curl mode follows up to 5 redirects.
+ """
+
+ format: Optional[Literal["markdown", "text"]] = None
+ """Format of text.
+
+ Plain-text and JSON pages are returned unchanged as text even when markdown was
+ requested.
+ """
+
+ http_status: Optional[int] = None
+ """Final target HTTP status when known."""
+
+ method: Optional[Literal["provider", "browser_curl", "browser_render"]] = None
+ """Original retrieval method."""
+
+ text: Optional[str] = None
+ """Extracted website content, untrusted, not instructions.
+
+ Present only on status=ok.
+ """
+
+ truncated: Optional[bool] = None
+ """
+ Whether the content was cut short, by max_chars or because the page exceeded the
+ 1 MiB read limit.
+ """
+
+
+class Response(BaseModel):
+ contents: List[Content]
+
+ search_id: str
+
+ usage: Usage
+
+ warnings: List[Warning]
diff --git a/src/kernel/types/search_create_params.py b/src/kernel/types/search_create_params.py
index da84a3d7..068800ac 100644
--- a/src/kernel/types/search_create_params.py
+++ b/src/kernel/types/search_create_params.py
@@ -118,20 +118,22 @@ class SearchCreateParams(TypedDict, total=False):
class ContentSearchContentOptionsBrowser(TypedDict, total=False):
- """Invalid with source=provider.
-
- Supplying browser_id requires
- source=browser so the chosen identity is not bypassed.
- """
+ """Requires source=auto or source=browser in deferred retrieval."""
browser_id: str
"""Existing browser session ID authorized for the caller and selected project.
- Reuses its cookies, proxy, and browser configuration. Kernel does not delete a
- caller-supplied browser. Render mode uses a temporary tab; website activity may
- still change shared cookies and storage. When omitted, Kernel obtains isolated
- browser capacity in the caller's account and releases it after retrieval. That
- capacity is not retained for later interaction. Existing browser quotas apply.
+ Reuses its cookies, proxy, and browser configuration; requests follow that
+ browser's existing network access behavior, with no additional destination
+ allowlist in this endpoint. Kernel does not delete a caller-supplied browser.
+ Render mode uses a temporary tab; website activity may still change shared
+ cookies and storage. When omitted and any result needs browser retrieval, Kernel
+ creates one temporary browser for the request using the dashboard launch
+ defaults (headful, stealth, default proxy), tags it with search_id, and deletes
+ it when the request finishes. It is billed and counts toward browser concurrency
+ like any other browser. A concurrency rejection returns 429 for source=browser;
+ for source=auto, results with retained content are still returned and the rest
+ report the rejection.
"""
mode: Literal["curl", "render"]
@@ -144,36 +146,36 @@ class ContentSearchContentOptionsBrowser(TypedDict, total=False):
class ContentSearchContentOptions(TypedDict, total=False):
browser: ContentSearchContentOptionsBrowser
- """Invalid with source=provider.
-
- Supplying browser_id requires source=browser so the chosen identity is not
- bypassed.
- """
+ """Requires source=auto or source=browser in deferred retrieval."""
format: Literal["markdown", "text"]
max_age_hours: int
- """Maximum acceptable age of cached page content, measured from origin retrieval.
-
- 0 forces a live fetch. Governs the Kernel content cache, which is scoped to the
- caller organization and project and separated by retrieval context; fetches
- through a caller-supplied browser_id bypass that cache. Mapped to the provider
- freshness control when source is provider and the provider supports one;
- otherwise provider content age is reported as unknown via fetched_at.
+ """
+ For source=auto, maximum acceptable age of retained provider content, measured
+ from when the search received it from the provider. A value of 0 disables reuse
+ of retained content, so every result is fetched through a browser.
+ source=provider reuses retained provider content without freshness validation.
+ source=browser always fetches through a browser and does not use this age limit.
"""
max_chars: int
- """Per-result Unicode character limit after extraction."""
+ """Per-result Unicode character limit after extraction.
+
+ Retained provider content cannot exceed what was stored at search time; such
+ results report truncated when the stored text was already truncated.
+ """
source: Literal["auto", "provider", "browser"]
"""
- provider uses the search provider's native content retrieval; browser fetches
- each URL through a Kernel browser; auto prefers Kernel browser retrieval and
- falls back to provider-native content when browser retrieval is unavailable or
- unsuitable. Defaults to auto for both inline and deferred retrieval. Deferred
- provider retrieval requires post_hoc capability; an explicit provider source
- without it is a 400. Missing documents produce per-result unavailable outcomes,
- not request failures.
+ auto uses retained provider content within max_age_hours; for deferred retrieval
+ it falls back to a Kernel browser (caller-supplied or temporary) for results
+ without it. Inline retrieval never uses a browser. provider reuses retained
+ provider content when available, without freshness validation, and never
+ provisions a browser. browser fetches each URL through a Kernel browser, either
+ caller-supplied or temporary. No option makes a new provider request. Defaults
+ to auto for both inline and deferred retrieval. Missing documents produce
+ per-result unavailable outcomes, not request failures.
"""
timeout_ms: int
diff --git a/src/kernel/types/usage.py b/src/kernel/types/usage.py
index 76be908c..5ca1c30c 100644
--- a/src/kernel/types/usage.py
+++ b/src/kernel/types/usage.py
@@ -10,8 +10,8 @@
class Usage(BaseModel):
content_fetches: int
"""
- Number of result URLs for which a Kernel browser retrieval was attempted,
- excluding cache-only hits.
+ Number of result URLs for which a Kernel browser retrieval received a response
+ from the target, excluding cache-only hits.
"""
results_count: int
diff --git a/tests/api_resources/search/test_contents.py b/tests/api_resources/search/test_contents.py
index 941e6018..b4de0578 100644
--- a/tests/api_resources/search/test_contents.py
+++ b/tests/api_resources/search/test_contents.py
@@ -8,6 +8,8 @@
import pytest
from kernel import Kernel, AsyncKernel
+from tests.utils import assert_matches_type
+from kernel.types.search import Response
base_url = os.environ.get("TEST_API_BASE_URL", "http://127.0.0.1:4010")
@@ -21,7 +23,7 @@ def test_method_fetch(self, client: Kernel) -> None:
content = client.search.contents.fetch(
id="srch_abc123",
)
- assert content is None
+ assert_matches_type(Response, content, path=["response"])
@pytest.mark.skip(reason="Mock server tests are disabled")
@parametrize
@@ -43,7 +45,7 @@ def test_method_fetch_with_all_params(self, client: Kernel) -> None:
result_ids=["string"],
timeout_ms=1000,
)
- assert content is None
+ assert_matches_type(Response, content, path=["response"])
@pytest.mark.skip(reason="Mock server tests are disabled")
@parametrize
@@ -55,7 +57,7 @@ def test_raw_response_fetch(self, client: Kernel) -> None:
assert response.is_closed is True
assert response.http_request.headers.get("X-Stainless-Lang") == "python"
content = response.parse()
- assert content is None
+ assert_matches_type(Response, content, path=["response"])
@pytest.mark.skip(reason="Mock server tests are disabled")
@parametrize
@@ -67,7 +69,7 @@ def test_streaming_response_fetch(self, client: Kernel) -> None:
assert response.http_request.headers.get("X-Stainless-Lang") == "python"
content = response.parse()
- assert content is None
+ assert_matches_type(Response, content, path=["response"])
assert cast(Any, response.is_closed) is True
@@ -91,7 +93,7 @@ async def test_method_fetch(self, async_client: AsyncKernel) -> None:
content = await async_client.search.contents.fetch(
id="srch_abc123",
)
- assert content is None
+ assert_matches_type(Response, content, path=["response"])
@pytest.mark.skip(reason="Mock server tests are disabled")
@parametrize
@@ -113,7 +115,7 @@ async def test_method_fetch_with_all_params(self, async_client: AsyncKernel) ->
result_ids=["string"],
timeout_ms=1000,
)
- assert content is None
+ assert_matches_type(Response, content, path=["response"])
@pytest.mark.skip(reason="Mock server tests are disabled")
@parametrize
@@ -125,7 +127,7 @@ async def test_raw_response_fetch(self, async_client: AsyncKernel) -> None:
assert response.is_closed is True
assert response.http_request.headers.get("X-Stainless-Lang") == "python"
content = await response.parse()
- assert content is None
+ assert_matches_type(Response, content, path=["response"])
@pytest.mark.skip(reason="Mock server tests are disabled")
@parametrize
@@ -137,7 +139,7 @@ async def test_streaming_response_fetch(self, async_client: AsyncKernel) -> None
assert response.http_request.headers.get("X-Stainless-Lang") == "python"
content = await response.parse()
- assert content is None
+ assert_matches_type(Response, content, path=["response"])
assert cast(Any, response.is_closed) is True
From 0c5a9df7ae6896dce40af9550ca93a516defd420 Mon Sep 17 00:00:00 2001
From: "kernel-internal[bot]"
<260533166+kernel-internal[bot]@users.noreply.github.com>
Date: Wed, 30 Sep 2026 17:47:28 +0000
Subject: [PATCH 314/366] release: 0.115.0
---
.release-please-manifest.json | 2 +-
CHANGELOG.md | 7 +++++++
pyproject.toml | 2 +-
src/kernel/_version.py | 2 +-
4 files changed, 10 insertions(+), 3 deletions(-)
diff --git a/.release-please-manifest.json b/.release-please-manifest.json
index d61779f3..d3dc9f51 100644
--- a/.release-please-manifest.json
+++ b/.release-please-manifest.json
@@ -1,3 +1,3 @@
{
- ".": "0.114.0"
+ ".": "0.115.0"
}
\ No newline at end of file
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 762517f3..8aef931d 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,12 @@
# Changelog
+## [0.115.0](https://github.com/kernel/kernel-python-sdk/compare/v0.114.0...v0.115.0) (2026-09-30)
+
+
+### Features
+
+* chore(stlc): seal custom-code tracking files ([56fcf27](https://github.com/kernel/kernel-python-sdk/commit/56fcf273db57e5bf4808687d7f5d05ea42db7e42))
+
## [0.114.0](https://github.com/kernel/kernel-python-sdk/compare/v0.113.0...v0.114.0) (2026-09-30)
diff --git a/pyproject.toml b/pyproject.toml
index 7c2a12fe..36366e3d 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -1,6 +1,6 @@
[project]
name = "kernel"
-version = "0.114.0"
+version = "0.115.0"
description = "The official Python library for the kernel API"
dynamic = ["readme"]
license = "Apache-2.0"
diff --git a/src/kernel/_version.py b/src/kernel/_version.py
index f1d9dca2..19bf404c 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.114.0" # x-release-please-version
+__version__ = "0.115.0" # x-release-please-version
From c5b764aded47fe8377a14375841a8b6aa0c1ac22 Mon Sep 17 00:00:00 2001
From: chruffins <23645059+chruffins@users.noreply.github.com>
Date: Wed, 30 Sep 2026 18:21:11 +0000
Subject: [PATCH 315/366] docs: describe deferred search release
---
CHANGELOG.md | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 8aef931d..2fb89f8f 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -5,7 +5,7 @@
### Features
-* chore(stlc): seal custom-code tracking files ([56fcf27](https://github.com/kernel/kernel-python-sdk/commit/56fcf273db57e5bf4808687d7f5d05ea42db7e42))
+* Fetch content for selected results from retained searches ([kernel/kernel#3978](https://github.com/kernel/kernel/pull/3978))
## [0.114.0](https://github.com/kernel/kernel-python-sdk/compare/v0.113.0...v0.114.0) (2026-09-30)
From f09793c1d2b81efa0ae1be5ae442a2419f25fb75 Mon Sep 17 00:00:00 2001
From: "kernel-internal[bot]"
<260533166+kernel-internal[bot]@users.noreply.github.com>
Date: Wed, 30 Sep 2026 18:26:48 +0000
Subject: [PATCH 316/366] feat: Handle AgentCard autopilot and declare
checkout_origin on the card spec
Stainless-Generated-From: 57daaccbc499b1ec1df09d9780eb23bda434acfb
---
src/kernel/types/vaults/card_vault_item_spec.py | 14 +++++++++++++-
.../types/vaults/card_vault_item_spec_param.py | 14 +++++++++++++-
2 files changed, 26 insertions(+), 2 deletions(-)
diff --git a/src/kernel/types/vaults/card_vault_item_spec.py b/src/kernel/types/vaults/card_vault_item_spec.py
index 0c73d9bf..880ce111 100644
--- a/src/kernel/types/vaults/card_vault_item_spec.py
+++ b/src/kernel/types/vaults/card_vault_item_spec.py
@@ -96,7 +96,7 @@ class LinkCardVaultItemSpec(BaseModel):
class AgentCardCardVaultItemSpec(BaseModel):
"""AgentCard reusable live payment card.
- Test-mode card creation is not supported. Each checkout creates an approval-gated authorization for spec.merchant / spec.amount. The card stays ready after each authorization.
+ Test-mode card creation is not supported. Each checkout creates an authorization for spec.merchant / spec.amount that the cardholder approves, unless AgentCard runs it under one of the cardholder's autopilot rules. The card stays ready after each authorization.
"""
amount: int
@@ -119,6 +119,18 @@ class AgentCardCardVaultItemSpec(BaseModel):
cardholder picks on the approval screen.
"""
+ checkout_origin: Optional[str] = None
+ """
+ Origin of the top-level checkout page, such as https://shop.example.com: https,
+ a lowercase host, a port only when it is not 443, and no path. http is accepted
+ only for localhost test pages. Checkouts without a preparation send it to
+ AgentCard, which uses it to match the cardholder's autopilot rules; prepared
+ checkouts send the preparation's merchant_origin instead. Kernel sends the
+ declared value and does not compare it with the page the browser has open.
+ Omitted, those checkouts ask the cardholder to approve. Card updates replace the
+ whole spec, so an update that omits it removes it.
+ """
+
CardVaultItemSpec: TypeAlias = Annotated[
Union[LinkCardVaultItemSpec, AgentCardCardVaultItemSpec], PropertyInfo(discriminator="provider")
diff --git a/src/kernel/types/vaults/card_vault_item_spec_param.py b/src/kernel/types/vaults/card_vault_item_spec_param.py
index 7172d066..3f27771a 100644
--- a/src/kernel/types/vaults/card_vault_item_spec_param.py
+++ b/src/kernel/types/vaults/card_vault_item_spec_param.py
@@ -95,7 +95,7 @@ class LinkCardVaultItemSpec(TypedDict, total=False):
class AgentCardCardVaultItemSpec(TypedDict, total=False):
"""AgentCard reusable live payment card.
- Test-mode card creation is not supported. Each checkout creates an approval-gated authorization for spec.merchant / spec.amount. The card stays ready after each authorization.
+ Test-mode card creation is not supported. Each checkout creates an authorization for spec.merchant / spec.amount that the cardholder approves, unless AgentCard runs it under one of the cardholder's autopilot rules. The card stays ready after each authorization.
"""
amount: Required[int]
@@ -118,5 +118,17 @@ class AgentCardCardVaultItemSpec(TypedDict, total=False):
cardholder picks on the approval screen.
"""
+ checkout_origin: str
+ """
+ Origin of the top-level checkout page, such as https://shop.example.com: https,
+ a lowercase host, a port only when it is not 443, and no path. http is accepted
+ only for localhost test pages. Checkouts without a preparation send it to
+ AgentCard, which uses it to match the cardholder's autopilot rules; prepared
+ checkouts send the preparation's merchant_origin instead. Kernel sends the
+ declared value and does not compare it with the page the browser has open.
+ Omitted, those checkouts ask the cardholder to approve. Card updates replace the
+ whole spec, so an update that omits it removes it.
+ """
+
CardVaultItemSpecParam: TypeAlias = Union[LinkCardVaultItemSpec, AgentCardCardVaultItemSpec]
From c053ce45e5d6bfaa4577310d28b3ecf0a7c16f4e Mon Sep 17 00:00:00 2001
From: "kernel-internal[bot]"
<260533166+kernel-internal[bot]@users.noreply.github.com>
Date: Thu, 1 Oct 2026 19:53:08 +0000
Subject: [PATCH 317/366] feat: chore(stlc): seal custom-code tracking files
Stainless-Generated-From: d3af3dfd71b0b695c14451f7a69c099a2c3625a3
---
src/kernel/resources/credentials.py | 89 ++++++++++++++++++--
src/kernel/types/credential.py | 21 ++++-
src/kernel/types/credential_create_params.py | 29 ++++++-
src/kernel/types/credential_update_params.py | 33 ++++++--
tests/api_resources/test_credentials.py | 12 +++
5 files changed, 166 insertions(+), 18 deletions(-)
diff --git a/src/kernel/resources/credentials.py b/src/kernel/resources/credentials.py
index f1a3ac01..47eb0aad 100644
--- a/src/kernel/resources/credentials.py
+++ b/src/kernel/resources/credentials.py
@@ -3,6 +3,7 @@
from __future__ import annotations
from typing import Dict, Optional
+from typing_extensions import Literal
import httpx
@@ -54,6 +55,9 @@ def create(
name: str,
values: Dict[str, str],
sso_provider: str | Omit = omit,
+ totp_algorithm: Literal["SHA1", "SHA256", "SHA512"] | Omit = omit,
+ totp_digits: int | Omit = omit,
+ totp_period: int | Omit = omit,
totp_secret: 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.
@@ -77,8 +81,20 @@ def create(
button, it will be clicked first before filling credential values on the
identity provider's login page.
- totp_secret: Base32-encoded TOTP secret for generating one-time passwords. Used for automatic
- 2FA during login.
+ totp_algorithm: HMAC algorithm used to generate TOTP codes. Defaults to SHA1 and is ignored when
+ an `otpauth://` URI supplies the algorithm.
+
+ totp_digits: Number of digits in generated TOTP codes. Defaults to 6 and is ignored when an
+ `otpauth://` URI supplies the digit count.
+
+ totp_period: TOTP rotation period in seconds. Defaults to 30 and is ignored when an
+ `otpauth://` URI supplies the period.
+
+ totp_secret: Accepts a 16-128 character base32-encoded TOTP secret or an `otpauth://totp/...`
+ URI. The range accepts existing shorter seeds and longer seeds regardless of
+ HMAC algorithm; RFC 6238 recommends unpadded base32 lengths of 32/52/103 for
+ SHA1/SHA256/SHA512. Only URI parameters present override the corresponding
+ explicit TOTP fields. Used for automatic 2FA during login.
extra_headers: Send extra headers
@@ -96,6 +112,9 @@ def create(
"name": name,
"values": values,
"sso_provider": sso_provider,
+ "totp_algorithm": totp_algorithm,
+ "totp_digits": totp_digits,
+ "totp_period": totp_period,
"totp_secret": totp_secret,
},
credential_create_params.CredentialCreateParams,
@@ -147,6 +166,9 @@ def update(
name: str | Omit = omit,
remove_value_keys: SequenceNotStr[str] | Omit = omit,
sso_provider: Optional[str] | Omit = omit,
+ totp_algorithm: Literal["SHA1", "SHA256", "SHA512"] | Omit = omit,
+ totp_digits: int | Omit = omit,
+ totp_period: int | Omit = omit,
totp_secret: str | Omit = omit,
values: Dict[str, str] | Omit = omit,
# Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
@@ -170,8 +192,20 @@ def update(
sso_provider: If set, indicates this credential should be used with the specified SSO
provider. Set to empty string or null to remove.
- totp_secret: Base32-encoded TOTP secret for generating one-time passwords. Spaces and
- formatting are automatically normalized. Set to empty string to remove.
+ totp_algorithm: HMAC algorithm used to generate TOTP codes. Requires totp_secret and is ignored
+ when an `otpauth://` URI supplies the algorithm.
+
+ totp_digits: Number of digits in generated TOTP codes. Requires totp_secret and is ignored
+ when an `otpauth://` URI supplies the digit count.
+
+ totp_period: TOTP rotation period in seconds. Requires totp_secret and is ignored when an
+ `otpauth://` URI supplies the period.
+
+ totp_secret: Accepts a 16-128 character base32-encoded TOTP secret or an `otpauth://totp/...`
+ URI. Only URI parameters present override the corresponding explicit TOTP
+ fields. When rotating a raw secret, omitted fields preserve their existing
+ values; a new URI defaults unspecified fields to SHA1/6/30. Set to empty string
+ to remove the secret and its metadata.
values: Field name to value mapping. Values are merged with existing values (new keys
added, existing keys overwritten).
@@ -193,6 +227,9 @@ def update(
"name": name,
"remove_value_keys": remove_value_keys,
"sso_provider": sso_provider,
+ "totp_algorithm": totp_algorithm,
+ "totp_digits": totp_digits,
+ "totp_period": totp_period,
"totp_secret": totp_secret,
"values": values,
},
@@ -360,6 +397,9 @@ async def create(
name: str,
values: Dict[str, str],
sso_provider: str | Omit = omit,
+ totp_algorithm: Literal["SHA1", "SHA256", "SHA512"] | Omit = omit,
+ totp_digits: int | Omit = omit,
+ totp_period: int | Omit = omit,
totp_secret: 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.
@@ -383,8 +423,20 @@ async def create(
button, it will be clicked first before filling credential values on the
identity provider's login page.
- totp_secret: Base32-encoded TOTP secret for generating one-time passwords. Used for automatic
- 2FA during login.
+ totp_algorithm: HMAC algorithm used to generate TOTP codes. Defaults to SHA1 and is ignored when
+ an `otpauth://` URI supplies the algorithm.
+
+ totp_digits: Number of digits in generated TOTP codes. Defaults to 6 and is ignored when an
+ `otpauth://` URI supplies the digit count.
+
+ totp_period: TOTP rotation period in seconds. Defaults to 30 and is ignored when an
+ `otpauth://` URI supplies the period.
+
+ totp_secret: Accepts a 16-128 character base32-encoded TOTP secret or an `otpauth://totp/...`
+ URI. The range accepts existing shorter seeds and longer seeds regardless of
+ HMAC algorithm; RFC 6238 recommends unpadded base32 lengths of 32/52/103 for
+ SHA1/SHA256/SHA512. Only URI parameters present override the corresponding
+ explicit TOTP fields. Used for automatic 2FA during login.
extra_headers: Send extra headers
@@ -402,6 +454,9 @@ async def create(
"name": name,
"values": values,
"sso_provider": sso_provider,
+ "totp_algorithm": totp_algorithm,
+ "totp_digits": totp_digits,
+ "totp_period": totp_period,
"totp_secret": totp_secret,
},
credential_create_params.CredentialCreateParams,
@@ -453,6 +508,9 @@ async def update(
name: str | Omit = omit,
remove_value_keys: SequenceNotStr[str] | Omit = omit,
sso_provider: Optional[str] | Omit = omit,
+ totp_algorithm: Literal["SHA1", "SHA256", "SHA512"] | Omit = omit,
+ totp_digits: int | Omit = omit,
+ totp_period: int | Omit = omit,
totp_secret: str | Omit = omit,
values: Dict[str, str] | Omit = omit,
# Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
@@ -476,8 +534,20 @@ async def update(
sso_provider: If set, indicates this credential should be used with the specified SSO
provider. Set to empty string or null to remove.
- totp_secret: Base32-encoded TOTP secret for generating one-time passwords. Spaces and
- formatting are automatically normalized. Set to empty string to remove.
+ totp_algorithm: HMAC algorithm used to generate TOTP codes. Requires totp_secret and is ignored
+ when an `otpauth://` URI supplies the algorithm.
+
+ totp_digits: Number of digits in generated TOTP codes. Requires totp_secret and is ignored
+ when an `otpauth://` URI supplies the digit count.
+
+ totp_period: TOTP rotation period in seconds. Requires totp_secret and is ignored when an
+ `otpauth://` URI supplies the period.
+
+ totp_secret: Accepts a 16-128 character base32-encoded TOTP secret or an `otpauth://totp/...`
+ URI. Only URI parameters present override the corresponding explicit TOTP
+ fields. When rotating a raw secret, omitted fields preserve their existing
+ values; a new URI defaults unspecified fields to SHA1/6/30. Set to empty string
+ to remove the secret and its metadata.
values: Field name to value mapping. Values are merged with existing values (new keys
added, existing keys overwritten).
@@ -499,6 +569,9 @@ async def update(
"name": name,
"remove_value_keys": remove_value_keys,
"sso_provider": sso_provider,
+ "totp_algorithm": totp_algorithm,
+ "totp_digits": totp_digits,
+ "totp_period": totp_period,
"totp_secret": totp_secret,
"values": values,
},
diff --git a/src/kernel/types/credential.py b/src/kernel/types/credential.py
index fb2ff6e1..3b54587c 100644
--- a/src/kernel/types/credential.py
+++ b/src/kernel/types/credential.py
@@ -2,6 +2,7 @@
from typing import List, Optional
from datetime import datetime
+from typing_extensions import Literal
from .._models import BaseModel
@@ -40,8 +41,14 @@ class Credential(BaseModel):
identity provider's login page.
"""
+ totp_algorithm: Optional[Literal["SHA1", "SHA256", "SHA512"]] = None
+ """HMAC algorithm used to generate TOTP codes.
+
+ Defaults to SHA1 for credentials created before this metadata was stored.
+ """
+
totp_code: Optional[str] = None
- """Current 6-digit TOTP code.
+ """Current TOTP code.
Only included in create/update responses when totp_secret was just set.
"""
@@ -49,6 +56,18 @@ class Credential(BaseModel):
totp_code_expires_at: Optional[datetime] = None
"""When the totp_code expires. Only included when totp_code is present."""
+ totp_digits: Optional[int] = None
+ """Number of digits in generated TOTP codes.
+
+ Defaults to 6 for credentials created before this metadata was stored.
+ """
+
+ totp_period: Optional[int] = None
+ """TOTP rotation period in seconds.
+
+ Defaults to 30 for credentials created before this metadata was stored.
+ """
+
value_keys: Optional[List[str]] = None
"""The field names stored in this credential's values (e.g., username, password).
diff --git a/src/kernel/types/credential_create_params.py b/src/kernel/types/credential_create_params.py
index 9d306844..526b7149 100644
--- a/src/kernel/types/credential_create_params.py
+++ b/src/kernel/types/credential_create_params.py
@@ -3,7 +3,7 @@
from __future__ import annotations
from typing import Dict
-from typing_extensions import Required, TypedDict
+from typing_extensions import Literal, Required, TypedDict
__all__ = ["CredentialCreateParams"]
@@ -26,8 +26,29 @@ class CredentialCreateParams(TypedDict, total=False):
identity provider's login page.
"""
- totp_secret: str
- """Base32-encoded TOTP secret for generating one-time passwords.
+ totp_algorithm: Literal["SHA1", "SHA256", "SHA512"]
+ """HMAC algorithm used to generate TOTP codes.
+
+ Defaults to SHA1 and is ignored when an `otpauth://` URI supplies the algorithm.
+ """
+
+ totp_digits: int
+ """Number of digits in generated TOTP codes.
+
+ Defaults to 6 and is ignored when an `otpauth://` URI supplies the digit count.
+ """
- Used for automatic 2FA during login.
+ totp_period: int
+ """TOTP rotation period in seconds.
+
+ Defaults to 30 and is ignored when an `otpauth://` URI supplies the period.
+ """
+
+ totp_secret: str
+ """
+ Accepts a 16-128 character base32-encoded TOTP secret or an `otpauth://totp/...`
+ URI. The range accepts existing shorter seeds and longer seeds regardless of
+ HMAC algorithm; RFC 6238 recommends unpadded base32 lengths of 32/52/103 for
+ SHA1/SHA256/SHA512. Only URI parameters present override the corresponding
+ explicit TOTP fields. Used for automatic 2FA during login.
"""
diff --git a/src/kernel/types/credential_update_params.py b/src/kernel/types/credential_update_params.py
index 9da3f09c..976c62c4 100644
--- a/src/kernel/types/credential_update_params.py
+++ b/src/kernel/types/credential_update_params.py
@@ -3,7 +3,7 @@
from __future__ import annotations
from typing import Dict, Optional
-from typing_extensions import TypedDict
+from typing_extensions import Literal, TypedDict
from .._types import SequenceNotStr
@@ -28,11 +28,34 @@ class CredentialUpdateParams(TypedDict, total=False):
Set to empty string or null to remove.
"""
- totp_secret: str
- """Base32-encoded TOTP secret for generating one-time passwords.
+ totp_algorithm: Literal["SHA1", "SHA256", "SHA512"]
+ """HMAC algorithm used to generate TOTP codes.
+
+ Requires totp_secret and is ignored when an `otpauth://` URI supplies the
+ algorithm.
+ """
+
+ totp_digits: int
+ """Number of digits in generated TOTP codes.
+
+ Requires totp_secret and is ignored when an `otpauth://` URI supplies the digit
+ count.
+ """
- Spaces and formatting are automatically normalized. Set to empty string to
- remove.
+ totp_period: int
+ """TOTP rotation period in seconds.
+
+ Requires totp_secret and is ignored when an `otpauth://` URI supplies the
+ period.
+ """
+
+ totp_secret: str
+ """
+ Accepts a 16-128 character base32-encoded TOTP secret or an `otpauth://totp/...`
+ URI. Only URI parameters present override the corresponding explicit TOTP
+ fields. When rotating a raw secret, omitted fields preserve their existing
+ values; a new URI defaults unspecified fields to SHA1/6/30. Set to empty string
+ to remove the secret and its metadata.
"""
values: Dict[str, str]
diff --git a/tests/api_resources/test_credentials.py b/tests/api_resources/test_credentials.py
index 9fddf1c0..b2079b5b 100644
--- a/tests/api_resources/test_credentials.py
+++ b/tests/api_resources/test_credentials.py
@@ -45,6 +45,9 @@ def test_method_create_with_all_params(self, client: Kernel) -> None:
"password": "mysecretpassword",
},
sso_provider="google",
+ totp_algorithm="SHA1",
+ totp_digits=6,
+ totp_period=30,
totp_secret="JBSWY3DPEHPK3PXP",
)
assert_matches_type(Credential, credential, path=["response"])
@@ -143,6 +146,9 @@ def test_method_update_with_all_params(self, client: Kernel) -> None:
name="my-updated-login",
remove_value_keys=["old_field"],
sso_provider="google",
+ totp_algorithm="SHA1",
+ totp_digits=6,
+ totp_period=30,
totp_secret="JBSWY3DPEHPK3PXP",
values={
"username": "user@example.com",
@@ -338,6 +344,9 @@ async def test_method_create_with_all_params(self, async_client: AsyncKernel) ->
"password": "mysecretpassword",
},
sso_provider="google",
+ totp_algorithm="SHA1",
+ totp_digits=6,
+ totp_period=30,
totp_secret="JBSWY3DPEHPK3PXP",
)
assert_matches_type(Credential, credential, path=["response"])
@@ -436,6 +445,9 @@ async def test_method_update_with_all_params(self, async_client: AsyncKernel) ->
name="my-updated-login",
remove_value_keys=["old_field"],
sso_provider="google",
+ totp_algorithm="SHA1",
+ totp_digits=6,
+ totp_period=30,
totp_secret="JBSWY3DPEHPK3PXP",
values={
"username": "user@example.com",
From 689d5a28de1666aa132c0dfc0ca2f7527cf0468b Mon Sep 17 00:00:00 2001
From: "kernel-internal[bot]"
<260533166+kernel-internal[bot]@users.noreply.github.com>
Date: Thu, 1 Oct 2026 19:57:53 +0000
Subject: [PATCH 318/366] release: 0.116.0
---
.release-please-manifest.json | 2 +-
CHANGELOG.md | 7 +++++++
pyproject.toml | 2 +-
src/kernel/_version.py | 2 +-
4 files changed, 10 insertions(+), 3 deletions(-)
diff --git a/.release-please-manifest.json b/.release-please-manifest.json
index d3dc9f51..988e843f 100644
--- a/.release-please-manifest.json
+++ b/.release-please-manifest.json
@@ -1,3 +1,3 @@
{
- ".": "0.115.0"
+ ".": "0.116.0"
}
\ No newline at end of file
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 2fb89f8f..416063a7 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,12 @@
# Changelog
+## [0.116.0](https://github.com/kernel/kernel-python-sdk/compare/v0.115.0...v0.116.0) (2026-10-01)
+
+
+### Features
+
+* chore(stlc): seal custom-code tracking files ([c053ce4](https://github.com/kernel/kernel-python-sdk/commit/c053ce45e5d6bfaa4577310d28b3ecf0a7c16f4e))
+
## [0.115.0](https://github.com/kernel/kernel-python-sdk/compare/v0.114.0...v0.115.0) (2026-09-30)
diff --git a/pyproject.toml b/pyproject.toml
index 36366e3d..4e31e28e 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -1,6 +1,6 @@
[project]
name = "kernel"
-version = "0.115.0"
+version = "0.116.0"
description = "The official Python library for the kernel API"
dynamic = ["readme"]
license = "Apache-2.0"
diff --git a/src/kernel/_version.py b/src/kernel/_version.py
index 19bf404c..ba815a7f 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.115.0" # x-release-please-version
+__version__ = "0.116.0" # x-release-please-version
From a75eb77bb3722e82b45836e1b9f5f6fc43dfa4ff Mon Sep 17 00:00:00 2001
From: "kernel-internal[bot]"
<260533166+kernel-internal[bot]@users.noreply.github.com>
Date: Fri, 2 Oct 2026 13:21:59 +0000
Subject: [PATCH 319/366] feat: Invoke WebMCP tools with vault item fields
Stainless-Generated-From: 0885f13f38fb56e245ab0532a5dc04ab12706eae
---
api.md | 3 +
src/kernel/resources/vaults/items.py | 161 +++++++-
src/kernel/types/vaults/__init__.py | 7 +
.../vaults/credential_account_vault_item.py | 1 +
.../types/vaults/credential_vault_item.py | 1 +
.../vaults/item_perform_operation_params.py | 38 +-
src/kernel/types/vaults/vault_item.py | 2 +
.../vaults/vault_item_operation_response.py | 4 +
.../vaults/vault_webmcp_binding_param.py | 28 ++
...voke_vault_item_operation_request_param.py | 53 +++
...bmcp_invoke_vault_item_operation_result.py | 37 ++
tests/api_resources/vaults/test_items.py | 344 ++++++++++++++++++
12 files changed, 675 insertions(+), 4 deletions(-)
create mode 100644 src/kernel/types/vaults/vault_webmcp_binding_param.py
create mode 100644 src/kernel/types/vaults/webmcp_invoke_vault_item_operation_request_param.py
create mode 100644 src/kernel/types/vaults/webmcp_invoke_vault_item_operation_result.py
diff --git a/api.md b/api.md
index ecc5c578..90bbc16d 100644
--- a/api.md
+++ b/api.md
@@ -596,8 +596,11 @@ from kernel.types.vaults import (
VaultItemEvent,
VaultItemOperationResponse,
VaultPaymentMethod,
+ VaultWebmcpBinding,
WalletVaultItemSpec,
WalletVaultItemState,
+ WebmcpInvokeVaultItemOperationRequest,
+ WebmcpInvokeVaultItemOperationResult,
ItemListResponse,
ItemEventsResponse,
)
diff --git a/src/kernel/resources/vaults/items.py b/src/kernel/resources/vaults/items.py
index 31aefa82..433551aa 100644
--- a/src/kernel/resources/vaults/items.py
+++ b/src/kernel/resources/vaults/items.py
@@ -2,7 +2,7 @@
from __future__ import annotations
-from typing import Any, List, Union, Iterable, cast
+from typing import Any, Dict, List, Union, Iterable, cast
from datetime import datetime
from typing_extensions import Literal, overload
@@ -31,6 +31,7 @@
from ...types.vaults.item_events_response import ItemEventsResponse
from ...types.vaults.vault_fill_field_param import VaultFillFieldParam
from ...types.vaults.card_vault_item_spec_param import CardVaultItemSpecParam
+from ...types.vaults.vault_webmcp_binding_param import VaultWebmcpBindingParam
from ...types.vaults.vault_checkout_context_param import VaultCheckoutContextParam
from ...types.vaults.vault_item_operation_response import VaultItemOperationResponse
from ...types.vaults.credential_vault_item_spec_input_param import CredentialVaultItemSpecInputParam
@@ -857,6 +858,73 @@ def perform_operation(
"""
...
+ @overload
+ def perform_operation(
+ self,
+ key: str,
+ *,
+ id_or_name: str,
+ bindings: Iterable[VaultWebmcpBindingParam],
+ browser_id: str,
+ input: Dict[str, object],
+ page_url: str,
+ tool_ref: str,
+ type: Literal["webmcp_invoke"],
+ timeout_sec: int | Omit = omit,
+ # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
+ # The extra values given here take precedence over values defined on the client or passed to this method.
+ extra_headers: Headers | None = None,
+ extra_query: Query | None = None,
+ extra_body: Body | None = None,
+ timeout: float | httpx.Timeout | None | NotGiven = not_given,
+ ) -> VaultItemOperationResponse:
+ """
+ Retrieve the item first and invoke only an operation listed in
+ `available_operations`, following its natural-language description. Availability
+ is rechecked at execution time; unavailable operations return 409. Authorization
+ and preparation may call an external provider and return updated state. Link
+ cards advertise authorize without checkout context. Eligible unused AgentCard
+ cards advertise prepare_checkout, which requires checkout context and obtains
+ device approval before native Square Pay. Keep the returned approval page open,
+ poll until ready_to_submit, then submit before preparation.expires_at. Unused
+ preparations expire automatically and cannot be reused. If spend-request
+ creation is rejected with a non-retryable provider error, the card item is
+ deleted and the provider's error code and message are returned. Rate limits
+ return HTTP 429 and retain the card item; stop, back off, and retry the same
+ authorize operation.
+
+ Fill returns a value-free execution result. Validation failures before writing
+ return 400 (invalid request or targets), 403 (access or destination denied), 404
+ (resource not found), or 409 (item or browser not ready). Once writing starts,
+ known partial failures and indeterminate field outcomes return 200 with status
+ `failed` or `unknown`, not an automatic-retry signal. A transport error may
+ leave the outcome unknown; do not automatically retry.
+
+ Args:
+ browser_id: Browser session ID, not a reusable browser name.
+
+ input: Public tool arguments with an existing null slot at each binding path. At most
+ 64 KiB after JSON serialization, including substituted values. Never include
+ vault values here.
+
+ page_url: Exact top-level URL from the discovered tool source (fragment omitted). This
+ pins the target page; it does not authorize a destination.
+
+ tool_ref: Opaque reference to the exact live WebMCP registration.
+
+ timeout_sec: Tool invocation timeout in seconds; preflight and response handling have an
+ additional bounded allowance. An indeterminate outcome is not retried.
+
+ 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
+ """
+ ...
+
@required_args(
["id_or_name", "type"],
["id_or_name", "checkout", "type"],
@@ -864,6 +932,7 @@ def perform_operation(
["id_or_name", "browser_id", "type"],
["id_or_name", "browser_id", "page_url", "type"],
["id_or_name", "access_token", "type"],
+ ["id_or_name", "bindings", "browser_id", "input", "page_url", "tool_ref", "type"],
)
def perform_operation(
self,
@@ -878,7 +947,8 @@ def perform_operation(
| Literal["1pw_access_request_status"]
| Literal["1pw_fill"]
| Literal["1pw_recover"]
- | Literal["1pw_update_access_token"],
+ | Literal["1pw_update_access_token"]
+ | Literal["webmcp_invoke"],
checkout: VaultCheckoutContextParam | Omit = omit,
browser_id: str | Omit = omit,
fields: Iterable[VaultFillFieldParam] | Omit = omit,
@@ -891,6 +961,10 @@ def perform_operation(
entry_id: str | Omit = omit,
access_token: str | Omit = omit,
access_token_expires_at: Union[str, datetime] | Omit = omit,
+ bindings: Iterable[VaultWebmcpBindingParam] | Omit = omit,
+ input: Dict[str, object] | Omit = omit,
+ tool_ref: str | Omit = omit,
+ timeout_sec: int | Omit = omit,
# Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
# The extra values given here take precedence over values defined on the client or passed to this method.
extra_headers: Headers | None = None,
@@ -921,6 +995,10 @@ def perform_operation(
"entry_id": entry_id,
"access_token": access_token,
"access_token_expires_at": access_token_expires_at,
+ "bindings": bindings,
+ "input": input,
+ "tool_ref": tool_ref,
+ "timeout_sec": timeout_sec,
},
item_perform_operation_params.ItemPerformOperationParams,
),
@@ -1954,6 +2032,73 @@ async def perform_operation(
"""
...
+ @overload
+ async def perform_operation(
+ self,
+ key: str,
+ *,
+ id_or_name: str,
+ bindings: Iterable[VaultWebmcpBindingParam],
+ browser_id: str,
+ input: Dict[str, object],
+ page_url: str,
+ tool_ref: str,
+ type: Literal["webmcp_invoke"],
+ timeout_sec: int | Omit = omit,
+ # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
+ # The extra values given here take precedence over values defined on the client or passed to this method.
+ extra_headers: Headers | None = None,
+ extra_query: Query | None = None,
+ extra_body: Body | None = None,
+ timeout: float | httpx.Timeout | None | NotGiven = not_given,
+ ) -> VaultItemOperationResponse:
+ """
+ Retrieve the item first and invoke only an operation listed in
+ `available_operations`, following its natural-language description. Availability
+ is rechecked at execution time; unavailable operations return 409. Authorization
+ and preparation may call an external provider and return updated state. Link
+ cards advertise authorize without checkout context. Eligible unused AgentCard
+ cards advertise prepare_checkout, which requires checkout context and obtains
+ device approval before native Square Pay. Keep the returned approval page open,
+ poll until ready_to_submit, then submit before preparation.expires_at. Unused
+ preparations expire automatically and cannot be reused. If spend-request
+ creation is rejected with a non-retryable provider error, the card item is
+ deleted and the provider's error code and message are returned. Rate limits
+ return HTTP 429 and retain the card item; stop, back off, and retry the same
+ authorize operation.
+
+ Fill returns a value-free execution result. Validation failures before writing
+ return 400 (invalid request or targets), 403 (access or destination denied), 404
+ (resource not found), or 409 (item or browser not ready). Once writing starts,
+ known partial failures and indeterminate field outcomes return 200 with status
+ `failed` or `unknown`, not an automatic-retry signal. A transport error may
+ leave the outcome unknown; do not automatically retry.
+
+ Args:
+ browser_id: Browser session ID, not a reusable browser name.
+
+ input: Public tool arguments with an existing null slot at each binding path. At most
+ 64 KiB after JSON serialization, including substituted values. Never include
+ vault values here.
+
+ page_url: Exact top-level URL from the discovered tool source (fragment omitted). This
+ pins the target page; it does not authorize a destination.
+
+ tool_ref: Opaque reference to the exact live WebMCP registration.
+
+ timeout_sec: Tool invocation timeout in seconds; preflight and response handling have an
+ additional bounded allowance. An indeterminate outcome is not retried.
+
+ 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
+ """
+ ...
+
@required_args(
["id_or_name", "type"],
["id_or_name", "checkout", "type"],
@@ -1961,6 +2106,7 @@ async def perform_operation(
["id_or_name", "browser_id", "type"],
["id_or_name", "browser_id", "page_url", "type"],
["id_or_name", "access_token", "type"],
+ ["id_or_name", "bindings", "browser_id", "input", "page_url", "tool_ref", "type"],
)
async def perform_operation(
self,
@@ -1975,7 +2121,8 @@ async def perform_operation(
| Literal["1pw_access_request_status"]
| Literal["1pw_fill"]
| Literal["1pw_recover"]
- | Literal["1pw_update_access_token"],
+ | Literal["1pw_update_access_token"]
+ | Literal["webmcp_invoke"],
checkout: VaultCheckoutContextParam | Omit = omit,
browser_id: str | Omit = omit,
fields: Iterable[VaultFillFieldParam] | Omit = omit,
@@ -1988,6 +2135,10 @@ async def perform_operation(
entry_id: str | Omit = omit,
access_token: str | Omit = omit,
access_token_expires_at: Union[str, datetime] | Omit = omit,
+ bindings: Iterable[VaultWebmcpBindingParam] | Omit = omit,
+ input: Dict[str, object] | Omit = omit,
+ tool_ref: str | Omit = omit,
+ timeout_sec: int | Omit = omit,
# Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
# The extra values given here take precedence over values defined on the client or passed to this method.
extra_headers: Headers | None = None,
@@ -2018,6 +2169,10 @@ async def perform_operation(
"entry_id": entry_id,
"access_token": access_token,
"access_token_expires_at": access_token_expires_at,
+ "bindings": bindings,
+ "input": input,
+ "tool_ref": tool_ref,
+ "timeout_sec": timeout_sec,
},
item_perform_operation_params.ItemPerformOperationParams,
),
diff --git a/src/kernel/types/vaults/__init__.py b/src/kernel/types/vaults/__init__.py
index 220eafc2..8458ebce 100644
--- a/src/kernel/types/vaults/__init__.py
+++ b/src/kernel/types/vaults/__init__.py
@@ -23,6 +23,7 @@
from .one_password_oauth_action import OnePasswordOAuthAction as OnePasswordOAuthAction
from .card_vault_item_spec_param import CardVaultItemSpecParam as CardVaultItemSpecParam
from .credential_vault_item_spec import CredentialVaultItemSpec as CredentialVaultItemSpec
+from .vault_webmcp_binding_param import VaultWebmcpBindingParam as VaultWebmcpBindingParam
from .credential_vault_field_type import CredentialVaultFieldType as CredentialVaultFieldType
from .credential_vault_item_state import CredentialVaultItemState as CredentialVaultItemState
from .agentcard_prepared_processor import AgentcardPreparedProcessor as AgentcardPreparedProcessor
@@ -60,6 +61,9 @@
from .one_password_credential_vault_item_state import (
OnePasswordCredentialVaultItemState as OnePasswordCredentialVaultItemState,
)
+from .webmcp_invoke_vault_item_operation_result import (
+ WebmcpInvokeVaultItemOperationResult as WebmcpInvokeVaultItemOperationResult,
+)
from .collect_vault_item_operation_request_param import (
CollectVaultItemOperationRequestParam as CollectVaultItemOperationRequestParam,
)
@@ -81,6 +85,9 @@
from .one_password_fill_vault_item_operation_result import (
OnePasswordFillVaultItemOperationResult as OnePasswordFillVaultItemOperationResult,
)
+from .webmcp_invoke_vault_item_operation_request_param import (
+ WebmcpInvokeVaultItemOperationRequestParam as WebmcpInvokeVaultItemOperationRequestParam,
+)
from .one_password_credential_vault_item_spec_input_param import (
OnePasswordCredentialVaultItemSpecInputParam as OnePasswordCredentialVaultItemSpecInputParam,
)
diff --git a/src/kernel/types/vaults/credential_account_vault_item.py b/src/kernel/types/vaults/credential_account_vault_item.py
index ef7555cb..109d5ce3 100644
--- a/src/kernel/types/vaults/credential_account_vault_item.py
+++ b/src/kernel/types/vaults/credential_account_vault_item.py
@@ -40,6 +40,7 @@ class AvailableOperation(BaseModel):
"1pw_fill",
"1pw_recover",
"1pw_update_access_token",
+ "webmcp_invoke",
]
diff --git a/src/kernel/types/vaults/credential_vault_item.py b/src/kernel/types/vaults/credential_vault_item.py
index 3cee5030..1ec5037e 100644
--- a/src/kernel/types/vaults/credential_vault_item.py
+++ b/src/kernel/types/vaults/credential_vault_item.py
@@ -47,6 +47,7 @@ class AvailableOperation(BaseModel):
"1pw_fill",
"1pw_recover",
"1pw_update_access_token",
+ "webmcp_invoke",
]
diff --git a/src/kernel/types/vaults/item_perform_operation_params.py b/src/kernel/types/vaults/item_perform_operation_params.py
index 8d1490b5..79871faf 100644
--- a/src/kernel/types/vaults/item_perform_operation_params.py
+++ b/src/kernel/types/vaults/item_perform_operation_params.py
@@ -2,13 +2,14 @@
from __future__ import annotations
-from typing import Union, Iterable
+from typing import Dict, Union, Iterable
from datetime import datetime
from typing_extensions import Literal, Required, Annotated, TypeAlias, TypedDict
from ..._types import SequenceNotStr
from ..._utils import PropertyInfo
from .vault_fill_field_param import VaultFillFieldParam
+from .vault_webmcp_binding_param import VaultWebmcpBindingParam
from .vault_checkout_context_param import VaultCheckoutContextParam
__all__ = [
@@ -22,6 +23,7 @@
"OnePasswordFillVaultItemOperationRequest",
"OnePasswordRecoverVaultItemOperationRequest",
"OnePasswordUpdateAccessTokenVaultItemOperationRequest",
+ "WebmcpInvokeVaultItemOperationRequest",
]
@@ -141,6 +143,39 @@ class OnePasswordUpdateAccessTokenVaultItemOperationRequest(TypedDict, total=Fal
"""Optional supplied expiry. Omit to clear the old expiry."""
+class WebmcpInvokeVaultItemOperationRequest(TypedDict, total=False):
+ id_or_name: Required[str]
+
+ bindings: Required[Iterable[VaultWebmcpBindingParam]]
+
+ browser_id: Required[str]
+ """Browser session ID, not a reusable browser name."""
+
+ input: Required[Dict[str, object]]
+ """Public tool arguments with an existing null slot at each binding path.
+
+ At most 64 KiB after JSON serialization, including substituted values. Never
+ include vault values here.
+ """
+
+ page_url: Required[str]
+ """Exact top-level URL from the discovered tool source (fragment omitted).
+
+ This pins the target page; it does not authorize a destination.
+ """
+
+ tool_ref: Required[str]
+ """Opaque reference to the exact live WebMCP registration."""
+
+ type: Required[Literal["webmcp_invoke"]]
+
+ timeout_sec: int
+ """
+ Tool invocation timeout in seconds; preflight and response handling have an
+ additional bounded allowance. An indeterminate outcome is not retried.
+ """
+
+
ItemPerformOperationParams: TypeAlias = Union[
AuthorizeVaultItemOperationRequest,
CollectVaultItemOperationRequest,
@@ -151,4 +186,5 @@ class OnePasswordUpdateAccessTokenVaultItemOperationRequest(TypedDict, total=Fal
OnePasswordFillVaultItemOperationRequest,
OnePasswordRecoverVaultItemOperationRequest,
OnePasswordUpdateAccessTokenVaultItemOperationRequest,
+ WebmcpInvokeVaultItemOperationRequest,
]
diff --git a/src/kernel/types/vaults/vault_item.py b/src/kernel/types/vaults/vault_item.py
index 7fcdfc5d..c1e8c4f3 100644
--- a/src/kernel/types/vaults/vault_item.py
+++ b/src/kernel/types/vaults/vault_item.py
@@ -55,6 +55,7 @@ class WalletVaultItemAvailableOperation(BaseModel):
"1pw_fill",
"1pw_recover",
"1pw_update_access_token",
+ "webmcp_invoke",
]
@@ -129,6 +130,7 @@ class CardVaultItemAvailableOperation(BaseModel):
"1pw_fill",
"1pw_recover",
"1pw_update_access_token",
+ "webmcp_invoke",
]
diff --git a/src/kernel/types/vaults/vault_item_operation_response.py b/src/kernel/types/vaults/vault_item_operation_response.py
index fd858295..fdc4bf99 100644
--- a/src/kernel/types/vaults/vault_item_operation_response.py
+++ b/src/kernel/types/vaults/vault_item_operation_response.py
@@ -14,6 +14,7 @@
from .wallet_vault_item_state import WalletVaultItemState
from .credential_account_vault_item import CredentialAccountVaultItem
from .fill_vault_item_operation_result import FillVaultItemOperationResult
+from .webmcp_invoke_vault_item_operation_result import WebmcpInvokeVaultItemOperationResult
from .one_password_fill_vault_item_operation_result import OnePasswordFillVaultItemOperationResult
__all__ = [
@@ -56,6 +57,7 @@ class WalletVaultItemAvailableOperation(BaseModel):
"1pw_fill",
"1pw_recover",
"1pw_update_access_token",
+ "webmcp_invoke",
]
@@ -130,6 +132,7 @@ class CardVaultItemAvailableOperation(BaseModel):
"1pw_fill",
"1pw_recover",
"1pw_update_access_token",
+ "webmcp_invoke",
]
@@ -170,4 +173,5 @@ class CardVaultItem(BaseModel):
CredentialVaultItem,
FillVaultItemOperationResult,
OnePasswordFillVaultItemOperationResult,
+ WebmcpInvokeVaultItemOperationResult,
]
diff --git a/src/kernel/types/vaults/vault_webmcp_binding_param.py b/src/kernel/types/vaults/vault_webmcp_binding_param.py
new file mode 100644
index 00000000..b7a18292
--- /dev/null
+++ b/src/kernel/types/vaults/vault_webmcp_binding_param.py
@@ -0,0 +1,28 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from __future__ import annotations
+
+from typing_extensions import Required, TypedDict
+
+__all__ = ["VaultWebmcpBindingParam"]
+
+
+class VaultWebmcpBindingParam(TypedDict, total=False):
+ field: Required[str]
+ """A declared, populated credential field or supported card field.
+
+ A TOTP field supplies a fresh code, never its seed.
+ """
+
+ input_path: Required[str]
+ """RFC 6901 JSON Pointer to an existing null value in input.
+
+ Object keys are exact; array indices must be canonical and in range. No root or
+ array-append paths. Each path and each field may occur only once.
+ """
+
+ format: str
+ """Required for card expiration (MM/YY or MM/YYYY), forbidden for other fields.
+
+ Invalid formats are rejected before invocation.
+ """
diff --git a/src/kernel/types/vaults/webmcp_invoke_vault_item_operation_request_param.py b/src/kernel/types/vaults/webmcp_invoke_vault_item_operation_request_param.py
new file mode 100644
index 00000000..21d85e6e
--- /dev/null
+++ b/src/kernel/types/vaults/webmcp_invoke_vault_item_operation_request_param.py
@@ -0,0 +1,53 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from __future__ import annotations
+
+from typing import Dict, Iterable
+from typing_extensions import Literal, Required, TypedDict
+
+from .vault_webmcp_binding_param import VaultWebmcpBindingParam
+
+__all__ = ["WebmcpInvokeVaultItemOperationRequestParam"]
+
+
+class WebmcpInvokeVaultItemOperationRequestParam(TypedDict, total=False):
+ """Invoke a WebMCP tool using values from a vaulted item.
+
+ The browser must be
+ attached to the item's vault. Discover the tool_ref, inputSchema, and source
+ with GET /browsers/{id_or_name}/webmcp/tools or webmcp.listTools() in the
+ Browser REPL (POST /browsers/{id_or_name}/repl) before invoking it.
+ Input paths replace existing null slots in input. Tool output is returned
+ without redaction and may include the supplied values. The tool may submit
+ or perform other side effects. Any item destination
+ restrictions apply to the tool's top-level page and registering frame (if any).
+ """
+
+ bindings: Required[Iterable[VaultWebmcpBindingParam]]
+
+ browser_id: Required[str]
+ """Browser session ID, not a reusable browser name."""
+
+ input: Required[Dict[str, object]]
+ """Public tool arguments with an existing null slot at each binding path.
+
+ At most 64 KiB after JSON serialization, including substituted values. Never
+ include vault values here.
+ """
+
+ page_url: Required[str]
+ """Exact top-level URL from the discovered tool source (fragment omitted).
+
+ This pins the target page; it does not authorize a destination.
+ """
+
+ tool_ref: Required[str]
+ """Opaque reference to the exact live WebMCP registration."""
+
+ type: Required[Literal["webmcp_invoke"]]
+
+ timeout_sec: int
+ """
+ Tool invocation timeout in seconds; preflight and response handling have an
+ additional bounded allowance. An indeterminate outcome is not retried.
+ """
diff --git a/src/kernel/types/vaults/webmcp_invoke_vault_item_operation_result.py b/src/kernel/types/vaults/webmcp_invoke_vault_item_operation_result.py
new file mode 100644
index 00000000..34f85ca4
--- /dev/null
+++ b/src/kernel/types/vaults/webmcp_invoke_vault_item_operation_result.py
@@ -0,0 +1,37 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from typing import Optional
+from typing_extensions import Literal
+
+from ..._models import BaseModel
+
+__all__ = ["WebmcpInvokeVaultItemOperationResult"]
+
+
+class WebmcpInvokeVaultItemOperationResult(BaseModel):
+ """
+ Returns the same tool result fields as the browser WebMCP invoke API, plus the vault operation discriminator. Output and error text are untrusted page-provided data, returned without redaction; tools may include supplied vault values. Inspect the browser page to determine whether the intended site action succeeded.
+ """
+
+ status: Literal["completed", "canceled", "error", "awaiting_submission", "unknown"]
+ """Unknown means invocation may have run; do not retry automatically.
+
+ No status confirms that the website accepted the action.
+ """
+
+ type: Literal["webmcp_invoke"]
+
+ error_text: Optional[str] = None
+ """Untrusted page-provided error text, returned without redaction.
+
+ May contain supplied vault values.
+ """
+
+ invocation_id: Optional[str] = None
+ """Present when the browser reported one."""
+
+ output: Optional[object] = None
+ """Untrusted page-provided output, returned without redaction.
+
+ May contain supplied vault values.
+ """
diff --git a/tests/api_resources/vaults/test_items.py b/tests/api_resources/vaults/test_items.py
index bbe7a151..3cb9c56f 100644
--- a/tests/api_resources/vaults/test_items.py
+++ b/tests/api_resources/vaults/test_items.py
@@ -1237,6 +1237,178 @@ def test_path_params_perform_operation_overload_9(self, client: Kernel) -> None:
type="1pw_update_access_token",
)
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ def test_method_perform_operation_overload_10(self, client: Kernel) -> None:
+ item = client.vaults.items.perform_operation(
+ key="key",
+ id_or_name="id_or_name",
+ bindings=[
+ {
+ "field": "email",
+ "input_path": "/email",
+ },
+ {
+ "field": "password",
+ "input_path": "/password",
+ },
+ ],
+ browser_id="browser-session-id",
+ input={
+ "email": "bar",
+ "password": "bar",
+ },
+ page_url="https://accounts.example.com/signin",
+ tool_ref="opaque-live-tool-ref",
+ type="webmcp_invoke",
+ )
+ assert_matches_type(VaultItemOperationResponse, item, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ def test_method_perform_operation_with_all_params_overload_10(self, client: Kernel) -> None:
+ item = client.vaults.items.perform_operation(
+ key="key",
+ id_or_name="id_or_name",
+ bindings=[
+ {
+ "field": "email",
+ "input_path": "/email",
+ "format": "format",
+ },
+ {
+ "field": "password",
+ "input_path": "/password",
+ "format": "format",
+ },
+ ],
+ browser_id="browser-session-id",
+ input={
+ "email": "bar",
+ "password": "bar",
+ },
+ page_url="https://accounts.example.com/signin",
+ tool_ref="opaque-live-tool-ref",
+ type="webmcp_invoke",
+ timeout_sec=1,
+ )
+ assert_matches_type(VaultItemOperationResponse, item, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ def test_raw_response_perform_operation_overload_10(self, client: Kernel) -> None:
+ response = client.vaults.items.with_raw_response.perform_operation(
+ key="key",
+ id_or_name="id_or_name",
+ bindings=[
+ {
+ "field": "email",
+ "input_path": "/email",
+ },
+ {
+ "field": "password",
+ "input_path": "/password",
+ },
+ ],
+ browser_id="browser-session-id",
+ input={
+ "email": "bar",
+ "password": "bar",
+ },
+ page_url="https://accounts.example.com/signin",
+ tool_ref="opaque-live-tool-ref",
+ type="webmcp_invoke",
+ )
+
+ assert response.is_closed is True
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+ item = response.parse()
+ assert_matches_type(VaultItemOperationResponse, item, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ def test_streaming_response_perform_operation_overload_10(self, client: Kernel) -> None:
+ with client.vaults.items.with_streaming_response.perform_operation(
+ key="key",
+ id_or_name="id_or_name",
+ bindings=[
+ {
+ "field": "email",
+ "input_path": "/email",
+ },
+ {
+ "field": "password",
+ "input_path": "/password",
+ },
+ ],
+ browser_id="browser-session-id",
+ input={
+ "email": "bar",
+ "password": "bar",
+ },
+ page_url="https://accounts.example.com/signin",
+ tool_ref="opaque-live-tool-ref",
+ type="webmcp_invoke",
+ ) as response:
+ assert not response.is_closed
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+
+ item = response.parse()
+ assert_matches_type(VaultItemOperationResponse, item, path=["response"])
+
+ assert cast(Any, response.is_closed) is True
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ def test_path_params_perform_operation_overload_10(self, client: Kernel) -> None:
+ with pytest.raises(ValueError, match=r"Expected a non-empty value for `id_or_name` but received ''"):
+ client.vaults.items.with_raw_response.perform_operation(
+ key="key",
+ id_or_name="",
+ bindings=[
+ {
+ "field": "email",
+ "input_path": "/email",
+ },
+ {
+ "field": "password",
+ "input_path": "/password",
+ },
+ ],
+ browser_id="browser-session-id",
+ input={
+ "email": "bar",
+ "password": "bar",
+ },
+ page_url="https://accounts.example.com/signin",
+ tool_ref="opaque-live-tool-ref",
+ type="webmcp_invoke",
+ )
+
+ with pytest.raises(ValueError, match=r"Expected a non-empty value for `key` but received ''"):
+ client.vaults.items.with_raw_response.perform_operation(
+ key="",
+ id_or_name="id_or_name",
+ bindings=[
+ {
+ "field": "email",
+ "input_path": "/email",
+ },
+ {
+ "field": "password",
+ "input_path": "/password",
+ },
+ ],
+ browser_id="browser-session-id",
+ input={
+ "email": "bar",
+ "password": "bar",
+ },
+ page_url="https://accounts.example.com/signin",
+ tool_ref="opaque-live-tool-ref",
+ type="webmcp_invoke",
+ )
+
@pytest.mark.skip(reason="Mock server tests are disabled")
@parametrize
def test_method_upsert_overload_1(self, client: Kernel) -> None:
@@ -2939,6 +3111,178 @@ async def test_path_params_perform_operation_overload_9(self, async_client: Asyn
type="1pw_update_access_token",
)
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ async def test_method_perform_operation_overload_10(self, async_client: AsyncKernel) -> None:
+ item = await async_client.vaults.items.perform_operation(
+ key="key",
+ id_or_name="id_or_name",
+ bindings=[
+ {
+ "field": "email",
+ "input_path": "/email",
+ },
+ {
+ "field": "password",
+ "input_path": "/password",
+ },
+ ],
+ browser_id="browser-session-id",
+ input={
+ "email": "bar",
+ "password": "bar",
+ },
+ page_url="https://accounts.example.com/signin",
+ tool_ref="opaque-live-tool-ref",
+ type="webmcp_invoke",
+ )
+ assert_matches_type(VaultItemOperationResponse, item, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ async def test_method_perform_operation_with_all_params_overload_10(self, async_client: AsyncKernel) -> None:
+ item = await async_client.vaults.items.perform_operation(
+ key="key",
+ id_or_name="id_or_name",
+ bindings=[
+ {
+ "field": "email",
+ "input_path": "/email",
+ "format": "format",
+ },
+ {
+ "field": "password",
+ "input_path": "/password",
+ "format": "format",
+ },
+ ],
+ browser_id="browser-session-id",
+ input={
+ "email": "bar",
+ "password": "bar",
+ },
+ page_url="https://accounts.example.com/signin",
+ tool_ref="opaque-live-tool-ref",
+ type="webmcp_invoke",
+ timeout_sec=1,
+ )
+ assert_matches_type(VaultItemOperationResponse, item, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ async def test_raw_response_perform_operation_overload_10(self, async_client: AsyncKernel) -> None:
+ response = await async_client.vaults.items.with_raw_response.perform_operation(
+ key="key",
+ id_or_name="id_or_name",
+ bindings=[
+ {
+ "field": "email",
+ "input_path": "/email",
+ },
+ {
+ "field": "password",
+ "input_path": "/password",
+ },
+ ],
+ browser_id="browser-session-id",
+ input={
+ "email": "bar",
+ "password": "bar",
+ },
+ page_url="https://accounts.example.com/signin",
+ tool_ref="opaque-live-tool-ref",
+ type="webmcp_invoke",
+ )
+
+ assert response.is_closed is True
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+ item = await response.parse()
+ assert_matches_type(VaultItemOperationResponse, item, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ async def test_streaming_response_perform_operation_overload_10(self, async_client: AsyncKernel) -> None:
+ async with async_client.vaults.items.with_streaming_response.perform_operation(
+ key="key",
+ id_or_name="id_or_name",
+ bindings=[
+ {
+ "field": "email",
+ "input_path": "/email",
+ },
+ {
+ "field": "password",
+ "input_path": "/password",
+ },
+ ],
+ browser_id="browser-session-id",
+ input={
+ "email": "bar",
+ "password": "bar",
+ },
+ page_url="https://accounts.example.com/signin",
+ tool_ref="opaque-live-tool-ref",
+ type="webmcp_invoke",
+ ) as response:
+ assert not response.is_closed
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+
+ item = await response.parse()
+ assert_matches_type(VaultItemOperationResponse, item, 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_perform_operation_overload_10(self, async_client: AsyncKernel) -> None:
+ with pytest.raises(ValueError, match=r"Expected a non-empty value for `id_or_name` but received ''"):
+ await async_client.vaults.items.with_raw_response.perform_operation(
+ key="key",
+ id_or_name="",
+ bindings=[
+ {
+ "field": "email",
+ "input_path": "/email",
+ },
+ {
+ "field": "password",
+ "input_path": "/password",
+ },
+ ],
+ browser_id="browser-session-id",
+ input={
+ "email": "bar",
+ "password": "bar",
+ },
+ page_url="https://accounts.example.com/signin",
+ tool_ref="opaque-live-tool-ref",
+ type="webmcp_invoke",
+ )
+
+ with pytest.raises(ValueError, match=r"Expected a non-empty value for `key` but received ''"):
+ await async_client.vaults.items.with_raw_response.perform_operation(
+ key="",
+ id_or_name="id_or_name",
+ bindings=[
+ {
+ "field": "email",
+ "input_path": "/email",
+ },
+ {
+ "field": "password",
+ "input_path": "/password",
+ },
+ ],
+ browser_id="browser-session-id",
+ input={
+ "email": "bar",
+ "password": "bar",
+ },
+ page_url="https://accounts.example.com/signin",
+ tool_ref="opaque-live-tool-ref",
+ type="webmcp_invoke",
+ )
+
@pytest.mark.skip(reason="Mock server tests are disabled")
@parametrize
async def test_method_upsert_overload_1(self, async_client: AsyncKernel) -> None:
From 37613408a2d9cc3324ce022aec1915082794757c Mon Sep 17 00:00:00 2001
From: "kernel-internal[bot]"
<260533166+kernel-internal[bot]@users.noreply.github.com>
Date: Fri, 2 Oct 2026 13:26:27 +0000
Subject: [PATCH 320/366] release: 0.117.0
---
.release-please-manifest.json | 2 +-
CHANGELOG.md | 7 +++++++
pyproject.toml | 2 +-
src/kernel/_version.py | 2 +-
4 files changed, 10 insertions(+), 3 deletions(-)
diff --git a/.release-please-manifest.json b/.release-please-manifest.json
index 988e843f..b6700282 100644
--- a/.release-please-manifest.json
+++ b/.release-please-manifest.json
@@ -1,3 +1,3 @@
{
- ".": "0.116.0"
+ ".": "0.117.0"
}
\ No newline at end of file
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 416063a7..378dbceb 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,12 @@
# Changelog
+## [0.117.0](https://github.com/kernel/kernel-python-sdk/compare/v0.116.0...v0.117.0) (2026-10-02)
+
+
+### Features
+
+* Invoke WebMCP tools with vault item fields ([a75eb77](https://github.com/kernel/kernel-python-sdk/commit/a75eb77bb3722e82b45836e1b9f5f6fc43dfa4ff))
+
## [0.116.0](https://github.com/kernel/kernel-python-sdk/compare/v0.115.0...v0.116.0) (2026-10-01)
diff --git a/pyproject.toml b/pyproject.toml
index 4e31e28e..e9124bb2 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -1,6 +1,6 @@
[project]
name = "kernel"
-version = "0.116.0"
+version = "0.117.0"
description = "The official Python library for the kernel API"
dynamic = ["readme"]
license = "Apache-2.0"
diff --git a/src/kernel/_version.py b/src/kernel/_version.py
index ba815a7f..f9d870e7 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.116.0" # x-release-please-version
+__version__ = "0.117.0" # x-release-please-version
From eaa17a1e7068bb70abe9f07f4bba1c818ddeeba1 Mon Sep 17 00:00:00 2001
From: "kernel-internal[bot]"
<260533166+kernel-internal[bot]@users.noreply.github.com>
Date: Fri, 2 Oct 2026 18:51:54 +0000
Subject: [PATCH 321/366] feat: Add a query filter to the vault list endpoint
Stainless-Generated-From: 2ad89a5e5d2f5cd1b76eac4a3291e961f8894bdd
---
src/kernel/resources/vaults/vaults.py | 8 ++++++++
src/kernel/types/vault_list_params.py | 3 +++
tests/api_resources/test_vaults.py | 2 ++
3 files changed, 13 insertions(+)
diff --git a/src/kernel/resources/vaults/vaults.py b/src/kernel/resources/vaults/vaults.py
index 16619462..4f3003b9 100644
--- a/src/kernel/resources/vaults/vaults.py
+++ b/src/kernel/resources/vaults/vaults.py
@@ -92,6 +92,7 @@ def list(
*,
limit: int | Omit = omit,
offset: int | Omit = omit,
+ query: 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,
@@ -103,6 +104,8 @@ def list(
List vaults in the current project
Args:
+ query: Case-insensitive substring match against vault name. IDs match by exact value.
+
extra_headers: Send extra headers
extra_query: Add additional query parameters to the request
@@ -123,6 +126,7 @@ def list(
{
"limit": limit,
"offset": offset,
+ "query": query,
},
vault_list_params.VaultListParams,
),
@@ -266,6 +270,7 @@ def list(
*,
limit: int | Omit = omit,
offset: int | Omit = omit,
+ query: 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,
@@ -277,6 +282,8 @@ def list(
List vaults in the current project
Args:
+ query: Case-insensitive substring match against vault name. IDs match by exact value.
+
extra_headers: Send extra headers
extra_query: Add additional query parameters to the request
@@ -297,6 +304,7 @@ def list(
{
"limit": limit,
"offset": offset,
+ "query": query,
},
vault_list_params.VaultListParams,
),
diff --git a/src/kernel/types/vault_list_params.py b/src/kernel/types/vault_list_params.py
index 7530bc28..7a0848df 100644
--- a/src/kernel/types/vault_list_params.py
+++ b/src/kernel/types/vault_list_params.py
@@ -11,3 +11,6 @@ class VaultListParams(TypedDict, total=False):
limit: int
offset: int
+
+ query: str
+ """Case-insensitive substring match against vault name. IDs match by exact value."""
diff --git a/tests/api_resources/test_vaults.py b/tests/api_resources/test_vaults.py
index f9b36c73..5d11ac98 100644
--- a/tests/api_resources/test_vaults.py
+++ b/tests/api_resources/test_vaults.py
@@ -72,6 +72,7 @@ def test_method_list_with_all_params(self, client: Kernel) -> None:
vault = client.vaults.list(
limit=1,
offset=0,
+ query="query",
)
assert_matches_type(SyncOffsetPagination[Vault], vault, path=["response"])
@@ -233,6 +234,7 @@ async def test_method_list_with_all_params(self, async_client: AsyncKernel) -> N
vault = await async_client.vaults.list(
limit=1,
offset=0,
+ query="query",
)
assert_matches_type(AsyncOffsetPagination[Vault], vault, path=["response"])
From 04d839e6bb948049d3683380efeaae5a3732c959 Mon Sep 17 00:00:00 2001
From: "kernel-internal[bot]"
<260533166+kernel-internal[bot]@users.noreply.github.com>
Date: Fri, 2 Oct 2026 19:55:46 +0000
Subject: [PATCH 322/366] feat: Add Kernel wallets backed by VGS agentic
network tokens, with hosted card capture
Stainless-Generated-From: 1b0aa5849d3ee8fe7ca69bec3d598286918fc571
---
api.md | 4 ++
src/kernel/resources/vaults/items.py | 22 ++++---
src/kernel/resources/vaults/vaults.py | 16 +++--
src/kernel/types/vaults/__init__.py | 6 ++
...rize_vault_item_operation_request_param.py | 2 +-
.../types/vaults/card_vault_item_spec.py | 4 +-
.../vaults/card_vault_item_spec_param.py | 6 +-
.../types/vaults/card_vault_item_state.py | 9 ++-
...fill_vault_item_operation_request_param.py | 6 +-
src/kernel/types/vaults/item_upsert_params.py | 5 +-
src/kernel/types/vaults/kernel_card_state.py | 60 +++++++++++++++++++
.../vaults/kernel_card_vault_item_spec.py | 37 ++++++++++++
.../kernel_card_vault_item_spec_param.py | 37 ++++++++++++
.../types/vaults/kernel_wallet_state.py | 23 +++++++
.../vaults/kernel_wallet_vault_item_spec.py | 15 +++++
.../kernel_wallet_vault_item_spec_param.py | 15 +++++
.../types/vaults/wallet_vault_item_spec.py | 4 +-
.../types/vaults/wallet_vault_item_state.py | 3 +-
18 files changed, 250 insertions(+), 24 deletions(-)
create mode 100644 src/kernel/types/vaults/kernel_card_state.py
create mode 100644 src/kernel/types/vaults/kernel_card_vault_item_spec.py
create mode 100644 src/kernel/types/vaults/kernel_card_vault_item_spec_param.py
create mode 100644 src/kernel/types/vaults/kernel_wallet_state.py
create mode 100644 src/kernel/types/vaults/kernel_wallet_vault_item_spec.py
create mode 100644 src/kernel/types/vaults/kernel_wallet_vault_item_spec_param.py
diff --git a/api.md b/api.md
index 90bbc16d..a043d74d 100644
--- a/api.md
+++ b/api.md
@@ -572,9 +572,13 @@ from kernel.types.vaults import (
CredentialVaultItemUpdateRequest,
FillVaultItemOperationRequest,
FillVaultItemOperationResult,
+ KernelCardState,
+ KernelCardVaultItemSpec,
KernelCredentialVaultItemSpec,
KernelCredentialVaultItemSpecInput,
KernelCredentialVaultItemState,
+ KernelWalletState,
+ KernelWalletVaultItemSpec,
OnePasswordCredentialAccountSpec,
OnePasswordCredentialAccountState,
OnePasswordCredentialVaultItemSpec,
diff --git a/src/kernel/resources/vaults/items.py b/src/kernel/resources/vaults/items.py
index 433551aa..ffda784c 100644
--- a/src/kernel/resources/vaults/items.py
+++ b/src/kernel/resources/vaults/items.py
@@ -309,10 +309,13 @@ def delete(
) -> None:
"""
Unresolved payment operations normally block deletion, including operations on
- child cards of a wallet. An AgentCard card in recovery_required whose checkout
- create response returned no authorization ID may be explicitly abandoned by
- deleting that card directly; deleting its wallet or vault remains blocked.
- Deleting or recreating an item is not proof that a payment did not occur.
+ child cards of a wallet. Deleting a connected Kernel wallet first blocks new
+ payments on it, then removes its enrolled card. If that fails, the wallet is
+ kept and keeps refusing payments; retry the deletion. An AgentCard card in
+ recovery_required whose checkout create response returned no authorization ID
+ may be explicitly abandoned by deleting that card directly; deleting its wallet
+ or vault remains blocked. Deleting or recreating an item is not proof that a
+ payment did not occur.
Args:
extra_headers: Send extra headers
@@ -1483,10 +1486,13 @@ async def delete(
) -> None:
"""
Unresolved payment operations normally block deletion, including operations on
- child cards of a wallet. An AgentCard card in recovery_required whose checkout
- create response returned no authorization ID may be explicitly abandoned by
- deleting that card directly; deleting its wallet or vault remains blocked.
- Deleting or recreating an item is not proof that a payment did not occur.
+ child cards of a wallet. Deleting a connected Kernel wallet first blocks new
+ payments on it, then removes its enrolled card. If that fails, the wallet is
+ kept and keeps refusing payments; retry the deletion. An AgentCard card in
+ recovery_required whose checkout create response returned no authorization ID
+ may be explicitly abandoned by deleting that card directly; deleting its wallet
+ or vault remains blocked. Deleting or recreating an item is not proof that a
+ payment did not occur.
Args:
extra_headers: Send extra headers
diff --git a/src/kernel/resources/vaults/vaults.py b/src/kernel/resources/vaults/vaults.py
index 4f3003b9..a8b5446f 100644
--- a/src/kernel/resources/vaults/vaults.py
+++ b/src/kernel/resources/vaults/vaults.py
@@ -147,9 +147,11 @@ def delete(
) -> None:
"""Unresolved payment operations block deletion.
- Reconcile the original attempt
- with the provider or support first; deleting or recreating an item is not proof
- that a payment did not occur.
+ Deleting a connected Kernel wallet
+ first blocks new payments on it, then removes its enrolled card. If that fails,
+ the wallet is kept and keeps refusing payments; retry the deletion. Reconcile
+ the original attempt with the provider or support first; deleting or recreating
+ an item is not proof that a payment did not occur.
Args:
extra_headers: Send extra headers
@@ -325,9 +327,11 @@ async def delete(
) -> None:
"""Unresolved payment operations block deletion.
- Reconcile the original attempt
- with the provider or support first; deleting or recreating an item is not proof
- that a payment did not occur.
+ Deleting a connected Kernel wallet
+ first blocks new payments on it, then removes its enrolled card. If that fails,
+ the wallet is kept and keeps refusing payments; retry the deletion. Reconcile
+ the original attempt with the provider or support first; deleting or recreating
+ an item is not proof that a payment did not occur.
Args:
extra_headers: Send extra headers
diff --git a/src/kernel/types/vaults/__init__.py b/src/kernel/types/vaults/__init__.py
index 8458ebce..75f523f0 100644
--- a/src/kernel/types/vaults/__init__.py
+++ b/src/kernel/types/vaults/__init__.py
@@ -4,12 +4,14 @@
from .vault_item import VaultItem as VaultItem
from .vault_item_event import VaultItemEvent as VaultItemEvent
+from .kernel_card_state import KernelCardState as KernelCardState
from .vault_item_action import VaultItemAction as VaultItemAction
from .item_events_params import ItemEventsParams as ItemEventsParams
from .item_list_response import ItemListResponse as ItemListResponse
from .item_update_params import ItemUpdateParams as ItemUpdateParams
from .item_upsert_params import ItemUpsertParams as ItemUpsertParams
from .vault_card_aliases import VaultCardAliases as VaultCardAliases
+from .kernel_wallet_state import KernelWalletState as KernelWalletState
from .card_vault_item_spec import CardVaultItemSpec as CardVaultItemSpec
from .item_events_response import ItemEventsResponse as ItemEventsResponse
from .item_retrieve_params import ItemRetrieveParams as ItemRetrieveParams
@@ -26,22 +28,26 @@
from .vault_webmcp_binding_param import VaultWebmcpBindingParam as VaultWebmcpBindingParam
from .credential_vault_field_type import CredentialVaultFieldType as CredentialVaultFieldType
from .credential_vault_item_state import CredentialVaultItemState as CredentialVaultItemState
+from .kernel_card_vault_item_spec import KernelCardVaultItemSpec as KernelCardVaultItemSpec
from .agentcard_prepared_processor import AgentcardPreparedProcessor as AgentcardPreparedProcessor
from .credential_collection_action import CredentialCollectionAction as CredentialCollectionAction
from .credential_vault_field_state import CredentialVaultFieldState as CredentialVaultFieldState
from .vault_checkout_context_param import VaultCheckoutContextParam as VaultCheckoutContextParam
from .credential_account_vault_item import CredentialAccountVaultItem as CredentialAccountVaultItem
from .item_perform_operation_params import ItemPerformOperationParams as ItemPerformOperationParams
+from .kernel_wallet_vault_item_spec import KernelWalletVaultItemSpec as KernelWalletVaultItemSpec
from .vault_item_operation_response import VaultItemOperationResponse as VaultItemOperationResponse
from .agentcard_checkout_preparation import AgentcardCheckoutPreparation as AgentcardCheckoutPreparation
from .agentcard_checkout_authorization import AgentcardCheckoutAuthorization as AgentcardCheckoutAuthorization
from .fill_vault_item_operation_result import FillVaultItemOperationResult as FillVaultItemOperationResult
from .credential_vault_field_definition import CredentialVaultFieldDefinition as CredentialVaultFieldDefinition
+from .kernel_card_vault_item_spec_param import KernelCardVaultItemSpecParam as KernelCardVaultItemSpecParam
from .kernel_credential_vault_item_spec import KernelCredentialVaultItemSpec as KernelCredentialVaultItemSpec
from .credential_vault_field_input_param import CredentialVaultFieldInputParam as CredentialVaultFieldInputParam
from .kernel_credential_vault_item_state import KernelCredentialVaultItemState as KernelCredentialVaultItemState
from .credential_vault_field_update_param import CredentialVaultFieldUpdateParam as CredentialVaultFieldUpdateParam
from .credential_vault_item_request_param import CredentialVaultItemRequestParam as CredentialVaultItemRequestParam
+from .kernel_wallet_vault_item_spec_param import KernelWalletVaultItemSpecParam as KernelWalletVaultItemSpecParam
from .one_password_credential_account_spec import OnePasswordCredentialAccountSpec as OnePasswordCredentialAccountSpec
from .one_password_credential_account_state import (
OnePasswordCredentialAccountState as OnePasswordCredentialAccountState,
diff --git a/src/kernel/types/vaults/authorize_vault_item_operation_request_param.py b/src/kernel/types/vaults/authorize_vault_item_operation_request_param.py
index 10ba11bc..d267b310 100644
--- a/src/kernel/types/vaults/authorize_vault_item_operation_request_param.py
+++ b/src/kernel/types/vaults/authorize_vault_item_operation_request_param.py
@@ -8,7 +8,7 @@
class AuthorizeVaultItemOperationRequestParam(TypedDict, total=False):
- """Authorize a Link card using its existing purchase specification.
+ """Authorize a Link or Kernel card using its existing purchase specification.
Use only after explicit user approval and when the item advertises authorize. Do not automatically retry provider failures or indeterminate outcomes. Checkout context is not accepted.
"""
diff --git a/src/kernel/types/vaults/card_vault_item_spec.py b/src/kernel/types/vaults/card_vault_item_spec.py
index 880ce111..61f8fff4 100644
--- a/src/kernel/types/vaults/card_vault_item_spec.py
+++ b/src/kernel/types/vaults/card_vault_item_spec.py
@@ -5,6 +5,7 @@
from ..._utils import PropertyInfo
from ..._models import BaseModel
+from .kernel_card_vault_item_spec import KernelCardVaultItemSpec
__all__ = [
"CardVaultItemSpec",
@@ -133,5 +134,6 @@ class AgentCardCardVaultItemSpec(BaseModel):
CardVaultItemSpec: TypeAlias = Annotated[
- Union[LinkCardVaultItemSpec, AgentCardCardVaultItemSpec], PropertyInfo(discriminator="provider")
+ Union[LinkCardVaultItemSpec, AgentCardCardVaultItemSpec, KernelCardVaultItemSpec],
+ PropertyInfo(discriminator="provider"),
]
diff --git a/src/kernel/types/vaults/card_vault_item_spec_param.py b/src/kernel/types/vaults/card_vault_item_spec_param.py
index 3f27771a..635bcdc1 100644
--- a/src/kernel/types/vaults/card_vault_item_spec_param.py
+++ b/src/kernel/types/vaults/card_vault_item_spec_param.py
@@ -5,6 +5,8 @@
from typing import Dict, Union, Iterable
from typing_extensions import Literal, Required, TypeAlias, TypedDict
+from .kernel_card_vault_item_spec_param import KernelCardVaultItemSpecParam
+
__all__ = [
"CardVaultItemSpecParam",
"LinkCardVaultItemSpec",
@@ -131,4 +133,6 @@ class AgentCardCardVaultItemSpec(TypedDict, total=False):
"""
-CardVaultItemSpecParam: TypeAlias = Union[LinkCardVaultItemSpec, AgentCardCardVaultItemSpec]
+CardVaultItemSpecParam: TypeAlias = Union[
+ LinkCardVaultItemSpec, AgentCardCardVaultItemSpec, KernelCardVaultItemSpecParam
+]
diff --git a/src/kernel/types/vaults/card_vault_item_state.py b/src/kernel/types/vaults/card_vault_item_state.py
index df7a4306..cead6d53 100644
--- a/src/kernel/types/vaults/card_vault_item_state.py
+++ b/src/kernel/types/vaults/card_vault_item_state.py
@@ -7,6 +7,7 @@
from ..._utils import PropertyInfo
from ..._models import BaseModel
+from .kernel_card_state import KernelCardState
from .vault_card_aliases import VaultCardAliases
from .agentcard_checkout_preparation import AgentcardCheckoutPreparation
from .agentcard_checkout_authorization import AgentcardCheckoutAuthorization
@@ -19,6 +20,9 @@ class LinkCardStateMasks(BaseModel):
last4: Optional[str] = None
+ token_last4: Optional[str] = None
+ """Last four digits of the network token presented to the merchant."""
+
if TYPE_CHECKING:
# Some versions of Pydantic <2.8.0 have a bug and don’t allow assigning a
# value to this field, so for compatibility we avoid doing it at runtime.
@@ -64,6 +68,9 @@ class AgentCardCardStateMasks(BaseModel):
last4: Optional[str] = None
+ token_last4: Optional[str] = None
+ """Last four digits of the network token presented to the merchant."""
+
if TYPE_CHECKING:
# Some versions of Pydantic <2.8.0 have a bug and don’t allow assigning a
# value to this field, so for compatibility we avoid doing it at runtime.
@@ -127,5 +134,5 @@ class AgentCardCardState(BaseModel):
CardVaultItemState: TypeAlias = Annotated[
- Union[LinkCardState, AgentCardCardState], PropertyInfo(discriminator="provider")
+ Union[LinkCardState, AgentCardCardState, KernelCardState], PropertyInfo(discriminator="provider")
]
diff --git a/src/kernel/types/vaults/fill_vault_item_operation_request_param.py b/src/kernel/types/vaults/fill_vault_item_operation_request_param.py
index 012ec2b9..da288f04 100644
--- a/src/kernel/types/vaults/fill_vault_item_operation_request_param.py
+++ b/src/kernel/types/vaults/fill_vault_item_operation_request_param.py
@@ -12,8 +12,8 @@
class FillVaultItemOperationRequestParam(TypedDict, total=False):
"""
- Fill selected fields from one ready credential or ready, unexpired Link card
- into a browser linked to its vault.
+ Fill selected fields from one ready credential or ready, unexpired Link or
+ Kernel card into a browser linked to its vault.
Only invoke when the item advertises `fill`. Browser and vault must belong
to the same project. Kernel checks access and allowed destinations before
filling; providing a page URL does not authorize a destination.
@@ -34,7 +34,7 @@ class FillVaultItemOperationRequestParam(TypedDict, total=False):
Fill in request order and stop on the first failure. This operation is
not atomic: previously filled fields are not rolled back. Never submit
the form or click buttons, though input/change events may trigger site
- behavior. Link cards use fill for browser checkout and do not expose
+ behavior. Link and Kernel cards use fill for browser checkout and do not expose
aliases or support egress substitution. Do not automatically retry a
failed or indeterminate operation.
diff --git a/src/kernel/types/vaults/item_upsert_params.py b/src/kernel/types/vaults/item_upsert_params.py
index fc9acbdf..e77fbabf 100644
--- a/src/kernel/types/vaults/item_upsert_params.py
+++ b/src/kernel/types/vaults/item_upsert_params.py
@@ -6,6 +6,7 @@
from typing_extensions import Literal, Required, TypeAlias, TypedDict
from .card_vault_item_spec_param import CardVaultItemSpecParam
+from .kernel_wallet_vault_item_spec_param import KernelWalletVaultItemSpecParam
from .credential_vault_item_spec_input_param import CredentialVaultItemSpecInputParam
from .one_password_credential_account_spec_param import OnePasswordCredentialAccountSpecParam
@@ -176,7 +177,9 @@ class WalletVaultItemRequestSpecAgentCardWalletVaultItemSpec(TypedDict, total=Fa
WalletVaultItemRequestSpec: TypeAlias = Union[
- WalletVaultItemRequestSpecLinkWalletVaultItemRequestSpec, WalletVaultItemRequestSpecAgentCardWalletVaultItemSpec
+ WalletVaultItemRequestSpecLinkWalletVaultItemRequestSpec,
+ WalletVaultItemRequestSpecAgentCardWalletVaultItemSpec,
+ KernelWalletVaultItemSpecParam,
]
diff --git a/src/kernel/types/vaults/kernel_card_state.py b/src/kernel/types/vaults/kernel_card_state.py
new file mode 100644
index 00000000..526eb4b3
--- /dev/null
+++ b/src/kernel/types/vaults/kernel_card_state.py
@@ -0,0 +1,60 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from typing import TYPE_CHECKING, Dict, List, Optional
+from typing_extensions import Literal
+
+from pydantic import Field as FieldInfo
+
+from ..._models import BaseModel
+
+__all__ = ["KernelCardState", "Masks"]
+
+
+class Masks(BaseModel):
+ brand: Optional[str] = None
+
+ last4: Optional[str] = None
+
+ token_last4: Optional[str] = None
+ """Last four digits of the network token presented to the merchant."""
+
+ if TYPE_CHECKING:
+ # Some versions of Pydantic <2.8.0 have a bug and don’t allow assigning a
+ # value to this field, so for compatibility we avoid doing it at runtime.
+ __pydantic_extra__: Dict[str, str] = FieldInfo(init=False) # pyright: ignore[reportIncompatibleVariableOverride]
+
+ # Stub to indicate that arbitrary properties are accepted.
+ # To access properties that are not valid identifiers you can use `getattr`, e.g.
+ # `getattr(obj, '$type')`
+ def __getattr__(self, attr: str) -> str: ...
+ else:
+ __pydantic_extra__: Dict[str, str]
+
+
+class KernelCardState(BaseModel):
+ """
+ A ready Kernel card retains its encrypted network token and one-time code for the fill operation until the item's expires_at. Fill and submit checkout before then. Visa cards can be enrolled, but Visa purchases are not yet supported and authorize returns 400; supported Mastercard purchases need no cardholder approval. masks.last4 is the enrolled card's last four digits; masks.token_last4 is the network token's last four digits shown to the merchant. Kernel cards do not expose aliases or support egress substitution. Kernel does not observe whether the merchant charged the card.
+ """
+
+ provider: Literal["kernel"]
+
+ status: Literal[
+ "requested", "pending_authorization", "ready", "consumed", "expired", "declined", "recovery_required"
+ ]
+ """recovery_required means issuing the one-time code has an unresolved outcome.
+
+ Kernel never issues another code for the item automatically, and the item cannot
+ be deleted or replaced until the original attempt is reconciled with support.
+ When status_reason says the provider refused retrieval before acceptance, no
+ code was issued and a later read retries.
+ """
+
+ domains: Optional[List[str]] = None
+ """Informational registrable domain.
+
+ Fill is locked to merchant_url's exact origin.
+ """
+
+ masks: Optional[Masks] = None
+
+ status_reason: Optional[str] = None
diff --git a/src/kernel/types/vaults/kernel_card_vault_item_spec.py b/src/kernel/types/vaults/kernel_card_vault_item_spec.py
new file mode 100644
index 00000000..65f75f8d
--- /dev/null
+++ b/src/kernel/types/vaults/kernel_card_vault_item_spec.py
@@ -0,0 +1,37 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from typing_extensions import Literal
+
+from ..._models import BaseModel
+
+__all__ = ["KernelCardVaultItemSpec"]
+
+
+class KernelCardVaultItemSpec(BaseModel):
+ """One live purchase with a Kernel-enrolled card.
+
+ Authorization obtains an agentic network token number, expiry and one-time 3-digit code. They are stored encrypted for the fill operation, which types them only on merchant_url's origin; the merchant's own checkout submits the payment. The one-time code is valid until the item's expires_at; fill and submit checkout before then. Visa cards can be enrolled, but Visa purchases are not yet supported: authorize returns 400. Supported Mastercard purchases need no cardholder approval. Card updates are not supported; delete and create a new item instead.
+ """
+
+ amount: int
+ """
+ Integer amount in minor currency units (at most 50000), bound to the one-time
+ code.
+ """
+
+ currency: str
+ """ISO 4217 code.
+
+ Supported: aud, brl, cad, chf, czk, dkk, eur, gbp, hkd, inr, jpy, krw, mxn, nok,
+ nzd, pln, sek, sgd, usd, zar.
+ """
+
+ merchant_name: str
+
+ merchant_url: str
+ """Merchant checkout URL. Fill is allowed only on this URL's origin."""
+
+ provider: Literal["kernel"]
+
+ wallet: str
+ """Key of the Kernel wallet item whose enrolled card pays."""
diff --git a/src/kernel/types/vaults/kernel_card_vault_item_spec_param.py b/src/kernel/types/vaults/kernel_card_vault_item_spec_param.py
new file mode 100644
index 00000000..c625dfac
--- /dev/null
+++ b/src/kernel/types/vaults/kernel_card_vault_item_spec_param.py
@@ -0,0 +1,37 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from __future__ import annotations
+
+from typing_extensions import Literal, Required, TypedDict
+
+__all__ = ["KernelCardVaultItemSpecParam"]
+
+
+class KernelCardVaultItemSpecParam(TypedDict, total=False):
+ """One live purchase with a Kernel-enrolled card.
+
+ Authorization obtains an agentic network token number, expiry and one-time 3-digit code. They are stored encrypted for the fill operation, which types them only on merchant_url's origin; the merchant's own checkout submits the payment. The one-time code is valid until the item's expires_at; fill and submit checkout before then. Visa cards can be enrolled, but Visa purchases are not yet supported: authorize returns 400. Supported Mastercard purchases need no cardholder approval. Card updates are not supported; delete and create a new item instead.
+ """
+
+ amount: Required[int]
+ """
+ Integer amount in minor currency units (at most 50000), bound to the one-time
+ code.
+ """
+
+ currency: Required[str]
+ """ISO 4217 code.
+
+ Supported: aud, brl, cad, chf, czk, dkk, eur, gbp, hkd, inr, jpy, krw, mxn, nok,
+ nzd, pln, sek, sgd, usd, zar.
+ """
+
+ merchant_name: Required[str]
+
+ merchant_url: Required[str]
+ """Merchant checkout URL. Fill is allowed only on this URL's origin."""
+
+ provider: Required[Literal["kernel"]]
+
+ wallet: Required[str]
+ """Key of the Kernel wallet item whose enrolled card pays."""
diff --git a/src/kernel/types/vaults/kernel_wallet_state.py b/src/kernel/types/vaults/kernel_wallet_state.py
new file mode 100644
index 00000000..4051c709
--- /dev/null
+++ b/src/kernel/types/vaults/kernel_wallet_state.py
@@ -0,0 +1,23 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from typing import Optional
+from typing_extensions import Literal
+
+from ..._models import BaseModel
+
+__all__ = ["KernelWalletState"]
+
+
+class KernelWalletState(BaseModel):
+ provider: Literal["kernel"]
+
+ status: Literal["pending_authorization", "connected", "reconnect_required", "degraded"]
+ """pending_authorization asks the cardholder to use the card_enrollment action.
+
+ connected is ready for supported purchases. reconnect_required asks the
+ cardholder to use a new card_enrollment action after an uncertain enrollment was
+ safely removed. degraded means the enrollment outcome is unknown and the wallet
+ must be deleted before adding another card.
+ """
+
+ status_reason: Optional[str] = None
diff --git a/src/kernel/types/vaults/kernel_wallet_vault_item_spec.py b/src/kernel/types/vaults/kernel_wallet_vault_item_spec.py
new file mode 100644
index 00000000..70b95ea4
--- /dev/null
+++ b/src/kernel/types/vaults/kernel_wallet_vault_item_spec.py
@@ -0,0 +1,15 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from typing_extensions import Literal
+
+from ..._models import BaseModel
+
+__all__ = ["KernelWalletVaultItemSpec"]
+
+
+class KernelWalletVaultItemSpec(BaseModel):
+ """
+ One card Kernel enrolls for Visa or Mastercard agentic network tokens using Kernel-managed credentials. Creation returns a card_enrollment action: the cardholder enters the card and their email on a Kernel-hosted page, then Kernel enrolls the securely stored card. The card number never reaches Kernel. The connected wallet's payment_methods expansion lists the enrolled card. Visa cards can be enrolled, but Visa purchases are not yet supported: authorize returns 400.
+ """
+
+ provider: Literal["kernel"]
diff --git a/src/kernel/types/vaults/kernel_wallet_vault_item_spec_param.py b/src/kernel/types/vaults/kernel_wallet_vault_item_spec_param.py
new file mode 100644
index 00000000..df1337c5
--- /dev/null
+++ b/src/kernel/types/vaults/kernel_wallet_vault_item_spec_param.py
@@ -0,0 +1,15 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from __future__ import annotations
+
+from typing_extensions import Literal, Required, TypedDict
+
+__all__ = ["KernelWalletVaultItemSpecParam"]
+
+
+class KernelWalletVaultItemSpecParam(TypedDict, total=False):
+ """
+ One card Kernel enrolls for Visa or Mastercard agentic network tokens using Kernel-managed credentials. Creation returns a card_enrollment action: the cardholder enters the card and their email on a Kernel-hosted page, then Kernel enrolls the securely stored card. The card number never reaches Kernel. The connected wallet's payment_methods expansion lists the enrolled card. Visa cards can be enrolled, but Visa purchases are not yet supported: authorize returns 400.
+ """
+
+ provider: Required[Literal["kernel"]]
diff --git a/src/kernel/types/vaults/wallet_vault_item_spec.py b/src/kernel/types/vaults/wallet_vault_item_spec.py
index a09456a0..288fd7e3 100644
--- a/src/kernel/types/vaults/wallet_vault_item_spec.py
+++ b/src/kernel/types/vaults/wallet_vault_item_spec.py
@@ -5,6 +5,7 @@
from ..._utils import PropertyInfo
from ..._models import BaseModel
+from .kernel_wallet_vault_item_spec import KernelWalletVaultItemSpec
__all__ = [
"WalletVaultItemSpec",
@@ -95,5 +96,6 @@ class AgentCardWalletVaultItemSpec(BaseModel):
WalletVaultItemSpec: TypeAlias = Annotated[
- Union[LinkWalletVaultItemSpec, AgentCardWalletVaultItemSpec], PropertyInfo(discriminator="provider")
+ Union[LinkWalletVaultItemSpec, AgentCardWalletVaultItemSpec, KernelWalletVaultItemSpec],
+ PropertyInfo(discriminator="provider"),
]
diff --git a/src/kernel/types/vaults/wallet_vault_item_state.py b/src/kernel/types/vaults/wallet_vault_item_state.py
index 9b76f419..e91b08d8 100644
--- a/src/kernel/types/vaults/wallet_vault_item_state.py
+++ b/src/kernel/types/vaults/wallet_vault_item_state.py
@@ -5,6 +5,7 @@
from ..._utils import PropertyInfo
from ..._models import BaseModel
+from .kernel_wallet_state import KernelWalletState
__all__ = ["WalletVaultItemState", "LinkWalletState", "AgentCardWalletState"]
@@ -29,5 +30,5 @@ class AgentCardWalletState(BaseModel):
WalletVaultItemState: TypeAlias = Annotated[
- Union[LinkWalletState, AgentCardWalletState], PropertyInfo(discriminator="provider")
+ Union[LinkWalletState, AgentCardWalletState, KernelWalletState], PropertyInfo(discriminator="provider")
]
From 8415f29726a11e2436e4a45ee1c02955abe6352b Mon Sep 17 00:00:00 2001
From: "kernel-internal[bot]"
<260533166+kernel-internal[bot]@users.noreply.github.com>
Date: Fri, 2 Oct 2026 20:00:08 +0000
Subject: [PATCH 323/366] release: 0.118.0
---
.release-please-manifest.json | 2 +-
CHANGELOG.md | 8 ++++++++
pyproject.toml | 2 +-
src/kernel/_version.py | 2 +-
4 files changed, 11 insertions(+), 3 deletions(-)
diff --git a/.release-please-manifest.json b/.release-please-manifest.json
index b6700282..617f7502 100644
--- a/.release-please-manifest.json
+++ b/.release-please-manifest.json
@@ -1,3 +1,3 @@
{
- ".": "0.117.0"
+ ".": "0.118.0"
}
\ No newline at end of file
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 378dbceb..003737b4 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,13 @@
# Changelog
+## [0.118.0](https://github.com/kernel/kernel-python-sdk/compare/v0.117.0...v0.118.0) (2026-10-02)
+
+
+### Features
+
+* Add a query filter to the vault list endpoint ([eaa17a1](https://github.com/kernel/kernel-python-sdk/commit/eaa17a1e7068bb70abe9f07f4bba1c818ddeeba1))
+* Add Kernel wallets backed by VGS agentic network tokens, with hosted card capture ([04d839e](https://github.com/kernel/kernel-python-sdk/commit/04d839e6bb948049d3683380efeaae5a3732c959))
+
## [0.117.0](https://github.com/kernel/kernel-python-sdk/compare/v0.116.0...v0.117.0) (2026-10-02)
diff --git a/pyproject.toml b/pyproject.toml
index e9124bb2..6cee807d 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -1,6 +1,6 @@
[project]
name = "kernel"
-version = "0.117.0"
+version = "0.118.0"
description = "The official Python library for the kernel API"
dynamic = ["readme"]
license = "Apache-2.0"
diff --git a/src/kernel/_version.py b/src/kernel/_version.py
index f9d870e7..e2ba1a1e 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.117.0" # x-release-please-version
+__version__ = "0.118.0" # x-release-please-version
From 795d8241dfd22dc7667d7fd74c70182cc7467928 Mon Sep 17 00:00:00 2001
From: "kernel-internal[bot]"
<260533166+kernel-internal[bot]@users.noreply.github.com>
Date: Fri, 2 Oct 2026 22:47:00 +0000
Subject: [PATCH 324/366] feat: chore(stlc): seal custom-code tracking files
Stainless-Generated-From: 03862c7ecd9d15666da711700d4d19784452fe64
---
api.md | 4 ++
src/kernel/resources/vaults/items.py | 8 +++-
src/kernel/types/vaults/__init__.py | 10 +++++
.../types/vaults/credential_vault_item.py | 6 ++-
.../credential_vault_item_request_param.py | 9 ++--
.../vaults/credential_vault_item_spec.py | 4 +-
.../credential_vault_item_spec_input_param.py | 5 ++-
.../vaults/credential_vault_item_state.py | 4 +-
...dential_vault_item_update_request_param.py | 1 +
.../types/vaults/item_retrieve_params.py | 1 +
.../managed_auth_credential_vault_field.py | 20 +++++++++
...managed_auth_credential_vault_item_spec.py | 21 ++++++++++
..._credential_vault_item_spec_input_param.py | 37 +++++++++++++++++
...anaged_auth_credential_vault_item_state.py | 41 +++++++++++++++++++
.../types/vaults/vault_fill_field_param.py | 7 ++--
15 files changed, 165 insertions(+), 13 deletions(-)
create mode 100644 src/kernel/types/vaults/managed_auth_credential_vault_field.py
create mode 100644 src/kernel/types/vaults/managed_auth_credential_vault_item_spec.py
create mode 100644 src/kernel/types/vaults/managed_auth_credential_vault_item_spec_input_param.py
create mode 100644 src/kernel/types/vaults/managed_auth_credential_vault_item_state.py
diff --git a/api.md b/api.md
index a043d74d..51c1a9ea 100644
--- a/api.md
+++ b/api.md
@@ -579,6 +579,10 @@ from kernel.types.vaults import (
KernelCredentialVaultItemState,
KernelWalletState,
KernelWalletVaultItemSpec,
+ ManagedAuthCredentialVaultField,
+ ManagedAuthCredentialVaultItemSpec,
+ ManagedAuthCredentialVaultItemSpecInput,
+ ManagedAuthCredentialVaultItemState,
OnePasswordCredentialAccountSpec,
OnePasswordCredentialAccountState,
OnePasswordCredentialVaultItemSpec,
diff --git a/src/kernel/resources/vaults/items.py b/src/kernel/resources/vaults/items.py
index ffda784c..6a71734a 100644
--- a/src/kernel/resources/vaults/items.py
+++ b/src/kernel/resources/vaults/items.py
@@ -93,6 +93,7 @@ def retrieve(
approval, or credential collection. Return the current item when ready or when
the wait elapses. This does not wait for edits to an already-ready credential;
poll GET without wait and compare version to observe changes after collect.
+ Managed auth credentials are created ready, so wait returns immediately.
extra_headers: Send extra headers
@@ -315,7 +316,8 @@ def delete(
recovery_required whose checkout create response returned no authorization ID
may be explicitly abandoned by deleting that card directly; deleting its wallet
or vault remains blocked. Deleting or recreating an item is not proof that a
- payment did not occur.
+ payment did not occur. Deleting a managed auth credential item leaves the
+ connection and its saved credential unchanged.
Args:
extra_headers: Send extra headers
@@ -1270,6 +1272,7 @@ async def retrieve(
approval, or credential collection. Return the current item when ready or when
the wait elapses. This does not wait for edits to an already-ready credential;
poll GET without wait and compare version to observe changes after collect.
+ Managed auth credentials are created ready, so wait returns immediately.
extra_headers: Send extra headers
@@ -1492,7 +1495,8 @@ async def delete(
recovery_required whose checkout create response returned no authorization ID
may be explicitly abandoned by deleting that card directly; deleting its wallet
or vault remains blocked. Deleting or recreating an item is not proof that a
- payment did not occur.
+ payment did not occur. Deleting a managed auth credential item leaves the
+ connection and its saved credential unchanged.
Args:
extra_headers: Send extra headers
diff --git a/src/kernel/types/vaults/__init__.py b/src/kernel/types/vaults/__init__.py
index 75f523f0..dc84fc10 100644
--- a/src/kernel/types/vaults/__init__.py
+++ b/src/kernel/types/vaults/__init__.py
@@ -48,6 +48,7 @@
from .credential_vault_field_update_param import CredentialVaultFieldUpdateParam as CredentialVaultFieldUpdateParam
from .credential_vault_item_request_param import CredentialVaultItemRequestParam as CredentialVaultItemRequestParam
from .kernel_wallet_vault_item_spec_param import KernelWalletVaultItemSpecParam as KernelWalletVaultItemSpecParam
+from .managed_auth_credential_vault_field import ManagedAuthCredentialVaultField as ManagedAuthCredentialVaultField
from .one_password_credential_account_spec import OnePasswordCredentialAccountSpec as OnePasswordCredentialAccountSpec
from .one_password_credential_account_state import (
OnePasswordCredentialAccountState as OnePasswordCredentialAccountState,
@@ -61,9 +62,15 @@
from .fill_vault_item_operation_request_param import (
FillVaultItemOperationRequestParam as FillVaultItemOperationRequestParam,
)
+from .managed_auth_credential_vault_item_spec import (
+ ManagedAuthCredentialVaultItemSpec as ManagedAuthCredentialVaultItemSpec,
+)
from .one_password_credential_vault_item_spec import (
OnePasswordCredentialVaultItemSpec as OnePasswordCredentialVaultItemSpec,
)
+from .managed_auth_credential_vault_item_state import (
+ ManagedAuthCredentialVaultItemState as ManagedAuthCredentialVaultItemState,
+)
from .one_password_credential_vault_item_state import (
OnePasswordCredentialVaultItemState as OnePasswordCredentialVaultItemState,
)
@@ -94,6 +101,9 @@
from .webmcp_invoke_vault_item_operation_request_param import (
WebmcpInvokeVaultItemOperationRequestParam as WebmcpInvokeVaultItemOperationRequestParam,
)
+from .managed_auth_credential_vault_item_spec_input_param import (
+ ManagedAuthCredentialVaultItemSpecInputParam as ManagedAuthCredentialVaultItemSpecInputParam,
+)
from .one_password_credential_vault_item_spec_input_param import (
OnePasswordCredentialVaultItemSpecInputParam as OnePasswordCredentialVaultItemSpecInputParam,
)
diff --git a/src/kernel/types/vaults/credential_vault_item.py b/src/kernel/types/vaults/credential_vault_item.py
index 1ec5037e..2f28e21b 100644
--- a/src/kernel/types/vaults/credential_vault_item.py
+++ b/src/kernel/types/vaults/credential_vault_item.py
@@ -83,7 +83,8 @@ class CredentialVaultItem(BaseModel):
1Password credentials advertise 1pw_create_access_request until a request is
made, 1pw_access_request_status while its approval is pending, and 1pw_fill
- after access is granted.
+ after access is granted. Managed auth credentials advertise fill while ready and
+ nothing otherwise.
"""
created_at: datetime
@@ -106,7 +107,8 @@ class CredentialVaultItem(BaseModel):
version: int
"""
Starts at 1 and increments on PATCH and successful hosted submission, but not
- collection-link renewal.
+ collection-link renewal. Managed auth credentials stay at 1; changes to the
+ underlying credential are read at fill time and do not change the version.
"""
action: Optional[Action] = None
diff --git a/src/kernel/types/vaults/credential_vault_item_request_param.py b/src/kernel/types/vaults/credential_vault_item_request_param.py
index 84d03bf5..d316f7bd 100644
--- a/src/kernel/types/vaults/credential_vault_item_request_param.py
+++ b/src/kernel/types/vaults/credential_vault_item_request_param.py
@@ -20,9 +20,12 @@ class CredentialVaultItemRequestParam(TypedDict, total=False):
Kernel credentials declare fields and may enter pending_collection.
1Password credentials either reference a connected credential_account or
store a supplied access token and integration key encrypted on the item.
- They store no login values or selectors. Repeating the original creation
- request returns the current item without overwriting later state. A
- different request at the same key returns 409.
+ They store no login values or selectors.
+ Managed auth credentials reference a managed auth connection in the same
+ project that already has a saved credential, and read it at fill time;
+ they store no values.
+ Repeating the original creation request returns the current item without
+ overwriting later state. A different request at the same key returns 409.
"""
spec: Required[CredentialVaultItemSpecInputParam]
diff --git a/src/kernel/types/vaults/credential_vault_item_spec.py b/src/kernel/types/vaults/credential_vault_item_spec.py
index fe452071..14d9503b 100644
--- a/src/kernel/types/vaults/credential_vault_item_spec.py
+++ b/src/kernel/types/vaults/credential_vault_item_spec.py
@@ -5,10 +5,12 @@
from ..._utils import PropertyInfo
from .kernel_credential_vault_item_spec import KernelCredentialVaultItemSpec
+from .managed_auth_credential_vault_item_spec import ManagedAuthCredentialVaultItemSpec
from .one_password_credential_vault_item_spec import OnePasswordCredentialVaultItemSpec
__all__ = ["CredentialVaultItemSpec"]
CredentialVaultItemSpec: TypeAlias = Annotated[
- Union[KernelCredentialVaultItemSpec, OnePasswordCredentialVaultItemSpec], PropertyInfo(discriminator="provider")
+ Union[KernelCredentialVaultItemSpec, OnePasswordCredentialVaultItemSpec, ManagedAuthCredentialVaultItemSpec],
+ PropertyInfo(discriminator="provider"),
]
diff --git a/src/kernel/types/vaults/credential_vault_item_spec_input_param.py b/src/kernel/types/vaults/credential_vault_item_spec_input_param.py
index 3b36cbeb..b285d31d 100644
--- a/src/kernel/types/vaults/credential_vault_item_spec_input_param.py
+++ b/src/kernel/types/vaults/credential_vault_item_spec_input_param.py
@@ -6,10 +6,13 @@
from typing_extensions import TypeAlias
from .kernel_credential_vault_item_spec_input_param import KernelCredentialVaultItemSpecInputParam
+from .managed_auth_credential_vault_item_spec_input_param import ManagedAuthCredentialVaultItemSpecInputParam
from .one_password_credential_vault_item_spec_input_param import OnePasswordCredentialVaultItemSpecInputParam
__all__ = ["CredentialVaultItemSpecInputParam"]
CredentialVaultItemSpecInputParam: TypeAlias = Union[
- KernelCredentialVaultItemSpecInputParam, OnePasswordCredentialVaultItemSpecInputParam
+ KernelCredentialVaultItemSpecInputParam,
+ OnePasswordCredentialVaultItemSpecInputParam,
+ ManagedAuthCredentialVaultItemSpecInputParam,
]
diff --git a/src/kernel/types/vaults/credential_vault_item_state.py b/src/kernel/types/vaults/credential_vault_item_state.py
index 8c0077e0..d8564703 100644
--- a/src/kernel/types/vaults/credential_vault_item_state.py
+++ b/src/kernel/types/vaults/credential_vault_item_state.py
@@ -5,10 +5,12 @@
from ..._utils import PropertyInfo
from .kernel_credential_vault_item_state import KernelCredentialVaultItemState
+from .managed_auth_credential_vault_item_state import ManagedAuthCredentialVaultItemState
from .one_password_credential_vault_item_state import OnePasswordCredentialVaultItemState
__all__ = ["CredentialVaultItemState"]
CredentialVaultItemState: TypeAlias = Annotated[
- Union[KernelCredentialVaultItemState, OnePasswordCredentialVaultItemState], PropertyInfo(discriminator="provider")
+ Union[KernelCredentialVaultItemState, OnePasswordCredentialVaultItemState, ManagedAuthCredentialVaultItemState],
+ PropertyInfo(discriminator="provider"),
]
diff --git a/src/kernel/types/vaults/credential_vault_item_update_request_param.py b/src/kernel/types/vaults/credential_vault_item_update_request_param.py
index b76c4b8f..ca34cbc9 100644
--- a/src/kernel/types/vaults/credential_vault_item_update_request_param.py
+++ b/src/kernel/types/vaults/credential_vault_item_update_request_param.py
@@ -20,6 +20,7 @@ class CredentialVaultItemUpdateRequestParam(TypedDict, total=False):
values remain missing, return pending_collection and a fresh collection
action. Otherwise return ready without an action; collect can open the form
again without clearing values. Customer URLs have no Kernel-managed expiry.
+ 1Password and managed auth credentials return 409.
"""
spec: Required[CredentialVaultItemSpecUpdateParam]
diff --git a/src/kernel/types/vaults/item_retrieve_params.py b/src/kernel/types/vaults/item_retrieve_params.py
index d07f8167..1903a36a 100644
--- a/src/kernel/types/vaults/item_retrieve_params.py
+++ b/src/kernel/types/vaults/item_retrieve_params.py
@@ -20,4 +20,5 @@ class ItemRetrieveParams(TypedDict, total=False):
approval, or credential collection. Return the current item when ready or when
the wait elapses. This does not wait for edits to an already-ready credential;
poll GET without wait and compare version to observe changes after collect.
+ Managed auth credentials are created ready, so wait returns immediately.
"""
diff --git a/src/kernel/types/vaults/managed_auth_credential_vault_field.py b/src/kernel/types/vaults/managed_auth_credential_vault_field.py
new file mode 100644
index 00000000..5554832b
--- /dev/null
+++ b/src/kernel/types/vaults/managed_auth_credential_vault_field.py
@@ -0,0 +1,20 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from ..._models import BaseModel
+from .credential_vault_field_type import CredentialVaultFieldType
+
+__all__ = ["ManagedAuthCredentialVaultField"]
+
+
+class ManagedAuthCredentialVaultField(BaseModel):
+ type: CredentialVaultFieldType
+ """
+ Text, email, and password have form inputs; totp does not and is omitted from
+ both Kernel-hosted and customer React forms. Password and totp must be
+ sensitive. A totp value is an RFC 4648 Base32 generator seed (case-insensitive,
+ optional trailing padding), not an otpauth URI or current code. Reject invalid
+ or empty decoded seeds. Browser fill generates an RFC 6238 code at execution
+ time using HMAC-SHA1, 6 digits, and a 30-second period. Preserve leading zeros;
+ never fill the seed. Custom algorithms, digits, periods, and form enrollment are
+ unsupported.
+ """
diff --git a/src/kernel/types/vaults/managed_auth_credential_vault_item_spec.py b/src/kernel/types/vaults/managed_auth_credential_vault_item_spec.py
new file mode 100644
index 00000000..b5ec8bb2
--- /dev/null
+++ b/src/kernel/types/vaults/managed_auth_credential_vault_item_spec.py
@@ -0,0 +1,21 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from typing import Optional
+from typing_extensions import Literal
+
+from ..._models import BaseModel
+
+__all__ = ["ManagedAuthCredentialVaultItemSpec"]
+
+
+class ManagedAuthCredentialVaultItemSpec(BaseModel):
+ connection_id: str
+ """ID of the managed auth connection whose saved credential this item reads.
+
+ List connections with `GET /auth/connections`.
+ """
+
+ provider: Literal["managed_auth"]
+
+ description: Optional[str] = None
+ """Display text supplied when the item was created. Omitted when none was given."""
diff --git a/src/kernel/types/vaults/managed_auth_credential_vault_item_spec_input_param.py b/src/kernel/types/vaults/managed_auth_credential_vault_item_spec_input_param.py
new file mode 100644
index 00000000..26a08fe7
--- /dev/null
+++ b/src/kernel/types/vaults/managed_auth_credential_vault_item_spec_input_param.py
@@ -0,0 +1,37 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from __future__ import annotations
+
+from typing_extensions import Literal, Required, TypedDict
+
+__all__ = ["ManagedAuthCredentialVaultItemSpecInputParam"]
+
+
+class ManagedAuthCredentialVaultItemSpecInputParam(TypedDict, total=False):
+ """A credential backed by a managed auth connection.
+
+ The item stores no values:
+ it reads the connection's saved credential at fill time, so updates made
+ through managed auth apply immediately. The connection must be in the vault's
+ project and hold a saved Kernel credential. The check is the saved credential,
+ not connection status: a connection that needs re-authentication qualifies,
+ and an authenticated one without a saved credential does not. Returns 404 for
+ an unknown connection, and 409 when the connection is in another project, has
+ no saved credential yet, or uses an external credential provider such as
+ 1Password. No collection form is offered; managed auth collects the login.
+ """
+
+ connection_id: Required[str]
+ """ID of the managed auth connection whose saved credential this item reads.
+
+ List connections with `GET /auth/connections`.
+ """
+
+ provider: Required[Literal["managed_auth"]]
+
+ description: str
+ """Optional display text for the item, such as the site or service name.
+
+ Set at creation and cannot be changed later; repeating the request with a
+ different description returns 409. At most 16 KiB in UTF-8 bytes.
+ """
diff --git a/src/kernel/types/vaults/managed_auth_credential_vault_item_state.py b/src/kernel/types/vaults/managed_auth_credential_vault_item_state.py
new file mode 100644
index 00000000..3d66ef5e
--- /dev/null
+++ b/src/kernel/types/vaults/managed_auth_credential_vault_item_state.py
@@ -0,0 +1,41 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from typing import Dict, Optional
+from typing_extensions import Literal
+
+from ..._models import BaseModel
+from .managed_auth_credential_vault_field import ManagedAuthCredentialVaultField
+
+__all__ = ["ManagedAuthCredentialVaultItemState"]
+
+
+class ManagedAuthCredentialVaultItemState(BaseModel):
+ provider: Literal["managed_auth"]
+
+ status: Literal["ready", "unavailable"]
+ """
+ Items are created ready, which means the connection has a saved Kernel
+ credential that stores values or a TOTP seed (what has_values or has_totp_secret
+ report on credentials), not that a login succeeded. Unavailable means it no
+ longer does; such items advertise no operations.
+ """
+
+ fields: Optional[Dict[str, ManagedAuthCredentialVaultField]] = None
+ """
+ One entry per non-empty value on the connection's saved credential, keyed by the
+ field name to use in fill bindings. Included on single-item responses (create
+ and get) and omitted from list responses, like `value_keys` on credentials.
+ Empty when unavailable or when every stored value is empty. A stored value named
+ totp_secret is never listed or filled. Values are never returned. A totp entry
+ is present when the credential has a TOTP seed; fill writes a generated code for
+ it. Entries follow the connection's credential.
+ """
+
+ status_reason: Optional[Literal["connection_not_found", "no_credential", "external_credential"]] = None
+ """Present when status is unavailable.
+
+ connection_not_found means the connection was deleted. no_credential means the
+ connection's credential was deleted or emptied after the item was created.
+ external_credential means the connection was moved to an external credential
+ provider.
+ """
diff --git a/src/kernel/types/vaults/vault_fill_field_param.py b/src/kernel/types/vaults/vault_fill_field_param.py
index a7fc5255..99e0c107 100644
--- a/src/kernel/types/vaults/vault_fill_field_param.py
+++ b/src/kernel/types/vaults/vault_fill_field_param.py
@@ -9,9 +9,10 @@
class VaultFillFieldParam(TypedDict, total=False):
field: Required[str]
- """A declared credential field name or a supported card field.
-
- Unset credential fields cannot be filled.
+ """
+ A credential field name from the item's declared fields (Kernel) or state.fields
+ from a single-item read (managed auth), or a supported card field. Unset
+ credential fields cannot be filled.
"""
selector: Required[str]
From 0e7698961e82d944540e0524f0f2162d54f19163 Mon Sep 17 00:00:00 2001
From: "kernel-internal[bot]"
<260533166+kernel-internal[bot]@users.noreply.github.com>
Date: Fri, 2 Oct 2026 22:51:50 +0000
Subject: [PATCH 325/366] release: 0.119.0
---
.release-please-manifest.json | 2 +-
CHANGELOG.md | 7 +++++++
pyproject.toml | 2 +-
src/kernel/_version.py | 2 +-
4 files changed, 10 insertions(+), 3 deletions(-)
diff --git a/.release-please-manifest.json b/.release-please-manifest.json
index 617f7502..124b9841 100644
--- a/.release-please-manifest.json
+++ b/.release-please-manifest.json
@@ -1,3 +1,3 @@
{
- ".": "0.118.0"
+ ".": "0.119.0"
}
\ No newline at end of file
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 003737b4..dba5ea9f 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,12 @@
# Changelog
+## [0.119.0](https://github.com/kernel/kernel-python-sdk/compare/v0.118.0...v0.119.0) (2026-10-02)
+
+
+### Features
+
+* chore(stlc): seal custom-code tracking files ([795d824](https://github.com/kernel/kernel-python-sdk/commit/795d8241dfd22dc7667d7fd74c70182cc7467928))
+
## [0.118.0](https://github.com/kernel/kernel-python-sdk/compare/v0.117.0...v0.118.0) (2026-10-02)
diff --git a/pyproject.toml b/pyproject.toml
index 6cee807d..aa2a10b2 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -1,6 +1,6 @@
[project]
name = "kernel"
-version = "0.118.0"
+version = "0.119.0"
description = "The official Python library for the kernel API"
dynamic = ["readme"]
license = "Apache-2.0"
diff --git a/src/kernel/_version.py b/src/kernel/_version.py
index e2ba1a1e..fb178235 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.118.0" # x-release-please-version
+__version__ = "0.119.0" # x-release-please-version
From e0ee6551df5580acf44ccac954f857f1bb42cf60 Mon Sep 17 00:00:00 2001
From: Behroz Saadat
Date: Fri, 2 Oct 2026 18:03:37 -0500
Subject: [PATCH 326/366] docs: correct 0.119.0 changelog entry for managed
auth vault credentials
Co-Authored-By: Claude Opus 5.5 (1M context)
---
CHANGELOG.md | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index dba5ea9f..a874179c 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -5,7 +5,7 @@
### Features
-* chore(stlc): seal custom-code tracking files ([795d824](https://github.com/kernel/kernel-python-sdk/commit/795d8241dfd22dc7667d7fd74c70182cc7467928))
+* Add managed auth provider to vault credential items ([795d824](https://github.com/kernel/kernel-python-sdk/commit/795d8241dfd22dc7667d7fd74c70182cc7467928))
## [0.118.0](https://github.com/kernel/kernel-python-sdk/compare/v0.117.0...v0.118.0) (2026-10-02)
From 761b766f80e9ccfabf65a412bf777e5a861b400b Mon Sep 17 00:00:00 2001
From: "kernel-internal[bot]"
<260533166+kernel-internal[bot]@users.noreply.github.com>
Date: Mon, 5 Oct 2026 14:20:12 +0000
Subject: [PATCH 327/366] feat: Enable Exa highlights and default Perplexity to
medium context
Stainless-Generated-From: 5defbfe3264ec41772f12257aa4ae698ff774182
---
src/kernel/types/provider_target_param.py | 10 ++++++++--
1 file changed, 8 insertions(+), 2 deletions(-)
diff --git a/src/kernel/types/provider_target_param.py b/src/kernel/types/provider_target_param.py
index 7c3c314f..4251880f 100644
--- a/src/kernel/types/provider_target_param.py
+++ b/src/kernel/types/provider_target_param.py
@@ -99,7 +99,10 @@ class SearchExaTargetOptionsContents(TypedDict, total=False):
"""
highlights: bool
- """Return query-relevant provider excerpts."""
+ """Return query-relevant provider excerpts.
+
+ Defaults to true for Exa search requests; set false to disable.
+ """
text: bool
"""Return provider page text."""
@@ -166,7 +169,10 @@ class SearchPerplexityTargetOptions(TypedDict, total=False):
"""
search_context_size: Literal["low", "medium", "high"]
- """Provider context size supported by the selected model."""
+ """Amount of page content Perplexity extracts per result.
+
+ Defaults to medium unless max_tokens or max_tokens_per_page is set.
+ """
search_language_filter: SequenceNotStr[str]
"""ISO 639-1 language codes, max 20."""
From da49f007e5b00cdca00f08500db43bfc6332c4c3 Mon Sep 17 00:00:00 2001
From: "kernel-internal[bot]"
<260533166+kernel-internal[bot]@users.noreply.github.com>
Date: Tue, 6 Oct 2026 17:17:25 +0000
Subject: [PATCH 328/366] feat: Connect Kernel wallets once the card is stored,
with best-effort network tokens
Stainless-Generated-From: e1a019f24061298df8d1360397d091fa2b480e1e
---
src/kernel/types/vaults/kernel_wallet_state.py | 9 +++++----
src/kernel/types/vaults/kernel_wallet_vault_item_spec.py | 5 +++--
.../types/vaults/kernel_wallet_vault_item_spec_param.py | 5 +++--
3 files changed, 11 insertions(+), 8 deletions(-)
diff --git a/src/kernel/types/vaults/kernel_wallet_state.py b/src/kernel/types/vaults/kernel_wallet_state.py
index 4051c709..a35239d7 100644
--- a/src/kernel/types/vaults/kernel_wallet_state.py
+++ b/src/kernel/types/vaults/kernel_wallet_state.py
@@ -14,10 +14,11 @@ class KernelWalletState(BaseModel):
status: Literal["pending_authorization", "connected", "reconnect_required", "degraded"]
"""pending_authorization asks the cardholder to use the card_enrollment action.
- connected is ready for supported purchases. reconnect_required asks the
- cardholder to use a new card_enrollment action after an uncertain enrollment was
- safely removed. degraded means the enrollment outcome is unknown and the wallet
- must be deleted before adding another card.
+ connected means the card is stored; it can pay once payment_methods reports
+ capabilities.single_use_card.eligible. reconnect_required asks the cardholder to
+ use a new card_enrollment action after an uncertain enrollment was safely
+ removed. degraded means the enrollment outcome is unknown and the wallet must be
+ deleted before adding another card.
"""
status_reason: Optional[str] = None
diff --git a/src/kernel/types/vaults/kernel_wallet_vault_item_spec.py b/src/kernel/types/vaults/kernel_wallet_vault_item_spec.py
index 70b95ea4..a989f2ef 100644
--- a/src/kernel/types/vaults/kernel_wallet_vault_item_spec.py
+++ b/src/kernel/types/vaults/kernel_wallet_vault_item_spec.py
@@ -8,8 +8,9 @@
class KernelWalletVaultItemSpec(BaseModel):
- """
- One card Kernel enrolls for Visa or Mastercard agentic network tokens using Kernel-managed credentials. Creation returns a card_enrollment action: the cardholder enters the card and their email on a Kernel-hosted page, then Kernel enrolls the securely stored card. The card number never reaches Kernel. The connected wallet's payment_methods expansion lists the enrolled card. Visa cards can be enrolled, but Visa purchases are not yet supported: authorize returns 400.
+ """One card stored with Kernel-managed credentials.
+
+ Creation returns a card_enrollment action: the cardholder enters the card and their email on a Kernel-hosted page, and the wallet connects once the card is stored. Kernel then enrolls it for an agentic network token when the issuer supports it. The card number never reaches Kernel. The connected wallet's payment_methods expansion lists the card; capabilities.single_use_card.eligible is false, with a network_token_* reason, until the card has a network token, and authorize returns 400 for such a card. Visa cards can be enrolled, but Visa purchases are not yet supported: authorize returns 400.
"""
provider: Literal["kernel"]
diff --git a/src/kernel/types/vaults/kernel_wallet_vault_item_spec_param.py b/src/kernel/types/vaults/kernel_wallet_vault_item_spec_param.py
index df1337c5..395c4653 100644
--- a/src/kernel/types/vaults/kernel_wallet_vault_item_spec_param.py
+++ b/src/kernel/types/vaults/kernel_wallet_vault_item_spec_param.py
@@ -8,8 +8,9 @@
class KernelWalletVaultItemSpecParam(TypedDict, total=False):
- """
- One card Kernel enrolls for Visa or Mastercard agentic network tokens using Kernel-managed credentials. Creation returns a card_enrollment action: the cardholder enters the card and their email on a Kernel-hosted page, then Kernel enrolls the securely stored card. The card number never reaches Kernel. The connected wallet's payment_methods expansion lists the enrolled card. Visa cards can be enrolled, but Visa purchases are not yet supported: authorize returns 400.
+ """One card stored with Kernel-managed credentials.
+
+ Creation returns a card_enrollment action: the cardholder enters the card and their email on a Kernel-hosted page, and the wallet connects once the card is stored. Kernel then enrolls it for an agentic network token when the issuer supports it. The card number never reaches Kernel. The connected wallet's payment_methods expansion lists the card; capabilities.single_use_card.eligible is false, with a network_token_* reason, until the card has a network token, and authorize returns 400 for such a card. Visa cards can be enrolled, but Visa purchases are not yet supported: authorize returns 400.
"""
provider: Required[Literal["kernel"]]
From 3178829222a373a69fdb7617abcab02f409d2d6e Mon Sep 17 00:00:00 2001
From: "kernel-internal[bot]"
<260533166+kernel-internal[bot]@users.noreply.github.com>
Date: Tue, 6 Oct 2026 18:37:22 +0000
Subject: [PATCH 329/366] feat: Accept network.allowed_hosts on browser create
behind an org flag
Stainless-Generated-From: 4730b0210d37bd7d159874f8951d3e3b3d13ead7
---
src/kernel/types/browser_network_config.py | 23 +++++++++++++++++++
.../types/browser_network_config_param.py | 23 +++++++++++++++++++
.../browsers/browser_proxy_error_event.py | 22 ++++++++++--------
tests/api_resources/test_browser_pools.py | 4 ++++
tests/api_resources/test_browsers.py | 2 ++
5 files changed, 65 insertions(+), 9 deletions(-)
diff --git a/src/kernel/types/browser_network_config.py b/src/kernel/types/browser_network_config.py
index ec2c7fbf..453db6bd 100644
--- a/src/kernel/types/browser_network_config.py
+++ b/src/kernel/types/browser_network_config.py
@@ -31,6 +31,29 @@ class ProxyRoute(BaseModel):
class BrowserNetworkConfig(BaseModel):
"""Network configuration for a browser session or browser pool."""
+ allowed_hosts: Optional[List[str]] = None
+ """
+ Egress allowlist for a browser session: the only destinations the browser may
+ reach through Kernel-managed egress. Any other destination is refused with a 403
+ whose X-Kernel-Proxy-Error header is network_policy_denied, so pages cannot load
+ or send data to unlisted hosts, including with fetch() and WebSockets. Omit the
+ field for unfiltered egress; an empty list is invalid. The allowlist applies
+ from the browser's first request, and start_url must be allowed by it. Entries
+ are exact hostnames ("example.com"), one leading "_." wildcard that matches
+ subdomains at any depth but not the domain itself ("_.example.com" matches
+ api.example.com, not example.com), public IPv4 addresses ("8.8.8.8"), bracketed
+ public IPv6 addresses ("[2001:4860:4860::8888]"), or public CIDR ranges in
+ canonical form ("8.8.4.0/24", "2001:4860::/32"). IP and CIDR entries only match
+ destinations written as an IP address, never hostnames that resolve into the
+ range. Entries cannot include ports, paths, or schemes, and match every port on
+ their host. Wildcards over a public suffix ("_.com", "_.github.io") and private
+ or reserved IP ranges are rejected, as are entries that overlap private_hosts.
+ Enforced at Kernel's egress proxy only: destinations in private_hosts, and
+ processes in the browser VM that do not use the browser's proxy, are not
+ filtered, and Kernel's own control traffic is always allowed. Requires proxy v3.
+ Not supported on browser pools.
+ """
+
private_hosts: Optional[List[str]] = None
"""
Destinations the browser reaches directly through the session's own network
diff --git a/src/kernel/types/browser_network_config_param.py b/src/kernel/types/browser_network_config_param.py
index 298f9d13..c643b6ae 100644
--- a/src/kernel/types/browser_network_config_param.py
+++ b/src/kernel/types/browser_network_config_param.py
@@ -34,6 +34,29 @@ class ProxyRoute(TypedDict, total=False):
class BrowserNetworkConfigParam(TypedDict, total=False):
"""Network configuration for a browser session or browser pool."""
+ allowed_hosts: SequenceNotStr[str]
+ """
+ Egress allowlist for a browser session: the only destinations the browser may
+ reach through Kernel-managed egress. Any other destination is refused with a 403
+ whose X-Kernel-Proxy-Error header is network_policy_denied, so pages cannot load
+ or send data to unlisted hosts, including with fetch() and WebSockets. Omit the
+ field for unfiltered egress; an empty list is invalid. The allowlist applies
+ from the browser's first request, and start_url must be allowed by it. Entries
+ are exact hostnames ("example.com"), one leading "_." wildcard that matches
+ subdomains at any depth but not the domain itself ("_.example.com" matches
+ api.example.com, not example.com), public IPv4 addresses ("8.8.8.8"), bracketed
+ public IPv6 addresses ("[2001:4860:4860::8888]"), or public CIDR ranges in
+ canonical form ("8.8.4.0/24", "2001:4860::/32"). IP and CIDR entries only match
+ destinations written as an IP address, never hostnames that resolve into the
+ range. Entries cannot include ports, paths, or schemes, and match every port on
+ their host. Wildcards over a public suffix ("_.com", "_.github.io") and private
+ or reserved IP ranges are rejected, as are entries that overlap private_hosts.
+ Enforced at Kernel's egress proxy only: destinations in private_hosts, and
+ processes in the browser VM that do not use the browser's proxy, are not
+ filtered, and Kernel's own control traffic is always allowed. Requires proxy v3.
+ Not supported on browser pools.
+ """
+
private_hosts: SequenceNotStr[str]
"""
Destinations the browser reaches directly through the session's own network
diff --git a/src/kernel/types/browsers/browser_proxy_error_event.py b/src/kernel/types/browsers/browser_proxy_error_event.py
index 44961139..574f525d 100644
--- a/src/kernel/types/browsers/browser_proxy_error_event.py
+++ b/src/kernel/types/browsers/browser_proxy_error_event.py
@@ -19,6 +19,7 @@ class Data(BrowserEventContext):
code: Literal[
"destination_blocked",
"destination_route_unavailable",
+ "network_policy_denied",
"provider_blacklisted",
"provider_unreachable",
"provider_rejected",
@@ -33,20 +34,23 @@ class Data(BrowserEventContext):
]
"""
Proxy-layer error code: the X-Kernel-Proxy-Error response header value from a
- branded 5xx error page served by the metro egress host-proxy. Values mirror what
- the proxy emits: destination_blocked, destination_route_unavailable,
- provider_blacklisted, provider_unreachable, provider_rejected,
- origin_tls_timeout, origin_response_incomplete, proxy_unavailable,
- restricted_route_unavailable, upstream_timeout, upstream_dns_failure,
- upstream_connect_failed. A header value the browser image does not recognize is
- reported as unknown, with the header value in raw_code.
+ branded error page served by the metro egress host-proxy. Values mirror what the
+ proxy emits: destination_blocked, destination_route_unavailable,
+ network_policy_denied, provider_blacklisted, provider_unreachable,
+ provider_rejected, origin_tls_timeout, origin_response_incomplete,
+ proxy_unavailable, restricted_route_unavailable, upstream_timeout,
+ upstream_dns_failure, upstream_connect_failed. A header value the browser image
+ does not recognize is reported as unknown, with the header value in raw_code.
"""
request_id: str
"""CDP request identifier matching the originating request."""
status: int
- """HTTP response status of the branded error page (502)."""
+ """
+ HTTP response status of the branded error page (502, or 403 for
+ network_policy_denied).
+ """
method: Optional[str] = None
"""HTTP method of the failed request, when known."""
@@ -66,7 +70,7 @@ class Data(BrowserEventContext):
class BrowserProxyErrorEvent(BaseModel):
"""A branded proxy-layer failure observed by the browser.
- Emitted when the metro egress host-proxy serves a branded 5xx error page whose response carries the X-Kernel-Proxy-Error header. Low-volume and carries a typed code. Its value is per-session and per-URL attribution for sessions that already capture the network stream: proxy failures are only observable while the CDP network collector is running, so this is an opt-in refinement of the raw network events rather than a default-on alerting signal.
+ Emitted when the metro egress host-proxy serves a branded error page whose response carries the X-Kernel-Proxy-Error header: a 502 for proxy-layer failures, or a 403 when the session's network policy blocks the destination. Low-volume and carries a typed code. Its value is per-session and per-URL attribution for sessions that already capture the network stream: proxy failures are only observable while the CDP network collector is running, so this is an opt-in refinement of the raw network events rather than a default-on alerting signal.
"""
category: Literal["network"]
diff --git a/tests/api_resources/test_browser_pools.py b/tests/api_resources/test_browser_pools.py
index cd0c1b98..863c39c2 100644
--- a/tests/api_resources/test_browser_pools.py
+++ b/tests/api_resources/test_browser_pools.py
@@ -47,6 +47,7 @@ def test_method_create_with_all_params(self, client: Kernel) -> None:
memory="8GiB",
name="my-pool",
network={
+ "allowed_hosts": ["example.com", "*.example.com"],
"private_hosts": ["*.example.ts.net", "100.64.0.0/10"],
"proxy_routes": [
{
@@ -199,6 +200,7 @@ def test_method_update_with_all_params(self, client: Kernel) -> None:
memory="8GiB",
name="my-pool",
network={
+ "allowed_hosts": ["example.com", "*.example.com"],
"private_hosts": ["*.example.ts.net", "100.64.0.0/10"],
"proxy_routes": [
{
@@ -601,6 +603,7 @@ async def test_method_create_with_all_params(self, async_client: AsyncKernel) ->
memory="8GiB",
name="my-pool",
network={
+ "allowed_hosts": ["example.com", "*.example.com"],
"private_hosts": ["*.example.ts.net", "100.64.0.0/10"],
"proxy_routes": [
{
@@ -753,6 +756,7 @@ async def test_method_update_with_all_params(self, async_client: AsyncKernel) ->
memory="8GiB",
name="my-pool",
network={
+ "allowed_hosts": ["example.com", "*.example.com"],
"private_hosts": ["*.example.ts.net", "100.64.0.0/10"],
"proxy_routes": [
{
diff --git a/tests/api_resources/test_browsers.py b/tests/api_resources/test_browsers.py
index a9f0c46b..92f2149d 100644
--- a/tests/api_resources/test_browsers.py
+++ b/tests/api_resources/test_browsers.py
@@ -49,6 +49,7 @@ def test_method_create_with_all_params(self, client: Kernel) -> None:
memory="8GiB",
name="checkout-flow-1",
network={
+ "allowed_hosts": ["example.com", "*.example.com"],
"private_hosts": ["*.example.ts.net", "100.64.0.0/10"],
"proxy_routes": [
{
@@ -593,6 +594,7 @@ async def test_method_create_with_all_params(self, async_client: AsyncKernel) ->
memory="8GiB",
name="checkout-flow-1",
network={
+ "allowed_hosts": ["example.com", "*.example.com"],
"private_hosts": ["*.example.ts.net", "100.64.0.0/10"],
"proxy_routes": [
{
From fadc7c95c7fe45df163e6f186384a7d534c8c014 Mon Sep 17 00:00:00 2001
From: "kernel-internal[bot]"
<260533166+kernel-internal[bot]@users.noreply.github.com>
Date: Tue, 6 Oct 2026 19:22:49 +0000
Subject: [PATCH 330/366] feat: Add Visa passkey approval for VGS purchases
Stainless-Generated-From: cf61a9aa55fabc1ef1999557cbe0f5b07fbb3dbe
---
src/kernel/types/vaults/kernel_card_state.py | 17 ++++++++++-------
.../types/vaults/kernel_card_vault_item_spec.py | 10 +++++++++-
.../vaults/kernel_card_vault_item_spec_param.py | 9 ++++++++-
.../vaults/kernel_wallet_vault_item_spec.py | 2 +-
.../kernel_wallet_vault_item_spec_param.py | 2 +-
5 files changed, 29 insertions(+), 11 deletions(-)
diff --git a/src/kernel/types/vaults/kernel_card_state.py b/src/kernel/types/vaults/kernel_card_state.py
index 526eb4b3..6983c617 100644
--- a/src/kernel/types/vaults/kernel_card_state.py
+++ b/src/kernel/types/vaults/kernel_card_state.py
@@ -33,7 +33,7 @@ def __getattr__(self, attr: str) -> str: ...
class KernelCardState(BaseModel):
"""
- A ready Kernel card retains its encrypted network token and one-time code for the fill operation until the item's expires_at. Fill and submit checkout before then. Visa cards can be enrolled, but Visa purchases are not yet supported and authorize returns 400; supported Mastercard purchases need no cardholder approval. masks.last4 is the enrolled card's last four digits; masks.token_last4 is the network token's last four digits shown to the merchant. Kernel cards do not expose aliases or support egress substitution. Kernel does not observe whether the merchant charged the card.
+ A ready Kernel card retains its encrypted network token and one-time code for the fill operation until the item's expires_at. Fill and submit checkout before then. Visa purchases require a spend_approval action before the code is issued; Mastercard purchases need no hosted approval. masks.last4 is the enrolled card's last four digits; masks.token_last4 is the network token's last four digits shown to the merchant. Kernel cards do not expose aliases or support egress substitution. Kernel does not observe whether the merchant charged the card.
"""
provider: Literal["kernel"]
@@ -41,12 +41,15 @@ class KernelCardState(BaseModel):
status: Literal[
"requested", "pending_authorization", "ready", "consumed", "expired", "declined", "recovery_required"
]
- """recovery_required means issuing the one-time code has an unresolved outcome.
-
- Kernel never issues another code for the item automatically, and the item cannot
- be deleted or replaced until the original attempt is reconciled with support.
- When status_reason says the provider refused retrieval before acceptance, no
- code was issued and a later read retries.
+ """
+ pending_authorization on a Visa purchase waits for the cardholder to approve it
+ through the spend_approval action; an unused link expires after 30 minutes.
+ recovery_required means approving the purchase or issuing the one-time code has
+ an unresolved outcome. Kernel never approves again or issues another code for
+ the item automatically, and the item cannot be deleted or replaced until the
+ original attempt is reconciled with support. When status_reason says the
+ provider refused retrieval before acceptance, no code was issued and a later
+ read retries. declined means the card network refused to issue a code.
"""
domains: Optional[List[str]] = None
diff --git a/src/kernel/types/vaults/kernel_card_vault_item_spec.py b/src/kernel/types/vaults/kernel_card_vault_item_spec.py
index 65f75f8d..85d2d13d 100644
--- a/src/kernel/types/vaults/kernel_card_vault_item_spec.py
+++ b/src/kernel/types/vaults/kernel_card_vault_item_spec.py
@@ -1,5 +1,6 @@
# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+from typing import Optional
from typing_extensions import Literal
from ..._models import BaseModel
@@ -10,7 +11,7 @@
class KernelCardVaultItemSpec(BaseModel):
"""One live purchase with a Kernel-enrolled card.
- Authorization obtains an agentic network token number, expiry and one-time 3-digit code. They are stored encrypted for the fill operation, which types them only on merchant_url's origin; the merchant's own checkout submits the payment. The one-time code is valid until the item's expires_at; fill and submit checkout before then. Visa cards can be enrolled, but Visa purchases are not yet supported: authorize returns 400. Supported Mastercard purchases need no cardholder approval. Card updates are not supported; delete and create a new item instead.
+ Authorization obtains an agentic network token number, expiry and one-time 3-digit code. They are stored encrypted for the fill operation, which types them only on merchant_url's origin; the merchant's own checkout submits the payment. The one-time code is valid until the item's expires_at; fill and submit checkout before then. Mastercard purchases need no cardholder approval. A Visa purchase returns a spend_approval action: the cardholder approves it with a Visa passkey, and Kernel registers a Visa intent for one transaction up to the amount before issuing the code. Card updates are not supported; delete and create a new item instead.
"""
amount: int
@@ -35,3 +36,10 @@ class KernelCardVaultItemSpec(BaseModel):
wallet: str
"""Key of the Kernel wallet item whose enrolled card pays."""
+
+ merchant_country: Optional[str] = None
+ """The merchant's ISO 3166-1 alpha-2 country code.
+
+ Required for Visa cards, whose one-time code is issued for the merchant's
+ country.
+ """
diff --git a/src/kernel/types/vaults/kernel_card_vault_item_spec_param.py b/src/kernel/types/vaults/kernel_card_vault_item_spec_param.py
index c625dfac..096e8c7b 100644
--- a/src/kernel/types/vaults/kernel_card_vault_item_spec_param.py
+++ b/src/kernel/types/vaults/kernel_card_vault_item_spec_param.py
@@ -10,7 +10,7 @@
class KernelCardVaultItemSpecParam(TypedDict, total=False):
"""One live purchase with a Kernel-enrolled card.
- Authorization obtains an agentic network token number, expiry and one-time 3-digit code. They are stored encrypted for the fill operation, which types them only on merchant_url's origin; the merchant's own checkout submits the payment. The one-time code is valid until the item's expires_at; fill and submit checkout before then. Visa cards can be enrolled, but Visa purchases are not yet supported: authorize returns 400. Supported Mastercard purchases need no cardholder approval. Card updates are not supported; delete and create a new item instead.
+ Authorization obtains an agentic network token number, expiry and one-time 3-digit code. They are stored encrypted for the fill operation, which types them only on merchant_url's origin; the merchant's own checkout submits the payment. The one-time code is valid until the item's expires_at; fill and submit checkout before then. Mastercard purchases need no cardholder approval. A Visa purchase returns a spend_approval action: the cardholder approves it with a Visa passkey, and Kernel registers a Visa intent for one transaction up to the amount before issuing the code. Card updates are not supported; delete and create a new item instead.
"""
amount: Required[int]
@@ -35,3 +35,10 @@ class KernelCardVaultItemSpecParam(TypedDict, total=False):
wallet: Required[str]
"""Key of the Kernel wallet item whose enrolled card pays."""
+
+ merchant_country: str
+ """The merchant's ISO 3166-1 alpha-2 country code.
+
+ Required for Visa cards, whose one-time code is issued for the merchant's
+ country.
+ """
diff --git a/src/kernel/types/vaults/kernel_wallet_vault_item_spec.py b/src/kernel/types/vaults/kernel_wallet_vault_item_spec.py
index a989f2ef..728d54f2 100644
--- a/src/kernel/types/vaults/kernel_wallet_vault_item_spec.py
+++ b/src/kernel/types/vaults/kernel_wallet_vault_item_spec.py
@@ -10,7 +10,7 @@
class KernelWalletVaultItemSpec(BaseModel):
"""One card stored with Kernel-managed credentials.
- Creation returns a card_enrollment action: the cardholder enters the card and their email on a Kernel-hosted page, and the wallet connects once the card is stored. Kernel then enrolls it for an agentic network token when the issuer supports it. The card number never reaches Kernel. The connected wallet's payment_methods expansion lists the card; capabilities.single_use_card.eligible is false, with a network_token_* reason, until the card has a network token, and authorize returns 400 for such a card. Visa cards can be enrolled, but Visa purchases are not yet supported: authorize returns 400.
+ Creation returns a card_enrollment action: the cardholder enters the card and their email on a Kernel-hosted page, and the wallet connects once the card is stored. Kernel then enrolls it for an agentic network token when the issuer supports it. The card number never reaches Kernel. The connected wallet's payment_methods expansion lists the card; capabilities.single_use_card.eligible is false, with a network_token_* reason, until the card has a network token, and authorize returns 400 for such a card. Visa purchases require the cardholder to complete a spend_approval action before Kernel issues a one-time code; Mastercard purchases need no hosted approval.
"""
provider: Literal["kernel"]
diff --git a/src/kernel/types/vaults/kernel_wallet_vault_item_spec_param.py b/src/kernel/types/vaults/kernel_wallet_vault_item_spec_param.py
index 395c4653..c29066ce 100644
--- a/src/kernel/types/vaults/kernel_wallet_vault_item_spec_param.py
+++ b/src/kernel/types/vaults/kernel_wallet_vault_item_spec_param.py
@@ -10,7 +10,7 @@
class KernelWalletVaultItemSpecParam(TypedDict, total=False):
"""One card stored with Kernel-managed credentials.
- Creation returns a card_enrollment action: the cardholder enters the card and their email on a Kernel-hosted page, and the wallet connects once the card is stored. Kernel then enrolls it for an agentic network token when the issuer supports it. The card number never reaches Kernel. The connected wallet's payment_methods expansion lists the card; capabilities.single_use_card.eligible is false, with a network_token_* reason, until the card has a network token, and authorize returns 400 for such a card. Visa cards can be enrolled, but Visa purchases are not yet supported: authorize returns 400.
+ Creation returns a card_enrollment action: the cardholder enters the card and their email on a Kernel-hosted page, and the wallet connects once the card is stored. Kernel then enrolls it for an agentic network token when the issuer supports it. The card number never reaches Kernel. The connected wallet's payment_methods expansion lists the card; capabilities.single_use_card.eligible is false, with a network_token_* reason, until the card has a network token, and authorize returns 400 for such a card. Visa purchases require the cardholder to complete a spend_approval action before Kernel issues a one-time code; Mastercard purchases need no hosted approval.
"""
provider: Required[Literal["kernel"]]
From 915eb0d4c9fc70df904515df6f3e07ce6c1ac2c0 Mon Sep 17 00:00:00 2001
From: "kernel-internal[bot]"
<260533166+kernel-internal[bot]@users.noreply.github.com>
Date: Tue, 6 Oct 2026 20:13:17 +0000
Subject: [PATCH 331/366] feat: Add us-west placement region support
Stainless-Generated-From: 87c1ea4e146cd77dd37441e37add98a699a11f25
---
src/kernel/resources/browser_pools.py | 8 ++++----
src/kernel/resources/browsers/browsers.py | 8 ++++----
src/kernel/types/auth/managed_auth_browser_config.py | 2 +-
.../types/auth/managed_auth_browser_config_param.py | 2 +-
src/kernel/types/browser_create_params.py | 2 +-
src/kernel/types/browser_create_response.py | 2 +-
src/kernel/types/browser_list_params.py | 2 +-
src/kernel/types/browser_list_response.py | 2 +-
src/kernel/types/browser_pool.py | 2 +-
src/kernel/types/browser_pool_acquire_response.py | 2 +-
src/kernel/types/browser_pool_create_params.py | 2 +-
src/kernel/types/browser_pool_list_params.py | 2 +-
src/kernel/types/browser_retrieve_response.py | 2 +-
src/kernel/types/browser_update_response.py | 2 +-
src/kernel/types/invocation_list_browsers_response.py | 2 +-
15 files changed, 21 insertions(+), 21 deletions(-)
diff --git a/src/kernel/resources/browser_pools.py b/src/kernel/resources/browser_pools.py
index 20fdc7fa..6c34209e 100644
--- a/src/kernel/resources/browser_pools.py
+++ b/src/kernel/resources/browser_pools.py
@@ -77,7 +77,7 @@ def create(
profile: browser_pool_create_params.Profile | Omit = omit,
proxy_id: str | Omit = omit,
refresh_on_profile_update: bool | Omit = omit,
- region: Literal["us-east", "eu-west", "ap-southeast"] | Omit = omit,
+ region: Literal["us-east", "us-west", "eu-west", "ap-southeast"] | Omit = omit,
start_url: str | Omit = omit,
stealth: bool | Omit = omit,
telemetry: Optional[browser_pool_create_params.Telemetry] | Omit = omit,
@@ -419,7 +419,7 @@ def list(
name: str | Omit = omit,
offset: int | Omit = omit,
query: str | Omit = omit,
- region: Literal["us-east", "eu-west", "ap-southeast"] | Omit = omit,
+ region: Literal["us-east", "us-west", "eu-west", "ap-southeast"] | 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,
@@ -724,7 +724,7 @@ async def create(
profile: browser_pool_create_params.Profile | Omit = omit,
proxy_id: str | Omit = omit,
refresh_on_profile_update: bool | Omit = omit,
- region: Literal["us-east", "eu-west", "ap-southeast"] | Omit = omit,
+ region: Literal["us-east", "us-west", "eu-west", "ap-southeast"] | Omit = omit,
start_url: str | Omit = omit,
stealth: bool | Omit = omit,
telemetry: Optional[browser_pool_create_params.Telemetry] | Omit = omit,
@@ -1066,7 +1066,7 @@ def list(
name: str | Omit = omit,
offset: int | Omit = omit,
query: str | Omit = omit,
- region: Literal["us-east", "eu-west", "ap-southeast"] | Omit = omit,
+ region: Literal["us-east", "us-west", "eu-west", "ap-southeast"] | 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,
diff --git a/src/kernel/resources/browsers/browsers.py b/src/kernel/resources/browsers/browsers.py
index b1cf74d6..e66bf22e 100644
--- a/src/kernel/resources/browsers/browsers.py
+++ b/src/kernel/resources/browsers/browsers.py
@@ -196,7 +196,7 @@ def create(
profile: BrowserProfile | Omit = omit,
proxy: BrowserProxyConfigParam | Omit = omit,
proxy_id: str | Omit = omit,
- region: Literal["us-east", "eu-west", "ap-southeast"] | Omit = omit,
+ region: Literal["us-east", "us-west", "eu-west", "ap-southeast"] | Omit = omit,
start_url: str | Omit = omit,
stealth: bool | Omit = omit,
tags: TagsParam | Omit = omit,
@@ -485,7 +485,7 @@ def list(
limit: int | Omit = omit,
offset: int | Omit = omit,
query: str | Omit = omit,
- region: Literal["us-east", "eu-west", "ap-southeast"] | Omit = omit,
+ region: Literal["us-east", "us-west", "eu-west", "ap-southeast"] | Omit = omit,
status: Literal["active", "deleted", "all"] | Omit = omit,
tags: Dict[str, str] | Omit = omit,
# Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
@@ -898,7 +898,7 @@ async def create(
profile: BrowserProfile | Omit = omit,
proxy: BrowserProxyConfigParam | Omit = omit,
proxy_id: str | Omit = omit,
- region: Literal["us-east", "eu-west", "ap-southeast"] | Omit = omit,
+ region: Literal["us-east", "us-west", "eu-west", "ap-southeast"] | Omit = omit,
start_url: str | Omit = omit,
stealth: bool | Omit = omit,
tags: TagsParam | Omit = omit,
@@ -1187,7 +1187,7 @@ def list(
limit: int | Omit = omit,
offset: int | Omit = omit,
query: str | Omit = omit,
- region: Literal["us-east", "eu-west", "ap-southeast"] | Omit = omit,
+ region: Literal["us-east", "us-west", "eu-west", "ap-southeast"] | Omit = omit,
status: Literal["active", "deleted", "all"] | Omit = omit,
tags: Dict[str, str] | Omit = omit,
# Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
diff --git a/src/kernel/types/auth/managed_auth_browser_config.py b/src/kernel/types/auth/managed_auth_browser_config.py
index 1cbab85a..0de4499d 100644
--- a/src/kernel/types/auth/managed_auth_browser_config.py
+++ b/src/kernel/types/auth/managed_auth_browser_config.py
@@ -124,7 +124,7 @@ class ManagedAuthBrowserConfig(BaseModel):
preserve or inherit the connection default.
"""
- region: Optional[Literal["us-east", "eu-west", "ap-southeast"]] = None
+ region: Optional[Literal["us-east", "us-west", "eu-west", "ap-southeast"]] = None
"""Browser region.
Omit on create to use us-east, on update to keep the current region, or on login
diff --git a/src/kernel/types/auth/managed_auth_browser_config_param.py b/src/kernel/types/auth/managed_auth_browser_config_param.py
index 47651227..98577698 100644
--- a/src/kernel/types/auth/managed_auth_browser_config_param.py
+++ b/src/kernel/types/auth/managed_auth_browser_config_param.py
@@ -125,7 +125,7 @@ class ManagedAuthBrowserConfigParam(TypedDict, total=False):
preserve or inherit the connection default.
"""
- region: Literal["us-east", "eu-west", "ap-southeast"]
+ region: Literal["us-east", "us-west", "eu-west", "ap-southeast"]
"""Browser region.
Omit on create to use us-east, on update to keep the current region, or on login
diff --git a/src/kernel/types/browser_create_params.py b/src/kernel/types/browser_create_params.py
index fd33cee5..e77d2be1 100644
--- a/src/kernel/types/browser_create_params.py
+++ b/src/kernel/types/browser_create_params.py
@@ -103,7 +103,7 @@ class BrowserCreateParams(TypedDict, total=False):
favor of proxy.
"""
- region: Literal["us-east", "eu-west", "ap-southeast"]
+ region: Literal["us-east", "us-west", "eu-west", "ap-southeast"]
"""Geographic region for the browser session.
It is fixed once the session is created. Region selection requires a Start-Up or
diff --git a/src/kernel/types/browser_create_response.py b/src/kernel/types/browser_create_response.py
index a5e674f7..09c88f9a 100644
--- a/src/kernel/types/browser_create_response.py
+++ b/src/kernel/types/browser_create_response.py
@@ -32,7 +32,7 @@ class BrowserCreateResponse(BaseModel):
memory: BrowserMemory
"""Memory allocated to the browser session."""
- region: Literal["us-east", "eu-west", "ap-southeast"]
+ region: Literal["us-east", "us-west", "eu-west", "ap-southeast"]
"""Geographic region of the browser session. Fixed once the session is created."""
session_id: str
diff --git a/src/kernel/types/browser_list_params.py b/src/kernel/types/browser_list_params.py
index eb5b91dc..5bf673b5 100644
--- a/src/kernel/types/browser_list_params.py
+++ b/src/kernel/types/browser_list_params.py
@@ -27,7 +27,7 @@ class BrowserListParams(TypedDict, total=False):
Search browsers by name, session ID, profile name or ID, proxy ID, or pool name.
"""
- region: Literal["us-east", "eu-west", "ap-southeast"]
+ region: Literal["us-east", "us-west", "eu-west", "ap-southeast"]
"""Filter sessions by geographic region. Omit to list sessions in all regions."""
status: Literal["active", "deleted", "all"]
diff --git a/src/kernel/types/browser_list_response.py b/src/kernel/types/browser_list_response.py
index 3cc62ebe..6f1aad11 100644
--- a/src/kernel/types/browser_list_response.py
+++ b/src/kernel/types/browser_list_response.py
@@ -32,7 +32,7 @@ class BrowserListResponse(BaseModel):
memory: BrowserMemory
"""Memory allocated to the browser session."""
- region: Literal["us-east", "eu-west", "ap-southeast"]
+ region: Literal["us-east", "us-west", "eu-west", "ap-southeast"]
"""Geographic region of the browser session. Fixed once the session is created."""
session_id: str
diff --git a/src/kernel/types/browser_pool.py b/src/kernel/types/browser_pool.py
index 58a1a9cd..035b5722 100644
--- a/src/kernel/types/browser_pool.py
+++ b/src/kernel/types/browser_pool.py
@@ -178,7 +178,7 @@ class BrowserPool(BaseModel):
(echoed as sent on create).
"""
- region: Literal["us-east", "eu-west", "ap-southeast"]
+ region: Literal["us-east", "us-west", "eu-west", "ap-southeast"]
"""Geographic region of the browser pool. Fixed once the pool is created."""
name: Optional[str] = None
diff --git a/src/kernel/types/browser_pool_acquire_response.py b/src/kernel/types/browser_pool_acquire_response.py
index c223c927..a0617cb6 100644
--- a/src/kernel/types/browser_pool_acquire_response.py
+++ b/src/kernel/types/browser_pool_acquire_response.py
@@ -32,7 +32,7 @@ class BrowserPoolAcquireResponse(BaseModel):
memory: BrowserMemory
"""Memory allocated to the browser session."""
- region: Literal["us-east", "eu-west", "ap-southeast"]
+ region: Literal["us-east", "us-west", "eu-west", "ap-southeast"]
"""Geographic region of the browser session. Fixed once the session is created."""
session_id: str
diff --git a/src/kernel/types/browser_pool_create_params.py b/src/kernel/types/browser_pool_create_params.py
index fa1f5f89..3929b44c 100644
--- a/src/kernel/types/browser_pool_create_params.py
+++ b/src/kernel/types/browser_pool_create_params.py
@@ -92,7 +92,7 @@ class BrowserPoolCreateParams(TypedDict, total=False):
creation, this defaults to true. Requires a profile to be set on the pool.
"""
- region: Literal["us-east", "eu-west", "ap-southeast"]
+ region: Literal["us-east", "us-west", "eu-west", "ap-southeast"]
"""Geographic region for the browser pool.
It is fixed once the pool is created. Region selection requires a Start-Up or
diff --git a/src/kernel/types/browser_pool_list_params.py b/src/kernel/types/browser_pool_list_params.py
index 7d7a268c..8f9961f4 100644
--- a/src/kernel/types/browser_pool_list_params.py
+++ b/src/kernel/types/browser_pool_list_params.py
@@ -28,5 +28,5 @@ class BrowserPoolListParams(TypedDict, total=False):
IDs match by exact value.
"""
- region: Literal["us-east", "eu-west", "ap-southeast"]
+ region: Literal["us-east", "us-west", "eu-west", "ap-southeast"]
"""Filter pools by geographic region. Omit to list pools in all regions."""
diff --git a/src/kernel/types/browser_retrieve_response.py b/src/kernel/types/browser_retrieve_response.py
index 936b41c6..1642e55e 100644
--- a/src/kernel/types/browser_retrieve_response.py
+++ b/src/kernel/types/browser_retrieve_response.py
@@ -32,7 +32,7 @@ class BrowserRetrieveResponse(BaseModel):
memory: BrowserMemory
"""Memory allocated to the browser session."""
- region: Literal["us-east", "eu-west", "ap-southeast"]
+ region: Literal["us-east", "us-west", "eu-west", "ap-southeast"]
"""Geographic region of the browser session. Fixed once the session is created."""
session_id: str
diff --git a/src/kernel/types/browser_update_response.py b/src/kernel/types/browser_update_response.py
index d2296c21..98484751 100644
--- a/src/kernel/types/browser_update_response.py
+++ b/src/kernel/types/browser_update_response.py
@@ -32,7 +32,7 @@ class BrowserUpdateResponse(BaseModel):
memory: BrowserMemory
"""Memory allocated to the browser session."""
- region: Literal["us-east", "eu-west", "ap-southeast"]
+ region: Literal["us-east", "us-west", "eu-west", "ap-southeast"]
"""Geographic region of the browser session. Fixed once the session is created."""
session_id: str
diff --git a/src/kernel/types/invocation_list_browsers_response.py b/src/kernel/types/invocation_list_browsers_response.py
index a88f33f9..6589ad92 100644
--- a/src/kernel/types/invocation_list_browsers_response.py
+++ b/src/kernel/types/invocation_list_browsers_response.py
@@ -32,7 +32,7 @@ class Browser(BaseModel):
memory: BrowserMemory
"""Memory allocated to the browser session."""
- region: Literal["us-east", "eu-west", "ap-southeast"]
+ region: Literal["us-east", "us-west", "eu-west", "ap-southeast"]
"""Geographic region of the browser session. Fixed once the session is created."""
session_id: str
From ba8d4365b7bb84695108fde76fe2ab31f703724a Mon Sep 17 00:00:00 2001
From: ulziibay-kernel <253135130+ulziibay-kernel@users.noreply.github.com>
Date: Tue, 6 Oct 2026 20:15:03 +0000
Subject: [PATCH 332/366] fix(deps): require anyio>=4.14.2 where a patched
release exists
Bumps the anyio floor to 4.14.2 (GHSA-82r6-8w77-94w6, CVE-2026-63374)
for python >= 3.10 and pins 4.14.2 in both lockfiles. anyio dropped
python 3.9 in 4.13, so 3.9 environments keep the previous constraint;
no patched anyio release exists for 3.9.
Moves the dev interpreter to 3.10.16 so the lockfiles can carry the
patched pin, and bumps filelock, which virtualenv requires >= 3.20.1
of on python >= 3.10.
---
.python-version | 2 +-
pyproject.toml | 3 ++-
requirements-dev.lock | 4 ++--
requirements.lock | 2 +-
4 files changed, 6 insertions(+), 5 deletions(-)
diff --git a/.python-version b/.python-version
index 43077b24..251b3507 100644
--- a/.python-version
+++ b/.python-version
@@ -1 +1 @@
-3.9.18
+3.10.16
diff --git a/pyproject.toml b/pyproject.toml
index aa2a10b2..660886c1 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -12,7 +12,8 @@ dependencies = [
"httpx>=0.23.0, <1",
"pydantic>=1.9.0, <3",
"typing-extensions>=4.14, <5",
- "anyio>=3.5.0, <5",
+ "anyio>=4.14.2, <5; python_version >= '3.10'",
+ "anyio>=3.5.0, <5; python_version < '3.10'",
"distro>=1.7.0, <2",
"sniffio",
]
diff --git a/requirements-dev.lock b/requirements-dev.lock
index 4a38b203..dd39217d 100644
--- a/requirements-dev.lock
+++ b/requirements-dev.lock
@@ -19,7 +19,7 @@ aiosignal==1.4.0
# via aiohttp
annotated-types==0.7.0
# via pydantic
-anyio==4.12.1
+anyio==4.14.2
# via httpx
# via kernel
argcomplete==3.6.3
@@ -48,7 +48,7 @@ exceptiongroup==1.3.1
# via pytest
execnet==2.1.2
# via pytest-xdist
-filelock==3.19.1
+filelock==3.32.7
# via virtualenv
frozenlist==1.8.0
# via aiohttp
diff --git a/requirements.lock b/requirements.lock
index 5f6c7ff4..ed2c30f2 100644
--- a/requirements.lock
+++ b/requirements.lock
@@ -19,7 +19,7 @@ aiosignal==1.4.0
# via aiohttp
annotated-types==0.7.0
# via pydantic
-anyio==4.12.1
+anyio==4.14.2
# via httpx
# via kernel
async-timeout==5.0.1
From 8c80fc14441a149e4c6b5cd5e7a9a7a1db4706c2 Mon Sep 17 00:00:00 2001
From: "kernel-internal[bot]"
<260533166+kernel-internal[bot]@users.noreply.github.com>
Date: Tue, 6 Oct 2026 20:34:50 +0000
Subject: [PATCH 333/366] feat: Expose named Playwright executors on the
browser API
Stainless-Generated-From: 33fbf58325444e8ba81af6eda3ee77b3ede0633e
---
.stats.yml | 2 +-
api.md | 17 +-
src/kernel/resources/browsers/browsers.py | 40 ++-
.../resources/browsers/playwright/__init__.py | 33 ++
.../browsers/playwright/executors.py | 307 ++++++++++++++++++
.../browsers/{ => playwright}/playwright.py | 158 ++++++++-
src/kernel/types/browsers/__init__.py | 1 +
.../types/browsers/playwright/__init__.py | 7 +
.../types/browsers/playwright/executor.py | 38 +++
.../playwright/executor_delete_params.py | 14 +
.../browsers/playwright/executor_list.py | 14 +
.../browsers/playwright_execute_params.py | 8 +
.../browsers/playwright_execute_response.py | 7 +
src/kernel/types/browsers/tab.py | 23 ++
.../browsers/playwright/__init__.py | 1 +
.../browsers/playwright/test_executors.py | 232 +++++++++++++
.../api_resources/browsers/test_playwright.py | 2 +
17 files changed, 877 insertions(+), 27 deletions(-)
create mode 100644 src/kernel/resources/browsers/playwright/__init__.py
create mode 100644 src/kernel/resources/browsers/playwright/executors.py
rename src/kernel/resources/browsers/{ => playwright}/playwright.py (50%)
create mode 100644 src/kernel/types/browsers/playwright/__init__.py
create mode 100644 src/kernel/types/browsers/playwright/executor.py
create mode 100644 src/kernel/types/browsers/playwright/executor_delete_params.py
create mode 100644 src/kernel/types/browsers/playwright/executor_list.py
create mode 100644 src/kernel/types/browsers/tab.py
create mode 100644 tests/api_resources/browsers/playwright/__init__.py
create mode 100644 tests/api_resources/browsers/playwright/test_executors.py
diff --git a/.stats.yml b/.stats.yml
index ec91ee34..8e11d075 100644
--- a/.stats.yml
+++ b/.stats.yml
@@ -1 +1 @@
-configured_endpoints: 172
+configured_endpoints: 174
diff --git a/api.md b/api.md
index 51c1a9ea..798034d1 100644
--- a/api.md
+++ b/api.md
@@ -334,12 +334,25 @@ Methods:
Types:
```python
-from kernel.types.browsers import PlaywrightExecuteResponse
+from kernel.types.browsers import ExecutorLimitError, Tab, PlaywrightExecuteResponse
```
Methods:
-- client.browsers.playwright.execute(id_or_name, \*\*params) -> PlaywrightExecuteResponse
+- client.browsers.playwright.execute(id_or_name, \*\*params) -> PlaywrightExecuteResponse
+
+### Executors
+
+Types:
+
+```python
+from kernel.types.browsers.playwright import Executor, ExecutorList
+```
+
+Methods:
+
+- client.browsers.playwright.executors.list(id_or_name) -> ExecutorList
+- client.browsers.playwright.executors.delete(name, \*, id_or_name, \*\*params) -> None
## Webmcp
diff --git a/src/kernel/resources/browsers/browsers.py b/src/kernel/resources/browsers/browsers.py
index e66bf22e..289368a3 100644
--- a/src/kernel/resources/browsers/browsers.py
+++ b/src/kernel/resources/browsers/browsers.py
@@ -70,14 +70,6 @@
TelemetryResourceWithStreamingResponse,
AsyncTelemetryResourceWithStreamingResponse,
)
-from .playwright import (
- PlaywrightResource,
- AsyncPlaywrightResource,
- PlaywrightResourceWithRawResponse,
- AsyncPlaywrightResourceWithRawResponse,
- PlaywrightResourceWithStreamingResponse,
- AsyncPlaywrightResourceWithStreamingResponse,
-)
from ..._resource import SyncAPIResource, AsyncAPIResource
from ..._response import (
to_raw_response_wrapper,
@@ -96,6 +88,14 @@
)
from ..._base_client import AsyncPaginator, make_request_options
from ...types.tags_param import TagsParam
+from .playwright.playwright import (
+ PlaywrightResource,
+ AsyncPlaywrightResource,
+ PlaywrightResourceWithRawResponse,
+ AsyncPlaywrightResourceWithRawResponse,
+ PlaywrightResourceWithStreamingResponse,
+ AsyncPlaywrightResourceWithStreamingResponse,
+)
from ...types.browser_repl_result import BrowserReplResult
from ...types.browser_curl_response import BrowserCurlResponse
from ...types.browser_list_response import BrowserListResponse
@@ -154,7 +154,9 @@ def computer(self) -> ComputerResource:
@cached_property
def playwright(self) -> PlaywrightResource:
- """Execute Playwright code against the browser instance."""
+ """
+ Execute Playwright code against the browser instance and manage the executors it runs in.
+ """
return PlaywrightResource(self._client)
@cached_property
@@ -856,7 +858,9 @@ def computer(self) -> AsyncComputerResource:
@cached_property
def playwright(self) -> AsyncPlaywrightResource:
- """Execute Playwright code against the browser instance."""
+ """
+ Execute Playwright code against the browser instance and manage the executors it runs in.
+ """
return AsyncPlaywrightResource(self._client)
@cached_property
@@ -1586,7 +1590,9 @@ def computer(self) -> ComputerResourceWithRawResponse:
@cached_property
def playwright(self) -> PlaywrightResourceWithRawResponse:
- """Execute Playwright code against the browser instance."""
+ """
+ Execute Playwright code against the browser instance and manage the executors it runs in.
+ """
return PlaywrightResourceWithRawResponse(self._browsers.playwright)
@cached_property
@@ -1658,7 +1664,9 @@ def computer(self) -> AsyncComputerResourceWithRawResponse:
@cached_property
def playwright(self) -> AsyncPlaywrightResourceWithRawResponse:
- """Execute Playwright code against the browser instance."""
+ """
+ Execute Playwright code against the browser instance and manage the executors it runs in.
+ """
return AsyncPlaywrightResourceWithRawResponse(self._browsers.playwright)
@cached_property
@@ -1730,7 +1738,9 @@ def computer(self) -> ComputerResourceWithStreamingResponse:
@cached_property
def playwright(self) -> PlaywrightResourceWithStreamingResponse:
- """Execute Playwright code against the browser instance."""
+ """
+ Execute Playwright code against the browser instance and manage the executors it runs in.
+ """
return PlaywrightResourceWithStreamingResponse(self._browsers.playwright)
@cached_property
@@ -1802,7 +1812,9 @@ def computer(self) -> AsyncComputerResourceWithStreamingResponse:
@cached_property
def playwright(self) -> AsyncPlaywrightResourceWithStreamingResponse:
- """Execute Playwright code against the browser instance."""
+ """
+ Execute Playwright code against the browser instance and manage the executors it runs in.
+ """
return AsyncPlaywrightResourceWithStreamingResponse(self._browsers.playwright)
@cached_property
diff --git a/src/kernel/resources/browsers/playwright/__init__.py b/src/kernel/resources/browsers/playwright/__init__.py
new file mode 100644
index 00000000..fa98144e
--- /dev/null
+++ b/src/kernel/resources/browsers/playwright/__init__.py
@@ -0,0 +1,33 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from .executors import (
+ ExecutorsResource,
+ AsyncExecutorsResource,
+ ExecutorsResourceWithRawResponse,
+ AsyncExecutorsResourceWithRawResponse,
+ ExecutorsResourceWithStreamingResponse,
+ AsyncExecutorsResourceWithStreamingResponse,
+)
+from .playwright import (
+ PlaywrightResource,
+ AsyncPlaywrightResource,
+ PlaywrightResourceWithRawResponse,
+ AsyncPlaywrightResourceWithRawResponse,
+ PlaywrightResourceWithStreamingResponse,
+ AsyncPlaywrightResourceWithStreamingResponse,
+)
+
+__all__ = [
+ "ExecutorsResource",
+ "AsyncExecutorsResource",
+ "ExecutorsResourceWithRawResponse",
+ "AsyncExecutorsResourceWithRawResponse",
+ "ExecutorsResourceWithStreamingResponse",
+ "AsyncExecutorsResourceWithStreamingResponse",
+ "PlaywrightResource",
+ "AsyncPlaywrightResource",
+ "PlaywrightResourceWithRawResponse",
+ "AsyncPlaywrightResourceWithRawResponse",
+ "PlaywrightResourceWithStreamingResponse",
+ "AsyncPlaywrightResourceWithStreamingResponse",
+]
diff --git a/src/kernel/resources/browsers/playwright/executors.py b/src/kernel/resources/browsers/playwright/executors.py
new file mode 100644
index 00000000..11d78f10
--- /dev/null
+++ b/src/kernel/resources/browsers/playwright/executors.py
@@ -0,0 +1,307 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from __future__ import annotations
+
+import httpx
+
+from ...._types import Body, Omit, Query, Headers, NoneType, NotGiven, omit, not_given
+from ...._utils import path_template, maybe_transform, async_maybe_transform
+from ...._compat import cached_property
+from ...._resource import SyncAPIResource, AsyncAPIResource
+from ...._response import (
+ to_raw_response_wrapper,
+ to_streamed_response_wrapper,
+ async_to_raw_response_wrapper,
+ async_to_streamed_response_wrapper,
+)
+from ...._base_client import make_request_options
+from ....types.browsers.playwright import executor_delete_params
+from ....types.browsers.playwright.executor_list import ExecutorList
+
+__all__ = ["ExecutorsResource", "AsyncExecutorsResource"]
+
+
+class ExecutorsResource(SyncAPIResource):
+ """
+ Execute Playwright code against the browser instance and manage the executors it runs in.
+ """
+
+ @cached_property
+ def with_raw_response(self) -> ExecutorsResourceWithRawResponse:
+ """
+ This property can be used as a prefix for any HTTP method call to return
+ the raw response object instead of the parsed content.
+
+ For more information, see https://www.github.com/kernel/kernel-python-sdk#accessing-raw-response-data-eg-headers
+ """
+ return ExecutorsResourceWithRawResponse(self)
+
+ @cached_property
+ def with_streaming_response(self) -> ExecutorsResourceWithStreamingResponse:
+ """
+ An alternative to `.with_raw_response` that doesn't eagerly read the response body.
+
+ For more information, see https://www.github.com/kernel/kernel-python-sdk#with_streaming_response
+ """
+ return ExecutorsResourceWithStreamingResponse(self)
+
+ def list(
+ self,
+ id_or_name: str,
+ *,
+ # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
+ # The extra values given here take precedence over values defined on the client or passed to this method.
+ extra_headers: Headers | None = None,
+ extra_query: Query | None = None,
+ extra_body: Body | None = None,
+ timeout: float | httpx.Timeout | None | NotGiven = not_given,
+ ) -> ExecutorList:
+ """
+ Lists the default executor first, then the named executors created by POST
+ /browsers/{id_or_name}/playwright/execute. Each entry reports whether a call is
+ running on it and, for named executors, the target ID and URL of the tab it
+ owns. Returns 404 for a browser whose image predates executors.
+
+ Args:
+ extra_headers: Send extra headers
+
+ extra_query: Add additional query parameters to the request
+
+ extra_body: Add additional JSON properties to the request
+
+ timeout: Override the client-level default timeout for this request, in seconds
+ """
+ if not id_or_name:
+ raise ValueError(f"Expected a non-empty value for `id_or_name` but received {id_or_name!r}")
+ return self._get(
+ path_template("/browsers/{id_or_name}/playwright/executors", id_or_name=id_or_name),
+ options=make_request_options(
+ extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
+ ),
+ cast_to=ExecutorList,
+ )
+
+ def delete(
+ self,
+ name: str,
+ *,
+ id_or_name: str,
+ close_tab: 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,
+ ) -> None:
+ """Stops the executor's process.
+
+ A call running on it fails with an error saying
+ the executor was deleted. By default the executor's tab is closed too. The name
+ can be reused; the next call with it creates a new executor and tab.
+
+ The default executor is restarted instead of removed: its process is stopped, a
+ call running on it fails with an error saying the executor was restarted, and
+ queued and later calls run on a new process. It owns no tab, so 'close_tab' has
+ no effect on it.
+
+ Args:
+ name: Name of a Playwright executor. Calls with the same name run in the same
+ executor, one at a time; the first call with a new name creates it. Calls on
+ different executors run concurrently. 'default' names the executor that runs
+ calls without a name.
+
+ close_tab: Close the executor's tab. Defaults to true.
+
+ extra_headers: Send extra headers
+
+ extra_query: Add additional query parameters to the request
+
+ extra_body: Add additional JSON properties to the request
+
+ timeout: Override the client-level default timeout for this request, in seconds
+ """
+ if not id_or_name:
+ raise ValueError(f"Expected a non-empty value for `id_or_name` but received {id_or_name!r}")
+ if not name:
+ raise ValueError(f"Expected a non-empty value for `name` but received {name!r}")
+ extra_headers = {"Accept": "*/*", **(extra_headers or {})}
+ return self._delete(
+ path_template("/browsers/{id_or_name}/playwright/executors/{name}", id_or_name=id_or_name, name=name),
+ options=make_request_options(
+ extra_headers=extra_headers,
+ extra_query=extra_query,
+ extra_body=extra_body,
+ timeout=timeout,
+ query=maybe_transform({"close_tab": close_tab}, executor_delete_params.ExecutorDeleteParams),
+ ),
+ cast_to=NoneType,
+ )
+
+
+class AsyncExecutorsResource(AsyncAPIResource):
+ """
+ Execute Playwright code against the browser instance and manage the executors it runs in.
+ """
+
+ @cached_property
+ def with_raw_response(self) -> AsyncExecutorsResourceWithRawResponse:
+ """
+ This property can be used as a prefix for any HTTP method call to return
+ the raw response object instead of the parsed content.
+
+ For more information, see https://www.github.com/kernel/kernel-python-sdk#accessing-raw-response-data-eg-headers
+ """
+ return AsyncExecutorsResourceWithRawResponse(self)
+
+ @cached_property
+ def with_streaming_response(self) -> AsyncExecutorsResourceWithStreamingResponse:
+ """
+ An alternative to `.with_raw_response` that doesn't eagerly read the response body.
+
+ For more information, see https://www.github.com/kernel/kernel-python-sdk#with_streaming_response
+ """
+ return AsyncExecutorsResourceWithStreamingResponse(self)
+
+ async def list(
+ self,
+ id_or_name: str,
+ *,
+ # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
+ # The extra values given here take precedence over values defined on the client or passed to this method.
+ extra_headers: Headers | None = None,
+ extra_query: Query | None = None,
+ extra_body: Body | None = None,
+ timeout: float | httpx.Timeout | None | NotGiven = not_given,
+ ) -> ExecutorList:
+ """
+ Lists the default executor first, then the named executors created by POST
+ /browsers/{id_or_name}/playwright/execute. Each entry reports whether a call is
+ running on it and, for named executors, the target ID and URL of the tab it
+ owns. Returns 404 for a browser whose image predates executors.
+
+ Args:
+ extra_headers: Send extra headers
+
+ extra_query: Add additional query parameters to the request
+
+ extra_body: Add additional JSON properties to the request
+
+ timeout: Override the client-level default timeout for this request, in seconds
+ """
+ if not id_or_name:
+ raise ValueError(f"Expected a non-empty value for `id_or_name` but received {id_or_name!r}")
+ return await self._get(
+ path_template("/browsers/{id_or_name}/playwright/executors", id_or_name=id_or_name),
+ options=make_request_options(
+ extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
+ ),
+ cast_to=ExecutorList,
+ )
+
+ async def delete(
+ self,
+ name: str,
+ *,
+ id_or_name: str,
+ close_tab: 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,
+ ) -> None:
+ """Stops the executor's process.
+
+ A call running on it fails with an error saying
+ the executor was deleted. By default the executor's tab is closed too. The name
+ can be reused; the next call with it creates a new executor and tab.
+
+ The default executor is restarted instead of removed: its process is stopped, a
+ call running on it fails with an error saying the executor was restarted, and
+ queued and later calls run on a new process. It owns no tab, so 'close_tab' has
+ no effect on it.
+
+ Args:
+ name: Name of a Playwright executor. Calls with the same name run in the same
+ executor, one at a time; the first call with a new name creates it. Calls on
+ different executors run concurrently. 'default' names the executor that runs
+ calls without a name.
+
+ close_tab: Close the executor's tab. Defaults to true.
+
+ extra_headers: Send extra headers
+
+ extra_query: Add additional query parameters to the request
+
+ extra_body: Add additional JSON properties to the request
+
+ timeout: Override the client-level default timeout for this request, in seconds
+ """
+ if not id_or_name:
+ raise ValueError(f"Expected a non-empty value for `id_or_name` but received {id_or_name!r}")
+ if not name:
+ raise ValueError(f"Expected a non-empty value for `name` but received {name!r}")
+ extra_headers = {"Accept": "*/*", **(extra_headers or {})}
+ return await self._delete(
+ path_template("/browsers/{id_or_name}/playwright/executors/{name}", id_or_name=id_or_name, name=name),
+ options=make_request_options(
+ extra_headers=extra_headers,
+ extra_query=extra_query,
+ extra_body=extra_body,
+ timeout=timeout,
+ query=await async_maybe_transform(
+ {"close_tab": close_tab}, executor_delete_params.ExecutorDeleteParams
+ ),
+ ),
+ cast_to=NoneType,
+ )
+
+
+class ExecutorsResourceWithRawResponse:
+ def __init__(self, executors: ExecutorsResource) -> None:
+ self._executors = executors
+
+ self.list = to_raw_response_wrapper(
+ executors.list,
+ )
+ self.delete = to_raw_response_wrapper(
+ executors.delete,
+ )
+
+
+class AsyncExecutorsResourceWithRawResponse:
+ def __init__(self, executors: AsyncExecutorsResource) -> None:
+ self._executors = executors
+
+ self.list = async_to_raw_response_wrapper(
+ executors.list,
+ )
+ self.delete = async_to_raw_response_wrapper(
+ executors.delete,
+ )
+
+
+class ExecutorsResourceWithStreamingResponse:
+ def __init__(self, executors: ExecutorsResource) -> None:
+ self._executors = executors
+
+ self.list = to_streamed_response_wrapper(
+ executors.list,
+ )
+ self.delete = to_streamed_response_wrapper(
+ executors.delete,
+ )
+
+
+class AsyncExecutorsResourceWithStreamingResponse:
+ def __init__(self, executors: AsyncExecutorsResource) -> None:
+ self._executors = executors
+
+ self.list = async_to_streamed_response_wrapper(
+ executors.list,
+ )
+ self.delete = async_to_streamed_response_wrapper(
+ executors.delete,
+ )
diff --git a/src/kernel/resources/browsers/playwright.py b/src/kernel/resources/browsers/playwright/playwright.py
similarity index 50%
rename from src/kernel/resources/browsers/playwright.py
rename to src/kernel/resources/browsers/playwright/playwright.py
index c1f9cea5..50ddf810 100644
--- a/src/kernel/resources/browsers/playwright.py
+++ b/src/kernel/resources/browsers/playwright/playwright.py
@@ -4,25 +4,42 @@
import httpx
-from ..._types import Body, Omit, Query, Headers, NotGiven, omit, not_given
-from ..._utils import path_template, maybe_transform, async_maybe_transform
-from ..._compat import cached_property
-from ..._resource import SyncAPIResource, AsyncAPIResource
-from ..._response import (
+from ...._types import Body, Omit, Query, Headers, NotGiven, omit, not_given
+from ...._utils import path_template, maybe_transform, async_maybe_transform
+from .executors import (
+ ExecutorsResource,
+ AsyncExecutorsResource,
+ ExecutorsResourceWithRawResponse,
+ AsyncExecutorsResourceWithRawResponse,
+ ExecutorsResourceWithStreamingResponse,
+ AsyncExecutorsResourceWithStreamingResponse,
+)
+from ...._compat import cached_property
+from ...._resource import SyncAPIResource, AsyncAPIResource
+from ...._response import (
to_raw_response_wrapper,
to_streamed_response_wrapper,
async_to_raw_response_wrapper,
async_to_streamed_response_wrapper,
)
-from ..._base_client import make_request_options
-from ...types.browsers import playwright_execute_params
-from ...types.browsers.playwright_execute_response import PlaywrightExecuteResponse
+from ...._base_client import make_request_options
+from ....types.browsers import playwright_execute_params
+from ....types.browsers.playwright_execute_response import PlaywrightExecuteResponse
__all__ = ["PlaywrightResource", "AsyncPlaywrightResource"]
class PlaywrightResource(SyncAPIResource):
- """Execute Playwright code against the browser instance."""
+ """
+ Execute Playwright code against the browser instance and manage the executors it runs in.
+ """
+
+ @cached_property
+ def executors(self) -> ExecutorsResource:
+ """
+ Execute Playwright code against the browser instance and manage the executors it runs in.
+ """
+ return ExecutorsResource(self._client)
@cached_property
def with_raw_response(self) -> PlaywrightResourceWithRawResponse:
@@ -48,6 +65,7 @@ def execute(
id_or_name: str,
*,
code: str,
+ executor: str | Omit = omit,
timeout_sec: int | Omit = omit,
# Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
# The extra values given here take precedence over values defined on the client or passed to this method.
@@ -65,6 +83,41 @@ def execute(
exact registration. It can `return` a value, and this value is returned in the
response.
+ Every call runs in an executor: a dedicated Node.js process with its own browser
+ connection. Calls on different executors run concurrently; calls on the same
+ executor run one at a time. A timeout, crash, or blocked event loop in one
+ executor does not affect other executors. After a timeout the executor keeps its
+ process and drops its browser connection, so code abandoned by the timeout
+ cannot keep driving the browser. After a crash or a blocked event loop, the next
+ call on that executor starts a fresh process.
+
+ Calls without 'executor' run in the executor named 'default', which always
+ exists and is the same as passing 'executor: "default"'. In the default
+ executor, 'page' is bound to an active tab reported by Chrome. In single-window
+ sessions, this is the foreground tab. When multiple browser windows are open,
+ Chrome reports one active tab per window and the selected window is unspecified.
+ 'context' is the BrowserContext that owns the selected page. Use
+ 'browser.contexts()' to select a context or page explicitly.
+
+ Pass any other name to run the call in a named executor. The first call with a
+ new name creates it. Each named executor owns a tab: its first call opens a new
+ background tab in the default browser context, and 'page' is bound to that tab
+ on every later call while it stays open. Opening it does not change the active
+ tab of an existing window. If the tab is closed, the next call opens a new one
+ and reports 'tab.created: true'. Executor code can still reach other tabs
+ through 'context' and 'browser'; ownership only decides what 'page' is bound to.
+ Use named executors to drive several tabs of one browser in parallel.
+
+ A browser can have at most 8 named executors; the default executor does not
+ count. A call that would create another returns 409 with the current executors;
+ delete one with DELETE /browsers/{id_or_name}/playwright/executors/{name}. Named
+ executors are not removed automatically while the browser runs; when it shuts
+ down, they are removed and their tabs closed.
+
+ A named call to a browser whose image predates executors fails with 400 instead
+ of running on the active tab; calls without 'executor' keep working on every
+ image.
+
Args:
code: TypeScript/JavaScript code to execute. The code has access to 'page', 'context',
and 'browser' variables. It runs within a function, so you can use a return
@@ -72,6 +125,11 @@ def execute(
property in the response. Example: "await page.goto('https://example.com');
return await page.title();"
+ executor: Name of a Playwright executor. Calls with the same name run in the same
+ executor, one at a time; the first call with a new name creates it. Calls on
+ different executors run concurrently. 'default' names the executor that runs
+ calls without a name.
+
timeout_sec: Maximum execution time in seconds. Default is 60.
extra_headers: Send extra headers
@@ -89,6 +147,7 @@ def execute(
body=maybe_transform(
{
"code": code,
+ "executor": executor,
"timeout_sec": timeout_sec,
},
playwright_execute_params.PlaywrightExecuteParams,
@@ -101,7 +160,16 @@ def execute(
class AsyncPlaywrightResource(AsyncAPIResource):
- """Execute Playwright code against the browser instance."""
+ """
+ Execute Playwright code against the browser instance and manage the executors it runs in.
+ """
+
+ @cached_property
+ def executors(self) -> AsyncExecutorsResource:
+ """
+ Execute Playwright code against the browser instance and manage the executors it runs in.
+ """
+ return AsyncExecutorsResource(self._client)
@cached_property
def with_raw_response(self) -> AsyncPlaywrightResourceWithRawResponse:
@@ -127,6 +195,7 @@ async def execute(
id_or_name: str,
*,
code: str,
+ executor: str | Omit = omit,
timeout_sec: int | Omit = omit,
# Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
# The extra values given here take precedence over values defined on the client or passed to this method.
@@ -144,6 +213,41 @@ async def execute(
exact registration. It can `return` a value, and this value is returned in the
response.
+ Every call runs in an executor: a dedicated Node.js process with its own browser
+ connection. Calls on different executors run concurrently; calls on the same
+ executor run one at a time. A timeout, crash, or blocked event loop in one
+ executor does not affect other executors. After a timeout the executor keeps its
+ process and drops its browser connection, so code abandoned by the timeout
+ cannot keep driving the browser. After a crash or a blocked event loop, the next
+ call on that executor starts a fresh process.
+
+ Calls without 'executor' run in the executor named 'default', which always
+ exists and is the same as passing 'executor: "default"'. In the default
+ executor, 'page' is bound to an active tab reported by Chrome. In single-window
+ sessions, this is the foreground tab. When multiple browser windows are open,
+ Chrome reports one active tab per window and the selected window is unspecified.
+ 'context' is the BrowserContext that owns the selected page. Use
+ 'browser.contexts()' to select a context or page explicitly.
+
+ Pass any other name to run the call in a named executor. The first call with a
+ new name creates it. Each named executor owns a tab: its first call opens a new
+ background tab in the default browser context, and 'page' is bound to that tab
+ on every later call while it stays open. Opening it does not change the active
+ tab of an existing window. If the tab is closed, the next call opens a new one
+ and reports 'tab.created: true'. Executor code can still reach other tabs
+ through 'context' and 'browser'; ownership only decides what 'page' is bound to.
+ Use named executors to drive several tabs of one browser in parallel.
+
+ A browser can have at most 8 named executors; the default executor does not
+ count. A call that would create another returns 409 with the current executors;
+ delete one with DELETE /browsers/{id_or_name}/playwright/executors/{name}. Named
+ executors are not removed automatically while the browser runs; when it shuts
+ down, they are removed and their tabs closed.
+
+ A named call to a browser whose image predates executors fails with 400 instead
+ of running on the active tab; calls without 'executor' keep working on every
+ image.
+
Args:
code: TypeScript/JavaScript code to execute. The code has access to 'page', 'context',
and 'browser' variables. It runs within a function, so you can use a return
@@ -151,6 +255,11 @@ async def execute(
property in the response. Example: "await page.goto('https://example.com');
return await page.title();"
+ executor: Name of a Playwright executor. Calls with the same name run in the same
+ executor, one at a time; the first call with a new name creates it. Calls on
+ different executors run concurrently. 'default' names the executor that runs
+ calls without a name.
+
timeout_sec: Maximum execution time in seconds. Default is 60.
extra_headers: Send extra headers
@@ -168,6 +277,7 @@ async def execute(
body=await async_maybe_transform(
{
"code": code,
+ "executor": executor,
"timeout_sec": timeout_sec,
},
playwright_execute_params.PlaywrightExecuteParams,
@@ -187,6 +297,13 @@ def __init__(self, playwright: PlaywrightResource) -> None:
playwright.execute,
)
+ @cached_property
+ def executors(self) -> ExecutorsResourceWithRawResponse:
+ """
+ Execute Playwright code against the browser instance and manage the executors it runs in.
+ """
+ return ExecutorsResourceWithRawResponse(self._playwright.executors)
+
class AsyncPlaywrightResourceWithRawResponse:
def __init__(self, playwright: AsyncPlaywrightResource) -> None:
@@ -196,6 +313,13 @@ def __init__(self, playwright: AsyncPlaywrightResource) -> None:
playwright.execute,
)
+ @cached_property
+ def executors(self) -> AsyncExecutorsResourceWithRawResponse:
+ """
+ Execute Playwright code against the browser instance and manage the executors it runs in.
+ """
+ return AsyncExecutorsResourceWithRawResponse(self._playwright.executors)
+
class PlaywrightResourceWithStreamingResponse:
def __init__(self, playwright: PlaywrightResource) -> None:
@@ -205,6 +329,13 @@ def __init__(self, playwright: PlaywrightResource) -> None:
playwright.execute,
)
+ @cached_property
+ def executors(self) -> ExecutorsResourceWithStreamingResponse:
+ """
+ Execute Playwright code against the browser instance and manage the executors it runs in.
+ """
+ return ExecutorsResourceWithStreamingResponse(self._playwright.executors)
+
class AsyncPlaywrightResourceWithStreamingResponse:
def __init__(self, playwright: AsyncPlaywrightResource) -> None:
@@ -213,3 +344,10 @@ def __init__(self, playwright: AsyncPlaywrightResource) -> None:
self.execute = async_to_streamed_response_wrapper(
playwright.execute,
)
+
+ @cached_property
+ def executors(self) -> AsyncExecutorsResourceWithStreamingResponse:
+ """
+ Execute Playwright code against the browser instance and manage the executors it runs in.
+ """
+ return AsyncExecutorsResourceWithStreamingResponse(self._playwright.executors)
diff --git a/src/kernel/types/browsers/__init__.py b/src/kernel/types/browsers/__init__.py
index 2decde3a..27104cff 100644
--- a/src/kernel/types/browsers/__init__.py
+++ b/src/kernel/types/browsers/__init__.py
@@ -2,6 +2,7 @@
from __future__ import annotations
+from .tab import Tab as Tab
from .tool import Tool as Tool
from .tool_frame import ToolFrame as ToolFrame
from .tool_source import ToolSource as ToolSource
diff --git a/src/kernel/types/browsers/playwright/__init__.py b/src/kernel/types/browsers/playwright/__init__.py
new file mode 100644
index 00000000..74b73b59
--- /dev/null
+++ b/src/kernel/types/browsers/playwright/__init__.py
@@ -0,0 +1,7 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from __future__ import annotations
+
+from .executor import Executor as Executor
+from .executor_list import ExecutorList as ExecutorList
+from .executor_delete_params import ExecutorDeleteParams as ExecutorDeleteParams
diff --git a/src/kernel/types/browsers/playwright/executor.py b/src/kernel/types/browsers/playwright/executor.py
new file mode 100644
index 00000000..0085af9a
--- /dev/null
+++ b/src/kernel/types/browsers/playwright/executor.py
@@ -0,0 +1,38 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from typing import Optional
+from datetime import datetime
+
+from ...._models import BaseModel
+
+__all__ = ["Executor"]
+
+
+class Executor(BaseModel):
+ """A Playwright executor on the browser"""
+
+ busy: bool
+ """Whether a call is running on the executor"""
+
+ created_at: datetime
+ """When the executor was created"""
+
+ last_used_at: datetime
+ """When the most recent call on the executor started"""
+
+ name: str
+ """Name of a Playwright executor.
+
+ Calls with the same name run in the same executor, one at a time; the first call
+ with a new name creates it. Calls on different executors run concurrently.
+ 'default' names the executor that runs calls without a name.
+ """
+
+ target_id: Optional[str] = None
+ """CDP page target ID of the executor's tab, once it has one.
+
+ The default executor owns no tab.
+ """
+
+ url: Optional[str] = None
+ """Current URL of the executor's tab, when it is open"""
diff --git a/src/kernel/types/browsers/playwright/executor_delete_params.py b/src/kernel/types/browsers/playwright/executor_delete_params.py
new file mode 100644
index 00000000..e553f435
--- /dev/null
+++ b/src/kernel/types/browsers/playwright/executor_delete_params.py
@@ -0,0 +1,14 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from __future__ import annotations
+
+from typing_extensions import Required, TypedDict
+
+__all__ = ["ExecutorDeleteParams"]
+
+
+class ExecutorDeleteParams(TypedDict, total=False):
+ id_or_name: Required[str]
+
+ close_tab: bool
+ """Close the executor's tab. Defaults to true."""
diff --git a/src/kernel/types/browsers/playwright/executor_list.py b/src/kernel/types/browsers/playwright/executor_list.py
new file mode 100644
index 00000000..c3e89c1f
--- /dev/null
+++ b/src/kernel/types/browsers/playwright/executor_list.py
@@ -0,0 +1,14 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from typing import List
+
+from .executor import Executor
+from ...._models import BaseModel
+
+__all__ = ["ExecutorList"]
+
+
+class ExecutorList(BaseModel):
+ """The default executor followed by the named executors"""
+
+ executors: List[Executor]
diff --git a/src/kernel/types/browsers/playwright_execute_params.py b/src/kernel/types/browsers/playwright_execute_params.py
index 948a74c1..85980a9d 100644
--- a/src/kernel/types/browsers/playwright_execute_params.py
+++ b/src/kernel/types/browsers/playwright_execute_params.py
@@ -17,5 +17,13 @@ class PlaywrightExecuteParams(TypedDict, total=False):
"await page.goto('https://example.com'); return await page.title();"
"""
+ executor: str
+ """Name of a Playwright executor.
+
+ Calls with the same name run in the same executor, one at a time; the first call
+ with a new name creates it. Calls on different executors run concurrently.
+ 'default' names the executor that runs calls without a name.
+ """
+
timeout_sec: int
"""Maximum execution time in seconds. Default is 60."""
diff --git a/src/kernel/types/browsers/playwright_execute_response.py b/src/kernel/types/browsers/playwright_execute_response.py
index d53080de..63080119 100644
--- a/src/kernel/types/browsers/playwright_execute_response.py
+++ b/src/kernel/types/browsers/playwright_execute_response.py
@@ -2,6 +2,7 @@
from typing import Optional
+from .tab import Tab
from ..._models import BaseModel
__all__ = ["PlaywrightExecuteResponse"]
@@ -24,3 +25,9 @@ class PlaywrightExecuteResponse(BaseModel):
stdout: Optional[str] = None
"""Standard output from the execution"""
+
+ tab: Optional[Tab] = None
+ """The tab 'page' was bound to for this call.
+
+ Absent if the call failed before binding a tab.
+ """
diff --git a/src/kernel/types/browsers/tab.py b/src/kernel/types/browsers/tab.py
new file mode 100644
index 00000000..9f53eeca
--- /dev/null
+++ b/src/kernel/types/browsers/tab.py
@@ -0,0 +1,23 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from ..._models import BaseModel
+
+__all__ = ["Tab"]
+
+
+class Tab(BaseModel):
+ """The tab 'page' was bound to for this call.
+
+ Absent if the call failed before binding a tab.
+ """
+
+ created: bool
+ """Whether this call opened the tab.
+
+ For a named executor this happens on its first call and after its previous tab
+ was closed. For the default executor it happens only when the browser had no
+ open page.
+ """
+
+ target_id: str
+ """CDP page target ID of the tab"""
diff --git a/tests/api_resources/browsers/playwright/__init__.py b/tests/api_resources/browsers/playwright/__init__.py
new file mode 100644
index 00000000..fd8019a9
--- /dev/null
+++ b/tests/api_resources/browsers/playwright/__init__.py
@@ -0,0 +1 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
diff --git a/tests/api_resources/browsers/playwright/test_executors.py b/tests/api_resources/browsers/playwright/test_executors.py
new file mode 100644
index 00000000..4b3b564e
--- /dev/null
+++ b/tests/api_resources/browsers/playwright/test_executors.py
@@ -0,0 +1,232 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from __future__ import annotations
+
+import os
+from typing import Any, cast
+
+import pytest
+
+from kernel import Kernel, AsyncKernel
+from tests.utils import assert_matches_type
+from kernel.types.browsers.playwright import ExecutorList
+
+base_url = os.environ.get("TEST_API_BASE_URL", "http://127.0.0.1:4010")
+
+
+class TestExecutors:
+ parametrize = pytest.mark.parametrize("client", [False, True], indirect=True, ids=["loose", "strict"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ def test_method_list(self, client: Kernel) -> None:
+ executor = client.browsers.playwright.executors.list(
+ "htzv5orfit78e1m2biiifpbv",
+ )
+ assert_matches_type(ExecutorList, executor, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ def test_raw_response_list(self, client: Kernel) -> None:
+ response = client.browsers.playwright.executors.with_raw_response.list(
+ "htzv5orfit78e1m2biiifpbv",
+ )
+
+ assert response.is_closed is True
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+ executor = response.parse()
+ assert_matches_type(ExecutorList, executor, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ def test_streaming_response_list(self, client: Kernel) -> None:
+ with client.browsers.playwright.executors.with_streaming_response.list(
+ "htzv5orfit78e1m2biiifpbv",
+ ) as response:
+ assert not response.is_closed
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+
+ executor = response.parse()
+ assert_matches_type(ExecutorList, executor, path=["response"])
+
+ assert cast(Any, response.is_closed) is True
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ def test_path_params_list(self, client: Kernel) -> None:
+ with pytest.raises(ValueError, match=r"Expected a non-empty value for `id_or_name` but received ''"):
+ client.browsers.playwright.executors.with_raw_response.list(
+ "",
+ )
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ def test_method_delete(self, client: Kernel) -> None:
+ executor = client.browsers.playwright.executors.delete(
+ name="checkout",
+ id_or_name="htzv5orfit78e1m2biiifpbv",
+ )
+ assert executor is None
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ def test_method_delete_with_all_params(self, client: Kernel) -> None:
+ executor = client.browsers.playwright.executors.delete(
+ name="checkout",
+ id_or_name="htzv5orfit78e1m2biiifpbv",
+ close_tab=True,
+ )
+ assert executor is None
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ def test_raw_response_delete(self, client: Kernel) -> None:
+ response = client.browsers.playwright.executors.with_raw_response.delete(
+ name="checkout",
+ id_or_name="htzv5orfit78e1m2biiifpbv",
+ )
+
+ assert response.is_closed is True
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+ executor = response.parse()
+ assert executor is None
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ def test_streaming_response_delete(self, client: Kernel) -> None:
+ with client.browsers.playwright.executors.with_streaming_response.delete(
+ name="checkout",
+ id_or_name="htzv5orfit78e1m2biiifpbv",
+ ) as response:
+ assert not response.is_closed
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+
+ executor = response.parse()
+ assert executor is None
+
+ assert cast(Any, response.is_closed) is True
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ def test_path_params_delete(self, client: Kernel) -> None:
+ with pytest.raises(ValueError, match=r"Expected a non-empty value for `id_or_name` but received ''"):
+ client.browsers.playwright.executors.with_raw_response.delete(
+ name="checkout",
+ id_or_name="",
+ )
+
+ with pytest.raises(ValueError, match=r"Expected a non-empty value for `name` but received ''"):
+ client.browsers.playwright.executors.with_raw_response.delete(
+ name="",
+ id_or_name="htzv5orfit78e1m2biiifpbv",
+ )
+
+
+class TestAsyncExecutors:
+ parametrize = pytest.mark.parametrize(
+ "async_client", [False, True, {"http_client": "aiohttp"}], indirect=True, ids=["loose", "strict", "aiohttp"]
+ )
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ async def test_method_list(self, async_client: AsyncKernel) -> None:
+ executor = await async_client.browsers.playwright.executors.list(
+ "htzv5orfit78e1m2biiifpbv",
+ )
+ assert_matches_type(ExecutorList, executor, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ async def test_raw_response_list(self, async_client: AsyncKernel) -> None:
+ response = await async_client.browsers.playwright.executors.with_raw_response.list(
+ "htzv5orfit78e1m2biiifpbv",
+ )
+
+ assert response.is_closed is True
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+ executor = await response.parse()
+ assert_matches_type(ExecutorList, executor, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ async def test_streaming_response_list(self, async_client: AsyncKernel) -> None:
+ async with async_client.browsers.playwright.executors.with_streaming_response.list(
+ "htzv5orfit78e1m2biiifpbv",
+ ) as response:
+ assert not response.is_closed
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+
+ executor = await response.parse()
+ assert_matches_type(ExecutorList, executor, 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_list(self, async_client: AsyncKernel) -> None:
+ with pytest.raises(ValueError, match=r"Expected a non-empty value for `id_or_name` but received ''"):
+ await async_client.browsers.playwright.executors.with_raw_response.list(
+ "",
+ )
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ async def test_method_delete(self, async_client: AsyncKernel) -> None:
+ executor = await async_client.browsers.playwright.executors.delete(
+ name="checkout",
+ id_or_name="htzv5orfit78e1m2biiifpbv",
+ )
+ assert executor is None
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ async def test_method_delete_with_all_params(self, async_client: AsyncKernel) -> None:
+ executor = await async_client.browsers.playwright.executors.delete(
+ name="checkout",
+ id_or_name="htzv5orfit78e1m2biiifpbv",
+ close_tab=True,
+ )
+ assert executor is None
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ async def test_raw_response_delete(self, async_client: AsyncKernel) -> None:
+ response = await async_client.browsers.playwright.executors.with_raw_response.delete(
+ name="checkout",
+ id_or_name="htzv5orfit78e1m2biiifpbv",
+ )
+
+ assert response.is_closed is True
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+ executor = await response.parse()
+ assert executor is None
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ async def test_streaming_response_delete(self, async_client: AsyncKernel) -> None:
+ async with async_client.browsers.playwright.executors.with_streaming_response.delete(
+ name="checkout",
+ id_or_name="htzv5orfit78e1m2biiifpbv",
+ ) as response:
+ assert not response.is_closed
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+
+ executor = await response.parse()
+ assert executor is None
+
+ assert cast(Any, response.is_closed) is True
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ async def test_path_params_delete(self, async_client: AsyncKernel) -> None:
+ with pytest.raises(ValueError, match=r"Expected a non-empty value for `id_or_name` but received ''"):
+ await async_client.browsers.playwright.executors.with_raw_response.delete(
+ name="checkout",
+ id_or_name="",
+ )
+
+ with pytest.raises(ValueError, match=r"Expected a non-empty value for `name` but received ''"):
+ await async_client.browsers.playwright.executors.with_raw_response.delete(
+ name="",
+ id_or_name="htzv5orfit78e1m2biiifpbv",
+ )
diff --git a/tests/api_resources/browsers/test_playwright.py b/tests/api_resources/browsers/test_playwright.py
index 5adc553f..3a2c457f 100644
--- a/tests/api_resources/browsers/test_playwright.py
+++ b/tests/api_resources/browsers/test_playwright.py
@@ -32,6 +32,7 @@ def test_method_execute_with_all_params(self, client: Kernel) -> None:
playwright = client.browsers.playwright.execute(
id_or_name="htzv5orfit78e1m2biiifpbv",
code="code",
+ executor="checkout",
timeout_sec=1,
)
assert_matches_type(PlaywrightExecuteResponse, playwright, path=["response"])
@@ -94,6 +95,7 @@ async def test_method_execute_with_all_params(self, async_client: AsyncKernel) -
playwright = await async_client.browsers.playwright.execute(
id_or_name="htzv5orfit78e1m2biiifpbv",
code="code",
+ executor="checkout",
timeout_sec=1,
)
assert_matches_type(PlaywrightExecuteResponse, playwright, path=["response"])
From e2bde7dab0cba41f71be4f3926a08a9749625886 Mon Sep 17 00:00:00 2001
From: "kernel-internal[bot]"
<260533166+kernel-internal[bot]@users.noreply.github.com>
Date: Tue, 6 Oct 2026 20:40:35 +0000
Subject: [PATCH 334/366] release: 0.120.0
---
.release-please-manifest.json | 2 +-
CHANGELOG.md | 12 ++++++++++++
pyproject.toml | 2 +-
src/kernel/_version.py | 2 +-
4 files changed, 15 insertions(+), 3 deletions(-)
diff --git a/.release-please-manifest.json b/.release-please-manifest.json
index 124b9841..486a203f 100644
--- a/.release-please-manifest.json
+++ b/.release-please-manifest.json
@@ -1,3 +1,3 @@
{
- ".": "0.119.0"
+ ".": "0.120.0"
}
\ No newline at end of file
diff --git a/CHANGELOG.md b/CHANGELOG.md
index a874179c..976a2d24 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,17 @@
# Changelog
+## [0.120.0](https://github.com/kernel/kernel-python-sdk/compare/v0.119.0...v0.120.0) (2026-10-06)
+
+
+### Features
+
+* Accept network.allowed_hosts on browser create behind an org flag ([3178829](https://github.com/kernel/kernel-python-sdk/commit/3178829222a373a69fdb7617abcab02f409d2d6e))
+* Add us-west placement region support ([915eb0d](https://github.com/kernel/kernel-python-sdk/commit/915eb0d4c9fc70df904515df6f3e07ce6c1ac2c0))
+* Add Visa passkey approval for VGS purchases ([fadc7c9](https://github.com/kernel/kernel-python-sdk/commit/fadc7c95c7fe45df163e6f186384a7d534c8c014))
+* Connect Kernel wallets once the card is stored, with best-effort network tokens ([da49f00](https://github.com/kernel/kernel-python-sdk/commit/da49f007e5b00cdca00f08500db43bfc6332c4c3))
+* Enable Exa highlights and default Perplexity to medium context ([761b766](https://github.com/kernel/kernel-python-sdk/commit/761b766f80e9ccfabf65a412bf777e5a861b400b))
+* Expose named Playwright executors on the browser API ([8c80fc1](https://github.com/kernel/kernel-python-sdk/commit/8c80fc14441a149e4c6b5cd5e7a9a7a1db4706c2))
+
## [0.119.0](https://github.com/kernel/kernel-python-sdk/compare/v0.118.0...v0.119.0) (2026-10-02)
diff --git a/pyproject.toml b/pyproject.toml
index aa2a10b2..7f92defc 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -1,6 +1,6 @@
[project]
name = "kernel"
-version = "0.119.0"
+version = "0.120.0"
description = "The official Python library for the kernel API"
dynamic = ["readme"]
license = "Apache-2.0"
diff --git a/src/kernel/_version.py b/src/kernel/_version.py
index fb178235..a819522f 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.119.0" # x-release-please-version
+__version__ = "0.120.0" # x-release-please-version
From c34882c77d247ace0fa23d37da1c3574d21bd1b0 Mon Sep 17 00:00:00 2001
From: "kernel-internal[bot]"
<260533166+kernel-internal[bot]@users.noreply.github.com>
Date: Wed, 7 Oct 2026 19:27:06 +0000
Subject: [PATCH 335/366] feat: Deprecate datacenter proxies in the API
Stainless-Generated-From: 93ad1fda4dfb667b50676addc3ff6c415d10cc06
---
api.md | 1 +
src/kernel/resources/browsers/browsers.py | 15 +++-
src/kernel/resources/proxies.py | 70 +++++++++----------
src/kernel/types/__init__.py | 1 +
src/kernel/types/browser_create_params.py | 3 +-
src/kernel/types/browser_network_config.py | 5 +-
.../types/browser_network_config_param.py | 5 +-
.../types/browser_network_update_param.py | 32 +++++++++
src/kernel/types/browser_update_params.py | 4 ++
src/kernel/types/proxy.py | 18 +----
src/kernel/types/proxy_check_params.py | 19 +++--
src/kernel/types/proxy_check_response.py | 23 +-----
src/kernel/types/proxy_create_params.py | 23 +-----
src/kernel/types/proxy_create_response.py | 23 +-----
src/kernel/types/proxy_list_response.py | 23 +-----
src/kernel/types/proxy_retrieve_response.py | 23 +-----
src/kernel/types/proxy_update_response.py | 23 +-----
tests/api_resources/test_browsers.py | 2 +
tests/api_resources/test_proxies.py | 16 ++---
19 files changed, 130 insertions(+), 199 deletions(-)
create mode 100644 src/kernel/types/browser_network_update_param.py
diff --git a/api.md b/api.md
index 798034d1..4f4ecf76 100644
--- a/api.md
+++ b/api.md
@@ -120,6 +120,7 @@ from kernel.types import (
BrowserMemory,
BrowserMemoryRequest,
BrowserNetworkConfig,
+ BrowserNetworkUpdate,
BrowserPoolRef,
BrowserProxy,
BrowserProxyConfig,
diff --git a/src/kernel/resources/browsers/browsers.py b/src/kernel/resources/browsers/browsers.py
index 289368a3..38089c20 100644
--- a/src/kernel/resources/browsers/browsers.py
+++ b/src/kernel/resources/browsers/browsers.py
@@ -112,6 +112,7 @@
from ...types.browser_retrieve_response import BrowserRetrieveResponse
from ...types.browser_proxy_config_param import BrowserProxyConfigParam
from ...types.browser_network_config_param import BrowserNetworkConfigParam
+from ...types.browser_network_update_param import BrowserNetworkUpdateParam
from ...types.shared_params.browser_profile import BrowserProfile
from ...types.shared_params.browser_viewport import BrowserViewport
from ...types.shared_params.browser_extension import BrowserExtension
@@ -241,7 +242,8 @@ def create(
the dashboard. Must be unique among active sessions within the project. Can be
changed later via PATCH /browsers/{id_or_name}.
- network: Network configuration for the browser session. Cannot be changed after creation.
+ network: Network configuration for the browser session. Only allowed_hosts can be changed
+ after creation, with PATCH /browsers/{id_or_name}.
profile: Profile selection for the browser session. Provide either id or name. If
specified, the matching profile will be loaded into the browser session.
@@ -393,6 +395,7 @@ def update(
*,
disable_default_proxy: bool | Omit = omit,
name: Optional[str] | Omit = omit,
+ network: BrowserNetworkUpdateParam | Omit = omit,
profile: BrowserProfile | Omit = omit,
proxy: BrowserProxyConfigParam | Omit = omit,
proxy_id: Optional[str] | Omit = omit,
@@ -418,6 +421,8 @@ def update(
empty string to clear the name. When set, must be unique among active sessions
within the project.
+ network: Network configuration changes to apply to the running browser session.
+
profile: Profile to load into the browser session. Only allowed if the session does not
already have a profile loaded.
@@ -464,6 +469,7 @@ def update(
{
"disable_default_proxy": disable_default_proxy,
"name": name,
+ "network": network,
"profile": profile,
"proxy": proxy,
"proxy_id": proxy_id,
@@ -945,7 +951,8 @@ async def create(
the dashboard. Must be unique among active sessions within the project. Can be
changed later via PATCH /browsers/{id_or_name}.
- network: Network configuration for the browser session. Cannot be changed after creation.
+ network: Network configuration for the browser session. Only allowed_hosts can be changed
+ after creation, with PATCH /browsers/{id_or_name}.
profile: Profile selection for the browser session. Provide either id or name. If
specified, the matching profile will be loaded into the browser session.
@@ -1097,6 +1104,7 @@ async def update(
*,
disable_default_proxy: bool | Omit = omit,
name: Optional[str] | Omit = omit,
+ network: BrowserNetworkUpdateParam | Omit = omit,
profile: BrowserProfile | Omit = omit,
proxy: BrowserProxyConfigParam | Omit = omit,
proxy_id: Optional[str] | Omit = omit,
@@ -1122,6 +1130,8 @@ async def update(
empty string to clear the name. When set, must be unique among active sessions
within the project.
+ network: Network configuration changes to apply to the running browser session.
+
profile: Profile to load into the browser session. Only allowed if the session does not
already have a profile loaded.
@@ -1168,6 +1178,7 @@ async def update(
{
"disable_default_proxy": disable_default_proxy,
"name": name,
+ "network": network,
"profile": profile,
"proxy": proxy,
"proxy_id": proxy_id,
diff --git a/src/kernel/resources/proxies.py b/src/kernel/resources/proxies.py
index ee9f9084..240d18d1 100644
--- a/src/kernel/resources/proxies.py
+++ b/src/kernel/resources/proxies.py
@@ -53,7 +53,7 @@ def with_streaming_response(self) -> ProxiesResourceWithStreamingResponse:
def create(
self,
*,
- type: Literal["datacenter", "isp", "residential", "mobile", "custom"],
+ type: Literal["isp", "residential", "mobile", "custom"],
bypass_hosts: SequenceNotStr[str] | Omit = omit,
config: proxy_create_params.Config | Omit = omit,
name: str | Omit = omit,
@@ -69,12 +69,9 @@ def create(
Create a new proxy configuration in the resolved project.
Args:
- type: Proxy type to use. In terms of quality for avoiding bot-detection, from best to
- worst: `mobile` > `residential` > `isp` > `datacenter`.
-
bypass_hosts: Hostnames that should bypass the parent proxy and connect directly.
- config: Configuration specific to the selected proxy `type`.
+ config: Configuration for an ISP proxy.
name: Readable name of the proxy.
@@ -292,25 +289,25 @@ def check(
"""Run a health check on the proxy to verify it's working.
Optionally specify a URL
- to test reachability against a specific target. For ISP and datacenter proxies,
- this reliably tests whether the target site is reachable from the proxy's stable
- exit IP. For residential and mobile proxies, the exit node varies between
- requests, so this validates proxy configuration and connectivity rather than
- guaranteeing site-specific reachability.
+ to test reachability against a specific target. For ISP proxies, this reliably
+ tests whether the target site is reachable from the proxy's stable exit IP. For
+ residential and mobile proxies, the exit node varies between requests, so this
+ validates proxy configuration and connectivity rather than guaranteeing
+ site-specific reachability.
Args:
url: An optional URL to test reachability against. If provided, the proxy check will
test connectivity to this URL instead of the default test URLs. Only HTTP and
HTTPS schemes are allowed, and the URL must resolve to a public IP address. For
- ISP and datacenter proxies, the exit IP is stable, so a successful check
- reliably indicates that subsequent browser sessions will reach the target site
- with the same IP. For residential and mobile proxies, the exit node changes
- between requests, so a successful check validates proxy configuration but does
- not guarantee that a subsequent browser session will use the same exit IP or
- reach the same site — it is useful for verifying credentials and connectivity,
- not for predicting site-specific behavior. When provided, the check result does
- not update the proxy's health status, since a failure may indicate a problem
- with the target site rather than the proxy itself.
+ ISP proxies, the exit IP is stable, so a successful check reliably indicates
+ that subsequent browser sessions will reach the target site with the same IP.
+ For residential and mobile proxies, the exit node changes between requests, so a
+ successful check validates proxy configuration but does not guarantee that a
+ subsequent browser session will use the same exit IP or reach the same site — it
+ is useful for verifying credentials and connectivity, not for predicting
+ site-specific behavior. When provided, the check result does not update the
+ proxy's health status, since a failure may indicate a problem with the target
+ site rather than the proxy itself.
extra_headers: Send extra headers
@@ -357,7 +354,7 @@ def with_streaming_response(self) -> AsyncProxiesResourceWithStreamingResponse:
async def create(
self,
*,
- type: Literal["datacenter", "isp", "residential", "mobile", "custom"],
+ type: Literal["isp", "residential", "mobile", "custom"],
bypass_hosts: SequenceNotStr[str] | Omit = omit,
config: proxy_create_params.Config | Omit = omit,
name: str | Omit = omit,
@@ -373,12 +370,9 @@ async def create(
Create a new proxy configuration in the resolved project.
Args:
- type: Proxy type to use. In terms of quality for avoiding bot-detection, from best to
- worst: `mobile` > `residential` > `isp` > `datacenter`.
-
bypass_hosts: Hostnames that should bypass the parent proxy and connect directly.
- config: Configuration specific to the selected proxy `type`.
+ config: Configuration for an ISP proxy.
name: Readable name of the proxy.
@@ -596,25 +590,25 @@ async def check(
"""Run a health check on the proxy to verify it's working.
Optionally specify a URL
- to test reachability against a specific target. For ISP and datacenter proxies,
- this reliably tests whether the target site is reachable from the proxy's stable
- exit IP. For residential and mobile proxies, the exit node varies between
- requests, so this validates proxy configuration and connectivity rather than
- guaranteeing site-specific reachability.
+ to test reachability against a specific target. For ISP proxies, this reliably
+ tests whether the target site is reachable from the proxy's stable exit IP. For
+ residential and mobile proxies, the exit node varies between requests, so this
+ validates proxy configuration and connectivity rather than guaranteeing
+ site-specific reachability.
Args:
url: An optional URL to test reachability against. If provided, the proxy check will
test connectivity to this URL instead of the default test URLs. Only HTTP and
HTTPS schemes are allowed, and the URL must resolve to a public IP address. For
- ISP and datacenter proxies, the exit IP is stable, so a successful check
- reliably indicates that subsequent browser sessions will reach the target site
- with the same IP. For residential and mobile proxies, the exit node changes
- between requests, so a successful check validates proxy configuration but does
- not guarantee that a subsequent browser session will use the same exit IP or
- reach the same site — it is useful for verifying credentials and connectivity,
- not for predicting site-specific behavior. When provided, the check result does
- not update the proxy's health status, since a failure may indicate a problem
- with the target site rather than the proxy itself.
+ ISP proxies, the exit IP is stable, so a successful check reliably indicates
+ that subsequent browser sessions will reach the target site with the same IP.
+ For residential and mobile proxies, the exit node changes between requests, so a
+ successful check validates proxy configuration but does not guarantee that a
+ subsequent browser session will use the same exit IP or reach the same site — it
+ is useful for verifying credentials and connectivity, not for predicting
+ site-specific behavior. When provided, the check result does not update the
+ proxy's health status, since a failure may indicate a problem with the target
+ site rather than the proxy itself.
extra_headers: Send extra headers
diff --git a/src/kernel/types/__init__.py b/src/kernel/types/__init__.py
index 968e4021..cbd6041d 100644
--- a/src/kernel/types/__init__.py
+++ b/src/kernel/types/__init__.py
@@ -132,6 +132,7 @@
from .browser_pool_release_params import BrowserPoolReleaseParams as BrowserPoolReleaseParams
from .config_registry_list_params import ConfigRegistryListParams as ConfigRegistryListParams
from .browser_network_config_param import BrowserNetworkConfigParam as BrowserNetworkConfigParam
+from .browser_network_update_param import BrowserNetworkUpdateParam as BrowserNetworkUpdateParam
from .deployment_retrieve_response import DeploymentRetrieveResponse as DeploymentRetrieveResponse
from .invocation_retrieve_response import InvocationRetrieveResponse as InvocationRetrieveResponse
from .audit_log_export_chunk_params import AuditLogExportChunkParams as AuditLogExportChunkParams
diff --git a/src/kernel/types/browser_create_params.py b/src/kernel/types/browser_create_params.py
index e77d2be1..2e456a80 100644
--- a/src/kernel/types/browser_create_params.py
+++ b/src/kernel/types/browser_create_params.py
@@ -74,7 +74,8 @@ class BrowserCreateParams(TypedDict, total=False):
network: BrowserNetworkConfigParam
"""Network configuration for the browser session.
- Cannot be changed after creation.
+ Only allowed_hosts can be changed after creation, with PATCH
+ /browsers/{id_or_name}.
"""
profile: BrowserProfile
diff --git a/src/kernel/types/browser_network_config.py b/src/kernel/types/browser_network_config.py
index 453db6bd..6e8f570a 100644
--- a/src/kernel/types/browser_network_config.py
+++ b/src/kernel/types/browser_network_config.py
@@ -50,8 +50,9 @@ class BrowserNetworkConfig(BaseModel):
or reserved IP ranges are rejected, as are entries that overlap private_hosts.
Enforced at Kernel's egress proxy only: destinations in private_hosts, and
processes in the browser VM that do not use the browser's proxy, are not
- filtered, and Kernel's own control traffic is always allowed. Requires proxy v3.
- Not supported on browser pools.
+ filtered, and Kernel's own control traffic is always allowed. Can be replaced or
+ removed while the session runs with PATCH /browsers/{id_or_name}. Requires proxy
+ v3. Not supported on browser pools.
"""
private_hosts: Optional[List[str]] = None
diff --git a/src/kernel/types/browser_network_config_param.py b/src/kernel/types/browser_network_config_param.py
index c643b6ae..3d2e2d88 100644
--- a/src/kernel/types/browser_network_config_param.py
+++ b/src/kernel/types/browser_network_config_param.py
@@ -53,8 +53,9 @@ class BrowserNetworkConfigParam(TypedDict, total=False):
or reserved IP ranges are rejected, as are entries that overlap private_hosts.
Enforced at Kernel's egress proxy only: destinations in private_hosts, and
processes in the browser VM that do not use the browser's proxy, are not
- filtered, and Kernel's own control traffic is always allowed. Requires proxy v3.
- Not supported on browser pools.
+ filtered, and Kernel's own control traffic is always allowed. Can be replaced or
+ removed while the session runs with PATCH /browsers/{id_or_name}. Requires proxy
+ v3. Not supported on browser pools.
"""
private_hosts: SequenceNotStr[str]
diff --git a/src/kernel/types/browser_network_update_param.py b/src/kernel/types/browser_network_update_param.py
new file mode 100644
index 00000000..e96fa723
--- /dev/null
+++ b/src/kernel/types/browser_network_update_param.py
@@ -0,0 +1,32 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from __future__ import annotations
+
+from typing import Optional
+from typing_extensions import TypedDict
+
+from .._types import SequenceNotStr
+
+__all__ = ["BrowserNetworkUpdateParam"]
+
+
+class BrowserNetworkUpdateParam(TypedDict, total=False):
+ """Network configuration changes for a running browser session.
+
+ Other network settings can only be set at creation.
+ """
+
+ allowed_hosts: Optional[SequenceNotStr[str]]
+ """
+ Replaces the session's egress allowlist, using the same entry rules as
+ network.allowed_hosts on create. Omit to leave the allowlist unchanged, or set
+ to null to remove it and return to unfiltered egress; an empty list is invalid.
+ The new list applies without restarting the browser: new requests to
+ destinations it no longer allows are refused within a few seconds, and open
+ connections to them are closed within about 30 seconds, or up to 10 minutes
+ during a Kernel deploy. Connections to destinations it still allows, such as
+ WebSockets, stay open. A start_url in the same request must be allowed by the
+ updated list, and is loaded only after the list takes effect. Requires a browser
+ created with proxy v3, and not supported on pooled browsers. If the request
+ fails, retry it: the new list may already apply to some requests.
+ """
diff --git a/src/kernel/types/browser_update_params.py b/src/kernel/types/browser_update_params.py
index 08cd43c2..475010bd 100644
--- a/src/kernel/types/browser_update_params.py
+++ b/src/kernel/types/browser_update_params.py
@@ -7,6 +7,7 @@
from .tags_param import TagsParam
from .browser_proxy_config_param import BrowserProxyConfigParam
+from .browser_network_update_param import BrowserNetworkUpdateParam
from .shared_params.browser_profile import BrowserProfile
from .shared_params.browser_viewport import BrowserViewport
from .browsers.browser_telemetry_categories_config_param import BrowserTelemetryCategoriesConfigParam
@@ -36,6 +37,9 @@ class BrowserUpdateParams(TypedDict, total=False):
must be unique among active sessions within the project.
"""
+ network: BrowserNetworkUpdateParam
+ """Network configuration changes to apply to the running browser session."""
+
profile: BrowserProfile
"""Profile to load into the browser session.
diff --git a/src/kernel/types/proxy.py b/src/kernel/types/proxy.py
index 9c945ce6..c4061b15 100644
--- a/src/kernel/types/proxy.py
+++ b/src/kernel/types/proxy.py
@@ -12,7 +12,6 @@
"ConfigRegistryManagedProxy",
"ConfigRegistryManagedProxyCreate",
"ConfigRegistryManagedProxyCreateConfig",
- "ConfigRegistryManagedProxyCreateConfigDatacenterProxyConfig",
"ConfigRegistryManagedProxyCreateConfigIspProxyConfig",
"ConfigRegistryManagedProxyCreateConfigResidentialProxyConfig",
"ConfigRegistryManagedProxyCreateConfigMobileProxyConfig",
@@ -26,13 +25,6 @@ class ConfigRegistryDirectProxy(BaseModel):
mode: Literal["direct"]
-class ConfigRegistryManagedProxyCreateConfigDatacenterProxyConfig(BaseModel):
- """Configuration for a datacenter proxy."""
-
- country: Optional[str] = None
- """ISO 3166 country code. Defaults to US if not provided."""
-
-
class ConfigRegistryManagedProxyCreateConfigIspProxyConfig(BaseModel):
"""Configuration for an ISP proxy."""
@@ -111,7 +103,6 @@ class ConfigRegistryManagedProxyCreateConfigCreateCustomProxyConfig(BaseModel):
ConfigRegistryManagedProxyCreateConfig: TypeAlias = Union[
- ConfigRegistryManagedProxyCreateConfigDatacenterProxyConfig,
ConfigRegistryManagedProxyCreateConfigIspProxyConfig,
ConfigRegistryManagedProxyCreateConfigResidentialProxyConfig,
ConfigRegistryManagedProxyCreateConfigMobileProxyConfig,
@@ -122,18 +113,13 @@ class ConfigRegistryManagedProxyCreateConfigCreateCustomProxyConfig(BaseModel):
class ConfigRegistryManagedProxyCreate(BaseModel):
"""Configuration for routing traffic through a proxy."""
- type: Literal["datacenter", "isp", "residential", "mobile", "custom"]
- """Proxy type to use.
-
- In terms of quality for avoiding bot-detection, from best to worst: `mobile` >
- `residential` > `isp` > `datacenter`.
- """
+ type: Literal["isp", "residential", "mobile", "custom"]
bypass_hosts: Optional[List[str]] = None
"""Hostnames that should bypass the parent proxy and connect directly."""
config: Optional[ConfigRegistryManagedProxyCreateConfig] = None
- """Configuration specific to the selected proxy `type`."""
+ """Configuration for an ISP proxy."""
name: Optional[str] = None
"""Readable name of the proxy."""
diff --git a/src/kernel/types/proxy_check_params.py b/src/kernel/types/proxy_check_params.py
index 47f99f04..056a91d0 100644
--- a/src/kernel/types/proxy_check_params.py
+++ b/src/kernel/types/proxy_check_params.py
@@ -13,14 +13,13 @@ class ProxyCheckParams(TypedDict, total=False):
If provided, the proxy check will test connectivity to this URL instead of the
default test URLs. Only HTTP and HTTPS schemes are allowed, and the URL must
- resolve to a public IP address. For ISP and datacenter proxies, the exit IP is
- stable, so a successful check reliably indicates that subsequent browser
- sessions will reach the target site with the same IP. For residential and mobile
- proxies, the exit node changes between requests, so a successful check validates
- proxy configuration but does not guarantee that a subsequent browser session
- will use the same exit IP or reach the same site — it is useful for verifying
- credentials and connectivity, not for predicting site-specific behavior. When
- provided, the check result does not update the proxy's health status, since a
- failure may indicate a problem with the target site rather than the proxy
- itself.
+ resolve to a public IP address. For ISP proxies, the exit IP is stable, so a
+ successful check reliably indicates that subsequent browser sessions will reach
+ the target site with the same IP. For residential and mobile proxies, the exit
+ node changes between requests, so a successful check validates proxy
+ configuration but does not guarantee that a subsequent browser session will use
+ the same exit IP or reach the same site — it is useful for verifying credentials
+ and connectivity, not for predicting site-specific behavior. When provided, the
+ check result does not update the proxy's health status, since a failure may
+ indicate a problem with the target site rather than the proxy itself.
"""
diff --git a/src/kernel/types/proxy_check_response.py b/src/kernel/types/proxy_check_response.py
index d2821093..ab7fdd8c 100644
--- a/src/kernel/types/proxy_check_response.py
+++ b/src/kernel/types/proxy_check_response.py
@@ -9,7 +9,6 @@
__all__ = [
"ProxyCheckResponse",
"Config",
- "ConfigDatacenterProxyConfig",
"ConfigIspProxyConfig",
"ConfigResidentialProxyConfig",
"ConfigMobileProxyConfig",
@@ -17,13 +16,6 @@
]
-class ConfigDatacenterProxyConfig(BaseModel):
- """Configuration for a datacenter proxy."""
-
- country: Optional[str] = None
- """ISO 3166 country code. Defaults to US if not provided."""
-
-
class ConfigIspProxyConfig(BaseModel):
"""Configuration for an ISP proxy."""
@@ -98,23 +90,14 @@ class ConfigCustomProxyConfig(BaseModel):
Config: TypeAlias = Union[
- ConfigDatacenterProxyConfig,
- ConfigIspProxyConfig,
- ConfigResidentialProxyConfig,
- ConfigMobileProxyConfig,
- ConfigCustomProxyConfig,
+ ConfigIspProxyConfig, ConfigResidentialProxyConfig, ConfigMobileProxyConfig, ConfigCustomProxyConfig
]
class ProxyCheckResponse(BaseModel):
"""Configuration for routing traffic through a proxy."""
- type: Literal["datacenter", "isp", "residential", "mobile", "custom"]
- """Proxy type to use.
-
- In terms of quality for avoiding bot-detection, from best to worst: `mobile` >
- `residential` > `isp` > `datacenter`.
- """
+ type: Literal["isp", "residential", "mobile", "custom"]
id: Optional[str] = None
@@ -122,7 +105,7 @@ class ProxyCheckResponse(BaseModel):
"""Hostnames that should bypass the parent proxy and connect directly."""
config: Optional[Config] = None
- """Configuration specific to the selected proxy `type`."""
+ """Configuration for an ISP proxy."""
ip_address: Optional[str] = None
"""IP address that the proxy uses when making requests."""
diff --git a/src/kernel/types/proxy_create_params.py b/src/kernel/types/proxy_create_params.py
index 3e5a7935..351c44b6 100644
--- a/src/kernel/types/proxy_create_params.py
+++ b/src/kernel/types/proxy_create_params.py
@@ -10,7 +10,6 @@
__all__ = [
"ProxyCreateParams",
"Config",
- "ConfigDatacenterProxyConfig",
"ConfigIspProxyConfig",
"ConfigResidentialProxyConfig",
"ConfigMobileProxyConfig",
@@ -19,18 +18,13 @@
class ProxyCreateParams(TypedDict, total=False):
- type: Required[Literal["datacenter", "isp", "residential", "mobile", "custom"]]
- """Proxy type to use.
-
- In terms of quality for avoiding bot-detection, from best to worst: `mobile` >
- `residential` > `isp` > `datacenter`.
- """
+ type: Required[Literal["isp", "residential", "mobile", "custom"]]
bypass_hosts: SequenceNotStr[str]
"""Hostnames that should bypass the parent proxy and connect directly."""
config: Config
- """Configuration specific to the selected proxy `type`."""
+ """Configuration for an ISP proxy."""
name: str
"""Readable name of the proxy."""
@@ -39,13 +33,6 @@ class ProxyCreateParams(TypedDict, total=False):
"""Protocol to use for the proxy connection."""
-class ConfigDatacenterProxyConfig(TypedDict, total=False):
- """Configuration for a datacenter proxy."""
-
- country: str
- """ISO 3166 country code. Defaults to US if not provided."""
-
-
class ConfigIspProxyConfig(TypedDict, total=False):
"""Configuration for an ISP proxy."""
@@ -124,9 +111,5 @@ class ConfigCreateCustomProxyConfig(TypedDict, total=False):
Config: TypeAlias = Union[
- ConfigDatacenterProxyConfig,
- ConfigIspProxyConfig,
- ConfigResidentialProxyConfig,
- ConfigMobileProxyConfig,
- ConfigCreateCustomProxyConfig,
+ ConfigIspProxyConfig, ConfigResidentialProxyConfig, ConfigMobileProxyConfig, ConfigCreateCustomProxyConfig
]
diff --git a/src/kernel/types/proxy_create_response.py b/src/kernel/types/proxy_create_response.py
index 7f0b1d7c..556eb680 100644
--- a/src/kernel/types/proxy_create_response.py
+++ b/src/kernel/types/proxy_create_response.py
@@ -9,7 +9,6 @@
__all__ = [
"ProxyCreateResponse",
"Config",
- "ConfigDatacenterProxyConfig",
"ConfigIspProxyConfig",
"ConfigResidentialProxyConfig",
"ConfigMobileProxyConfig",
@@ -17,13 +16,6 @@
]
-class ConfigDatacenterProxyConfig(BaseModel):
- """Configuration for a datacenter proxy."""
-
- country: Optional[str] = None
- """ISO 3166 country code. Defaults to US if not provided."""
-
-
class ConfigIspProxyConfig(BaseModel):
"""Configuration for an ISP proxy."""
@@ -98,23 +90,14 @@ class ConfigCustomProxyConfig(BaseModel):
Config: TypeAlias = Union[
- ConfigDatacenterProxyConfig,
- ConfigIspProxyConfig,
- ConfigResidentialProxyConfig,
- ConfigMobileProxyConfig,
- ConfigCustomProxyConfig,
+ ConfigIspProxyConfig, ConfigResidentialProxyConfig, ConfigMobileProxyConfig, ConfigCustomProxyConfig
]
class ProxyCreateResponse(BaseModel):
"""Configuration for routing traffic through a proxy."""
- type: Literal["datacenter", "isp", "residential", "mobile", "custom"]
- """Proxy type to use.
-
- In terms of quality for avoiding bot-detection, from best to worst: `mobile` >
- `residential` > `isp` > `datacenter`.
- """
+ type: Literal["isp", "residential", "mobile", "custom"]
id: Optional[str] = None
@@ -122,7 +105,7 @@ class ProxyCreateResponse(BaseModel):
"""Hostnames that should bypass the parent proxy and connect directly."""
config: Optional[Config] = None
- """Configuration specific to the selected proxy `type`."""
+ """Configuration for an ISP proxy."""
ip_address: Optional[str] = None
"""IP address that the proxy uses when making requests."""
diff --git a/src/kernel/types/proxy_list_response.py b/src/kernel/types/proxy_list_response.py
index eea62ec8..9093b081 100644
--- a/src/kernel/types/proxy_list_response.py
+++ b/src/kernel/types/proxy_list_response.py
@@ -9,7 +9,6 @@
__all__ = [
"ProxyListResponse",
"Config",
- "ConfigDatacenterProxyConfig",
"ConfigIspProxyConfig",
"ConfigResidentialProxyConfig",
"ConfigMobileProxyConfig",
@@ -17,13 +16,6 @@
]
-class ConfigDatacenterProxyConfig(BaseModel):
- """Configuration for a datacenter proxy."""
-
- country: Optional[str] = None
- """ISO 3166 country code. Defaults to US if not provided."""
-
-
class ConfigIspProxyConfig(BaseModel):
"""Configuration for an ISP proxy."""
@@ -98,23 +90,14 @@ class ConfigCustomProxyConfig(BaseModel):
Config: TypeAlias = Union[
- ConfigDatacenterProxyConfig,
- ConfigIspProxyConfig,
- ConfigResidentialProxyConfig,
- ConfigMobileProxyConfig,
- ConfigCustomProxyConfig,
+ ConfigIspProxyConfig, ConfigResidentialProxyConfig, ConfigMobileProxyConfig, ConfigCustomProxyConfig
]
class ProxyListResponse(BaseModel):
"""Configuration for routing traffic through a proxy."""
- type: Literal["datacenter", "isp", "residential", "mobile", "custom"]
- """Proxy type to use.
-
- In terms of quality for avoiding bot-detection, from best to worst: `mobile` >
- `residential` > `isp` > `datacenter`.
- """
+ type: Literal["isp", "residential", "mobile", "custom"]
id: Optional[str] = None
@@ -122,7 +105,7 @@ class ProxyListResponse(BaseModel):
"""Hostnames that should bypass the parent proxy and connect directly."""
config: Optional[Config] = None
- """Configuration specific to the selected proxy `type`."""
+ """Configuration for an ISP proxy."""
ip_address: Optional[str] = None
"""IP address that the proxy uses when making requests."""
diff --git a/src/kernel/types/proxy_retrieve_response.py b/src/kernel/types/proxy_retrieve_response.py
index 26a72906..bd140d8f 100644
--- a/src/kernel/types/proxy_retrieve_response.py
+++ b/src/kernel/types/proxy_retrieve_response.py
@@ -9,7 +9,6 @@
__all__ = [
"ProxyRetrieveResponse",
"Config",
- "ConfigDatacenterProxyConfig",
"ConfigIspProxyConfig",
"ConfigResidentialProxyConfig",
"ConfigMobileProxyConfig",
@@ -17,13 +16,6 @@
]
-class ConfigDatacenterProxyConfig(BaseModel):
- """Configuration for a datacenter proxy."""
-
- country: Optional[str] = None
- """ISO 3166 country code. Defaults to US if not provided."""
-
-
class ConfigIspProxyConfig(BaseModel):
"""Configuration for an ISP proxy."""
@@ -98,23 +90,14 @@ class ConfigCustomProxyConfig(BaseModel):
Config: TypeAlias = Union[
- ConfigDatacenterProxyConfig,
- ConfigIspProxyConfig,
- ConfigResidentialProxyConfig,
- ConfigMobileProxyConfig,
- ConfigCustomProxyConfig,
+ ConfigIspProxyConfig, ConfigResidentialProxyConfig, ConfigMobileProxyConfig, ConfigCustomProxyConfig
]
class ProxyRetrieveResponse(BaseModel):
"""Configuration for routing traffic through a proxy."""
- type: Literal["datacenter", "isp", "residential", "mobile", "custom"]
- """Proxy type to use.
-
- In terms of quality for avoiding bot-detection, from best to worst: `mobile` >
- `residential` > `isp` > `datacenter`.
- """
+ type: Literal["isp", "residential", "mobile", "custom"]
id: Optional[str] = None
@@ -122,7 +105,7 @@ class ProxyRetrieveResponse(BaseModel):
"""Hostnames that should bypass the parent proxy and connect directly."""
config: Optional[Config] = None
- """Configuration specific to the selected proxy `type`."""
+ """Configuration for an ISP proxy."""
ip_address: Optional[str] = None
"""IP address that the proxy uses when making requests."""
diff --git a/src/kernel/types/proxy_update_response.py b/src/kernel/types/proxy_update_response.py
index 85b8d5ab..04b81b1e 100644
--- a/src/kernel/types/proxy_update_response.py
+++ b/src/kernel/types/proxy_update_response.py
@@ -9,7 +9,6 @@
__all__ = [
"ProxyUpdateResponse",
"Config",
- "ConfigDatacenterProxyConfig",
"ConfigIspProxyConfig",
"ConfigResidentialProxyConfig",
"ConfigMobileProxyConfig",
@@ -17,13 +16,6 @@
]
-class ConfigDatacenterProxyConfig(BaseModel):
- """Configuration for a datacenter proxy."""
-
- country: Optional[str] = None
- """ISO 3166 country code. Defaults to US if not provided."""
-
-
class ConfigIspProxyConfig(BaseModel):
"""Configuration for an ISP proxy."""
@@ -98,23 +90,14 @@ class ConfigCustomProxyConfig(BaseModel):
Config: TypeAlias = Union[
- ConfigDatacenterProxyConfig,
- ConfigIspProxyConfig,
- ConfigResidentialProxyConfig,
- ConfigMobileProxyConfig,
- ConfigCustomProxyConfig,
+ ConfigIspProxyConfig, ConfigResidentialProxyConfig, ConfigMobileProxyConfig, ConfigCustomProxyConfig
]
class ProxyUpdateResponse(BaseModel):
"""Configuration for routing traffic through a proxy."""
- type: Literal["datacenter", "isp", "residential", "mobile", "custom"]
- """Proxy type to use.
-
- In terms of quality for avoiding bot-detection, from best to worst: `mobile` >
- `residential` > `isp` > `datacenter`.
- """
+ type: Literal["isp", "residential", "mobile", "custom"]
id: Optional[str] = None
@@ -122,7 +105,7 @@ class ProxyUpdateResponse(BaseModel):
"""Hostnames that should bypass the parent proxy and connect directly."""
config: Optional[Config] = None
- """Configuration specific to the selected proxy `type`."""
+ """Configuration for an ISP proxy."""
ip_address: Optional[str] = None
"""IP address that the proxy uses when making requests."""
diff --git a/tests/api_resources/test_browsers.py b/tests/api_resources/test_browsers.py
index 92f2149d..6aff8010 100644
--- a/tests/api_resources/test_browsers.py
+++ b/tests/api_resources/test_browsers.py
@@ -210,6 +210,7 @@ def test_method_update_with_all_params(self, client: Kernel) -> None:
id_or_name="htzv5orfit78e1m2biiifpbv",
disable_default_proxy=True,
name="checkout-flow-1",
+ network={"allowed_hosts": ["example.com", "*.example.com"]},
profile={
"id": "id",
"name": "name",
@@ -755,6 +756,7 @@ async def test_method_update_with_all_params(self, async_client: AsyncKernel) ->
id_or_name="htzv5orfit78e1m2biiifpbv",
disable_default_proxy=True,
name="checkout-flow-1",
+ network={"allowed_hosts": ["example.com", "*.example.com"]},
profile={
"id": "id",
"name": "name",
diff --git a/tests/api_resources/test_proxies.py b/tests/api_resources/test_proxies.py
index bdc48364..7fc6412c 100644
--- a/tests/api_resources/test_proxies.py
+++ b/tests/api_resources/test_proxies.py
@@ -28,7 +28,7 @@ class TestProxies:
@parametrize
def test_method_create(self, client: Kernel) -> None:
proxy = client.proxies.create(
- type="datacenter",
+ type="isp",
)
assert_matches_type(ProxyCreateResponse, proxy, path=["response"])
@@ -36,7 +36,7 @@ def test_method_create(self, client: Kernel) -> None:
@parametrize
def test_method_create_with_all_params(self, client: Kernel) -> None:
proxy = client.proxies.create(
- type="datacenter",
+ type="isp",
bypass_hosts=["string"],
config={"country": "US"},
name="name",
@@ -48,7 +48,7 @@ def test_method_create_with_all_params(self, client: Kernel) -> None:
@parametrize
def test_raw_response_create(self, client: Kernel) -> None:
response = client.proxies.with_raw_response.create(
- type="datacenter",
+ type="isp",
)
assert response.is_closed is True
@@ -60,7 +60,7 @@ def test_raw_response_create(self, client: Kernel) -> None:
@parametrize
def test_streaming_response_create(self, client: Kernel) -> None:
with client.proxies.with_streaming_response.create(
- type="datacenter",
+ type="isp",
) as response:
assert not response.is_closed
assert response.http_request.headers.get("X-Stainless-Lang") == "python"
@@ -300,7 +300,7 @@ class TestAsyncProxies:
@parametrize
async def test_method_create(self, async_client: AsyncKernel) -> None:
proxy = await async_client.proxies.create(
- type="datacenter",
+ type="isp",
)
assert_matches_type(ProxyCreateResponse, proxy, path=["response"])
@@ -308,7 +308,7 @@ async def test_method_create(self, async_client: AsyncKernel) -> None:
@parametrize
async def test_method_create_with_all_params(self, async_client: AsyncKernel) -> None:
proxy = await async_client.proxies.create(
- type="datacenter",
+ type="isp",
bypass_hosts=["string"],
config={"country": "US"},
name="name",
@@ -320,7 +320,7 @@ async def test_method_create_with_all_params(self, async_client: AsyncKernel) ->
@parametrize
async def test_raw_response_create(self, async_client: AsyncKernel) -> None:
response = await async_client.proxies.with_raw_response.create(
- type="datacenter",
+ type="isp",
)
assert response.is_closed is True
@@ -332,7 +332,7 @@ async def test_raw_response_create(self, async_client: AsyncKernel) -> None:
@parametrize
async def test_streaming_response_create(self, async_client: AsyncKernel) -> None:
async with async_client.proxies.with_streaming_response.create(
- type="datacenter",
+ type="isp",
) as response:
assert not response.is_closed
assert response.http_request.headers.get("X-Stainless-Lang") == "python"
From c59f4871e5dfafd294d9fe9338555d07a6ecd491 Mon Sep 17 00:00:00 2001
From: "kernel-internal[bot]"
<260533166+kernel-internal[bot]@users.noreply.github.com>
Date: Wed, 7 Oct 2026 19:54:00 +0000
Subject: [PATCH 336/366] feat: Create and poll 1Password access requests over
the 1Password API
Stainless-Generated-From: 1b3efec91270995f0e30cea529f9cb0388339547
---
src/kernel/resources/vaults/items.py | 10 --------
.../vaults/item_perform_operation_params.py | 5 ----
...cess_vault_item_operation_request_param.py | 5 +---
tests/api_resources/vaults/test_items.py | 24 -------------------
4 files changed, 1 insertion(+), 43 deletions(-)
diff --git a/src/kernel/resources/vaults/items.py b/src/kernel/resources/vaults/items.py
index 6a71734a..b2143a1f 100644
--- a/src/kernel/resources/vaults/items.py
+++ b/src/kernel/resources/vaults/items.py
@@ -610,7 +610,6 @@ def perform_operation(
key: str,
*,
id_or_name: str,
- browser_id: str,
type: Literal["1pw_create_access_request"],
goal: str | Omit = omit,
keywords: SequenceNotStr[str] | Omit = omit,
@@ -645,8 +644,6 @@ def perform_operation(
leave the outcome unknown; do not automatically retry.
Args:
- browser_id: Kernel browser session used to invoke the extension.
-
extra_headers: Send extra headers
extra_query: Add additional query parameters to the request
@@ -663,7 +660,6 @@ def perform_operation(
key: str,
*,
id_or_name: str,
- browser_id: str,
type: Literal["1pw_access_request_status"],
timeout_seconds: int | Omit = omit,
# Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
@@ -934,7 +930,6 @@ def perform_operation(
["id_or_name", "type"],
["id_or_name", "checkout", "type"],
["id_or_name", "browser_id", "fields", "type"],
- ["id_or_name", "browser_id", "type"],
["id_or_name", "browser_id", "page_url", "type"],
["id_or_name", "access_token", "type"],
["id_or_name", "bindings", "browser_id", "input", "page_url", "tool_ref", "type"],
@@ -1789,7 +1784,6 @@ async def perform_operation(
key: str,
*,
id_or_name: str,
- browser_id: str,
type: Literal["1pw_create_access_request"],
goal: str | Omit = omit,
keywords: SequenceNotStr[str] | Omit = omit,
@@ -1824,8 +1818,6 @@ async def perform_operation(
leave the outcome unknown; do not automatically retry.
Args:
- browser_id: Kernel browser session used to invoke the extension.
-
extra_headers: Send extra headers
extra_query: Add additional query parameters to the request
@@ -1842,7 +1834,6 @@ async def perform_operation(
key: str,
*,
id_or_name: str,
- browser_id: str,
type: Literal["1pw_access_request_status"],
timeout_seconds: int | Omit = omit,
# Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
@@ -2113,7 +2104,6 @@ async def perform_operation(
["id_or_name", "type"],
["id_or_name", "checkout", "type"],
["id_or_name", "browser_id", "fields", "type"],
- ["id_or_name", "browser_id", "type"],
["id_or_name", "browser_id", "page_url", "type"],
["id_or_name", "access_token", "type"],
["id_or_name", "bindings", "browser_id", "input", "page_url", "tool_ref", "type"],
diff --git a/src/kernel/types/vaults/item_perform_operation_params.py b/src/kernel/types/vaults/item_perform_operation_params.py
index 79871faf..0abfc780 100644
--- a/src/kernel/types/vaults/item_perform_operation_params.py
+++ b/src/kernel/types/vaults/item_perform_operation_params.py
@@ -81,9 +81,6 @@ class FillVaultItemOperationRequest(TypedDict, total=False):
class OnePasswordRequestAccessVaultItemOperationRequest(TypedDict, total=False):
id_or_name: Required[str]
- browser_id: Required[str]
- """Kernel browser session used to invoke the extension."""
-
type: Required[Literal["1pw_create_access_request"]]
goal: str
@@ -96,8 +93,6 @@ class OnePasswordRequestAccessVaultItemOperationRequest(TypedDict, total=False):
class OnePasswordPollAccessVaultItemOperationRequest(TypedDict, total=False):
id_or_name: Required[str]
- browser_id: Required[str]
-
type: Required[Literal["1pw_access_request_status"]]
timeout_seconds: int
diff --git a/src/kernel/types/vaults/one_password_request_access_vault_item_operation_request_param.py b/src/kernel/types/vaults/one_password_request_access_vault_item_operation_request_param.py
index 259a31b4..f856f17f 100644
--- a/src/kernel/types/vaults/one_password_request_access_vault_item_operation_request_param.py
+++ b/src/kernel/types/vaults/one_password_request_access_vault_item_operation_request_param.py
@@ -11,12 +11,9 @@
class OnePasswordRequestAccessVaultItemOperationRequestParam(TypedDict, total=False):
"""
- Request access to login entries in the end-user's own, non-shared 1Password vault through the browser extension, auto-loaded into the browser. The end-user approves access in the 1Password app. Shared-vault items and passkeys are not supported. Per-entry reason and keywords overrides are only supported for a single login entry.
+ Request access to login entries in the end-user's own, non-shared 1Password vault. No browser is needed; the end-user approves access in the 1Password app. Shared-vault items and passkeys are not supported. Per-entry reason and keywords overrides are only supported for a single login entry.
"""
- browser_id: Required[str]
- """Kernel browser session used to invoke the extension."""
-
type: Required[Literal["1pw_create_access_request"]]
goal: str
diff --git a/tests/api_resources/vaults/test_items.py b/tests/api_resources/vaults/test_items.py
index 3cb9c56f..3127f07e 100644
--- a/tests/api_resources/vaults/test_items.py
+++ b/tests/api_resources/vaults/test_items.py
@@ -881,7 +881,6 @@ def test_method_perform_operation_overload_5(self, client: Kernel) -> None:
item = client.vaults.items.perform_operation(
key="key",
id_or_name="id_or_name",
- browser_id="x",
type="1pw_create_access_request",
)
assert_matches_type(VaultItemOperationResponse, item, path=["response"])
@@ -892,7 +891,6 @@ def test_method_perform_operation_with_all_params_overload_5(self, client: Kerne
item = client.vaults.items.perform_operation(
key="key",
id_or_name="id_or_name",
- browser_id="x",
type="1pw_create_access_request",
goal="goal",
keywords=["x"],
@@ -906,7 +904,6 @@ def test_raw_response_perform_operation_overload_5(self, client: Kernel) -> None
response = client.vaults.items.with_raw_response.perform_operation(
key="key",
id_or_name="id_or_name",
- browser_id="x",
type="1pw_create_access_request",
)
@@ -921,7 +918,6 @@ def test_streaming_response_perform_operation_overload_5(self, client: Kernel) -
with client.vaults.items.with_streaming_response.perform_operation(
key="key",
id_or_name="id_or_name",
- browser_id="x",
type="1pw_create_access_request",
) as response:
assert not response.is_closed
@@ -939,7 +935,6 @@ def test_path_params_perform_operation_overload_5(self, client: Kernel) -> None:
client.vaults.items.with_raw_response.perform_operation(
key="key",
id_or_name="",
- browser_id="x",
type="1pw_create_access_request",
)
@@ -947,7 +942,6 @@ def test_path_params_perform_operation_overload_5(self, client: Kernel) -> None:
client.vaults.items.with_raw_response.perform_operation(
key="",
id_or_name="id_or_name",
- browser_id="x",
type="1pw_create_access_request",
)
@@ -957,7 +951,6 @@ def test_method_perform_operation_overload_6(self, client: Kernel) -> None:
item = client.vaults.items.perform_operation(
key="key",
id_or_name="id_or_name",
- browser_id="x",
type="1pw_access_request_status",
)
assert_matches_type(VaultItemOperationResponse, item, path=["response"])
@@ -968,7 +961,6 @@ def test_method_perform_operation_with_all_params_overload_6(self, client: Kerne
item = client.vaults.items.perform_operation(
key="key",
id_or_name="id_or_name",
- browser_id="x",
type="1pw_access_request_status",
timeout_seconds=0,
)
@@ -980,7 +972,6 @@ def test_raw_response_perform_operation_overload_6(self, client: Kernel) -> None
response = client.vaults.items.with_raw_response.perform_operation(
key="key",
id_or_name="id_or_name",
- browser_id="x",
type="1pw_access_request_status",
)
@@ -995,7 +986,6 @@ def test_streaming_response_perform_operation_overload_6(self, client: Kernel) -
with client.vaults.items.with_streaming_response.perform_operation(
key="key",
id_or_name="id_or_name",
- browser_id="x",
type="1pw_access_request_status",
) as response:
assert not response.is_closed
@@ -1013,7 +1003,6 @@ def test_path_params_perform_operation_overload_6(self, client: Kernel) -> None:
client.vaults.items.with_raw_response.perform_operation(
key="key",
id_or_name="",
- browser_id="x",
type="1pw_access_request_status",
)
@@ -1021,7 +1010,6 @@ def test_path_params_perform_operation_overload_6(self, client: Kernel) -> None:
client.vaults.items.with_raw_response.perform_operation(
key="",
id_or_name="id_or_name",
- browser_id="x",
type="1pw_access_request_status",
)
@@ -2755,7 +2743,6 @@ async def test_method_perform_operation_overload_5(self, async_client: AsyncKern
item = await async_client.vaults.items.perform_operation(
key="key",
id_or_name="id_or_name",
- browser_id="x",
type="1pw_create_access_request",
)
assert_matches_type(VaultItemOperationResponse, item, path=["response"])
@@ -2766,7 +2753,6 @@ async def test_method_perform_operation_with_all_params_overload_5(self, async_c
item = await async_client.vaults.items.perform_operation(
key="key",
id_or_name="id_or_name",
- browser_id="x",
type="1pw_create_access_request",
goal="goal",
keywords=["x"],
@@ -2780,7 +2766,6 @@ async def test_raw_response_perform_operation_overload_5(self, async_client: Asy
response = await async_client.vaults.items.with_raw_response.perform_operation(
key="key",
id_or_name="id_or_name",
- browser_id="x",
type="1pw_create_access_request",
)
@@ -2795,7 +2780,6 @@ async def test_streaming_response_perform_operation_overload_5(self, async_clien
async with async_client.vaults.items.with_streaming_response.perform_operation(
key="key",
id_or_name="id_or_name",
- browser_id="x",
type="1pw_create_access_request",
) as response:
assert not response.is_closed
@@ -2813,7 +2797,6 @@ async def test_path_params_perform_operation_overload_5(self, async_client: Asyn
await async_client.vaults.items.with_raw_response.perform_operation(
key="key",
id_or_name="",
- browser_id="x",
type="1pw_create_access_request",
)
@@ -2821,7 +2804,6 @@ async def test_path_params_perform_operation_overload_5(self, async_client: Asyn
await async_client.vaults.items.with_raw_response.perform_operation(
key="",
id_or_name="id_or_name",
- browser_id="x",
type="1pw_create_access_request",
)
@@ -2831,7 +2813,6 @@ async def test_method_perform_operation_overload_6(self, async_client: AsyncKern
item = await async_client.vaults.items.perform_operation(
key="key",
id_or_name="id_or_name",
- browser_id="x",
type="1pw_access_request_status",
)
assert_matches_type(VaultItemOperationResponse, item, path=["response"])
@@ -2842,7 +2823,6 @@ async def test_method_perform_operation_with_all_params_overload_6(self, async_c
item = await async_client.vaults.items.perform_operation(
key="key",
id_or_name="id_or_name",
- browser_id="x",
type="1pw_access_request_status",
timeout_seconds=0,
)
@@ -2854,7 +2834,6 @@ async def test_raw_response_perform_operation_overload_6(self, async_client: Asy
response = await async_client.vaults.items.with_raw_response.perform_operation(
key="key",
id_or_name="id_or_name",
- browser_id="x",
type="1pw_access_request_status",
)
@@ -2869,7 +2848,6 @@ async def test_streaming_response_perform_operation_overload_6(self, async_clien
async with async_client.vaults.items.with_streaming_response.perform_operation(
key="key",
id_or_name="id_or_name",
- browser_id="x",
type="1pw_access_request_status",
) as response:
assert not response.is_closed
@@ -2887,7 +2865,6 @@ async def test_path_params_perform_operation_overload_6(self, async_client: Asyn
await async_client.vaults.items.with_raw_response.perform_operation(
key="key",
id_or_name="",
- browser_id="x",
type="1pw_access_request_status",
)
@@ -2895,7 +2872,6 @@ async def test_path_params_perform_operation_overload_6(self, async_client: Asyn
await async_client.vaults.items.with_raw_response.perform_operation(
key="",
id_or_name="id_or_name",
- browser_id="x",
type="1pw_access_request_status",
)
From 57b81ba3b042c9dd75979a33d5b34be322f13025 Mon Sep 17 00:00:00 2001
From: "kernel-internal[bot]"
<260533166+kernel-internal[bot]@users.noreply.github.com>
Date: Wed, 7 Oct 2026 19:59:10 +0000
Subject: [PATCH 337/366] release: 0.121.0
---
.release-please-manifest.json | 2 +-
CHANGELOG.md | 8 ++++++++
pyproject.toml | 2 +-
src/kernel/_version.py | 2 +-
4 files changed, 11 insertions(+), 3 deletions(-)
diff --git a/.release-please-manifest.json b/.release-please-manifest.json
index 486a203f..4bab9967 100644
--- a/.release-please-manifest.json
+++ b/.release-please-manifest.json
@@ -1,3 +1,3 @@
{
- ".": "0.120.0"
+ ".": "0.121.0"
}
\ No newline at end of file
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 976a2d24..9117cbd6 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,13 @@
# Changelog
+## [0.121.0](https://github.com/kernel/kernel-python-sdk/compare/v0.120.0...v0.121.0) (2026-10-07)
+
+
+### Features
+
+* Create and poll 1Password access requests over the 1Password API ([c59f487](https://github.com/kernel/kernel-python-sdk/commit/c59f4871e5dfafd294d9fe9338555d07a6ecd491))
+* Deprecate datacenter proxies in the API ([c34882c](https://github.com/kernel/kernel-python-sdk/commit/c34882c77d247ace0fa23d37da1c3574d21bd1b0))
+
## [0.120.0](https://github.com/kernel/kernel-python-sdk/compare/v0.119.0...v0.120.0) (2026-10-06)
diff --git a/pyproject.toml b/pyproject.toml
index 7f92defc..67a0492d 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -1,6 +1,6 @@
[project]
name = "kernel"
-version = "0.120.0"
+version = "0.121.0"
description = "The official Python library for the kernel API"
dynamic = ["readme"]
license = "Apache-2.0"
diff --git a/src/kernel/_version.py b/src/kernel/_version.py
index a819522f..7bb50d4b 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.120.0" # x-release-please-version
+__version__ = "0.121.0" # x-release-please-version
From 669ea5294ab1da43e5042209992364ca009f7053 Mon Sep 17 00:00:00 2001
From: "kernel-internal[bot]"
<260533166+kernel-internal[bot]@users.noreply.github.com>
Date: Thu, 8 Oct 2026 18:32:36 +0000
Subject: [PATCH 338/366] feat: Support proxy routes in browser pools
Stainless-Generated-From: 79bf8dc71d82866e1c311325ca1fad69a10e1af5
---
src/kernel/resources/browser_pools.py | 24 ++++++++++++-------
src/kernel/types/browser_network_config.py | 19 ++++++++-------
.../types/browser_network_config_param.py | 19 ++++++++-------
.../types/browser_network_update_param.py | 22 +++++++++--------
.../types/browser_pool_create_params.py | 6 ++++-
.../types/browser_pool_update_params.py | 8 ++++---
.../browser_captcha_challenge_result_event.py | 18 +++++++++-----
.../browser_captcha_solve_result_event.py | 21 +++++++++++-----
.../browser_captcha_solve_started_event.py | 15 +++++++++---
9 files changed, 97 insertions(+), 55 deletions(-)
diff --git a/src/kernel/resources/browser_pools.py b/src/kernel/resources/browser_pools.py
index 6c34209e..a4257b83 100644
--- a/src/kernel/resources/browser_pools.py
+++ b/src/kernel/resources/browser_pools.py
@@ -123,7 +123,9 @@ def create(
name: Optional name for the browser pool. Must be unique within the project.
- network: Network configuration applied to browsers in this pool.
+ network: Network configuration applied to browsers in this pool. Proxy routes require
+ proxy-v3; the pool will not provision browsers through Envoy if proxy-v3 is
+ unavailable.
profile: Profile configuration for browsers in a pool. Provide either id or name.
Profiles must be created beforehand. Unlike single browser sessions, pools load
@@ -318,9 +320,11 @@ def update(
network: If provided, replaces the pool's network configuration. Omit to leave the
existing configuration unchanged; an empty object ({}) removes it, while
- network: {private_hosts: []} sets an explicit empty list. Only applied to
- browsers created in the pool after the update; browsers already in the pool keep
- their configuration until discarded (see discard_all_idle).
+ network: {private_hosts: []} or network: {proxy_routes: []} sets an explicit
+ empty list. Proxy routes require proxy-v3; the pool will not provision browsers
+ through Envoy if proxy-v3 is unavailable. Only applied to browsers created in
+ the pool after the update; browsers already in the pool keep their configuration
+ until discarded (see discard_all_idle).
profile: Profile configuration for browsers in a pool. Provide either id or name.
Profiles must be created beforehand. Unlike single browser sessions, pools load
@@ -770,7 +774,9 @@ async def create(
name: Optional name for the browser pool. Must be unique within the project.
- network: Network configuration applied to browsers in this pool.
+ network: Network configuration applied to browsers in this pool. Proxy routes require
+ proxy-v3; the pool will not provision browsers through Envoy if proxy-v3 is
+ unavailable.
profile: Profile configuration for browsers in a pool. Provide either id or name.
Profiles must be created beforehand. Unlike single browser sessions, pools load
@@ -965,9 +971,11 @@ async def update(
network: If provided, replaces the pool's network configuration. Omit to leave the
existing configuration unchanged; an empty object ({}) removes it, while
- network: {private_hosts: []} sets an explicit empty list. Only applied to
- browsers created in the pool after the update; browsers already in the pool keep
- their configuration until discarded (see discard_all_idle).
+ network: {private_hosts: []} or network: {proxy_routes: []} sets an explicit
+ empty list. Proxy routes require proxy-v3; the pool will not provision browsers
+ through Envoy if proxy-v3 is unavailable. Only applied to browsers created in
+ the pool after the update; browsers already in the pool keep their configuration
+ until discarded (see discard_all_idle).
profile: Profile configuration for browsers in a pool. Provide either id or name.
Profiles must be created beforehand. Unlike single browser sessions, pools load
diff --git a/src/kernel/types/browser_network_config.py b/src/kernel/types/browser_network_config.py
index 6e8f570a..19de4d72 100644
--- a/src/kernel/types/browser_network_config.py
+++ b/src/kernel/types/browser_network_config.py
@@ -51,8 +51,9 @@ class BrowserNetworkConfig(BaseModel):
Enforced at Kernel's egress proxy only: destinations in private_hosts, and
processes in the browser VM that do not use the browser's proxy, are not
filtered, and Kernel's own control traffic is always allowed. Can be replaced or
- removed while the session runs with PATCH /browsers/{id_or_name}. Requires proxy
- v3. Not supported on browser pools.
+ removed while the session runs with PATCH /browsers/{id_or_name}, but not added
+ to a browser created without one. Requires proxy v3. Not supported on browser
+ pools.
"""
private_hosts: Optional[List[str]] = None
@@ -84,9 +85,9 @@ class BrowserNetworkConfig(BaseModel):
proxy_routes: Optional[List[ProxyRoute]] = None
"""Per-destination proxy routes for a browser session.
- After setup, a destination hostname is matched against every route's hosts,
- regardless of port; route order does not matter. An exact hostname beats a
- wildcard, and a longer wildcard suffix beats a shorter one (for a.b.example.com:
+ A destination hostname is matched against every route's hosts, regardless of
+ port; route order does not matter. An exact hostname beats a wildcard, and a
+ longer wildcard suffix beats a shorter one (for a.b.example.com:
"a.b.example.com" > "_.b.example.com" > "_.example.com"). A host pattern may
appear in only one route. "\\**.example.com" matches subdomains only, not
example.com. A matched request selects the route's proxy instead of the
@@ -94,8 +95,8 @@ class BrowserNetworkConfig(BaseModel):
bypass_hosts still apply. If the route proxy becomes unavailable, matched
requests fail closed without falling back. Requests that match no route use the
session's default egress from the top-level proxy field (or the browser default
- when proxy is omitted: stealth proxy or direct egress). Routes take effect once
- the session is created; start_url and other traffic during browser setup use the
- top-level proxy. Setting routes requires proxy v3. Not supported on browser
- pools.
+ when proxy is omitted: stealth proxy or direct egress). Routes apply from the
+ start of the session, including to start_url and other traffic during browser
+ setup. Setting routes requires proxy v3. Browser pools also support these
+ routes.
"""
diff --git a/src/kernel/types/browser_network_config_param.py b/src/kernel/types/browser_network_config_param.py
index 3d2e2d88..94877452 100644
--- a/src/kernel/types/browser_network_config_param.py
+++ b/src/kernel/types/browser_network_config_param.py
@@ -54,8 +54,9 @@ class BrowserNetworkConfigParam(TypedDict, total=False):
Enforced at Kernel's egress proxy only: destinations in private_hosts, and
processes in the browser VM that do not use the browser's proxy, are not
filtered, and Kernel's own control traffic is always allowed. Can be replaced or
- removed while the session runs with PATCH /browsers/{id_or_name}. Requires proxy
- v3. Not supported on browser pools.
+ removed while the session runs with PATCH /browsers/{id_or_name}, but not added
+ to a browser created without one. Requires proxy v3. Not supported on browser
+ pools.
"""
private_hosts: SequenceNotStr[str]
@@ -87,9 +88,9 @@ class BrowserNetworkConfigParam(TypedDict, total=False):
proxy_routes: Iterable[ProxyRoute]
"""Per-destination proxy routes for a browser session.
- After setup, a destination hostname is matched against every route's hosts,
- regardless of port; route order does not matter. An exact hostname beats a
- wildcard, and a longer wildcard suffix beats a shorter one (for a.b.example.com:
+ A destination hostname is matched against every route's hosts, regardless of
+ port; route order does not matter. An exact hostname beats a wildcard, and a
+ longer wildcard suffix beats a shorter one (for a.b.example.com:
"a.b.example.com" > "_.b.example.com" > "_.example.com"). A host pattern may
appear in only one route. "\\**.example.com" matches subdomains only, not
example.com. A matched request selects the route's proxy instead of the
@@ -97,8 +98,8 @@ class BrowserNetworkConfigParam(TypedDict, total=False):
bypass_hosts still apply. If the route proxy becomes unavailable, matched
requests fail closed without falling back. Requests that match no route use the
session's default egress from the top-level proxy field (or the browser default
- when proxy is omitted: stealth proxy or direct egress). Routes take effect once
- the session is created; start_url and other traffic during browser setup use the
- top-level proxy. Setting routes requires proxy v3. Not supported on browser
- pools.
+ when proxy is omitted: stealth proxy or direct egress). Routes apply from the
+ start of the session, including to start_url and other traffic during browser
+ setup. Setting routes requires proxy v3. Browser pools also support these
+ routes.
"""
diff --git a/src/kernel/types/browser_network_update_param.py b/src/kernel/types/browser_network_update_param.py
index e96fa723..62163e13 100644
--- a/src/kernel/types/browser_network_update_param.py
+++ b/src/kernel/types/browser_network_update_param.py
@@ -19,14 +19,16 @@ class BrowserNetworkUpdateParam(TypedDict, total=False):
allowed_hosts: Optional[SequenceNotStr[str]]
"""
Replaces the session's egress allowlist, using the same entry rules as
- network.allowed_hosts on create. Omit to leave the allowlist unchanged, or set
- to null to remove it and return to unfiltered egress; an empty list is invalid.
- The new list applies without restarting the browser: new requests to
- destinations it no longer allows are refused within a few seconds, and open
- connections to them are closed within about 30 seconds, or up to 10 minutes
- during a Kernel deploy. Connections to destinations it still allows, such as
- WebSockets, stay open. A start_url in the same request must be allowed by the
- updated list, and is loaded only after the list takes effect. Requires a browser
- created with proxy v3, and not supported on pooled browsers. If the request
- fails, retry it: the new list may already apply to some requests.
+ network.allowed_hosts on create. Only an allowlist the browser was created with
+ can be changed: a browser created without one can't be given one, and an
+ allowlist removed with null can't be added back. Omit to leave the allowlist
+ unchanged, or set to null to remove it and return to unfiltered egress; an empty
+ list is invalid. The new list applies without restarting the browser: new
+ requests to destinations it no longer allows are refused within a few seconds,
+ and open connections to them are closed within about 30 seconds, or up to 10
+ minutes during a Kernel deploy. Connections to destinations it still allows,
+ such as WebSockets, stay open. A start_url in the same request must be allowed
+ by the updated list, and is loaded only after the list takes effect. Requires a
+ browser created with proxy v3, and not supported on pooled browsers. If the
+ request fails, retry it: the new list may already apply to some requests.
"""
diff --git a/src/kernel/types/browser_pool_create_params.py b/src/kernel/types/browser_pool_create_params.py
index 3929b44c..f078c393 100644
--- a/src/kernel/types/browser_pool_create_params.py
+++ b/src/kernel/types/browser_pool_create_params.py
@@ -68,7 +68,11 @@ class BrowserPoolCreateParams(TypedDict, total=False):
"""Optional name for the browser pool. Must be unique within the project."""
network: BrowserNetworkConfigParam
- """Network configuration applied to browsers in this pool."""
+ """Network configuration applied to browsers in this pool.
+
+ Proxy routes require proxy-v3; the pool will not provision browsers through
+ Envoy if proxy-v3 is unavailable.
+ """
profile: Profile
"""Profile configuration for browsers in a pool.
diff --git a/src/kernel/types/browser_pool_update_params.py b/src/kernel/types/browser_pool_update_params.py
index ebf5cd35..2731b6ea 100644
--- a/src/kernel/types/browser_pool_update_params.py
+++ b/src/kernel/types/browser_pool_update_params.py
@@ -80,9 +80,11 @@ class BrowserPoolUpdateParams(TypedDict, total=False):
"""If provided, replaces the pool's network configuration.
Omit to leave the existing configuration unchanged; an empty object ({}) removes
- it, while network: {private_hosts: []} sets an explicit empty list. Only applied
- to browsers created in the pool after the update; browsers already in the pool
- keep their configuration until discarded (see discard_all_idle).
+ it, while network: {private_hosts: []} or network: {proxy_routes: []} sets an
+ explicit empty list. Proxy routes require proxy-v3; the pool will not provision
+ browsers through Envoy if proxy-v3 is unavailable. Only applied to browsers
+ created in the pool after the update; browsers already in the pool keep their
+ configuration until discarded (see discard_all_idle).
"""
profile: Profile
diff --git a/src/kernel/types/browsers/browser_captcha_challenge_result_event.py b/src/kernel/types/browsers/browser_captcha_challenge_result_event.py
index 978c49cf..103dda73 100644
--- a/src/kernel/types/browsers/browser_captcha_challenge_result_event.py
+++ b/src/kernel/types/browsers/browser_captcha_challenge_result_event.py
@@ -16,12 +16,7 @@ class Data(BaseModel):
"""
captcha_type: Literal["hcaptcha", "recaptcha_v2", "recaptcha_v3", "turnstile", "geetest", "press_and_hold", "other"]
- """Captcha kind.
-
- Enterprise reCAPTCHA variants are grouped into their version bucket
- (recaptcha_v2 or recaptcha_v3), press-and-hold challenges use press_and_hold,
- and unlisted kinds use other.
- """
+ """Deprecated: use captcha_provider."""
challenge_id: str
"""Opaque identifier shared by events for one visible challenge.
@@ -53,6 +48,17 @@ class Data(BaseModel):
the guarded action succeeded.
"""
+ captcha_provider: Optional[
+ Literal["hcaptcha", "recaptcha_v2", "recaptcha_v3", "turnstile", "geetest", "arkose", "human", "other"]
+ ] = None
+ """Captcha product the challenge belongs to, not the service that solved it.
+
+ Enterprise reCAPTCHA variants are grouped into their version bucket
+ (recaptcha_v2 or recaptcha_v3), FunCaptcha uses arkose, press-and-hold
+ challenges served by HUMAN (formerly PerimeterX) use human, and unlisted
+ products use other.
+ """
+
website_host: Optional[str] = None
"""Host of the page where the challenge appeared."""
diff --git a/src/kernel/types/browsers/browser_captcha_solve_result_event.py b/src/kernel/types/browsers/browser_captcha_solve_result_event.py
index 2e023cc5..1feb5d77 100644
--- a/src/kernel/types/browsers/browser_captcha_solve_result_event.py
+++ b/src/kernel/types/browsers/browser_captcha_solve_result_event.py
@@ -11,12 +11,7 @@
class Data(BaseModel):
captcha_type: Literal["hcaptcha", "recaptcha_v2", "recaptcha_v3", "turnstile", "geetest", "press_and_hold", "other"]
- """Captcha kind.
-
- Enterprise reCAPTCHA variants are grouped into their version bucket
- (recaptcha_v2 or recaptcha_v3), press-and-hold challenges use press_and_hold,
- and unlisted kinds use other.
- """
+ """Deprecated: use captcha_provider and task_kind."""
duration_ms: float
"""Wall-clock duration from solve start to terminal outcome.
@@ -33,6 +28,17 @@ class Data(BaseModel):
budget. abandoned: caller cancelled or the page navigated away mid-solve.
"""
+ captcha_provider: Optional[
+ Literal["hcaptcha", "recaptcha_v2", "recaptcha_v3", "turnstile", "geetest", "arkose", "human", "other"]
+ ] = None
+ """Captcha product the challenge belongs to, not the service that solved it.
+
+ Enterprise reCAPTCHA variants are grouped into their version bucket
+ (recaptcha_v2 or recaptcha_v3), FunCaptcha uses arkose, press-and-hold
+ challenges served by HUMAN (formerly PerimeterX) use human, and unlisted
+ products use other.
+ """
+
challenge_id: Optional[str] = None
"""Opaque identifier shared by events for one visible challenge.
@@ -50,6 +56,9 @@ class Data(BaseModel):
task_id: Optional[str] = None
"""Opaque identifier shared with the matching captcha_solve_started."""
+ task_kind: Optional[Literal["token", "image_challenge", "press_and_hold"]] = None
+ """What the solver task produces. Absent when the producer cannot tell."""
+
website_host: Optional[str] = None
"""Host of the page where the captcha was solved."""
diff --git a/src/kernel/types/browsers/browser_captcha_solve_started_event.py b/src/kernel/types/browsers/browser_captcha_solve_started_event.py
index 546c1c38..12dd56e6 100644
--- a/src/kernel/types/browsers/browser_captcha_solve_started_event.py
+++ b/src/kernel/types/browsers/browser_captcha_solve_started_event.py
@@ -16,11 +16,17 @@ class Data(BaseModel):
"""
captcha_type: Literal["hcaptcha", "recaptcha_v2", "recaptcha_v3", "turnstile", "geetest", "press_and_hold", "other"]
- """Captcha kind.
+ """Deprecated: use captcha_provider and task_kind."""
+
+ captcha_provider: Optional[
+ Literal["hcaptcha", "recaptcha_v2", "recaptcha_v3", "turnstile", "geetest", "arkose", "human", "other"]
+ ] = None
+ """Captcha product the challenge belongs to, not the service that solved it.
Enterprise reCAPTCHA variants are grouped into their version bucket
- (recaptcha_v2 or recaptcha_v3), press-and-hold challenges use press_and_hold,
- and unlisted kinds use other.
+ (recaptcha_v2 or recaptcha_v3), FunCaptcha uses arkose, press-and-hold
+ challenges served by HUMAN (formerly PerimeterX) use human, and unlisted
+ products use other.
"""
challenge_id: Optional[str] = None
@@ -34,6 +40,9 @@ class Data(BaseModel):
task_id: Optional[str] = None
"""Opaque identifier shared with the matching captcha_solve_result."""
+ task_kind: Optional[Literal["token", "image_challenge", "press_and_hold"]] = None
+ """What the solver task produces. Absent when the producer cannot tell."""
+
website_host: Optional[str] = None
"""Host of the page where the captcha is being solved.
From 56785cad65cda7e757258204de47f1a4a5974088 Mon Sep 17 00:00:00 2001
From: "kernel-internal[bot]"
<260533166+kernel-internal[bot]@users.noreply.github.com>
Date: Thu, 8 Oct 2026 19:03:37 +0000
Subject: [PATCH 339/366] feat: Add feature-gated Korean ISP proxies
Stainless-Generated-From: 1df9968d48c97505d364e1d1dadc0386e4e71eb1
---
src/kernel/types/proxy.py | 3 ++-
src/kernel/types/proxy_check_response.py | 3 ++-
src/kernel/types/proxy_create_params.py | 3 ++-
src/kernel/types/proxy_create_response.py | 3 ++-
src/kernel/types/proxy_list_response.py | 3 ++-
src/kernel/types/proxy_retrieve_response.py | 3 ++-
src/kernel/types/proxy_update_response.py | 3 ++-
7 files changed, 14 insertions(+), 7 deletions(-)
diff --git a/src/kernel/types/proxy.py b/src/kernel/types/proxy.py
index c4061b15..af5045f9 100644
--- a/src/kernel/types/proxy.py
+++ b/src/kernel/types/proxy.py
@@ -31,7 +31,8 @@ class ConfigRegistryManagedProxyCreateConfigIspProxyConfig(BaseModel):
country: Optional[str] = None
"""ISO 3166 country code.
- Supported countries are US, GB, FR, DE, and SG. Defaults to US if not provided.
+ Supported countries are US, GB, FR, DE, SG, and KR. Defaults to US if not
+ provided.
"""
diff --git a/src/kernel/types/proxy_check_response.py b/src/kernel/types/proxy_check_response.py
index ab7fdd8c..75ee4655 100644
--- a/src/kernel/types/proxy_check_response.py
+++ b/src/kernel/types/proxy_check_response.py
@@ -22,7 +22,8 @@ class ConfigIspProxyConfig(BaseModel):
country: Optional[str] = None
"""ISO 3166 country code.
- Supported countries are US, GB, FR, DE, and SG. Defaults to US if not provided.
+ Supported countries are US, GB, FR, DE, SG, and KR. Defaults to US if not
+ provided.
"""
diff --git a/src/kernel/types/proxy_create_params.py b/src/kernel/types/proxy_create_params.py
index 351c44b6..6872e9fb 100644
--- a/src/kernel/types/proxy_create_params.py
+++ b/src/kernel/types/proxy_create_params.py
@@ -39,7 +39,8 @@ class ConfigIspProxyConfig(TypedDict, total=False):
country: str
"""ISO 3166 country code.
- Supported countries are US, GB, FR, DE, and SG. Defaults to US if not provided.
+ Supported countries are US, GB, FR, DE, SG, and KR. Defaults to US if not
+ provided.
"""
diff --git a/src/kernel/types/proxy_create_response.py b/src/kernel/types/proxy_create_response.py
index 556eb680..f4d96af8 100644
--- a/src/kernel/types/proxy_create_response.py
+++ b/src/kernel/types/proxy_create_response.py
@@ -22,7 +22,8 @@ class ConfigIspProxyConfig(BaseModel):
country: Optional[str] = None
"""ISO 3166 country code.
- Supported countries are US, GB, FR, DE, and SG. Defaults to US if not provided.
+ Supported countries are US, GB, FR, DE, SG, and KR. Defaults to US if not
+ provided.
"""
diff --git a/src/kernel/types/proxy_list_response.py b/src/kernel/types/proxy_list_response.py
index 9093b081..82d1bd18 100644
--- a/src/kernel/types/proxy_list_response.py
+++ b/src/kernel/types/proxy_list_response.py
@@ -22,7 +22,8 @@ class ConfigIspProxyConfig(BaseModel):
country: Optional[str] = None
"""ISO 3166 country code.
- Supported countries are US, GB, FR, DE, and SG. Defaults to US if not provided.
+ Supported countries are US, GB, FR, DE, SG, and KR. Defaults to US if not
+ provided.
"""
diff --git a/src/kernel/types/proxy_retrieve_response.py b/src/kernel/types/proxy_retrieve_response.py
index bd140d8f..c59de78a 100644
--- a/src/kernel/types/proxy_retrieve_response.py
+++ b/src/kernel/types/proxy_retrieve_response.py
@@ -22,7 +22,8 @@ class ConfigIspProxyConfig(BaseModel):
country: Optional[str] = None
"""ISO 3166 country code.
- Supported countries are US, GB, FR, DE, and SG. Defaults to US if not provided.
+ Supported countries are US, GB, FR, DE, SG, and KR. Defaults to US if not
+ provided.
"""
diff --git a/src/kernel/types/proxy_update_response.py b/src/kernel/types/proxy_update_response.py
index 04b81b1e..323a3731 100644
--- a/src/kernel/types/proxy_update_response.py
+++ b/src/kernel/types/proxy_update_response.py
@@ -22,7 +22,8 @@ class ConfigIspProxyConfig(BaseModel):
country: Optional[str] = None
"""ISO 3166 country code.
- Supported countries are US, GB, FR, DE, and SG. Defaults to US if not provided.
+ Supported countries are US, GB, FR, DE, SG, and KR. Defaults to US if not
+ provided.
"""
From e419d6d8970f0e78f91b4771969787bb835a6acf Mon Sep 17 00:00:00 2001
From: "kernel-internal[bot]"
<260533166+kernel-internal[bot]@users.noreply.github.com>
Date: Thu, 8 Oct 2026 22:13:30 +0000
Subject: [PATCH 340/366] feat: Describe vault fill as safe to retry
Stainless-Generated-From: 1764fa39f87baa779274e940153ed43cd3325574
---
src/kernel/resources/vaults/items.py | 80 +++++++++----------
...fill_vault_item_operation_request_param.py | 6 +-
2 files changed, 44 insertions(+), 42 deletions(-)
diff --git a/src/kernel/resources/vaults/items.py b/src/kernel/resources/vaults/items.py
index b2143a1f..badbd5bc 100644
--- a/src/kernel/resources/vaults/items.py
+++ b/src/kernel/resources/vaults/items.py
@@ -426,8 +426,8 @@ def perform_operation(
return 400 (invalid request or targets), 403 (access or destination denied), 404
(resource not found), or 409 (item or browser not ready). Once writing starts,
known partial failures and indeterminate field outcomes return 200 with status
- `failed` or `unknown`, not an automatic-retry signal. A transport error may
- leave the outcome unknown; do not automatically retry.
+ `failed` or `unknown`. Fill never submits the page, so it is safe to retry after
+ a failure, an `unknown` outcome, or a transport error.
Args:
extra_headers: Send extra headers
@@ -473,8 +473,8 @@ def perform_operation(
return 400 (invalid request or targets), 403 (access or destination denied), 404
(resource not found), or 409 (item or browser not ready). Once writing starts,
known partial failures and indeterminate field outcomes return 200 with status
- `failed` or `unknown`, not an automatic-retry signal. A transport error may
- leave the outcome unknown; do not automatically retry.
+ `failed` or `unknown`. Fill never submits the page, so it is safe to retry after
+ a failure, an `unknown` outcome, or a transport error.
Args:
extra_headers: Send extra headers
@@ -521,8 +521,8 @@ def perform_operation(
return 400 (invalid request or targets), 403 (access or destination denied), 404
(resource not found), or 409 (item or browser not ready). Once writing starts,
known partial failures and indeterminate field outcomes return 200 with status
- `failed` or `unknown`, not an automatic-retry signal. A transport error may
- leave the outcome unknown; do not automatically retry.
+ `failed` or `unknown`. Fill never submits the page, so it is safe to retry after
+ a failure, an `unknown` outcome, or a transport error.
Args:
checkout: Required when preparing an unused AgentCard card for a supported checkout
@@ -578,8 +578,8 @@ def perform_operation(
return 400 (invalid request or targets), 403 (access or destination denied), 404
(resource not found), or 409 (item or browser not ready). Once writing starts,
known partial failures and indeterminate field outcomes return 200 with status
- `failed` or `unknown`, not an automatic-retry signal. A transport error may
- leave the outcome unknown; do not automatically retry.
+ `failed` or `unknown`. Fill never submits the page, so it is safe to retry after
+ a failure, an `unknown` outcome, or a transport error.
Args:
browser_id: Browser session ID, not a reusable browser name.
@@ -640,8 +640,8 @@ def perform_operation(
return 400 (invalid request or targets), 403 (access or destination denied), 404
(resource not found), or 409 (item or browser not ready). Once writing starts,
known partial failures and indeterminate field outcomes return 200 with status
- `failed` or `unknown`, not an automatic-retry signal. A transport error may
- leave the outcome unknown; do not automatically retry.
+ `failed` or `unknown`. Fill never submits the page, so it is safe to retry after
+ a failure, an `unknown` outcome, or a transport error.
Args:
extra_headers: Send extra headers
@@ -688,8 +688,8 @@ def perform_operation(
return 400 (invalid request or targets), 403 (access or destination denied), 404
(resource not found), or 409 (item or browser not ready). Once writing starts,
known partial failures and indeterminate field outcomes return 200 with status
- `failed` or `unknown`, not an automatic-retry signal. A transport error may
- leave the outcome unknown; do not automatically retry.
+ `failed` or `unknown`. Fill never submits the page, so it is safe to retry after
+ a failure, an `unknown` outcome, or a transport error.
Args:
extra_headers: Send extra headers
@@ -739,8 +739,8 @@ def perform_operation(
return 400 (invalid request or targets), 403 (access or destination denied), 404
(resource not found), or 409 (item or browser not ready). Once writing starts,
known partial failures and indeterminate field outcomes return 200 with status
- `failed` or `unknown`, not an automatic-retry signal. A transport error may
- leave the outcome unknown; do not automatically retry.
+ `failed` or `unknown`. Fill never submits the page, so it is safe to retry after
+ a failure, an `unknown` outcome, or a transport error.
Args:
browser_id: Browser session ID, not a reusable browser name.
@@ -794,8 +794,8 @@ def perform_operation(
return 400 (invalid request or targets), 403 (access or destination denied), 404
(resource not found), or 409 (item or browser not ready). Once writing starts,
known partial failures and indeterminate field outcomes return 200 with status
- `failed` or `unknown`, not an automatic-retry signal. A transport error may
- leave the outcome unknown; do not automatically retry.
+ `failed` or `unknown`. Fill never submits the page, so it is safe to retry after
+ a failure, an `unknown` outcome, or a transport error.
Args:
extra_headers: Send extra headers
@@ -843,8 +843,8 @@ def perform_operation(
return 400 (invalid request or targets), 403 (access or destination denied), 404
(resource not found), or 409 (item or browser not ready). Once writing starts,
known partial failures and indeterminate field outcomes return 200 with status
- `failed` or `unknown`, not an automatic-retry signal. A transport error may
- leave the outcome unknown; do not automatically retry.
+ `failed` or `unknown`. Fill never submits the page, so it is safe to retry after
+ a failure, an `unknown` outcome, or a transport error.
Args:
access_token_expires_at: Optional supplied expiry. Omit to clear the old expiry.
@@ -898,8 +898,8 @@ def perform_operation(
return 400 (invalid request or targets), 403 (access or destination denied), 404
(resource not found), or 409 (item or browser not ready). Once writing starts,
known partial failures and indeterminate field outcomes return 200 with status
- `failed` or `unknown`, not an automatic-retry signal. A transport error may
- leave the outcome unknown; do not automatically retry.
+ `failed` or `unknown`. Fill never submits the page, so it is safe to retry after
+ a failure, an `unknown` outcome, or a transport error.
Args:
browser_id: Browser session ID, not a reusable browser name.
@@ -1600,8 +1600,8 @@ async def perform_operation(
return 400 (invalid request or targets), 403 (access or destination denied), 404
(resource not found), or 409 (item or browser not ready). Once writing starts,
known partial failures and indeterminate field outcomes return 200 with status
- `failed` or `unknown`, not an automatic-retry signal. A transport error may
- leave the outcome unknown; do not automatically retry.
+ `failed` or `unknown`. Fill never submits the page, so it is safe to retry after
+ a failure, an `unknown` outcome, or a transport error.
Args:
extra_headers: Send extra headers
@@ -1647,8 +1647,8 @@ async def perform_operation(
return 400 (invalid request or targets), 403 (access or destination denied), 404
(resource not found), or 409 (item or browser not ready). Once writing starts,
known partial failures and indeterminate field outcomes return 200 with status
- `failed` or `unknown`, not an automatic-retry signal. A transport error may
- leave the outcome unknown; do not automatically retry.
+ `failed` or `unknown`. Fill never submits the page, so it is safe to retry after
+ a failure, an `unknown` outcome, or a transport error.
Args:
extra_headers: Send extra headers
@@ -1695,8 +1695,8 @@ async def perform_operation(
return 400 (invalid request or targets), 403 (access or destination denied), 404
(resource not found), or 409 (item or browser not ready). Once writing starts,
known partial failures and indeterminate field outcomes return 200 with status
- `failed` or `unknown`, not an automatic-retry signal. A transport error may
- leave the outcome unknown; do not automatically retry.
+ `failed` or `unknown`. Fill never submits the page, so it is safe to retry after
+ a failure, an `unknown` outcome, or a transport error.
Args:
checkout: Required when preparing an unused AgentCard card for a supported checkout
@@ -1752,8 +1752,8 @@ async def perform_operation(
return 400 (invalid request or targets), 403 (access or destination denied), 404
(resource not found), or 409 (item or browser not ready). Once writing starts,
known partial failures and indeterminate field outcomes return 200 with status
- `failed` or `unknown`, not an automatic-retry signal. A transport error may
- leave the outcome unknown; do not automatically retry.
+ `failed` or `unknown`. Fill never submits the page, so it is safe to retry after
+ a failure, an `unknown` outcome, or a transport error.
Args:
browser_id: Browser session ID, not a reusable browser name.
@@ -1814,8 +1814,8 @@ async def perform_operation(
return 400 (invalid request or targets), 403 (access or destination denied), 404
(resource not found), or 409 (item or browser not ready). Once writing starts,
known partial failures and indeterminate field outcomes return 200 with status
- `failed` or `unknown`, not an automatic-retry signal. A transport error may
- leave the outcome unknown; do not automatically retry.
+ `failed` or `unknown`. Fill never submits the page, so it is safe to retry after
+ a failure, an `unknown` outcome, or a transport error.
Args:
extra_headers: Send extra headers
@@ -1862,8 +1862,8 @@ async def perform_operation(
return 400 (invalid request or targets), 403 (access or destination denied), 404
(resource not found), or 409 (item or browser not ready). Once writing starts,
known partial failures and indeterminate field outcomes return 200 with status
- `failed` or `unknown`, not an automatic-retry signal. A transport error may
- leave the outcome unknown; do not automatically retry.
+ `failed` or `unknown`. Fill never submits the page, so it is safe to retry after
+ a failure, an `unknown` outcome, or a transport error.
Args:
extra_headers: Send extra headers
@@ -1913,8 +1913,8 @@ async def perform_operation(
return 400 (invalid request or targets), 403 (access or destination denied), 404
(resource not found), or 409 (item or browser not ready). Once writing starts,
known partial failures and indeterminate field outcomes return 200 with status
- `failed` or `unknown`, not an automatic-retry signal. A transport error may
- leave the outcome unknown; do not automatically retry.
+ `failed` or `unknown`. Fill never submits the page, so it is safe to retry after
+ a failure, an `unknown` outcome, or a transport error.
Args:
browser_id: Browser session ID, not a reusable browser name.
@@ -1968,8 +1968,8 @@ async def perform_operation(
return 400 (invalid request or targets), 403 (access or destination denied), 404
(resource not found), or 409 (item or browser not ready). Once writing starts,
known partial failures and indeterminate field outcomes return 200 with status
- `failed` or `unknown`, not an automatic-retry signal. A transport error may
- leave the outcome unknown; do not automatically retry.
+ `failed` or `unknown`. Fill never submits the page, so it is safe to retry after
+ a failure, an `unknown` outcome, or a transport error.
Args:
extra_headers: Send extra headers
@@ -2017,8 +2017,8 @@ async def perform_operation(
return 400 (invalid request or targets), 403 (access or destination denied), 404
(resource not found), or 409 (item or browser not ready). Once writing starts,
known partial failures and indeterminate field outcomes return 200 with status
- `failed` or `unknown`, not an automatic-retry signal. A transport error may
- leave the outcome unknown; do not automatically retry.
+ `failed` or `unknown`. Fill never submits the page, so it is safe to retry after
+ a failure, an `unknown` outcome, or a transport error.
Args:
access_token_expires_at: Optional supplied expiry. Omit to clear the old expiry.
@@ -2072,8 +2072,8 @@ async def perform_operation(
return 400 (invalid request or targets), 403 (access or destination denied), 404
(resource not found), or 409 (item or browser not ready). Once writing starts,
known partial failures and indeterminate field outcomes return 200 with status
- `failed` or `unknown`, not an automatic-retry signal. A transport error may
- leave the outcome unknown; do not automatically retry.
+ `failed` or `unknown`. Fill never submits the page, so it is safe to retry after
+ a failure, an `unknown` outcome, or a transport error.
Args:
browser_id: Browser session ID, not a reusable browser name.
diff --git a/src/kernel/types/vaults/fill_vault_item_operation_request_param.py b/src/kernel/types/vaults/fill_vault_item_operation_request_param.py
index da288f04..f00f5369 100644
--- a/src/kernel/types/vaults/fill_vault_item_operation_request_param.py
+++ b/src/kernel/types/vaults/fill_vault_item_operation_request_param.py
@@ -35,8 +35,10 @@ class FillVaultItemOperationRequestParam(TypedDict, total=False):
not atomic: previously filled fields are not rolled back. Never submit
the form or click buttons, though input/change events may trigger site
behavior. Link and Kernel cards use fill for browser checkout and do not expose
- aliases or support egress substitution. Do not automatically retry a
- failed or indeterminate operation.
+ aliases or support egress substitution. Fill does not consume the item, so
+ a failed or indeterminate fill is safe to retry; a retry rewrites the same
+ fields. A retry right after an indeterminate fill may wait up to 15 seconds
+ for the earlier attempt's browser lock to expire.
Secret values are never returned or included in operation logs, traces,
audit events, or error details. This does not prevent an agent with
From 0e4734caedae1068b5f3f9eeb5212871dfc4a0ec Mon Sep 17 00:00:00 2001
From: "kernel-internal[bot]"
<260533166+kernel-internal[bot]@users.noreply.github.com>
Date: Fri, 9 Oct 2026 12:12:13 +0000
Subject: [PATCH 341/366] feat: Infer a challenge result for unobserved captcha
providers in the relay
Stainless-Generated-From: 0e92268c9f54dda6b9512e74bff0cebb24f4d759
---
.../browser_captcha_challenge_result_event.py | 79 ++++++++++++-------
1 file changed, 49 insertions(+), 30 deletions(-)
diff --git a/src/kernel/types/browsers/browser_captcha_challenge_result_event.py b/src/kernel/types/browsers/browser_captcha_challenge_result_event.py
index 103dda73..bed8fa7c 100644
--- a/src/kernel/types/browsers/browser_captcha_challenge_result_event.py
+++ b/src/kernel/types/browsers/browser_captcha_challenge_result_event.py
@@ -10,42 +10,36 @@
class Data(BaseModel):
- """Per-challenge payload.
+ """An observed challenge emits one outcome across any number of solver tasks.
- This event is emitted once per challenge and determines its overall outcome; captcha_solve_started and captcha_solve_result describe individual tasks and may occur multiple times within the challenge.
+ For eligible providers without a widget observer, each successful token task emits an inferred solved result instead; multiple such results may belong to one challenge. Failed tasks and image_challenge rounds produce no inferred result. Exactly one of challenge_id or task_id is present: challenge_id on an observed result, task_id on an inferred one. A challenge whose tasks all fail produces task events only, so consumers fall back to captcha_solve_result for it.
"""
captcha_type: Literal["hcaptcha", "recaptcha_v2", "recaptcha_v3", "turnstile", "geetest", "press_and_hold", "other"]
"""Deprecated: use captcha_provider."""
- challenge_id: str
- """Opaque identifier shared by events for one visible challenge.
-
- An image-grid captcha may create multiple task_id values for one challenge_id.
- The same value may continue across a page reload when the challenge episode
- continues. It does not indicate task ordering or challenge completion.
- """
-
duration_ms: float
"""
Wall-clock duration from the challenge appearing to its terminal outcome,
- covering every solver attempt in between.
+ covering every solver attempt in between. For an inferred result, the duration
+ of its solver task.
"""
status: Literal["solved", "failure", "timeout", "abandoned"]
- """Terminal outcome of the visible challenge.
-
- solved: the page observed the challenge clear after a solver attempt. failure: a
- terminal solver failure occurred, or all attempts ended while the challenge
- remained. timeout: the challenge-level wait budget expired while the challenge
- remained. abandoned: observation ended without an attributable terminal
- challenge outcome. This includes a dismissed widget or page unload without a
- solved signal or terminal solver outcome, and a token appearing while multiple
- same-provider challenges are open, because the producer cannot attribute that
- token to this visible challenge. A captcha_solve_result with the same
- challenge_id may therefore report success while the challenge result reports
- abandoned. A solved challenge does not prove the site accepted the token or that
- the guarded action succeeded.
+ """Terminal outcome of a challenge.
+
+ solved: the page observed the challenge clear after a solver attempt, or an
+ inferred result reports a token for the whole widget without page observation.
+ failure: a terminal solver failure occurred, or all attempts ended while the
+ challenge remained. timeout: the challenge-level wait budget expired while the
+ challenge remained. abandoned: observation ended without an attributable
+ terminal challenge outcome. This includes a dismissed widget or page unload
+ without a solved signal or terminal solver outcome, and a token appearing while
+ multiple same-provider challenges are open, because the producer cannot
+ attribute that token to this visible challenge. A captcha_solve_result with the
+ same challenge_id may therefore report success while the challenge result
+ reports abandoned. A solved challenge does not prove the site accepted the token
+ or that the guarded action succeeded.
"""
captcha_provider: Optional[
@@ -59,6 +53,28 @@ class Data(BaseModel):
products use other.
"""
+ challenge_id: Optional[str] = None
+ """Opaque identifier shared by events for one visible challenge.
+
+ An image-grid captcha may create multiple task_id values for one challenge_id.
+ The same value may continue across a page reload when the challenge episode
+ continues. It does not indicate task ordering or challenge completion.
+ """
+
+ inferred: Optional[bool] = None
+ """
+ True when the relay derived this result from a successful token task without
+ observing the page. An inferred result has task_id instead of challenge_id.
+ Absent on page-observed results.
+ """
+
+ task_id: Optional[str] = None
+ """The task_id of the solver task an inferred result was derived from.
+
+ Join on it to pair the result with that task's captcha_solve_started and
+ captcha_solve_result. Present only when inferred is true.
+ """
+
website_host: Optional[str] = None
"""Host of the page where the challenge appeared."""
@@ -67,16 +83,19 @@ class Data(BaseModel):
class BrowserCaptchaChallengeResultEvent(BaseModel):
- """A visible captcha challenge reached a terminal outcome."""
+ """A captcha challenge reached an observed or inferred terminal outcome."""
category: Literal["captcha"]
data: Data
- """Per-challenge payload.
-
- This event is emitted once per challenge and determines its overall outcome;
- captcha_solve_started and captcha_solve_result describe individual tasks and may
- occur multiple times within the challenge.
+ """An observed challenge emits one outcome across any number of solver tasks.
+
+ For eligible providers without a widget observer, each successful token task
+ emits an inferred solved result instead; multiple such results may belong to one
+ challenge. Failed tasks and image_challenge rounds produce no inferred result.
+ Exactly one of challenge_id or task_id is present: challenge_id on an observed
+ result, task_id on an inferred one. A challenge whose tasks all fail produces
+ task events only, so consumers fall back to captcha_solve_result for it.
"""
source: BrowserEventSource
From a289cba652251b6f42b60aad92619627efc9304b Mon Sep 17 00:00:00 2001
From: "kernel-internal[bot]"
<260533166+kernel-internal[bot]@users.noreply.github.com>
Date: Fri, 9 Oct 2026 15:52:10 +0000
Subject: [PATCH 342/366] feat: Accept client-encrypted credential values in
vault items
Stainless-Generated-From: 07412a4aeca64157ed1cf0679c95fda486a9b860
---
.stats.yml | 2 +-
api.md | 3 +-
src/kernel/resources/vaults/vaults.py | 93 +++++++++++++++++++
src/kernel/types/__init__.py | 1 +
src/kernel/types/vault_encryption_key.py | 40 ++++++++
.../credential_vault_field_input_param.py | 20 +++-
.../credential_vault_field_update_param.py | 25 ++++-
tests/api_resources/test_vaults.py | 86 ++++++++++++++++-
tests/api_resources/vaults/test_items.py | 16 +++-
9 files changed, 278 insertions(+), 8 deletions(-)
create mode 100644 src/kernel/types/vault_encryption_key.py
diff --git a/.stats.yml b/.stats.yml
index 8e11d075..caa7c062 100644
--- a/.stats.yml
+++ b/.stats.yml
@@ -1 +1 @@
-configured_endpoints: 174
+configured_endpoints: 175
diff --git a/api.md b/api.md
index 4f4ecf76..113f03a0 100644
--- a/api.md
+++ b/api.md
@@ -546,7 +546,7 @@ Methods:
Types:
```python
-from kernel.types import Vault
+from kernel.types import Vault, VaultEncryptionKey
```
Methods:
@@ -554,6 +554,7 @@ Methods:
- client.vaults.retrieve(id_or_name) -> Vault
- client.vaults.list(\*\*params) -> SyncOffsetPagination[Vault]
- client.vaults.delete(id_or_name) -> None
+- client.vaults.retrieve_encryption_key(id_or_name) -> VaultEncryptionKey
- client.vaults.upsert(\*\*params) -> Vault
## Items
diff --git a/src/kernel/resources/vaults/vaults.py b/src/kernel/resources/vaults/vaults.py
index a8b5446f..ee237487 100644
--- a/src/kernel/resources/vaults/vaults.py
+++ b/src/kernel/resources/vaults/vaults.py
@@ -26,6 +26,7 @@
from ...pagination import SyncOffsetPagination, AsyncOffsetPagination
from ...types.vault import Vault
from ..._base_client import AsyncPaginator, make_request_options
+from ...types.vault_encryption_key import VaultEncryptionKey
__all__ = ["VaultsResource", "AsyncVaultsResource"]
@@ -173,6 +174,46 @@ def delete(
cast_to=NoneType,
)
+ def retrieve_encryption_key(
+ self,
+ id_or_name: str,
+ *,
+ # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
+ # The extra values given here take precedence over values defined on the client or passed to this method.
+ extra_headers: Headers | None = None,
+ extra_query: Query | None = None,
+ extra_body: Body | None = None,
+ timeout: float | httpx.Timeout | None | NotGiven = not_given,
+ ) -> VaultEncryptionKey:
+ """
+ Returns the public key that custom credential collection web apps use to encrypt
+ values in the browser. Use this only if you run your own credential collection
+ web app and want values encrypted in the browser, sent to your backend still
+ encrypted, and forwarded to Kernel's API still encrypted. In every other case,
+ including server-side code that already holds the plaintext, use value. The page
+ encrypts each value to this key, your backend forwards the ciphertext unchanged
+ as encrypted_value when creating or updating a credential item in this vault,
+ and Kernel decrypts it.
+
+ Args:
+ extra_headers: Send extra headers
+
+ extra_query: Add additional query parameters to the request
+
+ extra_body: Add additional JSON properties to the request
+
+ timeout: Override the client-level default timeout for this request, in seconds
+ """
+ if not id_or_name:
+ raise ValueError(f"Expected a non-empty value for `id_or_name` but received {id_or_name!r}")
+ return self._get(
+ path_template("/vaults/{id_or_name}/encryption_key", id_or_name=id_or_name),
+ options=make_request_options(
+ extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
+ ),
+ cast_to=VaultEncryptionKey,
+ )
+
def upsert(
self,
*,
@@ -353,6 +394,46 @@ async def delete(
cast_to=NoneType,
)
+ async def retrieve_encryption_key(
+ self,
+ id_or_name: str,
+ *,
+ # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
+ # The extra values given here take precedence over values defined on the client or passed to this method.
+ extra_headers: Headers | None = None,
+ extra_query: Query | None = None,
+ extra_body: Body | None = None,
+ timeout: float | httpx.Timeout | None | NotGiven = not_given,
+ ) -> VaultEncryptionKey:
+ """
+ Returns the public key that custom credential collection web apps use to encrypt
+ values in the browser. Use this only if you run your own credential collection
+ web app and want values encrypted in the browser, sent to your backend still
+ encrypted, and forwarded to Kernel's API still encrypted. In every other case,
+ including server-side code that already holds the plaintext, use value. The page
+ encrypts each value to this key, your backend forwards the ciphertext unchanged
+ as encrypted_value when creating or updating a credential item in this vault,
+ and Kernel decrypts it.
+
+ Args:
+ extra_headers: Send extra headers
+
+ extra_query: Add additional query parameters to the request
+
+ extra_body: Add additional JSON properties to the request
+
+ timeout: Override the client-level default timeout for this request, in seconds
+ """
+ if not id_or_name:
+ raise ValueError(f"Expected a non-empty value for `id_or_name` but received {id_or_name!r}")
+ return await self._get(
+ path_template("/vaults/{id_or_name}/encryption_key", id_or_name=id_or_name),
+ options=make_request_options(
+ extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
+ ),
+ cast_to=VaultEncryptionKey,
+ )
+
async def upsert(
self,
*,
@@ -403,6 +484,9 @@ def __init__(self, vaults: VaultsResource) -> None:
self.delete = to_raw_response_wrapper(
vaults.delete,
)
+ self.retrieve_encryption_key = to_raw_response_wrapper(
+ vaults.retrieve_encryption_key,
+ )
self.upsert = to_raw_response_wrapper(
vaults.upsert,
)
@@ -425,6 +509,9 @@ def __init__(self, vaults: AsyncVaultsResource) -> None:
self.delete = async_to_raw_response_wrapper(
vaults.delete,
)
+ self.retrieve_encryption_key = async_to_raw_response_wrapper(
+ vaults.retrieve_encryption_key,
+ )
self.upsert = async_to_raw_response_wrapper(
vaults.upsert,
)
@@ -447,6 +534,9 @@ def __init__(self, vaults: VaultsResource) -> None:
self.delete = to_streamed_response_wrapper(
vaults.delete,
)
+ self.retrieve_encryption_key = to_streamed_response_wrapper(
+ vaults.retrieve_encryption_key,
+ )
self.upsert = to_streamed_response_wrapper(
vaults.upsert,
)
@@ -469,6 +559,9 @@ def __init__(self, vaults: AsyncVaultsResource) -> None:
self.delete = async_to_streamed_response_wrapper(
vaults.delete,
)
+ self.retrieve_encryption_key = async_to_streamed_response_wrapper(
+ vaults.retrieve_encryption_key,
+ )
self.upsert = async_to_streamed_response_wrapper(
vaults.upsert,
)
diff --git a/src/kernel/types/__init__.py b/src/kernel/types/__init__.py
index cbd6041d..8d605df8 100644
--- a/src/kernel/types/__init__.py
+++ b/src/kernel/types/__init__.py
@@ -67,6 +67,7 @@
from .browser_repl_content import BrowserReplContent as BrowserReplContent
from .proxy_check_response import ProxyCheckResponse as ProxyCheckResponse
from .search_create_params import SearchCreateParams as SearchCreateParams
+from .vault_encryption_key import VaultEncryptionKey as VaultEncryptionKey
from .api_key_create_params import APIKeyCreateParams as APIKeyCreateParams
from .api_key_rotate_params import APIKeyRotateParams as APIKeyRotateParams
from .api_key_update_params import APIKeyUpdateParams as APIKeyUpdateParams
diff --git a/src/kernel/types/vault_encryption_key.py b/src/kernel/types/vault_encryption_key.py
new file mode 100644
index 00000000..6dc53906
--- /dev/null
+++ b/src/kernel/types/vault_encryption_key.py
@@ -0,0 +1,40 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from typing_extensions import Literal
+
+from .._models import BaseModel
+
+__all__ = ["VaultEncryptionKey", "Jwk"]
+
+
+class Jwk(BaseModel):
+ """P-256 public key in JWK form."""
+
+ crv: Literal["P-256"]
+
+ kty: Literal["EC"]
+
+ x: str
+ """Base64url-encoded x coordinate."""
+
+ y: str
+ """Base64url-encoded y coordinate."""
+
+
+class VaultEncryptionKey(BaseModel):
+ """Public key for encrypted_value on credential fields.
+
+ Use this only if you run your own credential collection web app and want values encrypted in the browser, sent to your backend still encrypted, and forwarded to Kernel's API still encrypted. In every other case, including server-side code that already holds the plaintext, use value. Each vault has its own key; a value encrypted for one vault is rejected by every other vault. The key is created on first request and stays the same for the vault's lifetime, so it may be cached.
+ """
+
+ alg: Literal["ECDH-ES"]
+ """JWE key management algorithm."""
+
+ enc: Literal["A256GCM"]
+ """JWE content encryption algorithm."""
+
+ jwk: Jwk
+ """P-256 public key in JWK form."""
+
+ kid: str
+ """Key ID. Set it as the kid protected header of every encrypted_value."""
diff --git a/src/kernel/types/vaults/credential_vault_field_input_param.py b/src/kernel/types/vaults/credential_vault_field_input_param.py
index 8e5faf5a..2f481706 100644
--- a/src/kernel/types/vaults/credential_vault_field_input_param.py
+++ b/src/kernel/types/vaults/credential_vault_field_input_param.py
@@ -25,6 +25,22 @@ class CredentialVaultFieldInputParam(TypedDict, total=False):
unsupported.
"""
+ encrypted_value: str
+ """Alternative to value for custom credential collection web apps.
+
+ Use this only if you run your own credential collection web app and want values
+ encrypted in the browser, sent to your backend still encrypted, and forwarded to
+ Kernel's API still encrypted. In every other case, including server-side code
+ that already holds the plaintext, use value. The field's value encrypted
+ client-side as a compact JWE with alg ECDH-ES and enc A256GCM to the key from
+ GET /vaults/{id_or_name}/encryption_key, with that key's kid in the protected
+ header. Compression is not supported. The decrypted value follows the same rules
+ as value, including that an empty string clears the field on update. A kid that
+ is not this vault's key returns 400 encryption_key_mismatch; fetch the key again
+ and re-encrypt. Other malformed or undecryptable values return 400
+ invalid_request. The whole request body is limited to 128 KiB.
+ """
+
label: str
"""Optional human-readable display label.
@@ -48,5 +64,7 @@ class CredentialVaultFieldInputParam(TypedDict, total=False):
"""
Optional initial value satisfying the declared type, at most 16 KiB in UTF-8
bytes. Omit to leave unset; null and empty strings are rejected on creation.
- Sensitive values are encrypted and never copied into the returned spec.
+ Sensitive values are encrypted and never copied into the returned spec. Use this
+ unless your own credential collection web app encrypts values in the browser;
+ mutually exclusive with encrypted_value.
"""
diff --git a/src/kernel/types/vaults/credential_vault_field_update_param.py b/src/kernel/types/vaults/credential_vault_field_update_param.py
index 9d4a532b..823b21aa 100644
--- a/src/kernel/types/vaults/credential_vault_field_update_param.py
+++ b/src/kernel/types/vaults/credential_vault_field_update_param.py
@@ -3,13 +3,34 @@
from __future__ import annotations
from typing import Optional
-from typing_extensions import Required, TypedDict
+from typing_extensions import TypedDict
__all__ = ["CredentialVaultFieldUpdateParam"]
class CredentialVaultFieldUpdateParam(TypedDict, total=False):
- value: Required[Optional[str]]
+ """Set exactly one of value or encrypted_value.
+
+ Use value unless the update comes from your own credential collection web app that encrypts values in the browser; see encrypted_value.
+ """
+
+ encrypted_value: str
+ """Alternative to value for custom credential collection web apps.
+
+ Use this only if you run your own credential collection web app and want values
+ encrypted in the browser, sent to your backend still encrypted, and forwarded to
+ Kernel's API still encrypted. In every other case, including server-side code
+ that already holds the plaintext, use value. The field's value encrypted
+ client-side as a compact JWE with alg ECDH-ES and enc A256GCM to the key from
+ GET /vaults/{id_or_name}/encryption_key, with that key's kid in the protected
+ header. Compression is not supported. The decrypted value follows the same rules
+ as value, including that an empty string clears the field on update. A kid that
+ is not this vault's key returns 400 encryption_key_mismatch; fetch the key again
+ and re-encrypt. Other malformed or undecryptable values return 400
+ invalid_request. The whole request body is limited to 128 KiB.
+ """
+
+ value: Optional[str]
"""
Replacement value (at most 16 KiB in UTF-8 bytes), or null or an empty string to
immediately clear the stored value. Clearing a required form-supported field
diff --git a/tests/api_resources/test_vaults.py b/tests/api_resources/test_vaults.py
index 5d11ac98..7372f855 100644
--- a/tests/api_resources/test_vaults.py
+++ b/tests/api_resources/test_vaults.py
@@ -9,7 +9,7 @@
from kernel import Kernel, AsyncKernel
from tests.utils import assert_matches_type
-from kernel.types import Vault
+from kernel.types import Vault, VaultEncryptionKey
from kernel.pagination import SyncOffsetPagination, AsyncOffsetPagination
base_url = os.environ.get("TEST_API_BASE_URL", "http://127.0.0.1:4010")
@@ -140,6 +140,48 @@ def test_path_params_delete(self, client: Kernel) -> None:
"",
)
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ def test_method_retrieve_encryption_key(self, client: Kernel) -> None:
+ vault = client.vaults.retrieve_encryption_key(
+ "id_or_name",
+ )
+ assert_matches_type(VaultEncryptionKey, vault, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ def test_raw_response_retrieve_encryption_key(self, client: Kernel) -> None:
+ response = client.vaults.with_raw_response.retrieve_encryption_key(
+ "id_or_name",
+ )
+
+ assert response.is_closed is True
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+ vault = response.parse()
+ assert_matches_type(VaultEncryptionKey, vault, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ def test_streaming_response_retrieve_encryption_key(self, client: Kernel) -> None:
+ with client.vaults.with_streaming_response.retrieve_encryption_key(
+ "id_or_name",
+ ) as response:
+ assert not response.is_closed
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+
+ vault = response.parse()
+ assert_matches_type(VaultEncryptionKey, vault, path=["response"])
+
+ assert cast(Any, response.is_closed) is True
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ def test_path_params_retrieve_encryption_key(self, client: Kernel) -> None:
+ with pytest.raises(ValueError, match=r"Expected a non-empty value for `id_or_name` but received ''"):
+ client.vaults.with_raw_response.retrieve_encryption_key(
+ "",
+ )
+
@pytest.mark.skip(reason="Mock server tests are disabled")
@parametrize
def test_method_upsert(self, client: Kernel) -> None:
@@ -302,6 +344,48 @@ async def test_path_params_delete(self, async_client: AsyncKernel) -> None:
"",
)
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ async def test_method_retrieve_encryption_key(self, async_client: AsyncKernel) -> None:
+ vault = await async_client.vaults.retrieve_encryption_key(
+ "id_or_name",
+ )
+ assert_matches_type(VaultEncryptionKey, vault, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ async def test_raw_response_retrieve_encryption_key(self, async_client: AsyncKernel) -> None:
+ response = await async_client.vaults.with_raw_response.retrieve_encryption_key(
+ "id_or_name",
+ )
+
+ assert response.is_closed is True
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+ vault = await response.parse()
+ assert_matches_type(VaultEncryptionKey, vault, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ async def test_streaming_response_retrieve_encryption_key(self, async_client: AsyncKernel) -> None:
+ async with async_client.vaults.with_streaming_response.retrieve_encryption_key(
+ "id_or_name",
+ ) as response:
+ assert not response.is_closed
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+
+ vault = await response.parse()
+ assert_matches_type(VaultEncryptionKey, vault, 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_retrieve_encryption_key(self, async_client: AsyncKernel) -> None:
+ with pytest.raises(ValueError, match=r"Expected a non-empty value for `id_or_name` but received ''"):
+ await async_client.vaults.with_raw_response.retrieve_encryption_key(
+ "",
+ )
+
@pytest.mark.skip(reason="Mock server tests are disabled")
@parametrize
async def test_method_upsert(self, async_client: AsyncKernel) -> None:
diff --git a/tests/api_resources/vaults/test_items.py b/tests/api_resources/vaults/test_items.py
index 3127f07e..0a85a9d1 100644
--- a/tests/api_resources/vaults/test_items.py
+++ b/tests/api_resources/vaults/test_items.py
@@ -256,7 +256,12 @@ def test_method_update_with_all_params_overload_2(self, client: Kernel) -> None:
id_or_name="id_or_name",
spec={
"description": "description",
- "fields": {"foo": {"value": "value"}},
+ "fields": {
+ "foo": {
+ "encrypted_value": "x",
+ "value": "value",
+ }
+ },
},
type="credential",
version=1,
@@ -1783,6 +1788,7 @@ def test_method_upsert_with_all_params_overload_4(self, client: Kernel) -> None:
{
"name": "name",
"type": "text",
+ "encrypted_value": "x",
"label": "label",
"required": True,
"sensitive": True,
@@ -2118,7 +2124,12 @@ async def test_method_update_with_all_params_overload_2(self, async_client: Asyn
id_or_name="id_or_name",
spec={
"description": "description",
- "fields": {"foo": {"value": "value"}},
+ "fields": {
+ "foo": {
+ "encrypted_value": "x",
+ "value": "value",
+ }
+ },
},
type="credential",
version=1,
@@ -3645,6 +3656,7 @@ async def test_method_upsert_with_all_params_overload_4(self, async_client: Asyn
{
"name": "name",
"type": "text",
+ "encrypted_value": "x",
"label": "label",
"required": True,
"sensitive": True,
From ead7682bd1357b6134d45539a8a34d0188e31bf0 Mon Sep 17 00:00:00 2001
From: "kernel-internal[bot]"
<260533166+kernel-internal[bot]@users.noreply.github.com>
Date: Fri, 9 Oct 2026 15:57:15 +0000
Subject: [PATCH 343/366] release: 0.122.0
---
.release-please-manifest.json | 2 +-
CHANGELOG.md | 11 +++++++++++
pyproject.toml | 2 +-
src/kernel/_version.py | 2 +-
4 files changed, 14 insertions(+), 3 deletions(-)
diff --git a/.release-please-manifest.json b/.release-please-manifest.json
index 4bab9967..bd115b20 100644
--- a/.release-please-manifest.json
+++ b/.release-please-manifest.json
@@ -1,3 +1,3 @@
{
- ".": "0.121.0"
+ ".": "0.122.0"
}
\ No newline at end of file
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 9117cbd6..f7520574 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,16 @@
# Changelog
+## [0.122.0](https://github.com/kernel/kernel-python-sdk/compare/v0.121.0...v0.122.0) (2026-10-09)
+
+
+### Features
+
+* Accept client-encrypted credential values in vault items ([a289cba](https://github.com/kernel/kernel-python-sdk/commit/a289cba652251b6f42b60aad92619627efc9304b))
+* Add feature-gated Korean ISP proxies ([56785ca](https://github.com/kernel/kernel-python-sdk/commit/56785cad65cda7e757258204de47f1a4a5974088))
+* Describe vault fill as safe to retry ([e419d6d](https://github.com/kernel/kernel-python-sdk/commit/e419d6d8970f0e78f91b4771969787bb835a6acf))
+* Infer a challenge result for unobserved captcha providers in the relay ([0e4734c](https://github.com/kernel/kernel-python-sdk/commit/0e4734caedae1068b5f3f9eeb5212871dfc4a0ec))
+* Support proxy routes in browser pools ([669ea52](https://github.com/kernel/kernel-python-sdk/commit/669ea5294ab1da43e5042209992364ca009f7053))
+
## [0.121.0](https://github.com/kernel/kernel-python-sdk/compare/v0.120.0...v0.121.0) (2026-10-07)
diff --git a/pyproject.toml b/pyproject.toml
index f4062d32..1f59c57a 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -1,6 +1,6 @@
[project]
name = "kernel"
-version = "0.121.0"
+version = "0.122.0"
description = "The official Python library for the kernel API"
dynamic = ["readme"]
license = "Apache-2.0"
diff --git a/src/kernel/_version.py b/src/kernel/_version.py
index 7bb50d4b..72628b36 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.121.0" # x-release-please-version
+__version__ = "0.122.0" # x-release-please-version
From 31b7bda620f53bc6026b5eb3fac34d50859fa1df Mon Sep 17 00:00:00 2001
From: "kernel-internal[bot]"
<260533166+kernel-internal[bot]@users.noreply.github.com>
Date: Fri, 9 Oct 2026 17:52:25 +0000
Subject: [PATCH 344/366] feat: Add video_memory to request the 4GiB VRAM GPU
browser tier
Stainless-Generated-From: f581fece2ccb216a10620562f96439429da85527
---
api.md | 1 +
src/kernel/resources/browsers/browsers.py | 14 ++++++++++++++
src/kernel/types/__init__.py | 1 +
src/kernel/types/browser_create_params.py | 8 ++++++++
src/kernel/types/browser_create_response.py | 4 ++++
src/kernel/types/browser_list_response.py | 4 ++++
src/kernel/types/browser_memory.py | 2 +-
src/kernel/types/browser_pool_acquire_response.py | 4 ++++
src/kernel/types/browser_retrieve_response.py | 4 ++++
src/kernel/types/browser_update_response.py | 4 ++++
src/kernel/types/browser_video_memory.py | 7 +++++++
.../types/invocation_list_browsers_response.py | 4 ++++
tests/api_resources/test_browsers.py | 2 ++
13 files changed, 58 insertions(+), 1 deletion(-)
create mode 100644 src/kernel/types/browser_video_memory.py
diff --git a/api.md b/api.md
index 113f03a0..1b71010c 100644
--- a/api.md
+++ b/api.md
@@ -131,6 +131,7 @@ from kernel.types import (
BrowserReplResult,
BrowserReplTextContent,
BrowserUsage,
+ BrowserVideoMemory,
Profile,
Tags,
VaultReference,
diff --git a/src/kernel/resources/browsers/browsers.py b/src/kernel/resources/browsers/browsers.py
index 38089c20..ec5f0f42 100644
--- a/src/kernel/resources/browsers/browsers.py
+++ b/src/kernel/resources/browsers/browsers.py
@@ -25,6 +25,7 @@
AsyncFsResourceWithStreamingResponse,
)
from ...types import (
+ BrowserVideoMemory,
BrowserMemoryRequest,
browser_curl_params,
browser_list_params,
@@ -97,6 +98,7 @@
AsyncPlaywrightResourceWithStreamingResponse,
)
from ...types.browser_repl_result import BrowserReplResult
+from ...types.browser_video_memory import BrowserVideoMemory
from ...types.browser_curl_response import BrowserCurlResponse
from ...types.browser_list_response import BrowserListResponse
from ...types.vault_reference_param import VaultReferenceParam
@@ -206,6 +208,7 @@ def create(
telemetry: Optional[browser_create_params.Telemetry] | Omit = omit,
timeout_seconds: int | Omit = omit,
vaults: Iterable[VaultReferenceParam] | Omit = omit,
+ video_memory: BrowserVideoMemory | Omit = omit,
viewport: BrowserViewport | 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.
@@ -293,6 +296,10 @@ def create(
vaults: Project-scoped vaults to link to the browser session. Links are immutable after
creation.
+ video_memory: Video memory (VRAM) for a GPU browser session. Requires gpu=true. Defaults to
+ 2GiB, which comes with 4 vCPU and 6GiB memory. 4GiB comes with 8 vCPU and 12GiB
+ memory.
+
viewport: 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.
@@ -337,6 +344,7 @@ def create(
"telemetry": telemetry,
"timeout_seconds": timeout_seconds,
"vaults": vaults,
+ "video_memory": video_memory,
"viewport": viewport,
},
browser_create_params.BrowserCreateParams,
@@ -915,6 +923,7 @@ async def create(
telemetry: Optional[browser_create_params.Telemetry] | Omit = omit,
timeout_seconds: int | Omit = omit,
vaults: Iterable[VaultReferenceParam] | Omit = omit,
+ video_memory: BrowserVideoMemory | Omit = omit,
viewport: BrowserViewport | 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.
@@ -1002,6 +1011,10 @@ async def create(
vaults: Project-scoped vaults to link to the browser session. Links are immutable after
creation.
+ video_memory: Video memory (VRAM) for a GPU browser session. Requires gpu=true. Defaults to
+ 2GiB, which comes with 4 vCPU and 6GiB memory. 4GiB comes with 8 vCPU and 12GiB
+ memory.
+
viewport: 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.
@@ -1046,6 +1059,7 @@ async def create(
"telemetry": telemetry,
"timeout_seconds": timeout_seconds,
"vaults": vaults,
+ "video_memory": video_memory,
"viewport": viewport,
},
browser_create_params.BrowserCreateParams,
diff --git a/src/kernel/types/__init__.py b/src/kernel/types/__init__.py
index 8d605df8..a3eac5b8 100644
--- a/src/kernel/types/__init__.py
+++ b/src/kernel/types/__init__.py
@@ -65,6 +65,7 @@
from .vault_upsert_params import VaultUpsertParams as VaultUpsertParams
from .browser_proxy_config import BrowserProxyConfig as BrowserProxyConfig
from .browser_repl_content import BrowserReplContent as BrowserReplContent
+from .browser_video_memory import BrowserVideoMemory as BrowserVideoMemory
from .proxy_check_response import ProxyCheckResponse as ProxyCheckResponse
from .search_create_params import SearchCreateParams as SearchCreateParams
from .vault_encryption_key import VaultEncryptionKey as VaultEncryptionKey
diff --git a/src/kernel/types/browser_create_params.py b/src/kernel/types/browser_create_params.py
index 2e456a80..3fe435d5 100644
--- a/src/kernel/types/browser_create_params.py
+++ b/src/kernel/types/browser_create_params.py
@@ -6,6 +6,7 @@
from typing_extensions import Literal, TypedDict
from .tags_param import TagsParam
+from .browser_video_memory import BrowserVideoMemory
from .vault_reference_param import VaultReferenceParam
from .browser_memory_request import BrowserMemoryRequest
from .browser_proxy_config_param import BrowserProxyConfigParam
@@ -158,6 +159,13 @@ class BrowserCreateParams(TypedDict, total=False):
Links are immutable after creation.
"""
+ video_memory: BrowserVideoMemory
+ """Video memory (VRAM) for a GPU browser session.
+
+ Requires gpu=true. Defaults to 2GiB, which comes with 4 vCPU and 6GiB memory.
+ 4GiB comes with 8 vCPU and 12GiB memory.
+ """
+
viewport: BrowserViewport
"""
Initial browser window size in pixels with optional refresh rate. If omitted,
diff --git a/src/kernel/types/browser_create_response.py b/src/kernel/types/browser_create_response.py
index 09c88f9a..25e82277 100644
--- a/src/kernel/types/browser_create_response.py
+++ b/src/kernel/types/browser_create_response.py
@@ -12,6 +12,7 @@
from .browser_memory import BrowserMemory
from .vault_reference import VaultReference
from .browser_pool_ref import BrowserPoolRef
+from .browser_video_memory import BrowserVideoMemory
from .browser_network_config import BrowserNetworkConfig
from .shared.browser_viewport import BrowserViewport
from .browsers.browser_telemetry_config import BrowserTelemetryConfig
@@ -135,6 +136,9 @@ class BrowserCreateResponse(BaseModel):
vaults: Optional[List[VaultReference]] = None
"""Vaults linked when the browser session was created."""
+ video_memory: Optional[BrowserVideoMemory] = None
+ """Video memory (VRAM) of the GPU browser session. Only present when gpu is true."""
+
viewport: Optional[BrowserViewport] = None
"""
Initial browser window size in pixels with optional refresh rate. If omitted,
diff --git a/src/kernel/types/browser_list_response.py b/src/kernel/types/browser_list_response.py
index 6f1aad11..6527b9c2 100644
--- a/src/kernel/types/browser_list_response.py
+++ b/src/kernel/types/browser_list_response.py
@@ -12,6 +12,7 @@
from .browser_memory import BrowserMemory
from .vault_reference import VaultReference
from .browser_pool_ref import BrowserPoolRef
+from .browser_video_memory import BrowserVideoMemory
from .browser_network_config import BrowserNetworkConfig
from .shared.browser_viewport import BrowserViewport
from .browsers.browser_telemetry_config import BrowserTelemetryConfig
@@ -135,6 +136,9 @@ class BrowserListResponse(BaseModel):
vaults: Optional[List[VaultReference]] = None
"""Vaults linked when the browser session was created."""
+ video_memory: Optional[BrowserVideoMemory] = None
+ """Video memory (VRAM) of the GPU browser session. Only present when gpu is true."""
+
viewport: Optional[BrowserViewport] = None
"""
Initial browser window size in pixels with optional refresh rate. If omitted,
diff --git a/src/kernel/types/browser_memory.py b/src/kernel/types/browser_memory.py
index b86a12aa..31f362ee 100644
--- a/src/kernel/types/browser_memory.py
+++ b/src/kernel/types/browser_memory.py
@@ -4,4 +4,4 @@
__all__ = ["BrowserMemory"]
-BrowserMemory: TypeAlias = Literal["1GiB", "2GiB", "6GiB", "8GiB", "16GiB"]
+BrowserMemory: TypeAlias = Literal["1GiB", "2GiB", "6GiB", "8GiB", "12GiB", "16GiB"]
diff --git a/src/kernel/types/browser_pool_acquire_response.py b/src/kernel/types/browser_pool_acquire_response.py
index a0617cb6..4cbda2d3 100644
--- a/src/kernel/types/browser_pool_acquire_response.py
+++ b/src/kernel/types/browser_pool_acquire_response.py
@@ -12,6 +12,7 @@
from .browser_memory import BrowserMemory
from .vault_reference import VaultReference
from .browser_pool_ref import BrowserPoolRef
+from .browser_video_memory import BrowserVideoMemory
from .browser_network_config import BrowserNetworkConfig
from .shared.browser_viewport import BrowserViewport
from .browsers.browser_telemetry_config import BrowserTelemetryConfig
@@ -135,6 +136,9 @@ class BrowserPoolAcquireResponse(BaseModel):
vaults: Optional[List[VaultReference]] = None
"""Vaults linked when the browser session was created."""
+ video_memory: Optional[BrowserVideoMemory] = None
+ """Video memory (VRAM) of the GPU browser session. Only present when gpu is true."""
+
viewport: Optional[BrowserViewport] = None
"""
Initial browser window size in pixels with optional refresh rate. If omitted,
diff --git a/src/kernel/types/browser_retrieve_response.py b/src/kernel/types/browser_retrieve_response.py
index 1642e55e..735c9ba4 100644
--- a/src/kernel/types/browser_retrieve_response.py
+++ b/src/kernel/types/browser_retrieve_response.py
@@ -12,6 +12,7 @@
from .browser_memory import BrowserMemory
from .vault_reference import VaultReference
from .browser_pool_ref import BrowserPoolRef
+from .browser_video_memory import BrowserVideoMemory
from .browser_network_config import BrowserNetworkConfig
from .shared.browser_viewport import BrowserViewport
from .browsers.browser_telemetry_config import BrowserTelemetryConfig
@@ -135,6 +136,9 @@ class BrowserRetrieveResponse(BaseModel):
vaults: Optional[List[VaultReference]] = None
"""Vaults linked when the browser session was created."""
+ video_memory: Optional[BrowserVideoMemory] = None
+ """Video memory (VRAM) of the GPU browser session. Only present when gpu is true."""
+
viewport: Optional[BrowserViewport] = None
"""
Initial browser window size in pixels with optional refresh rate. If omitted,
diff --git a/src/kernel/types/browser_update_response.py b/src/kernel/types/browser_update_response.py
index 98484751..b53d2ac3 100644
--- a/src/kernel/types/browser_update_response.py
+++ b/src/kernel/types/browser_update_response.py
@@ -12,6 +12,7 @@
from .browser_memory import BrowserMemory
from .vault_reference import VaultReference
from .browser_pool_ref import BrowserPoolRef
+from .browser_video_memory import BrowserVideoMemory
from .browser_network_config import BrowserNetworkConfig
from .shared.browser_viewport import BrowserViewport
from .browsers.browser_telemetry_config import BrowserTelemetryConfig
@@ -135,6 +136,9 @@ class BrowserUpdateResponse(BaseModel):
vaults: Optional[List[VaultReference]] = None
"""Vaults linked when the browser session was created."""
+ video_memory: Optional[BrowserVideoMemory] = None
+ """Video memory (VRAM) of the GPU browser session. Only present when gpu is true."""
+
viewport: Optional[BrowserViewport] = None
"""
Initial browser window size in pixels with optional refresh rate. If omitted,
diff --git a/src/kernel/types/browser_video_memory.py b/src/kernel/types/browser_video_memory.py
new file mode 100644
index 00000000..28c3569e
--- /dev/null
+++ b/src/kernel/types/browser_video_memory.py
@@ -0,0 +1,7 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from typing_extensions import Literal, TypeAlias
+
+__all__ = ["BrowserVideoMemory"]
+
+BrowserVideoMemory: TypeAlias = Literal["2GiB", "4GiB"]
diff --git a/src/kernel/types/invocation_list_browsers_response.py b/src/kernel/types/invocation_list_browsers_response.py
index 6589ad92..dc99f0dc 100644
--- a/src/kernel/types/invocation_list_browsers_response.py
+++ b/src/kernel/types/invocation_list_browsers_response.py
@@ -12,6 +12,7 @@
from .browser_memory import BrowserMemory
from .vault_reference import VaultReference
from .browser_pool_ref import BrowserPoolRef
+from .browser_video_memory import BrowserVideoMemory
from .browser_network_config import BrowserNetworkConfig
from .shared.browser_viewport import BrowserViewport
from .browsers.browser_telemetry_config import BrowserTelemetryConfig
@@ -135,6 +136,9 @@ class Browser(BaseModel):
vaults: Optional[List[VaultReference]] = None
"""Vaults linked when the browser session was created."""
+ video_memory: Optional[BrowserVideoMemory] = None
+ """Video memory (VRAM) of the GPU browser session. Only present when gpu is true."""
+
viewport: Optional[BrowserViewport] = None
"""
Initial browser window size in pixels with optional refresh rate. If omitted,
diff --git a/tests/api_resources/test_browsers.py b/tests/api_resources/test_browsers.py
index 6aff8010..d12c67b2 100644
--- a/tests/api_resources/test_browsers.py
+++ b/tests/api_resources/test_browsers.py
@@ -114,6 +114,7 @@ def test_method_create_with_all_params(self, client: Kernel) -> None:
"name": "x",
}
],
+ video_memory="2GiB",
viewport={
"height": 800,
"width": 1280,
@@ -660,6 +661,7 @@ async def test_method_create_with_all_params(self, async_client: AsyncKernel) ->
"name": "x",
}
],
+ video_memory="2GiB",
viewport={
"height": 800,
"width": 1280,
From d387aab4847329eb4405318a20ee93a372399f70 Mon Sep 17 00:00:00 2001
From: "kernel-internal[bot]"
<260533166+kernel-internal[bot]@users.noreply.github.com>
Date: Fri, 9 Oct 2026 18:16:11 +0000
Subject: [PATCH 345/366] feat: Fix 1Password fill classification for repeat
fills and unconfirmed submits
Stainless-Generated-From: 399a7000e4351b576352ac5a8772b7dc28a15682
---
.../credential_vault_item_request_param.py | 3 ++-
...password_fill_vault_item_operation_result.py | 17 +++++++++--------
2 files changed, 11 insertions(+), 9 deletions(-)
diff --git a/src/kernel/types/vaults/credential_vault_item_request_param.py b/src/kernel/types/vaults/credential_vault_item_request_param.py
index d316f7bd..4034d9c5 100644
--- a/src/kernel/types/vaults/credential_vault_item_request_param.py
+++ b/src/kernel/types/vaults/credential_vault_item_request_param.py
@@ -15,7 +15,8 @@ class CredentialVaultItemRequestParam(TypedDict, total=False):
If they choose 1Password, connect their account and request access to a login
in their own non-shared vault; passkeys are not supported. If they decline
or that path fails, collect a Kernel-hosted credential item instead. Never
- automatically retry an uncertain 1Password request or fill.
+ automatically retry an uncertain 1Password request. After an uncertain fill,
+ inspect the page to see the result of the fill.
Do not use credential items for credit card data. Use wallet and card item types instead.
Kernel credentials declare fields and may enter pending_collection.
1Password credentials either reference a connected credential_account or
diff --git a/src/kernel/types/vaults/one_password_fill_vault_item_operation_result.py b/src/kernel/types/vaults/one_password_fill_vault_item_operation_result.py
index b1586f5b..9fa64aeb 100644
--- a/src/kernel/types/vaults/one_password_fill_vault_item_operation_result.py
+++ b/src/kernel/types/vaults/one_password_fill_vault_item_operation_result.py
@@ -18,10 +18,10 @@ class OnePasswordFillVaultItemOperationResult(BaseModel):
"""Kernel's outcome of the extension call.
fill_submitted means the extension reported submission, not website
- authentication. fill_failed means the extension returned a known failure and may
- include error_code. fill_unknown means submission may have happened without a
- conclusive response; it has no error_code and must not be retried in the same
- browser.
+ authentication. fill_failed means the extension returned a known failure and
+ includes error_code. fill_unknown means the form may have been filled or
+ submitted without a conclusive result; inspect the page to see the result of the
+ fill.
"""
type: Literal["1pw_fill"]
@@ -29,8 +29,9 @@ class OnePasswordFillVaultItemOperationResult(BaseModel):
error_code: Optional[Literal["fillFailed", "autosubmitFailed", "noExistingCredentials", "authenticationFailed"]] = (
None
)
- """Present only for a conclusive fill_failed response.
-
- These are allowlisted 1Password extension codes, never raw errors, secrets, or
- page content.
+ """
+ Present for fill_failed, and for fill_unknown when the extension reported
+ autosubmitFailed (it filled and submitted the form but could not confirm the
+ fill finished). These are allowlisted 1Password extension codes, never raw
+ errors, secrets, or page content.
"""
From 541fb3fc033b8b9542838a116684578cab18fba0 Mon Sep 17 00:00:00 2001
From: "kernel-internal[bot]"
<260533166+kernel-internal[bot]@users.noreply.github.com>
Date: Fri, 9 Oct 2026 19:14:19 +0000
Subject: [PATCH 346/366] feat: Support egress allowlists on browser pools
Stainless-Generated-From: 5799c8ad59001401cd999d6117d0a5733f3686a8
---
src/kernel/resources/browser_pools.py | 52 ++++++++++++-------
src/kernel/types/browser_network_config.py | 6 ++-
.../types/browser_network_config_param.py | 6 ++-
.../types/browser_network_update_param.py | 6 ++-
.../types/browser_pool_create_params.py | 4 +-
.../types/browser_pool_update_params.py | 20 ++++---
6 files changed, 59 insertions(+), 35 deletions(-)
diff --git a/src/kernel/resources/browser_pools.py b/src/kernel/resources/browser_pools.py
index a4257b83..5fc7147a 100644
--- a/src/kernel/resources/browser_pools.py
+++ b/src/kernel/resources/browser_pools.py
@@ -123,9 +123,9 @@ def create(
name: Optional name for the browser pool. Must be unique within the project.
- network: Network configuration applied to browsers in this pool. Proxy routes require
- proxy-v3; the pool will not provision browsers through Envoy if proxy-v3 is
- unavailable.
+ network: Network configuration applied to browsers in this pool. Egress allowlists and
+ proxy routes require proxy-v3; the pool will not provision browsers through
+ Envoy if proxy-v3 is unavailable.
profile: Profile configuration for browsers in a pool. Provide either id or name.
Profiles must be created beforehand. Unlike single browser sessions, pools load
@@ -296,9 +296,11 @@ def update(
discard_all_idle: Whether to discard all idle browsers and rebuild them immediately with the new
configuration. Defaults to false. Only browsers that are idle when the update
runs are rebuilt. A browser that is in use during the update keeps its original
- configuration, and if it is later released with `reuse: true` it returns to the
- pool with that stale configuration until it is discarded (by this flag on a
- later update, or by flushing the pool).
+ configuration, except that its allowlist is reset to the current pool baseline
+ on release or the browser is replaced. For other configuration fields, if it is
+ later released with `reuse: true` it returns to the pool with that stale
+ configuration until it is discarded (by this flag on a later update, or by
+ flushing the pool).
extensions: If provided, replaces the extension list. Empty array clears all
previously-selected extensions. Omit this field to leave extensions unchanged.
@@ -321,10 +323,14 @@ def update(
network: If provided, replaces the pool's network configuration. Omit to leave the
existing configuration unchanged; an empty object ({}) removes it, while
network: {private_hosts: []} or network: {proxy_routes: []} sets an explicit
- empty list. Proxy routes require proxy-v3; the pool will not provision browsers
- through Envoy if proxy-v3 is unavailable. Only applied to browsers created in
- the pool after the update; browsers already in the pool keep their configuration
- until discarded (see discard_all_idle).
+ empty list; allowed_hosts must be non-empty when supplied. Egress allowlists and
+ proxy routes require proxy-v3; the pool will not provision browsers through
+ Envoy if proxy-v3 is unavailable. Changing allowed_hosts automatically replaces
+ idle browsers. Leased browsers retain their allowlist until release, when it is
+ reset to the current pool baseline or the browser is replaced if it cannot
+ safely receive that baseline. Stale browsers cannot be acquired. Other network
+ fields only apply to browsers created after the update; existing browsers keep
+ those fields until discarded (see discard_all_idle).
profile: Profile configuration for browsers in a pool. Provide either id or name.
Profiles must be created beforehand. Unlike single browser sessions, pools load
@@ -774,9 +780,9 @@ async def create(
name: Optional name for the browser pool. Must be unique within the project.
- network: Network configuration applied to browsers in this pool. Proxy routes require
- proxy-v3; the pool will not provision browsers through Envoy if proxy-v3 is
- unavailable.
+ network: Network configuration applied to browsers in this pool. Egress allowlists and
+ proxy routes require proxy-v3; the pool will not provision browsers through
+ Envoy if proxy-v3 is unavailable.
profile: Profile configuration for browsers in a pool. Provide either id or name.
Profiles must be created beforehand. Unlike single browser sessions, pools load
@@ -947,9 +953,11 @@ async def update(
discard_all_idle: Whether to discard all idle browsers and rebuild them immediately with the new
configuration. Defaults to false. Only browsers that are idle when the update
runs are rebuilt. A browser that is in use during the update keeps its original
- configuration, and if it is later released with `reuse: true` it returns to the
- pool with that stale configuration until it is discarded (by this flag on a
- later update, or by flushing the pool).
+ configuration, except that its allowlist is reset to the current pool baseline
+ on release or the browser is replaced. For other configuration fields, if it is
+ later released with `reuse: true` it returns to the pool with that stale
+ configuration until it is discarded (by this flag on a later update, or by
+ flushing the pool).
extensions: If provided, replaces the extension list. Empty array clears all
previously-selected extensions. Omit this field to leave extensions unchanged.
@@ -972,10 +980,14 @@ async def update(
network: If provided, replaces the pool's network configuration. Omit to leave the
existing configuration unchanged; an empty object ({}) removes it, while
network: {private_hosts: []} or network: {proxy_routes: []} sets an explicit
- empty list. Proxy routes require proxy-v3; the pool will not provision browsers
- through Envoy if proxy-v3 is unavailable. Only applied to browsers created in
- the pool after the update; browsers already in the pool keep their configuration
- until discarded (see discard_all_idle).
+ empty list; allowed_hosts must be non-empty when supplied. Egress allowlists and
+ proxy routes require proxy-v3; the pool will not provision browsers through
+ Envoy if proxy-v3 is unavailable. Changing allowed_hosts automatically replaces
+ idle browsers. Leased browsers retain their allowlist until release, when it is
+ reset to the current pool baseline or the browser is replaced if it cannot
+ safely receive that baseline. Stale browsers cannot be acquired. Other network
+ fields only apply to browsers created after the update; existing browsers keep
+ those fields until discarded (see discard_all_idle).
profile: Profile configuration for browsers in a pool. Provide either id or name.
Profiles must be created beforehand. Unlike single browser sessions, pools load
diff --git a/src/kernel/types/browser_network_config.py b/src/kernel/types/browser_network_config.py
index 19de4d72..cd205e65 100644
--- a/src/kernel/types/browser_network_config.py
+++ b/src/kernel/types/browser_network_config.py
@@ -52,8 +52,10 @@ class BrowserNetworkConfig(BaseModel):
processes in the browser VM that do not use the browser's proxy, are not
filtered, and Kernel's own control traffic is always allowed. Can be replaced or
removed while the session runs with PATCH /browsers/{id_or_name}, but not added
- to a browser created without one. Requires proxy v3. Not supported on browser
- pools.
+ to a browser created without one. Supported on browser pools and their leased
+ browsers. Per-lease changes are reset to the pool's allowlist on release. If a
+ lessee removes the allowlist and the pool still requires one, the browser is
+ replaced on release rather than given an allowlist again. Requires proxy v3.
"""
private_hosts: Optional[List[str]] = None
diff --git a/src/kernel/types/browser_network_config_param.py b/src/kernel/types/browser_network_config_param.py
index 94877452..723fd6ee 100644
--- a/src/kernel/types/browser_network_config_param.py
+++ b/src/kernel/types/browser_network_config_param.py
@@ -55,8 +55,10 @@ class BrowserNetworkConfigParam(TypedDict, total=False):
processes in the browser VM that do not use the browser's proxy, are not
filtered, and Kernel's own control traffic is always allowed. Can be replaced or
removed while the session runs with PATCH /browsers/{id_or_name}, but not added
- to a browser created without one. Requires proxy v3. Not supported on browser
- pools.
+ to a browser created without one. Supported on browser pools and their leased
+ browsers. Per-lease changes are reset to the pool's allowlist on release. If a
+ lessee removes the allowlist and the pool still requires one, the browser is
+ replaced on release rather than given an allowlist again. Requires proxy v3.
"""
private_hosts: SequenceNotStr[str]
diff --git a/src/kernel/types/browser_network_update_param.py b/src/kernel/types/browser_network_update_param.py
index 62163e13..31158d46 100644
--- a/src/kernel/types/browser_network_update_param.py
+++ b/src/kernel/types/browser_network_update_param.py
@@ -29,6 +29,8 @@ class BrowserNetworkUpdateParam(TypedDict, total=False):
minutes during a Kernel deploy. Connections to destinations it still allows,
such as WebSockets, stay open. A start_url in the same request must be allowed
by the updated list, and is loaded only after the list takes effect. Requires a
- browser created with proxy v3, and not supported on pooled browsers. If the
- request fails, retry it: the new list may already apply to some requests.
+ browser created with proxy v3. Supported on leased pooled browsers; the pool's
+ allowlist is restored before reuse, or the browser is destroyed if it cannot be
+ safely restored. If the request fails, retry it: the new list may already apply
+ to some requests.
"""
diff --git a/src/kernel/types/browser_pool_create_params.py b/src/kernel/types/browser_pool_create_params.py
index f078c393..6850502a 100644
--- a/src/kernel/types/browser_pool_create_params.py
+++ b/src/kernel/types/browser_pool_create_params.py
@@ -70,8 +70,8 @@ class BrowserPoolCreateParams(TypedDict, total=False):
network: BrowserNetworkConfigParam
"""Network configuration applied to browsers in this pool.
- Proxy routes require proxy-v3; the pool will not provision browsers through
- Envoy if proxy-v3 is unavailable.
+ Egress allowlists and proxy routes require proxy-v3; the pool will not provision
+ browsers through Envoy if proxy-v3 is unavailable.
"""
profile: Profile
diff --git a/src/kernel/types/browser_pool_update_params.py b/src/kernel/types/browser_pool_update_params.py
index 2731b6ea..8374568e 100644
--- a/src/kernel/types/browser_pool_update_params.py
+++ b/src/kernel/types/browser_pool_update_params.py
@@ -37,9 +37,11 @@ class BrowserPoolUpdateParams(TypedDict, total=False):
Whether to discard all idle browsers and rebuild them immediately with the new
configuration. Defaults to false. Only browsers that are idle when the update
runs are rebuilt. A browser that is in use during the update keeps its original
- configuration, and if it is later released with `reuse: true` it returns to the
- pool with that stale configuration until it is discarded (by this flag on a
- later update, or by flushing the pool).
+ configuration, except that its allowlist is reset to the current pool baseline
+ on release or the browser is replaced. For other configuration fields, if it is
+ later released with `reuse: true` it returns to the pool with that stale
+ configuration until it is discarded (by this flag on a later update, or by
+ flushing the pool).
"""
extensions: Iterable[BrowserExtension]
@@ -81,10 +83,14 @@ class BrowserPoolUpdateParams(TypedDict, total=False):
Omit to leave the existing configuration unchanged; an empty object ({}) removes
it, while network: {private_hosts: []} or network: {proxy_routes: []} sets an
- explicit empty list. Proxy routes require proxy-v3; the pool will not provision
- browsers through Envoy if proxy-v3 is unavailable. Only applied to browsers
- created in the pool after the update; browsers already in the pool keep their
- configuration until discarded (see discard_all_idle).
+ explicit empty list; allowed_hosts must be non-empty when supplied. Egress
+ allowlists and proxy routes require proxy-v3; the pool will not provision
+ browsers through Envoy if proxy-v3 is unavailable. Changing allowed_hosts
+ automatically replaces idle browsers. Leased browsers retain their allowlist
+ until release, when it is reset to the current pool baseline or the browser is
+ replaced if it cannot safely receive that baseline. Stale browsers cannot be
+ acquired. Other network fields only apply to browsers created after the update;
+ existing browsers keep those fields until discarded (see discard_all_idle).
"""
profile: Profile
From f7daa043513d08f6cc6aefec7b058d5bd57f2590 Mon Sep 17 00:00:00 2001
From: "kernel-internal[bot]"
<260533166+kernel-internal[bot]@users.noreply.github.com>
Date: Fri, 9 Oct 2026 20:06:25 +0000
Subject: [PATCH 347/366] feat: Filter the telemetry SSE stream by event type
Stainless-Generated-From: d9acb7a1d1280429f6a722334df6386f763b66e1
---
src/kernel/resources/browsers/telemetry.py | 44 +++++++++++++++++--
.../types/browsers/telemetry_stream_params.py | 12 +++++
.../api_resources/browsers/test_telemetry.py | 2 +
3 files changed, 54 insertions(+), 4 deletions(-)
diff --git a/src/kernel/resources/browsers/telemetry.py b/src/kernel/resources/browsers/telemetry.py
index 45cc3516..c3e889b3 100644
--- a/src/kernel/resources/browsers/telemetry.py
+++ b/src/kernel/resources/browsers/telemetry.py
@@ -157,6 +157,7 @@ def stream(
id_or_name: str,
*,
replay: str | Omit = omit,
+ type: SequenceNotStr[str] | Omit = omit,
last_event_id: str | Omit = omit,
# Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
# The extra values given here take precedence over values defined on the client or passed to this method.
@@ -171,7 +172,10 @@ def stream(
stream closes when the browser session terminates. Each event frame includes an
id: field containing a monotonically increasing sequence number; pass it as
Last-Event-ID on reconnect to resume without gaps. The event: field is never
- set; all frames carry JSON in the data: field. A keepalive comment frame is sent
+ set; event frames carry JSON in the data: field. A frame with an id: field and
+ no data: field moves Last-Event-ID past events that were not delivered, because
+ a type filter excluded them or they could not be decoded or exceeded the size
+ limit; SSE clients dispatch no event for it. A keepalive comment frame is sent
every 15 seconds when no events arrive. Returns 404 if the browser session does
not exist. If telemetry was not enabled on the session, the stream opens but no
events are delivered. Fresh connections only see new events; pass replay=all to
@@ -182,6 +186,14 @@ def stream(
any other value is treated as from-now. The buffer is bounded, so the first
event id may be greater than 1 if older events were evicted.
+ type: Deliver only these event types, such as captcha_solve_started or
+ captcha_challenge_result. Repeat the parameter or pass comma-separated values
+ for multiple types. Keepalive frames are always delivered. Filtered-out events
+ are not sent. An id-only frame carrying the latest skipped id is sent about
+ every 15 seconds while skipped events keep arriving, and otherwise with the next
+ keepalive or when the stream ends, so the connection stays active and
+ Last-Event-ID moves past them; a reconnect resumes after the skipped events.
+
extra_headers: Send extra headers
extra_query: Add additional query parameters to the request
@@ -201,7 +213,13 @@ def stream(
extra_query=extra_query,
extra_body=extra_body,
timeout=timeout,
- query=maybe_transform({"replay": replay}, telemetry_stream_params.TelemetryStreamParams),
+ query=maybe_transform(
+ {
+ "replay": replay,
+ "type": type,
+ },
+ telemetry_stream_params.TelemetryStreamParams,
+ ),
),
cast_to=TelemetryStreamResponse,
stream=True,
@@ -339,6 +357,7 @@ async def stream(
id_or_name: str,
*,
replay: str | Omit = omit,
+ type: SequenceNotStr[str] | Omit = omit,
last_event_id: str | Omit = omit,
# Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
# The extra values given here take precedence over values defined on the client or passed to this method.
@@ -353,7 +372,10 @@ async def stream(
stream closes when the browser session terminates. Each event frame includes an
id: field containing a monotonically increasing sequence number; pass it as
Last-Event-ID on reconnect to resume without gaps. The event: field is never
- set; all frames carry JSON in the data: field. A keepalive comment frame is sent
+ set; event frames carry JSON in the data: field. A frame with an id: field and
+ no data: field moves Last-Event-ID past events that were not delivered, because
+ a type filter excluded them or they could not be decoded or exceeded the size
+ limit; SSE clients dispatch no event for it. A keepalive comment frame is sent
every 15 seconds when no events arrive. Returns 404 if the browser session does
not exist. If telemetry was not enabled on the session, the stream opens but no
events are delivered. Fresh connections only see new events; pass replay=all to
@@ -364,6 +386,14 @@ async def stream(
any other value is treated as from-now. The buffer is bounded, so the first
event id may be greater than 1 if older events were evicted.
+ type: Deliver only these event types, such as captcha_solve_started or
+ captcha_challenge_result. Repeat the parameter or pass comma-separated values
+ for multiple types. Keepalive frames are always delivered. Filtered-out events
+ are not sent. An id-only frame carrying the latest skipped id is sent about
+ every 15 seconds while skipped events keep arriving, and otherwise with the next
+ keepalive or when the stream ends, so the connection stays active and
+ Last-Event-ID moves past them; a reconnect resumes after the skipped events.
+
extra_headers: Send extra headers
extra_query: Add additional query parameters to the request
@@ -383,7 +413,13 @@ async def stream(
extra_query=extra_query,
extra_body=extra_body,
timeout=timeout,
- query=await async_maybe_transform({"replay": replay}, telemetry_stream_params.TelemetryStreamParams),
+ query=await async_maybe_transform(
+ {
+ "replay": replay,
+ "type": type,
+ },
+ telemetry_stream_params.TelemetryStreamParams,
+ ),
),
cast_to=TelemetryStreamResponse,
stream=True,
diff --git a/src/kernel/types/browsers/telemetry_stream_params.py b/src/kernel/types/browsers/telemetry_stream_params.py
index d9703bc7..3e99dd5e 100644
--- a/src/kernel/types/browsers/telemetry_stream_params.py
+++ b/src/kernel/types/browsers/telemetry_stream_params.py
@@ -4,6 +4,7 @@
from typing_extensions import Annotated, TypedDict
+from ..._types import SequenceNotStr
from ..._utils import PropertyInfo
__all__ = ["TelemetryStreamParams"]
@@ -17,4 +18,15 @@ class TelemetryStreamParams(TypedDict, total=False):
event id may be greater than 1 if older events were evicted.
"""
+ type: SequenceNotStr[str]
+ """
+ Deliver only these event types, such as captcha_solve_started or
+ captcha_challenge_result. Repeat the parameter or pass comma-separated values
+ for multiple types. Keepalive frames are always delivered. Filtered-out events
+ are not sent. An id-only frame carrying the latest skipped id is sent about
+ every 15 seconds while skipped events keep arriving, and otherwise with the next
+ keepalive or when the stream ends, so the connection stays active and
+ Last-Event-ID moves past them; a reconnect resumes after the skipped events.
+ """
+
last_event_id: Annotated[str, PropertyInfo(alias="Last-Event-ID")]
diff --git a/tests/api_resources/browsers/test_telemetry.py b/tests/api_resources/browsers/test_telemetry.py
index 6bf65cc9..62742f45 100644
--- a/tests/api_resources/browsers/test_telemetry.py
+++ b/tests/api_resources/browsers/test_telemetry.py
@@ -89,6 +89,7 @@ def test_method_stream_with_all_params(self, client: Kernel) -> None:
telemetry_stream = client.browsers.telemetry.stream(
id_or_name="htzv5orfit78e1m2biiifpbv",
replay="replay",
+ type=["string"],
last_event_id="Last-Event-ID",
)
telemetry_stream.response.close()
@@ -203,6 +204,7 @@ async def test_method_stream_with_all_params(self, async_client: AsyncKernel) ->
telemetry_stream = await async_client.browsers.telemetry.stream(
id_or_name="htzv5orfit78e1m2biiifpbv",
replay="replay",
+ type=["string"],
last_event_id="Last-Event-ID",
)
await telemetry_stream.response.aclose()
From 14c0301dec47634fa3239b33053a066d6c772359 Mon Sep 17 00:00:00 2001
From: "kernel-internal[bot]"
<260533166+kernel-internal[bot]@users.noreply.github.com>
Date: Fri, 9 Oct 2026 20:11:36 +0000
Subject: [PATCH 348/366] release: 0.123.0
---
.release-please-manifest.json | 2 +-
CHANGELOG.md | 10 ++++++++++
pyproject.toml | 2 +-
src/kernel/_version.py | 2 +-
4 files changed, 13 insertions(+), 3 deletions(-)
diff --git a/.release-please-manifest.json b/.release-please-manifest.json
index bd115b20..58ebf7f2 100644
--- a/.release-please-manifest.json
+++ b/.release-please-manifest.json
@@ -1,3 +1,3 @@
{
- ".": "0.122.0"
+ ".": "0.123.0"
}
\ No newline at end of file
diff --git a/CHANGELOG.md b/CHANGELOG.md
index f7520574..7fdca44b 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,15 @@
# Changelog
+## [0.123.0](https://github.com/kernel/kernel-python-sdk/compare/v0.122.0...v0.123.0) (2026-10-09)
+
+
+### Features
+
+* Add video_memory to request the 4GiB VRAM GPU browser tier ([31b7bda](https://github.com/kernel/kernel-python-sdk/commit/31b7bda620f53bc6026b5eb3fac34d50859fa1df))
+* Filter the telemetry SSE stream by event type ([f7daa04](https://github.com/kernel/kernel-python-sdk/commit/f7daa043513d08f6cc6aefec7b058d5bd57f2590))
+* Fix 1Password fill classification for repeat fills and unconfirmed submits ([d387aab](https://github.com/kernel/kernel-python-sdk/commit/d387aab4847329eb4405318a20ee93a372399f70))
+* Support egress allowlists on browser pools ([541fb3f](https://github.com/kernel/kernel-python-sdk/commit/541fb3fc033b8b9542838a116684578cab18fba0))
+
## [0.122.0](https://github.com/kernel/kernel-python-sdk/compare/v0.121.0...v0.122.0) (2026-10-09)
diff --git a/pyproject.toml b/pyproject.toml
index 1f59c57a..6e2be1a4 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -1,6 +1,6 @@
[project]
name = "kernel"
-version = "0.122.0"
+version = "0.123.0"
description = "The official Python library for the kernel API"
dynamic = ["readme"]
license = "Apache-2.0"
diff --git a/src/kernel/_version.py b/src/kernel/_version.py
index 72628b36..f0320775 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.122.0" # x-release-please-version
+__version__ = "0.123.0" # x-release-please-version
From fc4b0de79e3242361ef7b11acbd2cbaeb058265e Mon Sep 17 00:00:00 2001
From: "kernel-internal[bot]"
<260533166+kernel-internal[bot]@users.noreply.github.com>
Date: Sat, 10 Oct 2026 23:03:39 +0000
Subject: [PATCH 349/366] feat: Complete Visa vault verification and
transaction reporting
Stainless-Generated-From: 95755eb52964f48e3ecbcb51119caf08834cc53f
---
api.md | 1 +
src/kernel/resources/vaults/items.py | 150 ++++++++
src/kernel/types/vaults/__init__.py | 3 +
...tion_vault_item_operation_request_param.py | 35 ++
.../vaults/credential_account_vault_item.py | 1 +
.../types/vaults/credential_vault_item.py | 1 +
.../vaults/item_perform_operation_params.py | 23 ++
.../vaults/kernel_card_vault_item_spec.py | 9 +
.../kernel_card_vault_item_spec_param.py | 9 +
src/kernel/types/vaults/vault_item.py | 2 +
.../vaults/vault_item_operation_response.py | 2 +
tests/api_resources/vaults/test_items.py | 332 +++++++++++++-----
12 files changed, 484 insertions(+), 84 deletions(-)
create mode 100644 src/kernel/types/vaults/confirm_transaction_vault_item_operation_request_param.py
diff --git a/api.md b/api.md
index 1b71010c..ffe8f507 100644
--- a/api.md
+++ b/api.md
@@ -571,6 +571,7 @@ from kernel.types.vaults import (
CardVaultItemSpec,
CardVaultItemState,
CollectVaultItemOperationRequest,
+ ConfirmTransactionVaultItemOperationRequest,
CredentialAccountVaultItem,
CredentialAccountVaultItemRequest,
CredentialCollectionAction,
diff --git a/src/kernel/resources/vaults/items.py b/src/kernel/resources/vaults/items.py
index badbd5bc..7de1bee2 100644
--- a/src/kernel/resources/vaults/items.py
+++ b/src/kernel/resources/vaults/items.py
@@ -440,6 +440,66 @@ def perform_operation(
"""
...
+ @overload
+ def perform_operation(
+ self,
+ key: str,
+ *,
+ id_or_name: str,
+ amount: int,
+ currency: str,
+ occurred_at: Union[str, datetime],
+ status: Literal["APPROVED", "DECLINED", "PENDING", "ERROR", "CANCELLED"],
+ transaction_type: Literal[
+ "PURCHASE", "AUTHORIZATION", "CAPTURE", "REFUND", "REVERSAL", "VERIFICATION", "CHARGEBACK", "FRAUD"
+ ],
+ type: Literal["confirm_transaction"],
+ # 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,
+ ) -> VaultItemOperationResponse:
+ """
+ Retrieve the item first and invoke only an operation listed in
+ `available_operations`, following its natural-language description. Availability
+ is rechecked at execution time; unavailable operations return 409. Authorization
+ and preparation may call an external provider and return updated state. Link
+ cards advertise authorize without checkout context. Eligible unused AgentCard
+ cards advertise prepare_checkout, which requires checkout context and obtains
+ device approval before native Square Pay. Keep the returned approval page open,
+ poll until ready_to_submit, then submit before preparation.expires_at. Unused
+ preparations expire automatically and cannot be reused. If spend-request
+ creation is rejected with a non-retryable provider error, the card item is
+ deleted and the provider's error code and message are returned. Rate limits
+ return HTTP 429 and retain the card item; stop, back off, and retry the same
+ authorize operation.
+
+ Fill returns a value-free execution result. Validation failures before writing
+ return 400 (invalid request or targets), 403 (access or destination denied), 404
+ (resource not found), or 409 (item or browser not ready). Once writing starts,
+ known partial failures and indeterminate field outcomes return 200 with status
+ `failed` or `unknown`. Fill never submits the page, so it is safe to retry after
+ a failure, an `unknown` outcome, or a transport error.
+
+ Args:
+ amount: Actual transaction amount in minor units, no greater than the approved limit.
+
+ currency: Must match the approved purchase currency.
+
+ occurred_at: Time the merchant outcome was observed.
+
+ 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
+ """
+ ...
+
@overload
def perform_operation(
self,
@@ -928,6 +988,7 @@ def perform_operation(
@required_args(
["id_or_name", "type"],
+ ["id_or_name", "amount", "currency", "occurred_at", "status", "transaction_type", "type"],
["id_or_name", "checkout", "type"],
["id_or_name", "browser_id", "fields", "type"],
["id_or_name", "browser_id", "page_url", "type"],
@@ -940,6 +1001,7 @@ def perform_operation(
*,
id_or_name: str,
type: Literal["authorize"]
+ | Literal["confirm_transaction"]
| Literal["collect"]
| Literal["prepare_checkout"]
| Literal["fill"]
@@ -949,6 +1011,14 @@ def perform_operation(
| Literal["1pw_recover"]
| Literal["1pw_update_access_token"]
| Literal["webmcp_invoke"],
+ amount: int | Omit = omit,
+ currency: str | Omit = omit,
+ occurred_at: Union[str, datetime] | Omit = omit,
+ status: Literal["APPROVED", "DECLINED", "PENDING", "ERROR", "CANCELLED"] | Omit = omit,
+ transaction_type: Literal[
+ "PURCHASE", "AUTHORIZATION", "CAPTURE", "REFUND", "REVERSAL", "VERIFICATION", "CHARGEBACK", "FRAUD"
+ ]
+ | Omit = omit,
checkout: VaultCheckoutContextParam | Omit = omit,
browser_id: str | Omit = omit,
fields: Iterable[VaultFillFieldParam] | Omit = omit,
@@ -983,6 +1053,11 @@ def perform_operation(
body=maybe_transform(
{
"type": type,
+ "amount": amount,
+ "currency": currency,
+ "occurred_at": occurred_at,
+ "status": status,
+ "transaction_type": transaction_type,
"checkout": checkout,
"browser_id": browser_id,
"fields": fields,
@@ -1614,6 +1689,66 @@ async def perform_operation(
"""
...
+ @overload
+ async def perform_operation(
+ self,
+ key: str,
+ *,
+ id_or_name: str,
+ amount: int,
+ currency: str,
+ occurred_at: Union[str, datetime],
+ status: Literal["APPROVED", "DECLINED", "PENDING", "ERROR", "CANCELLED"],
+ transaction_type: Literal[
+ "PURCHASE", "AUTHORIZATION", "CAPTURE", "REFUND", "REVERSAL", "VERIFICATION", "CHARGEBACK", "FRAUD"
+ ],
+ type: Literal["confirm_transaction"],
+ # 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,
+ ) -> VaultItemOperationResponse:
+ """
+ Retrieve the item first and invoke only an operation listed in
+ `available_operations`, following its natural-language description. Availability
+ is rechecked at execution time; unavailable operations return 409. Authorization
+ and preparation may call an external provider and return updated state. Link
+ cards advertise authorize without checkout context. Eligible unused AgentCard
+ cards advertise prepare_checkout, which requires checkout context and obtains
+ device approval before native Square Pay. Keep the returned approval page open,
+ poll until ready_to_submit, then submit before preparation.expires_at. Unused
+ preparations expire automatically and cannot be reused. If spend-request
+ creation is rejected with a non-retryable provider error, the card item is
+ deleted and the provider's error code and message are returned. Rate limits
+ return HTTP 429 and retain the card item; stop, back off, and retry the same
+ authorize operation.
+
+ Fill returns a value-free execution result. Validation failures before writing
+ return 400 (invalid request or targets), 403 (access or destination denied), 404
+ (resource not found), or 409 (item or browser not ready). Once writing starts,
+ known partial failures and indeterminate field outcomes return 200 with status
+ `failed` or `unknown`. Fill never submits the page, so it is safe to retry after
+ a failure, an `unknown` outcome, or a transport error.
+
+ Args:
+ amount: Actual transaction amount in minor units, no greater than the approved limit.
+
+ currency: Must match the approved purchase currency.
+
+ occurred_at: Time the merchant outcome was observed.
+
+ 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
+ """
+ ...
+
@overload
async def perform_operation(
self,
@@ -2102,6 +2237,7 @@ async def perform_operation(
@required_args(
["id_or_name", "type"],
+ ["id_or_name", "amount", "currency", "occurred_at", "status", "transaction_type", "type"],
["id_or_name", "checkout", "type"],
["id_or_name", "browser_id", "fields", "type"],
["id_or_name", "browser_id", "page_url", "type"],
@@ -2114,6 +2250,7 @@ async def perform_operation(
*,
id_or_name: str,
type: Literal["authorize"]
+ | Literal["confirm_transaction"]
| Literal["collect"]
| Literal["prepare_checkout"]
| Literal["fill"]
@@ -2123,6 +2260,14 @@ async def perform_operation(
| Literal["1pw_recover"]
| Literal["1pw_update_access_token"]
| Literal["webmcp_invoke"],
+ amount: int | Omit = omit,
+ currency: str | Omit = omit,
+ occurred_at: Union[str, datetime] | Omit = omit,
+ status: Literal["APPROVED", "DECLINED", "PENDING", "ERROR", "CANCELLED"] | Omit = omit,
+ transaction_type: Literal[
+ "PURCHASE", "AUTHORIZATION", "CAPTURE", "REFUND", "REVERSAL", "VERIFICATION", "CHARGEBACK", "FRAUD"
+ ]
+ | Omit = omit,
checkout: VaultCheckoutContextParam | Omit = omit,
browser_id: str | Omit = omit,
fields: Iterable[VaultFillFieldParam] | Omit = omit,
@@ -2157,6 +2302,11 @@ async def perform_operation(
body=await async_maybe_transform(
{
"type": type,
+ "amount": amount,
+ "currency": currency,
+ "occurred_at": occurred_at,
+ "status": status,
+ "transaction_type": transaction_type,
"checkout": checkout,
"browser_id": browser_id,
"fields": fields,
diff --git a/src/kernel/types/vaults/__init__.py b/src/kernel/types/vaults/__init__.py
index dc84fc10..1b584d27 100644
--- a/src/kernel/types/vaults/__init__.py
+++ b/src/kernel/types/vaults/__init__.py
@@ -113,6 +113,9 @@
from .one_password_fill_vault_item_operation_request_param import (
OnePasswordFillVaultItemOperationRequestParam as OnePasswordFillVaultItemOperationRequestParam,
)
+from .confirm_transaction_vault_item_operation_request_param import (
+ ConfirmTransactionVaultItemOperationRequestParam as ConfirmTransactionVaultItemOperationRequestParam,
+)
from .one_password_recover_vault_item_operation_request_param import (
OnePasswordRecoverVaultItemOperationRequestParam as OnePasswordRecoverVaultItemOperationRequestParam,
)
diff --git a/src/kernel/types/vaults/confirm_transaction_vault_item_operation_request_param.py b/src/kernel/types/vaults/confirm_transaction_vault_item_operation_request_param.py
new file mode 100644
index 00000000..3b0e0485
--- /dev/null
+++ b/src/kernel/types/vaults/confirm_transaction_vault_item_operation_request_param.py
@@ -0,0 +1,35 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from __future__ import annotations
+
+from typing import Union
+from datetime import datetime
+from typing_extensions import Literal, Required, Annotated, TypedDict
+
+from ..._utils import PropertyInfo
+
+__all__ = ["ConfirmTransactionVaultItemOperationRequestParam"]
+
+
+class ConfirmTransactionVaultItemOperationRequestParam(TypedDict, total=False):
+ """Report a real merchant transaction outcome for an issued Kernel Visa credential.
+
+ Never infer success from filling checkout or receiving a credential. Supply the observed status, transaction type, actual amount, currency and timestamp. Unknown outcomes require manual reconciliation, not retry. Each purchase accepts one report.
+ """
+
+ amount: Required[int]
+ """Actual transaction amount in minor units, no greater than the approved limit."""
+
+ currency: Required[str]
+ """Must match the approved purchase currency."""
+
+ occurred_at: Required[Annotated[Union[str, datetime], PropertyInfo(format="iso8601")]]
+ """Time the merchant outcome was observed."""
+
+ status: Required[Literal["APPROVED", "DECLINED", "PENDING", "ERROR", "CANCELLED"]]
+
+ transaction_type: Required[
+ Literal["PURCHASE", "AUTHORIZATION", "CAPTURE", "REFUND", "REVERSAL", "VERIFICATION", "CHARGEBACK", "FRAUD"]
+ ]
+
+ type: Required[Literal["confirm_transaction"]]
diff --git a/src/kernel/types/vaults/credential_account_vault_item.py b/src/kernel/types/vaults/credential_account_vault_item.py
index 109d5ce3..7f86d264 100644
--- a/src/kernel/types/vaults/credential_account_vault_item.py
+++ b/src/kernel/types/vaults/credential_account_vault_item.py
@@ -32,6 +32,7 @@ class AvailableOperation(BaseModel):
type: Literal[
"authorize",
+ "confirm_transaction",
"collect",
"prepare_checkout",
"fill",
diff --git a/src/kernel/types/vaults/credential_vault_item.py b/src/kernel/types/vaults/credential_vault_item.py
index 2f28e21b..e16ae365 100644
--- a/src/kernel/types/vaults/credential_vault_item.py
+++ b/src/kernel/types/vaults/credential_vault_item.py
@@ -39,6 +39,7 @@ class AvailableOperation(BaseModel):
type: Literal[
"authorize",
+ "confirm_transaction",
"collect",
"prepare_checkout",
"fill",
diff --git a/src/kernel/types/vaults/item_perform_operation_params.py b/src/kernel/types/vaults/item_perform_operation_params.py
index 0abfc780..00600ecf 100644
--- a/src/kernel/types/vaults/item_perform_operation_params.py
+++ b/src/kernel/types/vaults/item_perform_operation_params.py
@@ -15,6 +15,7 @@
__all__ = [
"ItemPerformOperationParams",
"AuthorizeVaultItemOperationRequest",
+ "ConfirmTransactionVaultItemOperationRequest",
"CollectVaultItemOperationRequest",
"PrepareCheckoutVaultItemOperationRequest",
"FillVaultItemOperationRequest",
@@ -33,6 +34,27 @@ class AuthorizeVaultItemOperationRequest(TypedDict, total=False):
type: Required[Literal["authorize"]]
+class ConfirmTransactionVaultItemOperationRequest(TypedDict, total=False):
+ id_or_name: Required[str]
+
+ amount: Required[int]
+ """Actual transaction amount in minor units, no greater than the approved limit."""
+
+ currency: Required[str]
+ """Must match the approved purchase currency."""
+
+ occurred_at: Required[Annotated[Union[str, datetime], PropertyInfo(format="iso8601")]]
+ """Time the merchant outcome was observed."""
+
+ status: Required[Literal["APPROVED", "DECLINED", "PENDING", "ERROR", "CANCELLED"]]
+
+ transaction_type: Required[
+ Literal["PURCHASE", "AUTHORIZATION", "CAPTURE", "REFUND", "REVERSAL", "VERIFICATION", "CHARGEBACK", "FRAUD"]
+ ]
+
+ type: Required[Literal["confirm_transaction"]]
+
+
class CollectVaultItemOperationRequest(TypedDict, total=False):
id_or_name: Required[str]
@@ -173,6 +195,7 @@ class WebmcpInvokeVaultItemOperationRequest(TypedDict, total=False):
ItemPerformOperationParams: TypeAlias = Union[
AuthorizeVaultItemOperationRequest,
+ ConfirmTransactionVaultItemOperationRequest,
CollectVaultItemOperationRequest,
PrepareCheckoutVaultItemOperationRequest,
FillVaultItemOperationRequest,
diff --git a/src/kernel/types/vaults/kernel_card_vault_item_spec.py b/src/kernel/types/vaults/kernel_card_vault_item_spec.py
index 85d2d13d..1581c68c 100644
--- a/src/kernel/types/vaults/kernel_card_vault_item_spec.py
+++ b/src/kernel/types/vaults/kernel_card_vault_item_spec.py
@@ -37,6 +37,15 @@ class KernelCardVaultItemSpec(BaseModel):
wallet: str
"""Key of the Kernel wallet item whose enrolled card pays."""
+ merchant_category: Optional[str] = None
+ """Actual merchant category when known. Omit rather than invent a category."""
+
+ merchant_category_code: Optional[str] = None
+ """Actual merchant MCC when known.
+
+ Omit rather than invent a code. 0000 is not accepted.
+ """
+
merchant_country: Optional[str] = None
"""The merchant's ISO 3166-1 alpha-2 country code.
diff --git a/src/kernel/types/vaults/kernel_card_vault_item_spec_param.py b/src/kernel/types/vaults/kernel_card_vault_item_spec_param.py
index 096e8c7b..72d6dae8 100644
--- a/src/kernel/types/vaults/kernel_card_vault_item_spec_param.py
+++ b/src/kernel/types/vaults/kernel_card_vault_item_spec_param.py
@@ -36,6 +36,15 @@ class KernelCardVaultItemSpecParam(TypedDict, total=False):
wallet: Required[str]
"""Key of the Kernel wallet item whose enrolled card pays."""
+ merchant_category: str
+ """Actual merchant category when known. Omit rather than invent a category."""
+
+ merchant_category_code: str
+ """Actual merchant MCC when known.
+
+ Omit rather than invent a code. 0000 is not accepted.
+ """
+
merchant_country: str
"""The merchant's ISO 3166-1 alpha-2 country code.
diff --git a/src/kernel/types/vaults/vault_item.py b/src/kernel/types/vaults/vault_item.py
index c1e8c4f3..9ef7f5e2 100644
--- a/src/kernel/types/vaults/vault_item.py
+++ b/src/kernel/types/vaults/vault_item.py
@@ -47,6 +47,7 @@ class WalletVaultItemAvailableOperation(BaseModel):
type: Literal[
"authorize",
+ "confirm_transaction",
"collect",
"prepare_checkout",
"fill",
@@ -122,6 +123,7 @@ class CardVaultItemAvailableOperation(BaseModel):
type: Literal[
"authorize",
+ "confirm_transaction",
"collect",
"prepare_checkout",
"fill",
diff --git a/src/kernel/types/vaults/vault_item_operation_response.py b/src/kernel/types/vaults/vault_item_operation_response.py
index fdc4bf99..484a72b7 100644
--- a/src/kernel/types/vaults/vault_item_operation_response.py
+++ b/src/kernel/types/vaults/vault_item_operation_response.py
@@ -49,6 +49,7 @@ class WalletVaultItemAvailableOperation(BaseModel):
type: Literal[
"authorize",
+ "confirm_transaction",
"collect",
"prepare_checkout",
"fill",
@@ -124,6 +125,7 @@ class CardVaultItemAvailableOperation(BaseModel):
type: Literal[
"authorize",
+ "confirm_transaction",
"collect",
"prepare_checkout",
"fill",
diff --git a/tests/api_resources/vaults/test_items.py b/tests/api_resources/vaults/test_items.py
index 0a85a9d1..45b4573a 100644
--- a/tests/api_resources/vaults/test_items.py
+++ b/tests/api_resources/vaults/test_items.py
@@ -544,7 +544,12 @@ def test_method_perform_operation_overload_2(self, client: Kernel) -> None:
item = client.vaults.items.perform_operation(
key="key",
id_or_name="id_or_name",
- type="collect",
+ amount=0,
+ currency="SEW",
+ occurred_at=parse_datetime("2019-12-27T18:11:19.117Z"),
+ status="APPROVED",
+ transaction_type="PURCHASE",
+ type="confirm_transaction",
)
assert_matches_type(VaultItemOperationResponse, item, path=["response"])
@@ -554,7 +559,12 @@ def test_raw_response_perform_operation_overload_2(self, client: Kernel) -> None
response = client.vaults.items.with_raw_response.perform_operation(
key="key",
id_or_name="id_or_name",
- type="collect",
+ amount=0,
+ currency="SEW",
+ occurred_at=parse_datetime("2019-12-27T18:11:19.117Z"),
+ status="APPROVED",
+ transaction_type="PURCHASE",
+ type="confirm_transaction",
)
assert response.is_closed is True
@@ -568,7 +578,12 @@ def test_streaming_response_perform_operation_overload_2(self, client: Kernel) -
with client.vaults.items.with_streaming_response.perform_operation(
key="key",
id_or_name="id_or_name",
- type="collect",
+ amount=0,
+ currency="SEW",
+ occurred_at=parse_datetime("2019-12-27T18:11:19.117Z"),
+ status="APPROVED",
+ transaction_type="PURCHASE",
+ type="confirm_transaction",
) as response:
assert not response.is_closed
assert response.http_request.headers.get("X-Stainless-Lang") == "python"
@@ -581,6 +596,73 @@ def test_streaming_response_perform_operation_overload_2(self, client: Kernel) -
@pytest.mark.skip(reason="Mock server tests are disabled")
@parametrize
def test_path_params_perform_operation_overload_2(self, client: Kernel) -> None:
+ with pytest.raises(ValueError, match=r"Expected a non-empty value for `id_or_name` but received ''"):
+ client.vaults.items.with_raw_response.perform_operation(
+ key="key",
+ id_or_name="",
+ amount=0,
+ currency="SEW",
+ occurred_at=parse_datetime("2019-12-27T18:11:19.117Z"),
+ status="APPROVED",
+ transaction_type="PURCHASE",
+ type="confirm_transaction",
+ )
+
+ with pytest.raises(ValueError, match=r"Expected a non-empty value for `key` but received ''"):
+ client.vaults.items.with_raw_response.perform_operation(
+ key="",
+ id_or_name="id_or_name",
+ amount=0,
+ currency="SEW",
+ occurred_at=parse_datetime("2019-12-27T18:11:19.117Z"),
+ status="APPROVED",
+ transaction_type="PURCHASE",
+ type="confirm_transaction",
+ )
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ def test_method_perform_operation_overload_3(self, client: Kernel) -> None:
+ item = client.vaults.items.perform_operation(
+ key="key",
+ id_or_name="id_or_name",
+ type="collect",
+ )
+ assert_matches_type(VaultItemOperationResponse, item, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ def test_raw_response_perform_operation_overload_3(self, client: Kernel) -> None:
+ response = client.vaults.items.with_raw_response.perform_operation(
+ key="key",
+ id_or_name="id_or_name",
+ type="collect",
+ )
+
+ assert response.is_closed is True
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+ item = response.parse()
+ assert_matches_type(VaultItemOperationResponse, item, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ def test_streaming_response_perform_operation_overload_3(self, client: Kernel) -> None:
+ with client.vaults.items.with_streaming_response.perform_operation(
+ key="key",
+ id_or_name="id_or_name",
+ type="collect",
+ ) as response:
+ assert not response.is_closed
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+
+ item = response.parse()
+ assert_matches_type(VaultItemOperationResponse, item, path=["response"])
+
+ assert cast(Any, response.is_closed) is True
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ def test_path_params_perform_operation_overload_3(self, client: Kernel) -> None:
with pytest.raises(ValueError, match=r"Expected a non-empty value for `id_or_name` but received ''"):
client.vaults.items.with_raw_response.perform_operation(
key="key",
@@ -597,7 +679,7 @@ def test_path_params_perform_operation_overload_2(self, client: Kernel) -> None:
@pytest.mark.skip(reason="Mock server tests are disabled")
@parametrize
- def test_method_perform_operation_overload_3(self, client: Kernel) -> None:
+ def test_method_perform_operation_overload_4(self, client: Kernel) -> None:
item = client.vaults.items.perform_operation(
key="key",
id_or_name="id_or_name",
@@ -612,7 +694,7 @@ def test_method_perform_operation_overload_3(self, client: Kernel) -> None:
@pytest.mark.skip(reason="Mock server tests are disabled")
@parametrize
- def test_method_perform_operation_with_all_params_overload_3(self, client: Kernel) -> None:
+ def test_method_perform_operation_with_all_params_overload_4(self, client: Kernel) -> None:
item = client.vaults.items.perform_operation(
key="key",
id_or_name="id_or_name",
@@ -628,7 +710,7 @@ def test_method_perform_operation_with_all_params_overload_3(self, client: Kerne
@pytest.mark.skip(reason="Mock server tests are disabled")
@parametrize
- def test_raw_response_perform_operation_overload_3(self, client: Kernel) -> None:
+ def test_raw_response_perform_operation_overload_4(self, client: Kernel) -> None:
response = client.vaults.items.with_raw_response.perform_operation(
key="key",
id_or_name="id_or_name",
@@ -647,7 +729,7 @@ def test_raw_response_perform_operation_overload_3(self, client: Kernel) -> None
@pytest.mark.skip(reason="Mock server tests are disabled")
@parametrize
- def test_streaming_response_perform_operation_overload_3(self, client: Kernel) -> None:
+ def test_streaming_response_perform_operation_overload_4(self, client: Kernel) -> None:
with client.vaults.items.with_streaming_response.perform_operation(
key="key",
id_or_name="id_or_name",
@@ -668,7 +750,7 @@ def test_streaming_response_perform_operation_overload_3(self, client: Kernel) -
@pytest.mark.skip(reason="Mock server tests are disabled")
@parametrize
- def test_path_params_perform_operation_overload_3(self, client: Kernel) -> None:
+ def test_path_params_perform_operation_overload_4(self, client: Kernel) -> None:
with pytest.raises(ValueError, match=r"Expected a non-empty value for `id_or_name` but received ''"):
client.vaults.items.with_raw_response.perform_operation(
key="key",
@@ -695,7 +777,7 @@ def test_path_params_perform_operation_overload_3(self, client: Kernel) -> None:
@pytest.mark.skip(reason="Mock server tests are disabled")
@parametrize
- def test_method_perform_operation_overload_4(self, client: Kernel) -> None:
+ def test_method_perform_operation_overload_5(self, client: Kernel) -> None:
item = client.vaults.items.perform_operation(
key="key",
id_or_name="id_or_name",
@@ -724,7 +806,7 @@ def test_method_perform_operation_overload_4(self, client: Kernel) -> None:
@pytest.mark.skip(reason="Mock server tests are disabled")
@parametrize
- def test_method_perform_operation_with_all_params_overload_4(self, client: Kernel) -> None:
+ def test_method_perform_operation_with_all_params_overload_5(self, client: Kernel) -> None:
item = client.vaults.items.perform_operation(
key="key",
id_or_name="id_or_name",
@@ -759,7 +841,7 @@ def test_method_perform_operation_with_all_params_overload_4(self, client: Kerne
@pytest.mark.skip(reason="Mock server tests are disabled")
@parametrize
- def test_raw_response_perform_operation_overload_4(self, client: Kernel) -> None:
+ def test_raw_response_perform_operation_overload_5(self, client: Kernel) -> None:
response = client.vaults.items.with_raw_response.perform_operation(
key="key",
id_or_name="id_or_name",
@@ -792,7 +874,7 @@ def test_raw_response_perform_operation_overload_4(self, client: Kernel) -> None
@pytest.mark.skip(reason="Mock server tests are disabled")
@parametrize
- def test_streaming_response_perform_operation_overload_4(self, client: Kernel) -> None:
+ def test_streaming_response_perform_operation_overload_5(self, client: Kernel) -> None:
with client.vaults.items.with_streaming_response.perform_operation(
key="key",
id_or_name="id_or_name",
@@ -827,7 +909,7 @@ def test_streaming_response_perform_operation_overload_4(self, client: Kernel) -
@pytest.mark.skip(reason="Mock server tests are disabled")
@parametrize
- def test_path_params_perform_operation_overload_4(self, client: Kernel) -> None:
+ def test_path_params_perform_operation_overload_5(self, client: Kernel) -> None:
with pytest.raises(ValueError, match=r"Expected a non-empty value for `id_or_name` but received ''"):
client.vaults.items.with_raw_response.perform_operation(
key="key",
@@ -882,7 +964,7 @@ def test_path_params_perform_operation_overload_4(self, client: Kernel) -> None:
@pytest.mark.skip(reason="Mock server tests are disabled")
@parametrize
- def test_method_perform_operation_overload_5(self, client: Kernel) -> None:
+ def test_method_perform_operation_overload_6(self, client: Kernel) -> None:
item = client.vaults.items.perform_operation(
key="key",
id_or_name="id_or_name",
@@ -892,7 +974,7 @@ def test_method_perform_operation_overload_5(self, client: Kernel) -> None:
@pytest.mark.skip(reason="Mock server tests are disabled")
@parametrize
- def test_method_perform_operation_with_all_params_overload_5(self, client: Kernel) -> None:
+ def test_method_perform_operation_with_all_params_overload_6(self, client: Kernel) -> None:
item = client.vaults.items.perform_operation(
key="key",
id_or_name="id_or_name",
@@ -905,7 +987,7 @@ def test_method_perform_operation_with_all_params_overload_5(self, client: Kerne
@pytest.mark.skip(reason="Mock server tests are disabled")
@parametrize
- def test_raw_response_perform_operation_overload_5(self, client: Kernel) -> None:
+ def test_raw_response_perform_operation_overload_6(self, client: Kernel) -> None:
response = client.vaults.items.with_raw_response.perform_operation(
key="key",
id_or_name="id_or_name",
@@ -919,7 +1001,7 @@ def test_raw_response_perform_operation_overload_5(self, client: Kernel) -> None
@pytest.mark.skip(reason="Mock server tests are disabled")
@parametrize
- def test_streaming_response_perform_operation_overload_5(self, client: Kernel) -> None:
+ def test_streaming_response_perform_operation_overload_6(self, client: Kernel) -> None:
with client.vaults.items.with_streaming_response.perform_operation(
key="key",
id_or_name="id_or_name",
@@ -935,7 +1017,7 @@ def test_streaming_response_perform_operation_overload_5(self, client: Kernel) -
@pytest.mark.skip(reason="Mock server tests are disabled")
@parametrize
- def test_path_params_perform_operation_overload_5(self, client: Kernel) -> None:
+ def test_path_params_perform_operation_overload_6(self, client: Kernel) -> None:
with pytest.raises(ValueError, match=r"Expected a non-empty value for `id_or_name` but received ''"):
client.vaults.items.with_raw_response.perform_operation(
key="key",
@@ -952,7 +1034,7 @@ def test_path_params_perform_operation_overload_5(self, client: Kernel) -> None:
@pytest.mark.skip(reason="Mock server tests are disabled")
@parametrize
- def test_method_perform_operation_overload_6(self, client: Kernel) -> None:
+ def test_method_perform_operation_overload_7(self, client: Kernel) -> None:
item = client.vaults.items.perform_operation(
key="key",
id_or_name="id_or_name",
@@ -962,7 +1044,7 @@ def test_method_perform_operation_overload_6(self, client: Kernel) -> None:
@pytest.mark.skip(reason="Mock server tests are disabled")
@parametrize
- def test_method_perform_operation_with_all_params_overload_6(self, client: Kernel) -> None:
+ def test_method_perform_operation_with_all_params_overload_7(self, client: Kernel) -> None:
item = client.vaults.items.perform_operation(
key="key",
id_or_name="id_or_name",
@@ -973,7 +1055,7 @@ def test_method_perform_operation_with_all_params_overload_6(self, client: Kerne
@pytest.mark.skip(reason="Mock server tests are disabled")
@parametrize
- def test_raw_response_perform_operation_overload_6(self, client: Kernel) -> None:
+ def test_raw_response_perform_operation_overload_7(self, client: Kernel) -> None:
response = client.vaults.items.with_raw_response.perform_operation(
key="key",
id_or_name="id_or_name",
@@ -987,7 +1069,7 @@ def test_raw_response_perform_operation_overload_6(self, client: Kernel) -> None
@pytest.mark.skip(reason="Mock server tests are disabled")
@parametrize
- def test_streaming_response_perform_operation_overload_6(self, client: Kernel) -> None:
+ def test_streaming_response_perform_operation_overload_7(self, client: Kernel) -> None:
with client.vaults.items.with_streaming_response.perform_operation(
key="key",
id_or_name="id_or_name",
@@ -1003,7 +1085,7 @@ def test_streaming_response_perform_operation_overload_6(self, client: Kernel) -
@pytest.mark.skip(reason="Mock server tests are disabled")
@parametrize
- def test_path_params_perform_operation_overload_6(self, client: Kernel) -> None:
+ def test_path_params_perform_operation_overload_7(self, client: Kernel) -> None:
with pytest.raises(ValueError, match=r"Expected a non-empty value for `id_or_name` but received ''"):
client.vaults.items.with_raw_response.perform_operation(
key="key",
@@ -1020,7 +1102,7 @@ def test_path_params_perform_operation_overload_6(self, client: Kernel) -> None:
@pytest.mark.skip(reason="Mock server tests are disabled")
@parametrize
- def test_method_perform_operation_overload_7(self, client: Kernel) -> None:
+ def test_method_perform_operation_overload_8(self, client: Kernel) -> None:
item = client.vaults.items.perform_operation(
key="key",
id_or_name="id_or_name",
@@ -1032,7 +1114,7 @@ def test_method_perform_operation_overload_7(self, client: Kernel) -> None:
@pytest.mark.skip(reason="Mock server tests are disabled")
@parametrize
- def test_method_perform_operation_with_all_params_overload_7(self, client: Kernel) -> None:
+ def test_method_perform_operation_with_all_params_overload_8(self, client: Kernel) -> None:
item = client.vaults.items.perform_operation(
key="key",
id_or_name="id_or_name",
@@ -1046,7 +1128,7 @@ def test_method_perform_operation_with_all_params_overload_7(self, client: Kerne
@pytest.mark.skip(reason="Mock server tests are disabled")
@parametrize
- def test_raw_response_perform_operation_overload_7(self, client: Kernel) -> None:
+ def test_raw_response_perform_operation_overload_8(self, client: Kernel) -> None:
response = client.vaults.items.with_raw_response.perform_operation(
key="key",
id_or_name="id_or_name",
@@ -1062,7 +1144,7 @@ def test_raw_response_perform_operation_overload_7(self, client: Kernel) -> None
@pytest.mark.skip(reason="Mock server tests are disabled")
@parametrize
- def test_streaming_response_perform_operation_overload_7(self, client: Kernel) -> None:
+ def test_streaming_response_perform_operation_overload_8(self, client: Kernel) -> None:
with client.vaults.items.with_streaming_response.perform_operation(
key="key",
id_or_name="id_or_name",
@@ -1080,7 +1162,7 @@ def test_streaming_response_perform_operation_overload_7(self, client: Kernel) -
@pytest.mark.skip(reason="Mock server tests are disabled")
@parametrize
- def test_path_params_perform_operation_overload_7(self, client: Kernel) -> None:
+ def test_path_params_perform_operation_overload_8(self, client: Kernel) -> None:
with pytest.raises(ValueError, match=r"Expected a non-empty value for `id_or_name` but received ''"):
client.vaults.items.with_raw_response.perform_operation(
key="key",
@@ -1101,7 +1183,7 @@ def test_path_params_perform_operation_overload_7(self, client: Kernel) -> None:
@pytest.mark.skip(reason="Mock server tests are disabled")
@parametrize
- def test_method_perform_operation_overload_8(self, client: Kernel) -> None:
+ def test_method_perform_operation_overload_9(self, client: Kernel) -> None:
item = client.vaults.items.perform_operation(
key="key",
id_or_name="id_or_name",
@@ -1111,7 +1193,7 @@ def test_method_perform_operation_overload_8(self, client: Kernel) -> None:
@pytest.mark.skip(reason="Mock server tests are disabled")
@parametrize
- def test_raw_response_perform_operation_overload_8(self, client: Kernel) -> None:
+ def test_raw_response_perform_operation_overload_9(self, client: Kernel) -> None:
response = client.vaults.items.with_raw_response.perform_operation(
key="key",
id_or_name="id_or_name",
@@ -1125,7 +1207,7 @@ def test_raw_response_perform_operation_overload_8(self, client: Kernel) -> None
@pytest.mark.skip(reason="Mock server tests are disabled")
@parametrize
- def test_streaming_response_perform_operation_overload_8(self, client: Kernel) -> None:
+ def test_streaming_response_perform_operation_overload_9(self, client: Kernel) -> None:
with client.vaults.items.with_streaming_response.perform_operation(
key="key",
id_or_name="id_or_name",
@@ -1141,7 +1223,7 @@ def test_streaming_response_perform_operation_overload_8(self, client: Kernel) -
@pytest.mark.skip(reason="Mock server tests are disabled")
@parametrize
- def test_path_params_perform_operation_overload_8(self, client: Kernel) -> None:
+ def test_path_params_perform_operation_overload_9(self, client: Kernel) -> None:
with pytest.raises(ValueError, match=r"Expected a non-empty value for `id_or_name` but received ''"):
client.vaults.items.with_raw_response.perform_operation(
key="key",
@@ -1158,7 +1240,7 @@ def test_path_params_perform_operation_overload_8(self, client: Kernel) -> None:
@pytest.mark.skip(reason="Mock server tests are disabled")
@parametrize
- def test_method_perform_operation_overload_9(self, client: Kernel) -> None:
+ def test_method_perform_operation_overload_10(self, client: Kernel) -> None:
item = client.vaults.items.perform_operation(
key="key",
id_or_name="id_or_name",
@@ -1169,7 +1251,7 @@ def test_method_perform_operation_overload_9(self, client: Kernel) -> None:
@pytest.mark.skip(reason="Mock server tests are disabled")
@parametrize
- def test_method_perform_operation_with_all_params_overload_9(self, client: Kernel) -> None:
+ def test_method_perform_operation_with_all_params_overload_10(self, client: Kernel) -> None:
item = client.vaults.items.perform_operation(
key="key",
id_or_name="id_or_name",
@@ -1181,7 +1263,7 @@ def test_method_perform_operation_with_all_params_overload_9(self, client: Kerne
@pytest.mark.skip(reason="Mock server tests are disabled")
@parametrize
- def test_raw_response_perform_operation_overload_9(self, client: Kernel) -> None:
+ def test_raw_response_perform_operation_overload_10(self, client: Kernel) -> None:
response = client.vaults.items.with_raw_response.perform_operation(
key="key",
id_or_name="id_or_name",
@@ -1196,7 +1278,7 @@ def test_raw_response_perform_operation_overload_9(self, client: Kernel) -> None
@pytest.mark.skip(reason="Mock server tests are disabled")
@parametrize
- def test_streaming_response_perform_operation_overload_9(self, client: Kernel) -> None:
+ def test_streaming_response_perform_operation_overload_10(self, client: Kernel) -> None:
with client.vaults.items.with_streaming_response.perform_operation(
key="key",
id_or_name="id_or_name",
@@ -1213,7 +1295,7 @@ def test_streaming_response_perform_operation_overload_9(self, client: Kernel) -
@pytest.mark.skip(reason="Mock server tests are disabled")
@parametrize
- def test_path_params_perform_operation_overload_9(self, client: Kernel) -> None:
+ def test_path_params_perform_operation_overload_10(self, client: Kernel) -> None:
with pytest.raises(ValueError, match=r"Expected a non-empty value for `id_or_name` but received ''"):
client.vaults.items.with_raw_response.perform_operation(
key="key",
@@ -1232,7 +1314,7 @@ def test_path_params_perform_operation_overload_9(self, client: Kernel) -> None:
@pytest.mark.skip(reason="Mock server tests are disabled")
@parametrize
- def test_method_perform_operation_overload_10(self, client: Kernel) -> None:
+ def test_method_perform_operation_overload_11(self, client: Kernel) -> None:
item = client.vaults.items.perform_operation(
key="key",
id_or_name="id_or_name",
@@ -1259,7 +1341,7 @@ def test_method_perform_operation_overload_10(self, client: Kernel) -> None:
@pytest.mark.skip(reason="Mock server tests are disabled")
@parametrize
- def test_method_perform_operation_with_all_params_overload_10(self, client: Kernel) -> None:
+ def test_method_perform_operation_with_all_params_overload_11(self, client: Kernel) -> None:
item = client.vaults.items.perform_operation(
key="key",
id_or_name="id_or_name",
@@ -1289,7 +1371,7 @@ def test_method_perform_operation_with_all_params_overload_10(self, client: Kern
@pytest.mark.skip(reason="Mock server tests are disabled")
@parametrize
- def test_raw_response_perform_operation_overload_10(self, client: Kernel) -> None:
+ def test_raw_response_perform_operation_overload_11(self, client: Kernel) -> None:
response = client.vaults.items.with_raw_response.perform_operation(
key="key",
id_or_name="id_or_name",
@@ -1320,7 +1402,7 @@ def test_raw_response_perform_operation_overload_10(self, client: Kernel) -> Non
@pytest.mark.skip(reason="Mock server tests are disabled")
@parametrize
- def test_streaming_response_perform_operation_overload_10(self, client: Kernel) -> None:
+ def test_streaming_response_perform_operation_overload_11(self, client: Kernel) -> None:
with client.vaults.items.with_streaming_response.perform_operation(
key="key",
id_or_name="id_or_name",
@@ -1353,7 +1435,7 @@ def test_streaming_response_perform_operation_overload_10(self, client: Kernel)
@pytest.mark.skip(reason="Mock server tests are disabled")
@parametrize
- def test_path_params_perform_operation_overload_10(self, client: Kernel) -> None:
+ def test_path_params_perform_operation_overload_11(self, client: Kernel) -> None:
with pytest.raises(ValueError, match=r"Expected a non-empty value for `id_or_name` but received ''"):
client.vaults.items.with_raw_response.perform_operation(
key="key",
@@ -2412,7 +2494,12 @@ async def test_method_perform_operation_overload_2(self, async_client: AsyncKern
item = await async_client.vaults.items.perform_operation(
key="key",
id_or_name="id_or_name",
- type="collect",
+ amount=0,
+ currency="SEW",
+ occurred_at=parse_datetime("2019-12-27T18:11:19.117Z"),
+ status="APPROVED",
+ transaction_type="PURCHASE",
+ type="confirm_transaction",
)
assert_matches_type(VaultItemOperationResponse, item, path=["response"])
@@ -2422,7 +2509,12 @@ async def test_raw_response_perform_operation_overload_2(self, async_client: Asy
response = await async_client.vaults.items.with_raw_response.perform_operation(
key="key",
id_or_name="id_or_name",
- type="collect",
+ amount=0,
+ currency="SEW",
+ occurred_at=parse_datetime("2019-12-27T18:11:19.117Z"),
+ status="APPROVED",
+ transaction_type="PURCHASE",
+ type="confirm_transaction",
)
assert response.is_closed is True
@@ -2436,7 +2528,12 @@ async def test_streaming_response_perform_operation_overload_2(self, async_clien
async with async_client.vaults.items.with_streaming_response.perform_operation(
key="key",
id_or_name="id_or_name",
- type="collect",
+ amount=0,
+ currency="SEW",
+ occurred_at=parse_datetime("2019-12-27T18:11:19.117Z"),
+ status="APPROVED",
+ transaction_type="PURCHASE",
+ type="confirm_transaction",
) as response:
assert not response.is_closed
assert response.http_request.headers.get("X-Stainless-Lang") == "python"
@@ -2449,6 +2546,73 @@ async def test_streaming_response_perform_operation_overload_2(self, async_clien
@pytest.mark.skip(reason="Mock server tests are disabled")
@parametrize
async def test_path_params_perform_operation_overload_2(self, async_client: AsyncKernel) -> None:
+ with pytest.raises(ValueError, match=r"Expected a non-empty value for `id_or_name` but received ''"):
+ await async_client.vaults.items.with_raw_response.perform_operation(
+ key="key",
+ id_or_name="",
+ amount=0,
+ currency="SEW",
+ occurred_at=parse_datetime("2019-12-27T18:11:19.117Z"),
+ status="APPROVED",
+ transaction_type="PURCHASE",
+ type="confirm_transaction",
+ )
+
+ with pytest.raises(ValueError, match=r"Expected a non-empty value for `key` but received ''"):
+ await async_client.vaults.items.with_raw_response.perform_operation(
+ key="",
+ id_or_name="id_or_name",
+ amount=0,
+ currency="SEW",
+ occurred_at=parse_datetime("2019-12-27T18:11:19.117Z"),
+ status="APPROVED",
+ transaction_type="PURCHASE",
+ type="confirm_transaction",
+ )
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ async def test_method_perform_operation_overload_3(self, async_client: AsyncKernel) -> None:
+ item = await async_client.vaults.items.perform_operation(
+ key="key",
+ id_or_name="id_or_name",
+ type="collect",
+ )
+ assert_matches_type(VaultItemOperationResponse, item, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ async def test_raw_response_perform_operation_overload_3(self, async_client: AsyncKernel) -> None:
+ response = await async_client.vaults.items.with_raw_response.perform_operation(
+ key="key",
+ id_or_name="id_or_name",
+ type="collect",
+ )
+
+ assert response.is_closed is True
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+ item = await response.parse()
+ assert_matches_type(VaultItemOperationResponse, item, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ async def test_streaming_response_perform_operation_overload_3(self, async_client: AsyncKernel) -> None:
+ async with async_client.vaults.items.with_streaming_response.perform_operation(
+ key="key",
+ id_or_name="id_or_name",
+ type="collect",
+ ) as response:
+ assert not response.is_closed
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+
+ item = await response.parse()
+ assert_matches_type(VaultItemOperationResponse, item, 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_perform_operation_overload_3(self, async_client: AsyncKernel) -> None:
with pytest.raises(ValueError, match=r"Expected a non-empty value for `id_or_name` but received ''"):
await async_client.vaults.items.with_raw_response.perform_operation(
key="key",
@@ -2465,7 +2629,7 @@ async def test_path_params_perform_operation_overload_2(self, async_client: Asyn
@pytest.mark.skip(reason="Mock server tests are disabled")
@parametrize
- async def test_method_perform_operation_overload_3(self, async_client: AsyncKernel) -> None:
+ async def test_method_perform_operation_overload_4(self, async_client: AsyncKernel) -> None:
item = await async_client.vaults.items.perform_operation(
key="key",
id_or_name="id_or_name",
@@ -2480,7 +2644,7 @@ async def test_method_perform_operation_overload_3(self, async_client: AsyncKern
@pytest.mark.skip(reason="Mock server tests are disabled")
@parametrize
- async def test_method_perform_operation_with_all_params_overload_3(self, async_client: AsyncKernel) -> None:
+ async def test_method_perform_operation_with_all_params_overload_4(self, async_client: AsyncKernel) -> None:
item = await async_client.vaults.items.perform_operation(
key="key",
id_or_name="id_or_name",
@@ -2496,7 +2660,7 @@ async def test_method_perform_operation_with_all_params_overload_3(self, async_c
@pytest.mark.skip(reason="Mock server tests are disabled")
@parametrize
- async def test_raw_response_perform_operation_overload_3(self, async_client: AsyncKernel) -> None:
+ async def test_raw_response_perform_operation_overload_4(self, async_client: AsyncKernel) -> None:
response = await async_client.vaults.items.with_raw_response.perform_operation(
key="key",
id_or_name="id_or_name",
@@ -2515,7 +2679,7 @@ async def test_raw_response_perform_operation_overload_3(self, async_client: Asy
@pytest.mark.skip(reason="Mock server tests are disabled")
@parametrize
- async def test_streaming_response_perform_operation_overload_3(self, async_client: AsyncKernel) -> None:
+ async def test_streaming_response_perform_operation_overload_4(self, async_client: AsyncKernel) -> None:
async with async_client.vaults.items.with_streaming_response.perform_operation(
key="key",
id_or_name="id_or_name",
@@ -2536,7 +2700,7 @@ async def test_streaming_response_perform_operation_overload_3(self, async_clien
@pytest.mark.skip(reason="Mock server tests are disabled")
@parametrize
- async def test_path_params_perform_operation_overload_3(self, async_client: AsyncKernel) -> None:
+ async def test_path_params_perform_operation_overload_4(self, async_client: AsyncKernel) -> None:
with pytest.raises(ValueError, match=r"Expected a non-empty value for `id_or_name` but received ''"):
await async_client.vaults.items.with_raw_response.perform_operation(
key="key",
@@ -2563,7 +2727,7 @@ async def test_path_params_perform_operation_overload_3(self, async_client: Asyn
@pytest.mark.skip(reason="Mock server tests are disabled")
@parametrize
- async def test_method_perform_operation_overload_4(self, async_client: AsyncKernel) -> None:
+ async def test_method_perform_operation_overload_5(self, async_client: AsyncKernel) -> None:
item = await async_client.vaults.items.perform_operation(
key="key",
id_or_name="id_or_name",
@@ -2592,7 +2756,7 @@ async def test_method_perform_operation_overload_4(self, async_client: AsyncKern
@pytest.mark.skip(reason="Mock server tests are disabled")
@parametrize
- async def test_method_perform_operation_with_all_params_overload_4(self, async_client: AsyncKernel) -> None:
+ async def test_method_perform_operation_with_all_params_overload_5(self, async_client: AsyncKernel) -> None:
item = await async_client.vaults.items.perform_operation(
key="key",
id_or_name="id_or_name",
@@ -2627,7 +2791,7 @@ async def test_method_perform_operation_with_all_params_overload_4(self, async_c
@pytest.mark.skip(reason="Mock server tests are disabled")
@parametrize
- async def test_raw_response_perform_operation_overload_4(self, async_client: AsyncKernel) -> None:
+ async def test_raw_response_perform_operation_overload_5(self, async_client: AsyncKernel) -> None:
response = await async_client.vaults.items.with_raw_response.perform_operation(
key="key",
id_or_name="id_or_name",
@@ -2660,7 +2824,7 @@ async def test_raw_response_perform_operation_overload_4(self, async_client: Asy
@pytest.mark.skip(reason="Mock server tests are disabled")
@parametrize
- async def test_streaming_response_perform_operation_overload_4(self, async_client: AsyncKernel) -> None:
+ async def test_streaming_response_perform_operation_overload_5(self, async_client: AsyncKernel) -> None:
async with async_client.vaults.items.with_streaming_response.perform_operation(
key="key",
id_or_name="id_or_name",
@@ -2695,7 +2859,7 @@ async def test_streaming_response_perform_operation_overload_4(self, async_clien
@pytest.mark.skip(reason="Mock server tests are disabled")
@parametrize
- async def test_path_params_perform_operation_overload_4(self, async_client: AsyncKernel) -> None:
+ async def test_path_params_perform_operation_overload_5(self, async_client: AsyncKernel) -> None:
with pytest.raises(ValueError, match=r"Expected a non-empty value for `id_or_name` but received ''"):
await async_client.vaults.items.with_raw_response.perform_operation(
key="key",
@@ -2750,7 +2914,7 @@ async def test_path_params_perform_operation_overload_4(self, async_client: Asyn
@pytest.mark.skip(reason="Mock server tests are disabled")
@parametrize
- async def test_method_perform_operation_overload_5(self, async_client: AsyncKernel) -> None:
+ async def test_method_perform_operation_overload_6(self, async_client: AsyncKernel) -> None:
item = await async_client.vaults.items.perform_operation(
key="key",
id_or_name="id_or_name",
@@ -2760,7 +2924,7 @@ async def test_method_perform_operation_overload_5(self, async_client: AsyncKern
@pytest.mark.skip(reason="Mock server tests are disabled")
@parametrize
- async def test_method_perform_operation_with_all_params_overload_5(self, async_client: AsyncKernel) -> None:
+ async def test_method_perform_operation_with_all_params_overload_6(self, async_client: AsyncKernel) -> None:
item = await async_client.vaults.items.perform_operation(
key="key",
id_or_name="id_or_name",
@@ -2773,7 +2937,7 @@ async def test_method_perform_operation_with_all_params_overload_5(self, async_c
@pytest.mark.skip(reason="Mock server tests are disabled")
@parametrize
- async def test_raw_response_perform_operation_overload_5(self, async_client: AsyncKernel) -> None:
+ async def test_raw_response_perform_operation_overload_6(self, async_client: AsyncKernel) -> None:
response = await async_client.vaults.items.with_raw_response.perform_operation(
key="key",
id_or_name="id_or_name",
@@ -2787,7 +2951,7 @@ async def test_raw_response_perform_operation_overload_5(self, async_client: Asy
@pytest.mark.skip(reason="Mock server tests are disabled")
@parametrize
- async def test_streaming_response_perform_operation_overload_5(self, async_client: AsyncKernel) -> None:
+ async def test_streaming_response_perform_operation_overload_6(self, async_client: AsyncKernel) -> None:
async with async_client.vaults.items.with_streaming_response.perform_operation(
key="key",
id_or_name="id_or_name",
@@ -2803,7 +2967,7 @@ async def test_streaming_response_perform_operation_overload_5(self, async_clien
@pytest.mark.skip(reason="Mock server tests are disabled")
@parametrize
- async def test_path_params_perform_operation_overload_5(self, async_client: AsyncKernel) -> None:
+ async def test_path_params_perform_operation_overload_6(self, async_client: AsyncKernel) -> None:
with pytest.raises(ValueError, match=r"Expected a non-empty value for `id_or_name` but received ''"):
await async_client.vaults.items.with_raw_response.perform_operation(
key="key",
@@ -2820,7 +2984,7 @@ async def test_path_params_perform_operation_overload_5(self, async_client: Asyn
@pytest.mark.skip(reason="Mock server tests are disabled")
@parametrize
- async def test_method_perform_operation_overload_6(self, async_client: AsyncKernel) -> None:
+ async def test_method_perform_operation_overload_7(self, async_client: AsyncKernel) -> None:
item = await async_client.vaults.items.perform_operation(
key="key",
id_or_name="id_or_name",
@@ -2830,7 +2994,7 @@ async def test_method_perform_operation_overload_6(self, async_client: AsyncKern
@pytest.mark.skip(reason="Mock server tests are disabled")
@parametrize
- async def test_method_perform_operation_with_all_params_overload_6(self, async_client: AsyncKernel) -> None:
+ async def test_method_perform_operation_with_all_params_overload_7(self, async_client: AsyncKernel) -> None:
item = await async_client.vaults.items.perform_operation(
key="key",
id_or_name="id_or_name",
@@ -2841,7 +3005,7 @@ async def test_method_perform_operation_with_all_params_overload_6(self, async_c
@pytest.mark.skip(reason="Mock server tests are disabled")
@parametrize
- async def test_raw_response_perform_operation_overload_6(self, async_client: AsyncKernel) -> None:
+ async def test_raw_response_perform_operation_overload_7(self, async_client: AsyncKernel) -> None:
response = await async_client.vaults.items.with_raw_response.perform_operation(
key="key",
id_or_name="id_or_name",
@@ -2855,7 +3019,7 @@ async def test_raw_response_perform_operation_overload_6(self, async_client: Asy
@pytest.mark.skip(reason="Mock server tests are disabled")
@parametrize
- async def test_streaming_response_perform_operation_overload_6(self, async_client: AsyncKernel) -> None:
+ async def test_streaming_response_perform_operation_overload_7(self, async_client: AsyncKernel) -> None:
async with async_client.vaults.items.with_streaming_response.perform_operation(
key="key",
id_or_name="id_or_name",
@@ -2871,7 +3035,7 @@ async def test_streaming_response_perform_operation_overload_6(self, async_clien
@pytest.mark.skip(reason="Mock server tests are disabled")
@parametrize
- async def test_path_params_perform_operation_overload_6(self, async_client: AsyncKernel) -> None:
+ async def test_path_params_perform_operation_overload_7(self, async_client: AsyncKernel) -> None:
with pytest.raises(ValueError, match=r"Expected a non-empty value for `id_or_name` but received ''"):
await async_client.vaults.items.with_raw_response.perform_operation(
key="key",
@@ -2888,7 +3052,7 @@ async def test_path_params_perform_operation_overload_6(self, async_client: Asyn
@pytest.mark.skip(reason="Mock server tests are disabled")
@parametrize
- async def test_method_perform_operation_overload_7(self, async_client: AsyncKernel) -> None:
+ async def test_method_perform_operation_overload_8(self, async_client: AsyncKernel) -> None:
item = await async_client.vaults.items.perform_operation(
key="key",
id_or_name="id_or_name",
@@ -2900,7 +3064,7 @@ async def test_method_perform_operation_overload_7(self, async_client: AsyncKern
@pytest.mark.skip(reason="Mock server tests are disabled")
@parametrize
- async def test_method_perform_operation_with_all_params_overload_7(self, async_client: AsyncKernel) -> None:
+ async def test_method_perform_operation_with_all_params_overload_8(self, async_client: AsyncKernel) -> None:
item = await async_client.vaults.items.perform_operation(
key="key",
id_or_name="id_or_name",
@@ -2914,7 +3078,7 @@ async def test_method_perform_operation_with_all_params_overload_7(self, async_c
@pytest.mark.skip(reason="Mock server tests are disabled")
@parametrize
- async def test_raw_response_perform_operation_overload_7(self, async_client: AsyncKernel) -> None:
+ async def test_raw_response_perform_operation_overload_8(self, async_client: AsyncKernel) -> None:
response = await async_client.vaults.items.with_raw_response.perform_operation(
key="key",
id_or_name="id_or_name",
@@ -2930,7 +3094,7 @@ async def test_raw_response_perform_operation_overload_7(self, async_client: Asy
@pytest.mark.skip(reason="Mock server tests are disabled")
@parametrize
- async def test_streaming_response_perform_operation_overload_7(self, async_client: AsyncKernel) -> None:
+ async def test_streaming_response_perform_operation_overload_8(self, async_client: AsyncKernel) -> None:
async with async_client.vaults.items.with_streaming_response.perform_operation(
key="key",
id_or_name="id_or_name",
@@ -2948,7 +3112,7 @@ async def test_streaming_response_perform_operation_overload_7(self, async_clien
@pytest.mark.skip(reason="Mock server tests are disabled")
@parametrize
- async def test_path_params_perform_operation_overload_7(self, async_client: AsyncKernel) -> None:
+ async def test_path_params_perform_operation_overload_8(self, async_client: AsyncKernel) -> None:
with pytest.raises(ValueError, match=r"Expected a non-empty value for `id_or_name` but received ''"):
await async_client.vaults.items.with_raw_response.perform_operation(
key="key",
@@ -2969,7 +3133,7 @@ async def test_path_params_perform_operation_overload_7(self, async_client: Asyn
@pytest.mark.skip(reason="Mock server tests are disabled")
@parametrize
- async def test_method_perform_operation_overload_8(self, async_client: AsyncKernel) -> None:
+ async def test_method_perform_operation_overload_9(self, async_client: AsyncKernel) -> None:
item = await async_client.vaults.items.perform_operation(
key="key",
id_or_name="id_or_name",
@@ -2979,7 +3143,7 @@ async def test_method_perform_operation_overload_8(self, async_client: AsyncKern
@pytest.mark.skip(reason="Mock server tests are disabled")
@parametrize
- async def test_raw_response_perform_operation_overload_8(self, async_client: AsyncKernel) -> None:
+ async def test_raw_response_perform_operation_overload_9(self, async_client: AsyncKernel) -> None:
response = await async_client.vaults.items.with_raw_response.perform_operation(
key="key",
id_or_name="id_or_name",
@@ -2993,7 +3157,7 @@ async def test_raw_response_perform_operation_overload_8(self, async_client: Asy
@pytest.mark.skip(reason="Mock server tests are disabled")
@parametrize
- async def test_streaming_response_perform_operation_overload_8(self, async_client: AsyncKernel) -> None:
+ async def test_streaming_response_perform_operation_overload_9(self, async_client: AsyncKernel) -> None:
async with async_client.vaults.items.with_streaming_response.perform_operation(
key="key",
id_or_name="id_or_name",
@@ -3009,7 +3173,7 @@ async def test_streaming_response_perform_operation_overload_8(self, async_clien
@pytest.mark.skip(reason="Mock server tests are disabled")
@parametrize
- async def test_path_params_perform_operation_overload_8(self, async_client: AsyncKernel) -> None:
+ async def test_path_params_perform_operation_overload_9(self, async_client: AsyncKernel) -> None:
with pytest.raises(ValueError, match=r"Expected a non-empty value for `id_or_name` but received ''"):
await async_client.vaults.items.with_raw_response.perform_operation(
key="key",
@@ -3026,7 +3190,7 @@ async def test_path_params_perform_operation_overload_8(self, async_client: Asyn
@pytest.mark.skip(reason="Mock server tests are disabled")
@parametrize
- async def test_method_perform_operation_overload_9(self, async_client: AsyncKernel) -> None:
+ async def test_method_perform_operation_overload_10(self, async_client: AsyncKernel) -> None:
item = await async_client.vaults.items.perform_operation(
key="key",
id_or_name="id_or_name",
@@ -3037,7 +3201,7 @@ async def test_method_perform_operation_overload_9(self, async_client: AsyncKern
@pytest.mark.skip(reason="Mock server tests are disabled")
@parametrize
- async def test_method_perform_operation_with_all_params_overload_9(self, async_client: AsyncKernel) -> None:
+ async def test_method_perform_operation_with_all_params_overload_10(self, async_client: AsyncKernel) -> None:
item = await async_client.vaults.items.perform_operation(
key="key",
id_or_name="id_or_name",
@@ -3049,7 +3213,7 @@ async def test_method_perform_operation_with_all_params_overload_9(self, async_c
@pytest.mark.skip(reason="Mock server tests are disabled")
@parametrize
- async def test_raw_response_perform_operation_overload_9(self, async_client: AsyncKernel) -> None:
+ async def test_raw_response_perform_operation_overload_10(self, async_client: AsyncKernel) -> None:
response = await async_client.vaults.items.with_raw_response.perform_operation(
key="key",
id_or_name="id_or_name",
@@ -3064,7 +3228,7 @@ async def test_raw_response_perform_operation_overload_9(self, async_client: Asy
@pytest.mark.skip(reason="Mock server tests are disabled")
@parametrize
- async def test_streaming_response_perform_operation_overload_9(self, async_client: AsyncKernel) -> None:
+ async def test_streaming_response_perform_operation_overload_10(self, async_client: AsyncKernel) -> None:
async with async_client.vaults.items.with_streaming_response.perform_operation(
key="key",
id_or_name="id_or_name",
@@ -3081,7 +3245,7 @@ async def test_streaming_response_perform_operation_overload_9(self, async_clien
@pytest.mark.skip(reason="Mock server tests are disabled")
@parametrize
- async def test_path_params_perform_operation_overload_9(self, async_client: AsyncKernel) -> None:
+ async def test_path_params_perform_operation_overload_10(self, async_client: AsyncKernel) -> None:
with pytest.raises(ValueError, match=r"Expected a non-empty value for `id_or_name` but received ''"):
await async_client.vaults.items.with_raw_response.perform_operation(
key="key",
@@ -3100,7 +3264,7 @@ async def test_path_params_perform_operation_overload_9(self, async_client: Asyn
@pytest.mark.skip(reason="Mock server tests are disabled")
@parametrize
- async def test_method_perform_operation_overload_10(self, async_client: AsyncKernel) -> None:
+ async def test_method_perform_operation_overload_11(self, async_client: AsyncKernel) -> None:
item = await async_client.vaults.items.perform_operation(
key="key",
id_or_name="id_or_name",
@@ -3127,7 +3291,7 @@ async def test_method_perform_operation_overload_10(self, async_client: AsyncKer
@pytest.mark.skip(reason="Mock server tests are disabled")
@parametrize
- async def test_method_perform_operation_with_all_params_overload_10(self, async_client: AsyncKernel) -> None:
+ async def test_method_perform_operation_with_all_params_overload_11(self, async_client: AsyncKernel) -> None:
item = await async_client.vaults.items.perform_operation(
key="key",
id_or_name="id_or_name",
@@ -3157,7 +3321,7 @@ async def test_method_perform_operation_with_all_params_overload_10(self, async_
@pytest.mark.skip(reason="Mock server tests are disabled")
@parametrize
- async def test_raw_response_perform_operation_overload_10(self, async_client: AsyncKernel) -> None:
+ async def test_raw_response_perform_operation_overload_11(self, async_client: AsyncKernel) -> None:
response = await async_client.vaults.items.with_raw_response.perform_operation(
key="key",
id_or_name="id_or_name",
@@ -3188,7 +3352,7 @@ async def test_raw_response_perform_operation_overload_10(self, async_client: As
@pytest.mark.skip(reason="Mock server tests are disabled")
@parametrize
- async def test_streaming_response_perform_operation_overload_10(self, async_client: AsyncKernel) -> None:
+ async def test_streaming_response_perform_operation_overload_11(self, async_client: AsyncKernel) -> None:
async with async_client.vaults.items.with_streaming_response.perform_operation(
key="key",
id_or_name="id_or_name",
@@ -3221,7 +3385,7 @@ async def test_streaming_response_perform_operation_overload_10(self, async_clie
@pytest.mark.skip(reason="Mock server tests are disabled")
@parametrize
- async def test_path_params_perform_operation_overload_10(self, async_client: AsyncKernel) -> None:
+ async def test_path_params_perform_operation_overload_11(self, async_client: AsyncKernel) -> None:
with pytest.raises(ValueError, match=r"Expected a non-empty value for `id_or_name` but received ''"):
await async_client.vaults.items.with_raw_response.perform_operation(
key="key",
From e6e016a5cea493c505b16a05cb8ad2fa6ed9f13b Mon Sep 17 00:00:00 2001
From: "kernel-internal[bot]"
<260533166+kernel-internal[bot]@users.noreply.github.com>
Date: Mon, 12 Oct 2026 18:30:19 +0000
Subject: [PATCH 350/366] feat: Accept intent creation without a fixed status
Stainless-Generated-From: e2f63de376db35944d1bb3d31420d804f66d36b4
---
api.md | 1 +
src/kernel/resources/vaults/items.py | 96 ++++++
src/kernel/types/vaults/__init__.py | 3 +
.../vaults/credential_account_vault_item.py | 1 +
.../types/vaults/credential_vault_item.py | 1 +
.../vaults/item_perform_operation_params.py | 8 +
src/kernel/types/vaults/kernel_card_state.py | 16 +-
...tion_vault_item_operation_request_param.py | 15 +
src/kernel/types/vaults/vault_item.py | 2 +
.../vaults/vault_item_operation_response.py | 2 +
tests/api_resources/vaults/test_items.py | 298 ++++++++++++------
11 files changed, 345 insertions(+), 98 deletions(-)
create mode 100644 src/kernel/types/vaults/restart_verification_vault_item_operation_request_param.py
diff --git a/api.md b/api.md
index ffe8f507..f5243e5a 100644
--- a/api.md
+++ b/api.md
@@ -611,6 +611,7 @@ from kernel.types.vaults import (
OnePasswordRecoverVaultItemOperationRequest,
OnePasswordRequestAccessVaultItemOperationRequest,
PrepareCheckoutVaultItemOperationRequest,
+ RestartVerificationVaultItemOperationRequest,
VaultCardAliases,
VaultCardFillField,
VaultCheckoutContext,
diff --git a/src/kernel/resources/vaults/items.py b/src/kernel/resources/vaults/items.py
index 7de1bee2..79ca2ced 100644
--- a/src/kernel/resources/vaults/items.py
+++ b/src/kernel/resources/vaults/items.py
@@ -440,6 +440,53 @@ def perform_operation(
"""
...
+ @overload
+ def perform_operation(
+ self,
+ key: str,
+ *,
+ id_or_name: str,
+ type: Literal["restart_verification"],
+ # 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,
+ ) -> VaultItemOperationResponse:
+ """
+ Retrieve the item first and invoke only an operation listed in
+ `available_operations`, following its natural-language description. Availability
+ is rechecked at execution time; unavailable operations return 409. Authorization
+ and preparation may call an external provider and return updated state. Link
+ cards advertise authorize without checkout context. Eligible unused AgentCard
+ cards advertise prepare_checkout, which requires checkout context and obtains
+ device approval before native Square Pay. Keep the returned approval page open,
+ poll until ready_to_submit, then submit before preparation.expires_at. Unused
+ preparations expire automatically and cannot be reused. If spend-request
+ creation is rejected with a non-retryable provider error, the card item is
+ deleted and the provider's error code and message are returned. Rate limits
+ return HTTP 429 and retain the card item; stop, back off, and retry the same
+ authorize operation.
+
+ Fill returns a value-free execution result. Validation failures before writing
+ return 400 (invalid request or targets), 403 (access or destination denied), 404
+ (resource not found), or 409 (item or browser not ready). Once writing starts,
+ known partial failures and indeterminate field outcomes return 200 with status
+ `failed` or `unknown`. Fill never submits the page, so it is safe to retry after
+ a failure, an `unknown` outcome, or a transport error.
+
+ Args:
+ extra_headers: Send extra headers
+
+ extra_query: Add additional query parameters to the request
+
+ extra_body: Add additional JSON properties to the request
+
+ timeout: Override the client-level default timeout for this request, in seconds
+ """
+ ...
+
@overload
def perform_operation(
self,
@@ -1001,6 +1048,7 @@ def perform_operation(
*,
id_or_name: str,
type: Literal["authorize"]
+ | Literal["restart_verification"]
| Literal["confirm_transaction"]
| Literal["collect"]
| Literal["prepare_checkout"]
@@ -1689,6 +1737,53 @@ async def perform_operation(
"""
...
+ @overload
+ async def perform_operation(
+ self,
+ key: str,
+ *,
+ id_or_name: str,
+ type: Literal["restart_verification"],
+ # 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,
+ ) -> VaultItemOperationResponse:
+ """
+ Retrieve the item first and invoke only an operation listed in
+ `available_operations`, following its natural-language description. Availability
+ is rechecked at execution time; unavailable operations return 409. Authorization
+ and preparation may call an external provider and return updated state. Link
+ cards advertise authorize without checkout context. Eligible unused AgentCard
+ cards advertise prepare_checkout, which requires checkout context and obtains
+ device approval before native Square Pay. Keep the returned approval page open,
+ poll until ready_to_submit, then submit before preparation.expires_at. Unused
+ preparations expire automatically and cannot be reused. If spend-request
+ creation is rejected with a non-retryable provider error, the card item is
+ deleted and the provider's error code and message are returned. Rate limits
+ return HTTP 429 and retain the card item; stop, back off, and retry the same
+ authorize operation.
+
+ Fill returns a value-free execution result. Validation failures before writing
+ return 400 (invalid request or targets), 403 (access or destination denied), 404
+ (resource not found), or 409 (item or browser not ready). Once writing starts,
+ known partial failures and indeterminate field outcomes return 200 with status
+ `failed` or `unknown`. Fill never submits the page, so it is safe to retry after
+ a failure, an `unknown` outcome, or a transport error.
+
+ Args:
+ extra_headers: Send extra headers
+
+ extra_query: Add additional query parameters to the request
+
+ extra_body: Add additional JSON properties to the request
+
+ timeout: Override the client-level default timeout for this request, in seconds
+ """
+ ...
+
@overload
async def perform_operation(
self,
@@ -2250,6 +2345,7 @@ async def perform_operation(
*,
id_or_name: str,
type: Literal["authorize"]
+ | Literal["restart_verification"]
| Literal["confirm_transaction"]
| Literal["collect"]
| Literal["prepare_checkout"]
diff --git a/src/kernel/types/vaults/__init__.py b/src/kernel/types/vaults/__init__.py
index 1b584d27..ff4183c4 100644
--- a/src/kernel/types/vaults/__init__.py
+++ b/src/kernel/types/vaults/__init__.py
@@ -119,6 +119,9 @@
from .one_password_recover_vault_item_operation_request_param import (
OnePasswordRecoverVaultItemOperationRequestParam as OnePasswordRecoverVaultItemOperationRequestParam,
)
+from .restart_verification_vault_item_operation_request_param import (
+ RestartVerificationVaultItemOperationRequestParam as RestartVerificationVaultItemOperationRequestParam,
+)
from .one_password_request_access_vault_item_operation_request_param import (
OnePasswordRequestAccessVaultItemOperationRequestParam as OnePasswordRequestAccessVaultItemOperationRequestParam,
)
diff --git a/src/kernel/types/vaults/credential_account_vault_item.py b/src/kernel/types/vaults/credential_account_vault_item.py
index 7f86d264..a049c193 100644
--- a/src/kernel/types/vaults/credential_account_vault_item.py
+++ b/src/kernel/types/vaults/credential_account_vault_item.py
@@ -32,6 +32,7 @@ class AvailableOperation(BaseModel):
type: Literal[
"authorize",
+ "restart_verification",
"confirm_transaction",
"collect",
"prepare_checkout",
diff --git a/src/kernel/types/vaults/credential_vault_item.py b/src/kernel/types/vaults/credential_vault_item.py
index e16ae365..f9bd1158 100644
--- a/src/kernel/types/vaults/credential_vault_item.py
+++ b/src/kernel/types/vaults/credential_vault_item.py
@@ -39,6 +39,7 @@ class AvailableOperation(BaseModel):
type: Literal[
"authorize",
+ "restart_verification",
"confirm_transaction",
"collect",
"prepare_checkout",
diff --git a/src/kernel/types/vaults/item_perform_operation_params.py b/src/kernel/types/vaults/item_perform_operation_params.py
index 00600ecf..3db75e75 100644
--- a/src/kernel/types/vaults/item_perform_operation_params.py
+++ b/src/kernel/types/vaults/item_perform_operation_params.py
@@ -15,6 +15,7 @@
__all__ = [
"ItemPerformOperationParams",
"AuthorizeVaultItemOperationRequest",
+ "RestartVerificationVaultItemOperationRequest",
"ConfirmTransactionVaultItemOperationRequest",
"CollectVaultItemOperationRequest",
"PrepareCheckoutVaultItemOperationRequest",
@@ -34,6 +35,12 @@ class AuthorizeVaultItemOperationRequest(TypedDict, total=False):
type: Required[Literal["authorize"]]
+class RestartVerificationVaultItemOperationRequest(TypedDict, total=False):
+ id_or_name: Required[str]
+
+ type: Required[Literal["restart_verification"]]
+
+
class ConfirmTransactionVaultItemOperationRequest(TypedDict, total=False):
id_or_name: Required[str]
@@ -195,6 +202,7 @@ class WebmcpInvokeVaultItemOperationRequest(TypedDict, total=False):
ItemPerformOperationParams: TypeAlias = Union[
AuthorizeVaultItemOperationRequest,
+ RestartVerificationVaultItemOperationRequest,
ConfirmTransactionVaultItemOperationRequest,
CollectVaultItemOperationRequest,
PrepareCheckoutVaultItemOperationRequest,
diff --git a/src/kernel/types/vaults/kernel_card_state.py b/src/kernel/types/vaults/kernel_card_state.py
index 6983c617..047b5f9e 100644
--- a/src/kernel/types/vaults/kernel_card_state.py
+++ b/src/kernel/types/vaults/kernel_card_state.py
@@ -44,12 +44,16 @@ class KernelCardState(BaseModel):
"""
pending_authorization on a Visa purchase waits for the cardholder to approve it
through the spend_approval action; an unused link expires after 30 minutes.
- recovery_required means approving the purchase or issuing the one-time code has
- an unresolved outcome. Kernel never approves again or issues another code for
- the item automatically, and the item cannot be deleted or replaced until the
- original attempt is reconciled with support. When status_reason says the
- provider refused retrieval before acceptance, no code was issued and a later
- read retries. declined means the card network refused to issue a code.
+ recovery_required means verification, approving the purchase or issuing the
+ one-time code has an unresolved outcome. A server-classified initial attestation
+ failure before intent dispatch may advertise restart_verification to prepare a
+ fresh local verification link; this does not approve spending or replay any
+ provider request. Unclassified legacy recovery and uncertain intent creation
+ cannot restart. Kernel never approves again or issues another code for the item
+ automatically, and the item cannot be deleted or replaced until the original
+ attempt is reconciled with support. When status_reason says the provider refused
+ retrieval before acceptance, no code was issued and a later read retries.
+ declined means the card network refused to issue a code.
"""
domains: Optional[List[str]] = None
diff --git a/src/kernel/types/vaults/restart_verification_vault_item_operation_request_param.py b/src/kernel/types/vaults/restart_verification_vault_item_operation_request_param.py
new file mode 100644
index 00000000..89c4e3db
--- /dev/null
+++ b/src/kernel/types/vaults/restart_verification_vault_item_operation_request_param.py
@@ -0,0 +1,15 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from __future__ import annotations
+
+from typing_extensions import Literal, Required, TypedDict
+
+__all__ = ["RestartVerificationVaultItemOperationRequestParam"]
+
+
+class RestartVerificationVaultItemOperationRequestParam(TypedDict, total=False):
+ """
+ Explicitly prepare a fresh local verification session for the same Kernel Visa purchase and connected wallet, only when restart_verification is advertised. Kernel must have durable proof of an initial attestation failure before intent dispatch. Replaces the spend_approval capability and discards session-bound verification data without calling the provider or granting spend consent. The cardholder must use the new link, verify again and explicitly approve the purchase. Unknown intent creation, legacy unclassified recovery, enrollment completion, issuance and confirmation cannot restart. Do not automatically retry this operation.
+ """
+
+ type: Required[Literal["restart_verification"]]
diff --git a/src/kernel/types/vaults/vault_item.py b/src/kernel/types/vaults/vault_item.py
index 9ef7f5e2..697f56d9 100644
--- a/src/kernel/types/vaults/vault_item.py
+++ b/src/kernel/types/vaults/vault_item.py
@@ -47,6 +47,7 @@ class WalletVaultItemAvailableOperation(BaseModel):
type: Literal[
"authorize",
+ "restart_verification",
"confirm_transaction",
"collect",
"prepare_checkout",
@@ -123,6 +124,7 @@ class CardVaultItemAvailableOperation(BaseModel):
type: Literal[
"authorize",
+ "restart_verification",
"confirm_transaction",
"collect",
"prepare_checkout",
diff --git a/src/kernel/types/vaults/vault_item_operation_response.py b/src/kernel/types/vaults/vault_item_operation_response.py
index 484a72b7..0968dab7 100644
--- a/src/kernel/types/vaults/vault_item_operation_response.py
+++ b/src/kernel/types/vaults/vault_item_operation_response.py
@@ -49,6 +49,7 @@ class WalletVaultItemAvailableOperation(BaseModel):
type: Literal[
"authorize",
+ "restart_verification",
"confirm_transaction",
"collect",
"prepare_checkout",
@@ -125,6 +126,7 @@ class CardVaultItemAvailableOperation(BaseModel):
type: Literal[
"authorize",
+ "restart_verification",
"confirm_transaction",
"collect",
"prepare_checkout",
diff --git a/tests/api_resources/vaults/test_items.py b/tests/api_resources/vaults/test_items.py
index 45b4573a..f69942ef 100644
--- a/tests/api_resources/vaults/test_items.py
+++ b/tests/api_resources/vaults/test_items.py
@@ -541,6 +541,63 @@ def test_path_params_perform_operation_overload_1(self, client: Kernel) -> None:
@pytest.mark.skip(reason="Mock server tests are disabled")
@parametrize
def test_method_perform_operation_overload_2(self, client: Kernel) -> None:
+ item = client.vaults.items.perform_operation(
+ key="key",
+ id_or_name="id_or_name",
+ type="restart_verification",
+ )
+ assert_matches_type(VaultItemOperationResponse, item, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ def test_raw_response_perform_operation_overload_2(self, client: Kernel) -> None:
+ response = client.vaults.items.with_raw_response.perform_operation(
+ key="key",
+ id_or_name="id_or_name",
+ type="restart_verification",
+ )
+
+ assert response.is_closed is True
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+ item = response.parse()
+ assert_matches_type(VaultItemOperationResponse, item, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ def test_streaming_response_perform_operation_overload_2(self, client: Kernel) -> None:
+ with client.vaults.items.with_streaming_response.perform_operation(
+ key="key",
+ id_or_name="id_or_name",
+ type="restart_verification",
+ ) as response:
+ assert not response.is_closed
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+
+ item = response.parse()
+ assert_matches_type(VaultItemOperationResponse, item, path=["response"])
+
+ assert cast(Any, response.is_closed) is True
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ def test_path_params_perform_operation_overload_2(self, client: Kernel) -> None:
+ with pytest.raises(ValueError, match=r"Expected a non-empty value for `id_or_name` but received ''"):
+ client.vaults.items.with_raw_response.perform_operation(
+ key="key",
+ id_or_name="",
+ type="restart_verification",
+ )
+
+ with pytest.raises(ValueError, match=r"Expected a non-empty value for `key` but received ''"):
+ client.vaults.items.with_raw_response.perform_operation(
+ key="",
+ id_or_name="id_or_name",
+ type="restart_verification",
+ )
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ def test_method_perform_operation_overload_3(self, client: Kernel) -> None:
item = client.vaults.items.perform_operation(
key="key",
id_or_name="id_or_name",
@@ -555,7 +612,7 @@ def test_method_perform_operation_overload_2(self, client: Kernel) -> None:
@pytest.mark.skip(reason="Mock server tests are disabled")
@parametrize
- def test_raw_response_perform_operation_overload_2(self, client: Kernel) -> None:
+ def test_raw_response_perform_operation_overload_3(self, client: Kernel) -> None:
response = client.vaults.items.with_raw_response.perform_operation(
key="key",
id_or_name="id_or_name",
@@ -574,7 +631,7 @@ def test_raw_response_perform_operation_overload_2(self, client: Kernel) -> None
@pytest.mark.skip(reason="Mock server tests are disabled")
@parametrize
- def test_streaming_response_perform_operation_overload_2(self, client: Kernel) -> None:
+ def test_streaming_response_perform_operation_overload_3(self, client: Kernel) -> None:
with client.vaults.items.with_streaming_response.perform_operation(
key="key",
id_or_name="id_or_name",
@@ -595,7 +652,7 @@ def test_streaming_response_perform_operation_overload_2(self, client: Kernel) -
@pytest.mark.skip(reason="Mock server tests are disabled")
@parametrize
- def test_path_params_perform_operation_overload_2(self, client: Kernel) -> None:
+ def test_path_params_perform_operation_overload_3(self, client: Kernel) -> None:
with pytest.raises(ValueError, match=r"Expected a non-empty value for `id_or_name` but received ''"):
client.vaults.items.with_raw_response.perform_operation(
key="key",
@@ -622,7 +679,7 @@ def test_path_params_perform_operation_overload_2(self, client: Kernel) -> None:
@pytest.mark.skip(reason="Mock server tests are disabled")
@parametrize
- def test_method_perform_operation_overload_3(self, client: Kernel) -> None:
+ def test_method_perform_operation_overload_4(self, client: Kernel) -> None:
item = client.vaults.items.perform_operation(
key="key",
id_or_name="id_or_name",
@@ -632,7 +689,7 @@ def test_method_perform_operation_overload_3(self, client: Kernel) -> None:
@pytest.mark.skip(reason="Mock server tests are disabled")
@parametrize
- def test_raw_response_perform_operation_overload_3(self, client: Kernel) -> None:
+ def test_raw_response_perform_operation_overload_4(self, client: Kernel) -> None:
response = client.vaults.items.with_raw_response.perform_operation(
key="key",
id_or_name="id_or_name",
@@ -646,7 +703,7 @@ def test_raw_response_perform_operation_overload_3(self, client: Kernel) -> None
@pytest.mark.skip(reason="Mock server tests are disabled")
@parametrize
- def test_streaming_response_perform_operation_overload_3(self, client: Kernel) -> None:
+ def test_streaming_response_perform_operation_overload_4(self, client: Kernel) -> None:
with client.vaults.items.with_streaming_response.perform_operation(
key="key",
id_or_name="id_or_name",
@@ -662,7 +719,7 @@ def test_streaming_response_perform_operation_overload_3(self, client: Kernel) -
@pytest.mark.skip(reason="Mock server tests are disabled")
@parametrize
- def test_path_params_perform_operation_overload_3(self, client: Kernel) -> None:
+ def test_path_params_perform_operation_overload_4(self, client: Kernel) -> None:
with pytest.raises(ValueError, match=r"Expected a non-empty value for `id_or_name` but received ''"):
client.vaults.items.with_raw_response.perform_operation(
key="key",
@@ -679,7 +736,7 @@ def test_path_params_perform_operation_overload_3(self, client: Kernel) -> None:
@pytest.mark.skip(reason="Mock server tests are disabled")
@parametrize
- def test_method_perform_operation_overload_4(self, client: Kernel) -> None:
+ def test_method_perform_operation_overload_5(self, client: Kernel) -> None:
item = client.vaults.items.perform_operation(
key="key",
id_or_name="id_or_name",
@@ -694,7 +751,7 @@ def test_method_perform_operation_overload_4(self, client: Kernel) -> None:
@pytest.mark.skip(reason="Mock server tests are disabled")
@parametrize
- def test_method_perform_operation_with_all_params_overload_4(self, client: Kernel) -> None:
+ def test_method_perform_operation_with_all_params_overload_5(self, client: Kernel) -> None:
item = client.vaults.items.perform_operation(
key="key",
id_or_name="id_or_name",
@@ -710,7 +767,7 @@ def test_method_perform_operation_with_all_params_overload_4(self, client: Kerne
@pytest.mark.skip(reason="Mock server tests are disabled")
@parametrize
- def test_raw_response_perform_operation_overload_4(self, client: Kernel) -> None:
+ def test_raw_response_perform_operation_overload_5(self, client: Kernel) -> None:
response = client.vaults.items.with_raw_response.perform_operation(
key="key",
id_or_name="id_or_name",
@@ -729,7 +786,7 @@ def test_raw_response_perform_operation_overload_4(self, client: Kernel) -> None
@pytest.mark.skip(reason="Mock server tests are disabled")
@parametrize
- def test_streaming_response_perform_operation_overload_4(self, client: Kernel) -> None:
+ def test_streaming_response_perform_operation_overload_5(self, client: Kernel) -> None:
with client.vaults.items.with_streaming_response.perform_operation(
key="key",
id_or_name="id_or_name",
@@ -750,7 +807,7 @@ def test_streaming_response_perform_operation_overload_4(self, client: Kernel) -
@pytest.mark.skip(reason="Mock server tests are disabled")
@parametrize
- def test_path_params_perform_operation_overload_4(self, client: Kernel) -> None:
+ def test_path_params_perform_operation_overload_5(self, client: Kernel) -> None:
with pytest.raises(ValueError, match=r"Expected a non-empty value for `id_or_name` but received ''"):
client.vaults.items.with_raw_response.perform_operation(
key="key",
@@ -777,7 +834,7 @@ def test_path_params_perform_operation_overload_4(self, client: Kernel) -> None:
@pytest.mark.skip(reason="Mock server tests are disabled")
@parametrize
- def test_method_perform_operation_overload_5(self, client: Kernel) -> None:
+ def test_method_perform_operation_overload_6(self, client: Kernel) -> None:
item = client.vaults.items.perform_operation(
key="key",
id_or_name="id_or_name",
@@ -806,7 +863,7 @@ def test_method_perform_operation_overload_5(self, client: Kernel) -> None:
@pytest.mark.skip(reason="Mock server tests are disabled")
@parametrize
- def test_method_perform_operation_with_all_params_overload_5(self, client: Kernel) -> None:
+ def test_method_perform_operation_with_all_params_overload_6(self, client: Kernel) -> None:
item = client.vaults.items.perform_operation(
key="key",
id_or_name="id_or_name",
@@ -841,7 +898,7 @@ def test_method_perform_operation_with_all_params_overload_5(self, client: Kerne
@pytest.mark.skip(reason="Mock server tests are disabled")
@parametrize
- def test_raw_response_perform_operation_overload_5(self, client: Kernel) -> None:
+ def test_raw_response_perform_operation_overload_6(self, client: Kernel) -> None:
response = client.vaults.items.with_raw_response.perform_operation(
key="key",
id_or_name="id_or_name",
@@ -874,7 +931,7 @@ def test_raw_response_perform_operation_overload_5(self, client: Kernel) -> None
@pytest.mark.skip(reason="Mock server tests are disabled")
@parametrize
- def test_streaming_response_perform_operation_overload_5(self, client: Kernel) -> None:
+ def test_streaming_response_perform_operation_overload_6(self, client: Kernel) -> None:
with client.vaults.items.with_streaming_response.perform_operation(
key="key",
id_or_name="id_or_name",
@@ -909,7 +966,7 @@ def test_streaming_response_perform_operation_overload_5(self, client: Kernel) -
@pytest.mark.skip(reason="Mock server tests are disabled")
@parametrize
- def test_path_params_perform_operation_overload_5(self, client: Kernel) -> None:
+ def test_path_params_perform_operation_overload_6(self, client: Kernel) -> None:
with pytest.raises(ValueError, match=r"Expected a non-empty value for `id_or_name` but received ''"):
client.vaults.items.with_raw_response.perform_operation(
key="key",
@@ -964,7 +1021,7 @@ def test_path_params_perform_operation_overload_5(self, client: Kernel) -> None:
@pytest.mark.skip(reason="Mock server tests are disabled")
@parametrize
- def test_method_perform_operation_overload_6(self, client: Kernel) -> None:
+ def test_method_perform_operation_overload_7(self, client: Kernel) -> None:
item = client.vaults.items.perform_operation(
key="key",
id_or_name="id_or_name",
@@ -974,7 +1031,7 @@ def test_method_perform_operation_overload_6(self, client: Kernel) -> None:
@pytest.mark.skip(reason="Mock server tests are disabled")
@parametrize
- def test_method_perform_operation_with_all_params_overload_6(self, client: Kernel) -> None:
+ def test_method_perform_operation_with_all_params_overload_7(self, client: Kernel) -> None:
item = client.vaults.items.perform_operation(
key="key",
id_or_name="id_or_name",
@@ -987,7 +1044,7 @@ def test_method_perform_operation_with_all_params_overload_6(self, client: Kerne
@pytest.mark.skip(reason="Mock server tests are disabled")
@parametrize
- def test_raw_response_perform_operation_overload_6(self, client: Kernel) -> None:
+ def test_raw_response_perform_operation_overload_7(self, client: Kernel) -> None:
response = client.vaults.items.with_raw_response.perform_operation(
key="key",
id_or_name="id_or_name",
@@ -1001,7 +1058,7 @@ def test_raw_response_perform_operation_overload_6(self, client: Kernel) -> None
@pytest.mark.skip(reason="Mock server tests are disabled")
@parametrize
- def test_streaming_response_perform_operation_overload_6(self, client: Kernel) -> None:
+ def test_streaming_response_perform_operation_overload_7(self, client: Kernel) -> None:
with client.vaults.items.with_streaming_response.perform_operation(
key="key",
id_or_name="id_or_name",
@@ -1017,7 +1074,7 @@ def test_streaming_response_perform_operation_overload_6(self, client: Kernel) -
@pytest.mark.skip(reason="Mock server tests are disabled")
@parametrize
- def test_path_params_perform_operation_overload_6(self, client: Kernel) -> None:
+ def test_path_params_perform_operation_overload_7(self, client: Kernel) -> None:
with pytest.raises(ValueError, match=r"Expected a non-empty value for `id_or_name` but received ''"):
client.vaults.items.with_raw_response.perform_operation(
key="key",
@@ -1034,7 +1091,7 @@ def test_path_params_perform_operation_overload_6(self, client: Kernel) -> None:
@pytest.mark.skip(reason="Mock server tests are disabled")
@parametrize
- def test_method_perform_operation_overload_7(self, client: Kernel) -> None:
+ def test_method_perform_operation_overload_8(self, client: Kernel) -> None:
item = client.vaults.items.perform_operation(
key="key",
id_or_name="id_or_name",
@@ -1044,7 +1101,7 @@ def test_method_perform_operation_overload_7(self, client: Kernel) -> None:
@pytest.mark.skip(reason="Mock server tests are disabled")
@parametrize
- def test_method_perform_operation_with_all_params_overload_7(self, client: Kernel) -> None:
+ def test_method_perform_operation_with_all_params_overload_8(self, client: Kernel) -> None:
item = client.vaults.items.perform_operation(
key="key",
id_or_name="id_or_name",
@@ -1055,7 +1112,7 @@ def test_method_perform_operation_with_all_params_overload_7(self, client: Kerne
@pytest.mark.skip(reason="Mock server tests are disabled")
@parametrize
- def test_raw_response_perform_operation_overload_7(self, client: Kernel) -> None:
+ def test_raw_response_perform_operation_overload_8(self, client: Kernel) -> None:
response = client.vaults.items.with_raw_response.perform_operation(
key="key",
id_or_name="id_or_name",
@@ -1069,7 +1126,7 @@ def test_raw_response_perform_operation_overload_7(self, client: Kernel) -> None
@pytest.mark.skip(reason="Mock server tests are disabled")
@parametrize
- def test_streaming_response_perform_operation_overload_7(self, client: Kernel) -> None:
+ def test_streaming_response_perform_operation_overload_8(self, client: Kernel) -> None:
with client.vaults.items.with_streaming_response.perform_operation(
key="key",
id_or_name="id_or_name",
@@ -1085,7 +1142,7 @@ def test_streaming_response_perform_operation_overload_7(self, client: Kernel) -
@pytest.mark.skip(reason="Mock server tests are disabled")
@parametrize
- def test_path_params_perform_operation_overload_7(self, client: Kernel) -> None:
+ def test_path_params_perform_operation_overload_8(self, client: Kernel) -> None:
with pytest.raises(ValueError, match=r"Expected a non-empty value for `id_or_name` but received ''"):
client.vaults.items.with_raw_response.perform_operation(
key="key",
@@ -1102,7 +1159,7 @@ def test_path_params_perform_operation_overload_7(self, client: Kernel) -> None:
@pytest.mark.skip(reason="Mock server tests are disabled")
@parametrize
- def test_method_perform_operation_overload_8(self, client: Kernel) -> None:
+ def test_method_perform_operation_overload_9(self, client: Kernel) -> None:
item = client.vaults.items.perform_operation(
key="key",
id_or_name="id_or_name",
@@ -1114,7 +1171,7 @@ def test_method_perform_operation_overload_8(self, client: Kernel) -> None:
@pytest.mark.skip(reason="Mock server tests are disabled")
@parametrize
- def test_method_perform_operation_with_all_params_overload_8(self, client: Kernel) -> None:
+ def test_method_perform_operation_with_all_params_overload_9(self, client: Kernel) -> None:
item = client.vaults.items.perform_operation(
key="key",
id_or_name="id_or_name",
@@ -1128,7 +1185,7 @@ def test_method_perform_operation_with_all_params_overload_8(self, client: Kerne
@pytest.mark.skip(reason="Mock server tests are disabled")
@parametrize
- def test_raw_response_perform_operation_overload_8(self, client: Kernel) -> None:
+ def test_raw_response_perform_operation_overload_9(self, client: Kernel) -> None:
response = client.vaults.items.with_raw_response.perform_operation(
key="key",
id_or_name="id_or_name",
@@ -1144,7 +1201,7 @@ def test_raw_response_perform_operation_overload_8(self, client: Kernel) -> None
@pytest.mark.skip(reason="Mock server tests are disabled")
@parametrize
- def test_streaming_response_perform_operation_overload_8(self, client: Kernel) -> None:
+ def test_streaming_response_perform_operation_overload_9(self, client: Kernel) -> None:
with client.vaults.items.with_streaming_response.perform_operation(
key="key",
id_or_name="id_or_name",
@@ -1162,7 +1219,7 @@ def test_streaming_response_perform_operation_overload_8(self, client: Kernel) -
@pytest.mark.skip(reason="Mock server tests are disabled")
@parametrize
- def test_path_params_perform_operation_overload_8(self, client: Kernel) -> None:
+ def test_path_params_perform_operation_overload_9(self, client: Kernel) -> None:
with pytest.raises(ValueError, match=r"Expected a non-empty value for `id_or_name` but received ''"):
client.vaults.items.with_raw_response.perform_operation(
key="key",
@@ -1183,7 +1240,7 @@ def test_path_params_perform_operation_overload_8(self, client: Kernel) -> None:
@pytest.mark.skip(reason="Mock server tests are disabled")
@parametrize
- def test_method_perform_operation_overload_9(self, client: Kernel) -> None:
+ def test_method_perform_operation_overload_10(self, client: Kernel) -> None:
item = client.vaults.items.perform_operation(
key="key",
id_or_name="id_or_name",
@@ -1193,7 +1250,7 @@ def test_method_perform_operation_overload_9(self, client: Kernel) -> None:
@pytest.mark.skip(reason="Mock server tests are disabled")
@parametrize
- def test_raw_response_perform_operation_overload_9(self, client: Kernel) -> None:
+ def test_raw_response_perform_operation_overload_10(self, client: Kernel) -> None:
response = client.vaults.items.with_raw_response.perform_operation(
key="key",
id_or_name="id_or_name",
@@ -1207,7 +1264,7 @@ def test_raw_response_perform_operation_overload_9(self, client: Kernel) -> None
@pytest.mark.skip(reason="Mock server tests are disabled")
@parametrize
- def test_streaming_response_perform_operation_overload_9(self, client: Kernel) -> None:
+ def test_streaming_response_perform_operation_overload_10(self, client: Kernel) -> None:
with client.vaults.items.with_streaming_response.perform_operation(
key="key",
id_or_name="id_or_name",
@@ -1223,7 +1280,7 @@ def test_streaming_response_perform_operation_overload_9(self, client: Kernel) -
@pytest.mark.skip(reason="Mock server tests are disabled")
@parametrize
- def test_path_params_perform_operation_overload_9(self, client: Kernel) -> None:
+ def test_path_params_perform_operation_overload_10(self, client: Kernel) -> None:
with pytest.raises(ValueError, match=r"Expected a non-empty value for `id_or_name` but received ''"):
client.vaults.items.with_raw_response.perform_operation(
key="key",
@@ -1240,7 +1297,7 @@ def test_path_params_perform_operation_overload_9(self, client: Kernel) -> None:
@pytest.mark.skip(reason="Mock server tests are disabled")
@parametrize
- def test_method_perform_operation_overload_10(self, client: Kernel) -> None:
+ def test_method_perform_operation_overload_11(self, client: Kernel) -> None:
item = client.vaults.items.perform_operation(
key="key",
id_or_name="id_or_name",
@@ -1251,7 +1308,7 @@ def test_method_perform_operation_overload_10(self, client: Kernel) -> None:
@pytest.mark.skip(reason="Mock server tests are disabled")
@parametrize
- def test_method_perform_operation_with_all_params_overload_10(self, client: Kernel) -> None:
+ def test_method_perform_operation_with_all_params_overload_11(self, client: Kernel) -> None:
item = client.vaults.items.perform_operation(
key="key",
id_or_name="id_or_name",
@@ -1263,7 +1320,7 @@ def test_method_perform_operation_with_all_params_overload_10(self, client: Kern
@pytest.mark.skip(reason="Mock server tests are disabled")
@parametrize
- def test_raw_response_perform_operation_overload_10(self, client: Kernel) -> None:
+ def test_raw_response_perform_operation_overload_11(self, client: Kernel) -> None:
response = client.vaults.items.with_raw_response.perform_operation(
key="key",
id_or_name="id_or_name",
@@ -1278,7 +1335,7 @@ def test_raw_response_perform_operation_overload_10(self, client: Kernel) -> Non
@pytest.mark.skip(reason="Mock server tests are disabled")
@parametrize
- def test_streaming_response_perform_operation_overload_10(self, client: Kernel) -> None:
+ def test_streaming_response_perform_operation_overload_11(self, client: Kernel) -> None:
with client.vaults.items.with_streaming_response.perform_operation(
key="key",
id_or_name="id_or_name",
@@ -1295,7 +1352,7 @@ def test_streaming_response_perform_operation_overload_10(self, client: Kernel)
@pytest.mark.skip(reason="Mock server tests are disabled")
@parametrize
- def test_path_params_perform_operation_overload_10(self, client: Kernel) -> None:
+ def test_path_params_perform_operation_overload_11(self, client: Kernel) -> None:
with pytest.raises(ValueError, match=r"Expected a non-empty value for `id_or_name` but received ''"):
client.vaults.items.with_raw_response.perform_operation(
key="key",
@@ -1314,7 +1371,7 @@ def test_path_params_perform_operation_overload_10(self, client: Kernel) -> None
@pytest.mark.skip(reason="Mock server tests are disabled")
@parametrize
- def test_method_perform_operation_overload_11(self, client: Kernel) -> None:
+ def test_method_perform_operation_overload_12(self, client: Kernel) -> None:
item = client.vaults.items.perform_operation(
key="key",
id_or_name="id_or_name",
@@ -1341,7 +1398,7 @@ def test_method_perform_operation_overload_11(self, client: Kernel) -> None:
@pytest.mark.skip(reason="Mock server tests are disabled")
@parametrize
- def test_method_perform_operation_with_all_params_overload_11(self, client: Kernel) -> None:
+ def test_method_perform_operation_with_all_params_overload_12(self, client: Kernel) -> None:
item = client.vaults.items.perform_operation(
key="key",
id_or_name="id_or_name",
@@ -1371,7 +1428,7 @@ def test_method_perform_operation_with_all_params_overload_11(self, client: Kern
@pytest.mark.skip(reason="Mock server tests are disabled")
@parametrize
- def test_raw_response_perform_operation_overload_11(self, client: Kernel) -> None:
+ def test_raw_response_perform_operation_overload_12(self, client: Kernel) -> None:
response = client.vaults.items.with_raw_response.perform_operation(
key="key",
id_or_name="id_or_name",
@@ -1402,7 +1459,7 @@ def test_raw_response_perform_operation_overload_11(self, client: Kernel) -> Non
@pytest.mark.skip(reason="Mock server tests are disabled")
@parametrize
- def test_streaming_response_perform_operation_overload_11(self, client: Kernel) -> None:
+ def test_streaming_response_perform_operation_overload_12(self, client: Kernel) -> None:
with client.vaults.items.with_streaming_response.perform_operation(
key="key",
id_or_name="id_or_name",
@@ -1435,7 +1492,7 @@ def test_streaming_response_perform_operation_overload_11(self, client: Kernel)
@pytest.mark.skip(reason="Mock server tests are disabled")
@parametrize
- def test_path_params_perform_operation_overload_11(self, client: Kernel) -> None:
+ def test_path_params_perform_operation_overload_12(self, client: Kernel) -> None:
with pytest.raises(ValueError, match=r"Expected a non-empty value for `id_or_name` but received ''"):
client.vaults.items.with_raw_response.perform_operation(
key="key",
@@ -2491,6 +2548,63 @@ async def test_path_params_perform_operation_overload_1(self, async_client: Asyn
@pytest.mark.skip(reason="Mock server tests are disabled")
@parametrize
async def test_method_perform_operation_overload_2(self, async_client: AsyncKernel) -> None:
+ item = await async_client.vaults.items.perform_operation(
+ key="key",
+ id_or_name="id_or_name",
+ type="restart_verification",
+ )
+ assert_matches_type(VaultItemOperationResponse, item, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ async def test_raw_response_perform_operation_overload_2(self, async_client: AsyncKernel) -> None:
+ response = await async_client.vaults.items.with_raw_response.perform_operation(
+ key="key",
+ id_or_name="id_or_name",
+ type="restart_verification",
+ )
+
+ assert response.is_closed is True
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+ item = await response.parse()
+ assert_matches_type(VaultItemOperationResponse, item, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ async def test_streaming_response_perform_operation_overload_2(self, async_client: AsyncKernel) -> None:
+ async with async_client.vaults.items.with_streaming_response.perform_operation(
+ key="key",
+ id_or_name="id_or_name",
+ type="restart_verification",
+ ) as response:
+ assert not response.is_closed
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+
+ item = await response.parse()
+ assert_matches_type(VaultItemOperationResponse, item, 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_perform_operation_overload_2(self, async_client: AsyncKernel) -> None:
+ with pytest.raises(ValueError, match=r"Expected a non-empty value for `id_or_name` but received ''"):
+ await async_client.vaults.items.with_raw_response.perform_operation(
+ key="key",
+ id_or_name="",
+ type="restart_verification",
+ )
+
+ with pytest.raises(ValueError, match=r"Expected a non-empty value for `key` but received ''"):
+ await async_client.vaults.items.with_raw_response.perform_operation(
+ key="",
+ id_or_name="id_or_name",
+ type="restart_verification",
+ )
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ async def test_method_perform_operation_overload_3(self, async_client: AsyncKernel) -> None:
item = await async_client.vaults.items.perform_operation(
key="key",
id_or_name="id_or_name",
@@ -2505,7 +2619,7 @@ async def test_method_perform_operation_overload_2(self, async_client: AsyncKern
@pytest.mark.skip(reason="Mock server tests are disabled")
@parametrize
- async def test_raw_response_perform_operation_overload_2(self, async_client: AsyncKernel) -> None:
+ async def test_raw_response_perform_operation_overload_3(self, async_client: AsyncKernel) -> None:
response = await async_client.vaults.items.with_raw_response.perform_operation(
key="key",
id_or_name="id_or_name",
@@ -2524,7 +2638,7 @@ async def test_raw_response_perform_operation_overload_2(self, async_client: Asy
@pytest.mark.skip(reason="Mock server tests are disabled")
@parametrize
- async def test_streaming_response_perform_operation_overload_2(self, async_client: AsyncKernel) -> None:
+ async def test_streaming_response_perform_operation_overload_3(self, async_client: AsyncKernel) -> None:
async with async_client.vaults.items.with_streaming_response.perform_operation(
key="key",
id_or_name="id_or_name",
@@ -2545,7 +2659,7 @@ async def test_streaming_response_perform_operation_overload_2(self, async_clien
@pytest.mark.skip(reason="Mock server tests are disabled")
@parametrize
- async def test_path_params_perform_operation_overload_2(self, async_client: AsyncKernel) -> None:
+ async def test_path_params_perform_operation_overload_3(self, async_client: AsyncKernel) -> None:
with pytest.raises(ValueError, match=r"Expected a non-empty value for `id_or_name` but received ''"):
await async_client.vaults.items.with_raw_response.perform_operation(
key="key",
@@ -2572,7 +2686,7 @@ async def test_path_params_perform_operation_overload_2(self, async_client: Asyn
@pytest.mark.skip(reason="Mock server tests are disabled")
@parametrize
- async def test_method_perform_operation_overload_3(self, async_client: AsyncKernel) -> None:
+ async def test_method_perform_operation_overload_4(self, async_client: AsyncKernel) -> None:
item = await async_client.vaults.items.perform_operation(
key="key",
id_or_name="id_or_name",
@@ -2582,7 +2696,7 @@ async def test_method_perform_operation_overload_3(self, async_client: AsyncKern
@pytest.mark.skip(reason="Mock server tests are disabled")
@parametrize
- async def test_raw_response_perform_operation_overload_3(self, async_client: AsyncKernel) -> None:
+ async def test_raw_response_perform_operation_overload_4(self, async_client: AsyncKernel) -> None:
response = await async_client.vaults.items.with_raw_response.perform_operation(
key="key",
id_or_name="id_or_name",
@@ -2596,7 +2710,7 @@ async def test_raw_response_perform_operation_overload_3(self, async_client: Asy
@pytest.mark.skip(reason="Mock server tests are disabled")
@parametrize
- async def test_streaming_response_perform_operation_overload_3(self, async_client: AsyncKernel) -> None:
+ async def test_streaming_response_perform_operation_overload_4(self, async_client: AsyncKernel) -> None:
async with async_client.vaults.items.with_streaming_response.perform_operation(
key="key",
id_or_name="id_or_name",
@@ -2612,7 +2726,7 @@ async def test_streaming_response_perform_operation_overload_3(self, async_clien
@pytest.mark.skip(reason="Mock server tests are disabled")
@parametrize
- async def test_path_params_perform_operation_overload_3(self, async_client: AsyncKernel) -> None:
+ async def test_path_params_perform_operation_overload_4(self, async_client: AsyncKernel) -> None:
with pytest.raises(ValueError, match=r"Expected a non-empty value for `id_or_name` but received ''"):
await async_client.vaults.items.with_raw_response.perform_operation(
key="key",
@@ -2629,7 +2743,7 @@ async def test_path_params_perform_operation_overload_3(self, async_client: Asyn
@pytest.mark.skip(reason="Mock server tests are disabled")
@parametrize
- async def test_method_perform_operation_overload_4(self, async_client: AsyncKernel) -> None:
+ async def test_method_perform_operation_overload_5(self, async_client: AsyncKernel) -> None:
item = await async_client.vaults.items.perform_operation(
key="key",
id_or_name="id_or_name",
@@ -2644,7 +2758,7 @@ async def test_method_perform_operation_overload_4(self, async_client: AsyncKern
@pytest.mark.skip(reason="Mock server tests are disabled")
@parametrize
- async def test_method_perform_operation_with_all_params_overload_4(self, async_client: AsyncKernel) -> None:
+ async def test_method_perform_operation_with_all_params_overload_5(self, async_client: AsyncKernel) -> None:
item = await async_client.vaults.items.perform_operation(
key="key",
id_or_name="id_or_name",
@@ -2660,7 +2774,7 @@ async def test_method_perform_operation_with_all_params_overload_4(self, async_c
@pytest.mark.skip(reason="Mock server tests are disabled")
@parametrize
- async def test_raw_response_perform_operation_overload_4(self, async_client: AsyncKernel) -> None:
+ async def test_raw_response_perform_operation_overload_5(self, async_client: AsyncKernel) -> None:
response = await async_client.vaults.items.with_raw_response.perform_operation(
key="key",
id_or_name="id_or_name",
@@ -2679,7 +2793,7 @@ async def test_raw_response_perform_operation_overload_4(self, async_client: Asy
@pytest.mark.skip(reason="Mock server tests are disabled")
@parametrize
- async def test_streaming_response_perform_operation_overload_4(self, async_client: AsyncKernel) -> None:
+ async def test_streaming_response_perform_operation_overload_5(self, async_client: AsyncKernel) -> None:
async with async_client.vaults.items.with_streaming_response.perform_operation(
key="key",
id_or_name="id_or_name",
@@ -2700,7 +2814,7 @@ async def test_streaming_response_perform_operation_overload_4(self, async_clien
@pytest.mark.skip(reason="Mock server tests are disabled")
@parametrize
- async def test_path_params_perform_operation_overload_4(self, async_client: AsyncKernel) -> None:
+ async def test_path_params_perform_operation_overload_5(self, async_client: AsyncKernel) -> None:
with pytest.raises(ValueError, match=r"Expected a non-empty value for `id_or_name` but received ''"):
await async_client.vaults.items.with_raw_response.perform_operation(
key="key",
@@ -2727,7 +2841,7 @@ async def test_path_params_perform_operation_overload_4(self, async_client: Asyn
@pytest.mark.skip(reason="Mock server tests are disabled")
@parametrize
- async def test_method_perform_operation_overload_5(self, async_client: AsyncKernel) -> None:
+ async def test_method_perform_operation_overload_6(self, async_client: AsyncKernel) -> None:
item = await async_client.vaults.items.perform_operation(
key="key",
id_or_name="id_or_name",
@@ -2756,7 +2870,7 @@ async def test_method_perform_operation_overload_5(self, async_client: AsyncKern
@pytest.mark.skip(reason="Mock server tests are disabled")
@parametrize
- async def test_method_perform_operation_with_all_params_overload_5(self, async_client: AsyncKernel) -> None:
+ async def test_method_perform_operation_with_all_params_overload_6(self, async_client: AsyncKernel) -> None:
item = await async_client.vaults.items.perform_operation(
key="key",
id_or_name="id_or_name",
@@ -2791,7 +2905,7 @@ async def test_method_perform_operation_with_all_params_overload_5(self, async_c
@pytest.mark.skip(reason="Mock server tests are disabled")
@parametrize
- async def test_raw_response_perform_operation_overload_5(self, async_client: AsyncKernel) -> None:
+ async def test_raw_response_perform_operation_overload_6(self, async_client: AsyncKernel) -> None:
response = await async_client.vaults.items.with_raw_response.perform_operation(
key="key",
id_or_name="id_or_name",
@@ -2824,7 +2938,7 @@ async def test_raw_response_perform_operation_overload_5(self, async_client: Asy
@pytest.mark.skip(reason="Mock server tests are disabled")
@parametrize
- async def test_streaming_response_perform_operation_overload_5(self, async_client: AsyncKernel) -> None:
+ async def test_streaming_response_perform_operation_overload_6(self, async_client: AsyncKernel) -> None:
async with async_client.vaults.items.with_streaming_response.perform_operation(
key="key",
id_or_name="id_or_name",
@@ -2859,7 +2973,7 @@ async def test_streaming_response_perform_operation_overload_5(self, async_clien
@pytest.mark.skip(reason="Mock server tests are disabled")
@parametrize
- async def test_path_params_perform_operation_overload_5(self, async_client: AsyncKernel) -> None:
+ async def test_path_params_perform_operation_overload_6(self, async_client: AsyncKernel) -> None:
with pytest.raises(ValueError, match=r"Expected a non-empty value for `id_or_name` but received ''"):
await async_client.vaults.items.with_raw_response.perform_operation(
key="key",
@@ -2914,7 +3028,7 @@ async def test_path_params_perform_operation_overload_5(self, async_client: Asyn
@pytest.mark.skip(reason="Mock server tests are disabled")
@parametrize
- async def test_method_perform_operation_overload_6(self, async_client: AsyncKernel) -> None:
+ async def test_method_perform_operation_overload_7(self, async_client: AsyncKernel) -> None:
item = await async_client.vaults.items.perform_operation(
key="key",
id_or_name="id_or_name",
@@ -2924,7 +3038,7 @@ async def test_method_perform_operation_overload_6(self, async_client: AsyncKern
@pytest.mark.skip(reason="Mock server tests are disabled")
@parametrize
- async def test_method_perform_operation_with_all_params_overload_6(self, async_client: AsyncKernel) -> None:
+ async def test_method_perform_operation_with_all_params_overload_7(self, async_client: AsyncKernel) -> None:
item = await async_client.vaults.items.perform_operation(
key="key",
id_or_name="id_or_name",
@@ -2937,7 +3051,7 @@ async def test_method_perform_operation_with_all_params_overload_6(self, async_c
@pytest.mark.skip(reason="Mock server tests are disabled")
@parametrize
- async def test_raw_response_perform_operation_overload_6(self, async_client: AsyncKernel) -> None:
+ async def test_raw_response_perform_operation_overload_7(self, async_client: AsyncKernel) -> None:
response = await async_client.vaults.items.with_raw_response.perform_operation(
key="key",
id_or_name="id_or_name",
@@ -2951,7 +3065,7 @@ async def test_raw_response_perform_operation_overload_6(self, async_client: Asy
@pytest.mark.skip(reason="Mock server tests are disabled")
@parametrize
- async def test_streaming_response_perform_operation_overload_6(self, async_client: AsyncKernel) -> None:
+ async def test_streaming_response_perform_operation_overload_7(self, async_client: AsyncKernel) -> None:
async with async_client.vaults.items.with_streaming_response.perform_operation(
key="key",
id_or_name="id_or_name",
@@ -2967,7 +3081,7 @@ async def test_streaming_response_perform_operation_overload_6(self, async_clien
@pytest.mark.skip(reason="Mock server tests are disabled")
@parametrize
- async def test_path_params_perform_operation_overload_6(self, async_client: AsyncKernel) -> None:
+ async def test_path_params_perform_operation_overload_7(self, async_client: AsyncKernel) -> None:
with pytest.raises(ValueError, match=r"Expected a non-empty value for `id_or_name` but received ''"):
await async_client.vaults.items.with_raw_response.perform_operation(
key="key",
@@ -2984,7 +3098,7 @@ async def test_path_params_perform_operation_overload_6(self, async_client: Asyn
@pytest.mark.skip(reason="Mock server tests are disabled")
@parametrize
- async def test_method_perform_operation_overload_7(self, async_client: AsyncKernel) -> None:
+ async def test_method_perform_operation_overload_8(self, async_client: AsyncKernel) -> None:
item = await async_client.vaults.items.perform_operation(
key="key",
id_or_name="id_or_name",
@@ -2994,7 +3108,7 @@ async def test_method_perform_operation_overload_7(self, async_client: AsyncKern
@pytest.mark.skip(reason="Mock server tests are disabled")
@parametrize
- async def test_method_perform_operation_with_all_params_overload_7(self, async_client: AsyncKernel) -> None:
+ async def test_method_perform_operation_with_all_params_overload_8(self, async_client: AsyncKernel) -> None:
item = await async_client.vaults.items.perform_operation(
key="key",
id_or_name="id_or_name",
@@ -3005,7 +3119,7 @@ async def test_method_perform_operation_with_all_params_overload_7(self, async_c
@pytest.mark.skip(reason="Mock server tests are disabled")
@parametrize
- async def test_raw_response_perform_operation_overload_7(self, async_client: AsyncKernel) -> None:
+ async def test_raw_response_perform_operation_overload_8(self, async_client: AsyncKernel) -> None:
response = await async_client.vaults.items.with_raw_response.perform_operation(
key="key",
id_or_name="id_or_name",
@@ -3019,7 +3133,7 @@ async def test_raw_response_perform_operation_overload_7(self, async_client: Asy
@pytest.mark.skip(reason="Mock server tests are disabled")
@parametrize
- async def test_streaming_response_perform_operation_overload_7(self, async_client: AsyncKernel) -> None:
+ async def test_streaming_response_perform_operation_overload_8(self, async_client: AsyncKernel) -> None:
async with async_client.vaults.items.with_streaming_response.perform_operation(
key="key",
id_or_name="id_or_name",
@@ -3035,7 +3149,7 @@ async def test_streaming_response_perform_operation_overload_7(self, async_clien
@pytest.mark.skip(reason="Mock server tests are disabled")
@parametrize
- async def test_path_params_perform_operation_overload_7(self, async_client: AsyncKernel) -> None:
+ async def test_path_params_perform_operation_overload_8(self, async_client: AsyncKernel) -> None:
with pytest.raises(ValueError, match=r"Expected a non-empty value for `id_or_name` but received ''"):
await async_client.vaults.items.with_raw_response.perform_operation(
key="key",
@@ -3052,7 +3166,7 @@ async def test_path_params_perform_operation_overload_7(self, async_client: Asyn
@pytest.mark.skip(reason="Mock server tests are disabled")
@parametrize
- async def test_method_perform_operation_overload_8(self, async_client: AsyncKernel) -> None:
+ async def test_method_perform_operation_overload_9(self, async_client: AsyncKernel) -> None:
item = await async_client.vaults.items.perform_operation(
key="key",
id_or_name="id_or_name",
@@ -3064,7 +3178,7 @@ async def test_method_perform_operation_overload_8(self, async_client: AsyncKern
@pytest.mark.skip(reason="Mock server tests are disabled")
@parametrize
- async def test_method_perform_operation_with_all_params_overload_8(self, async_client: AsyncKernel) -> None:
+ async def test_method_perform_operation_with_all_params_overload_9(self, async_client: AsyncKernel) -> None:
item = await async_client.vaults.items.perform_operation(
key="key",
id_or_name="id_or_name",
@@ -3078,7 +3192,7 @@ async def test_method_perform_operation_with_all_params_overload_8(self, async_c
@pytest.mark.skip(reason="Mock server tests are disabled")
@parametrize
- async def test_raw_response_perform_operation_overload_8(self, async_client: AsyncKernel) -> None:
+ async def test_raw_response_perform_operation_overload_9(self, async_client: AsyncKernel) -> None:
response = await async_client.vaults.items.with_raw_response.perform_operation(
key="key",
id_or_name="id_or_name",
@@ -3094,7 +3208,7 @@ async def test_raw_response_perform_operation_overload_8(self, async_client: Asy
@pytest.mark.skip(reason="Mock server tests are disabled")
@parametrize
- async def test_streaming_response_perform_operation_overload_8(self, async_client: AsyncKernel) -> None:
+ async def test_streaming_response_perform_operation_overload_9(self, async_client: AsyncKernel) -> None:
async with async_client.vaults.items.with_streaming_response.perform_operation(
key="key",
id_or_name="id_or_name",
@@ -3112,7 +3226,7 @@ async def test_streaming_response_perform_operation_overload_8(self, async_clien
@pytest.mark.skip(reason="Mock server tests are disabled")
@parametrize
- async def test_path_params_perform_operation_overload_8(self, async_client: AsyncKernel) -> None:
+ async def test_path_params_perform_operation_overload_9(self, async_client: AsyncKernel) -> None:
with pytest.raises(ValueError, match=r"Expected a non-empty value for `id_or_name` but received ''"):
await async_client.vaults.items.with_raw_response.perform_operation(
key="key",
@@ -3133,7 +3247,7 @@ async def test_path_params_perform_operation_overload_8(self, async_client: Asyn
@pytest.mark.skip(reason="Mock server tests are disabled")
@parametrize
- async def test_method_perform_operation_overload_9(self, async_client: AsyncKernel) -> None:
+ async def test_method_perform_operation_overload_10(self, async_client: AsyncKernel) -> None:
item = await async_client.vaults.items.perform_operation(
key="key",
id_or_name="id_or_name",
@@ -3143,7 +3257,7 @@ async def test_method_perform_operation_overload_9(self, async_client: AsyncKern
@pytest.mark.skip(reason="Mock server tests are disabled")
@parametrize
- async def test_raw_response_perform_operation_overload_9(self, async_client: AsyncKernel) -> None:
+ async def test_raw_response_perform_operation_overload_10(self, async_client: AsyncKernel) -> None:
response = await async_client.vaults.items.with_raw_response.perform_operation(
key="key",
id_or_name="id_or_name",
@@ -3157,7 +3271,7 @@ async def test_raw_response_perform_operation_overload_9(self, async_client: Asy
@pytest.mark.skip(reason="Mock server tests are disabled")
@parametrize
- async def test_streaming_response_perform_operation_overload_9(self, async_client: AsyncKernel) -> None:
+ async def test_streaming_response_perform_operation_overload_10(self, async_client: AsyncKernel) -> None:
async with async_client.vaults.items.with_streaming_response.perform_operation(
key="key",
id_or_name="id_or_name",
@@ -3173,7 +3287,7 @@ async def test_streaming_response_perform_operation_overload_9(self, async_clien
@pytest.mark.skip(reason="Mock server tests are disabled")
@parametrize
- async def test_path_params_perform_operation_overload_9(self, async_client: AsyncKernel) -> None:
+ async def test_path_params_perform_operation_overload_10(self, async_client: AsyncKernel) -> None:
with pytest.raises(ValueError, match=r"Expected a non-empty value for `id_or_name` but received ''"):
await async_client.vaults.items.with_raw_response.perform_operation(
key="key",
@@ -3190,7 +3304,7 @@ async def test_path_params_perform_operation_overload_9(self, async_client: Asyn
@pytest.mark.skip(reason="Mock server tests are disabled")
@parametrize
- async def test_method_perform_operation_overload_10(self, async_client: AsyncKernel) -> None:
+ async def test_method_perform_operation_overload_11(self, async_client: AsyncKernel) -> None:
item = await async_client.vaults.items.perform_operation(
key="key",
id_or_name="id_or_name",
@@ -3201,7 +3315,7 @@ async def test_method_perform_operation_overload_10(self, async_client: AsyncKer
@pytest.mark.skip(reason="Mock server tests are disabled")
@parametrize
- async def test_method_perform_operation_with_all_params_overload_10(self, async_client: AsyncKernel) -> None:
+ async def test_method_perform_operation_with_all_params_overload_11(self, async_client: AsyncKernel) -> None:
item = await async_client.vaults.items.perform_operation(
key="key",
id_or_name="id_or_name",
@@ -3213,7 +3327,7 @@ async def test_method_perform_operation_with_all_params_overload_10(self, async_
@pytest.mark.skip(reason="Mock server tests are disabled")
@parametrize
- async def test_raw_response_perform_operation_overload_10(self, async_client: AsyncKernel) -> None:
+ async def test_raw_response_perform_operation_overload_11(self, async_client: AsyncKernel) -> None:
response = await async_client.vaults.items.with_raw_response.perform_operation(
key="key",
id_or_name="id_or_name",
@@ -3228,7 +3342,7 @@ async def test_raw_response_perform_operation_overload_10(self, async_client: As
@pytest.mark.skip(reason="Mock server tests are disabled")
@parametrize
- async def test_streaming_response_perform_operation_overload_10(self, async_client: AsyncKernel) -> None:
+ async def test_streaming_response_perform_operation_overload_11(self, async_client: AsyncKernel) -> None:
async with async_client.vaults.items.with_streaming_response.perform_operation(
key="key",
id_or_name="id_or_name",
@@ -3245,7 +3359,7 @@ async def test_streaming_response_perform_operation_overload_10(self, async_clie
@pytest.mark.skip(reason="Mock server tests are disabled")
@parametrize
- async def test_path_params_perform_operation_overload_10(self, async_client: AsyncKernel) -> None:
+ async def test_path_params_perform_operation_overload_11(self, async_client: AsyncKernel) -> None:
with pytest.raises(ValueError, match=r"Expected a non-empty value for `id_or_name` but received ''"):
await async_client.vaults.items.with_raw_response.perform_operation(
key="key",
@@ -3264,7 +3378,7 @@ async def test_path_params_perform_operation_overload_10(self, async_client: Asy
@pytest.mark.skip(reason="Mock server tests are disabled")
@parametrize
- async def test_method_perform_operation_overload_11(self, async_client: AsyncKernel) -> None:
+ async def test_method_perform_operation_overload_12(self, async_client: AsyncKernel) -> None:
item = await async_client.vaults.items.perform_operation(
key="key",
id_or_name="id_or_name",
@@ -3291,7 +3405,7 @@ async def test_method_perform_operation_overload_11(self, async_client: AsyncKer
@pytest.mark.skip(reason="Mock server tests are disabled")
@parametrize
- async def test_method_perform_operation_with_all_params_overload_11(self, async_client: AsyncKernel) -> None:
+ async def test_method_perform_operation_with_all_params_overload_12(self, async_client: AsyncKernel) -> None:
item = await async_client.vaults.items.perform_operation(
key="key",
id_or_name="id_or_name",
@@ -3321,7 +3435,7 @@ async def test_method_perform_operation_with_all_params_overload_11(self, async_
@pytest.mark.skip(reason="Mock server tests are disabled")
@parametrize
- async def test_raw_response_perform_operation_overload_11(self, async_client: AsyncKernel) -> None:
+ async def test_raw_response_perform_operation_overload_12(self, async_client: AsyncKernel) -> None:
response = await async_client.vaults.items.with_raw_response.perform_operation(
key="key",
id_or_name="id_or_name",
@@ -3352,7 +3466,7 @@ async def test_raw_response_perform_operation_overload_11(self, async_client: As
@pytest.mark.skip(reason="Mock server tests are disabled")
@parametrize
- async def test_streaming_response_perform_operation_overload_11(self, async_client: AsyncKernel) -> None:
+ async def test_streaming_response_perform_operation_overload_12(self, async_client: AsyncKernel) -> None:
async with async_client.vaults.items.with_streaming_response.perform_operation(
key="key",
id_or_name="id_or_name",
@@ -3385,7 +3499,7 @@ async def test_streaming_response_perform_operation_overload_11(self, async_clie
@pytest.mark.skip(reason="Mock server tests are disabled")
@parametrize
- async def test_path_params_perform_operation_overload_11(self, async_client: AsyncKernel) -> None:
+ async def test_path_params_perform_operation_overload_12(self, async_client: AsyncKernel) -> None:
with pytest.raises(ValueError, match=r"Expected a non-empty value for `id_or_name` but received ''"):
await async_client.vaults.items.with_raw_response.perform_operation(
key="key",
From 4d81af2b6103571b59cd1bb520eb85e28488b51c Mon Sep 17 00:00:00 2001
From: "kernel-internal[bot]"
<260533166+kernel-internal[bot]@users.noreply.github.com>
Date: Tue, 13 Oct 2026 22:29:51 +0000
Subject: [PATCH 351/366] fix: seal browser pool acquire SDK results
Stainless-Generated-From: f64758e79131d7749f23be27de0655ee48e5f835
---
src/kernel/_response.py | 9 ++-
src/kernel/_types.py | 6 ++
src/kernel/resources/browser_pools.py | 4 +-
.../browsers/playwright/playwright.py | 8 ---
.../types/browser_pool_acquire_response.py | 3 +-
tests/test_browser_pool_acquire_response.py | 70 +++++++++++++++++++
6 files changed, 88 insertions(+), 12 deletions(-)
create mode 100644 tests/test_browser_pool_acquire_response.py
diff --git a/src/kernel/_response.py b/src/kernel/_response.py
index a6c5fa30..95d81625 100644
--- a/src/kernel/_response.py
+++ b/src/kernel/_response.py
@@ -24,7 +24,7 @@
import httpx
import pydantic
-from ._types import NoneType
+from ._types import NoneType, NoContentAsNone
from ._utils import is_given, extract_type_arg, is_annotated_type, is_type_alias_type, extract_type_var_from_base
from ._models import BaseModel, is_basemodel
from ._constants import RAW_RESPONSE_HEADER, OVERRIDE_CAST_TO_HEADER
@@ -185,6 +185,13 @@ def _parse(self, *, to: type[_T] | None = None) -> R | _T:
return cast(R, None)
response = self.http_response
+ if (
+ response.status_code == 204
+ and to is None
+ and isinstance(origin, type)
+ and issubclass(origin, NoContentAsNone)
+ ):
+ return cast(R, None)
if cast_to == str:
return cast(R, response.text)
diff --git a/src/kernel/_types.py b/src/kernel/_types.py
index d924c204..94982715 100644
--- a/src/kernel/_types.py
+++ b/src/kernel/_types.py
@@ -44,6 +44,12 @@
Query = Mapping[str, object]
Body = object
AnyMapping = Mapping[str, object]
+
+
+class NoContentAsNone:
+ pass
+
+
ModelT = TypeVar("ModelT", bound=pydantic.BaseModel)
_T = TypeVar("_T")
diff --git a/src/kernel/resources/browser_pools.py b/src/kernel/resources/browser_pools.py
index 5fc7147a..645c0d27 100644
--- a/src/kernel/resources/browser_pools.py
+++ b/src/kernel/resources/browser_pools.py
@@ -542,7 +542,7 @@ def acquire(
extra_query: Query | None = None,
extra_body: Body | None = None,
timeout: float | httpx.Timeout | None | NotGiven = not_given,
- ) -> BrowserPoolAcquireResponse:
+ ) -> Optional[BrowserPoolAcquireResponse]:
"""Long-polling endpoint to acquire a browser from the pool.
Returns immediately
@@ -1199,7 +1199,7 @@ async def acquire(
extra_query: Query | None = None,
extra_body: Body | None = None,
timeout: float | httpx.Timeout | None | NotGiven = not_given,
- ) -> BrowserPoolAcquireResponse:
+ ) -> Optional[BrowserPoolAcquireResponse]:
"""Long-polling endpoint to acquire a browser from the pool.
Returns immediately
diff --git a/src/kernel/resources/browsers/playwright/playwright.py b/src/kernel/resources/browsers/playwright/playwright.py
index 50ddf810..00f5f10e 100644
--- a/src/kernel/resources/browsers/playwright/playwright.py
+++ b/src/kernel/resources/browsers/playwright/playwright.py
@@ -114,10 +114,6 @@ def execute(
executors are not removed automatically while the browser runs; when it shuts
down, they are removed and their tabs closed.
- A named call to a browser whose image predates executors fails with 400 instead
- of running on the active tab; calls without 'executor' keep working on every
- image.
-
Args:
code: TypeScript/JavaScript code to execute. The code has access to 'page', 'context',
and 'browser' variables. It runs within a function, so you can use a return
@@ -244,10 +240,6 @@ async def execute(
executors are not removed automatically while the browser runs; when it shuts
down, they are removed and their tabs closed.
- A named call to a browser whose image predates executors fails with 400 instead
- of running on the active tab; calls without 'executor' keep working on every
- image.
-
Args:
code: TypeScript/JavaScript code to execute. The code has access to 'page', 'context',
and 'browser' variables. It runs within a function, so you can use a return
diff --git a/src/kernel/types/browser_pool_acquire_response.py b/src/kernel/types/browser_pool_acquire_response.py
index 4cbda2d3..8791bb7b 100644
--- a/src/kernel/types/browser_pool_acquire_response.py
+++ b/src/kernel/types/browser_pool_acquire_response.py
@@ -5,6 +5,7 @@
from typing_extensions import Literal
from .tags import Tags
+from .._types import NoContentAsNone
from .profile import Profile
from .._models import BaseModel
from .browser_proxy import BrowserProxy
@@ -20,7 +21,7 @@
__all__ = ["BrowserPoolAcquireResponse"]
-class BrowserPoolAcquireResponse(BaseModel):
+class BrowserPoolAcquireResponse(NoContentAsNone, BaseModel):
cdp_ws_url: str
"""Websocket URL for Chrome DevTools Protocol connections to the browser session"""
diff --git a/tests/test_browser_pool_acquire_response.py b/tests/test_browser_pool_acquire_response.py
new file mode 100644
index 00000000..269838a6
--- /dev/null
+++ b/tests/test_browser_pool_acquire_response.py
@@ -0,0 +1,70 @@
+import httpx
+import pytest
+
+from kernel import Kernel, AsyncKernel, NotFoundError, APIResponseValidationError
+
+
+def test_acquire_returns_browser() -> None:
+ browser = {
+ "cdp_ws_url": "wss://example.test/cdp",
+ "created_at": "2026-10-09T00:00:00Z",
+ "headless": True,
+ "memory": "1GiB",
+ "region": "us-east",
+ "session_id": "session-1",
+ "stealth": False,
+ "webdriver_ws_url": "wss://example.test/webdriver",
+ }
+ with Kernel(
+ api_key="test",
+ http_client=httpx.Client(transport=httpx.MockTransport(lambda _request: httpx.Response(200, json=browser))),
+ ) as client:
+ result = client.browser_pools.acquire("pool")
+ assert result is not None
+ assert result.session_id == "session-1"
+
+
+def test_acquire_returns_none_on_poll_timeout() -> None:
+ calls = 0
+
+ def handle(_request: httpx.Request) -> httpx.Response:
+ nonlocal calls
+ calls += 1
+ return httpx.Response(204)
+
+ with Kernel(api_key="test", http_client=httpx.Client(transport=httpx.MockTransport(handle))) as client:
+ assert client.browser_pools.acquire("pool") is None
+ response = client.browser_pools.with_raw_response.acquire("pool")
+ assert response.status_code == 204
+ assert response.parse() is None
+ assert response.parse(to=str) == ""
+ assert response.parse(to=bytes) == b""
+ assert response.parse(to=httpx.Response) is response.http_response
+ assert calls == 2
+
+
+async def test_async_acquire_returns_none_on_poll_timeout() -> None:
+ async with AsyncKernel(
+ api_key="test",
+ http_client=httpx.AsyncClient(transport=httpx.MockTransport(lambda _request: httpx.Response(204))),
+ ) as client:
+ assert await client.browser_pools.acquire("pool") is None
+
+
+def test_acquire_still_raises_not_found() -> None:
+ with Kernel(
+ api_key="test",
+ http_client=httpx.Client(transport=httpx.MockTransport(lambda _request: httpx.Response(404, json={}))),
+ ) as client:
+ with pytest.raises(NotFoundError):
+ client.browser_pools.acquire("missing")
+
+
+def test_unexpected_204_still_fails_strict_validation() -> None:
+ with Kernel(
+ api_key="test",
+ _strict_response_validation=True,
+ http_client=httpx.Client(transport=httpx.MockTransport(lambda _request: httpx.Response(204))),
+ ) as client:
+ with pytest.raises(APIResponseValidationError):
+ client.browser_pools.retrieve("pool")
From a55231355a8b99c59de10f7e561bbb21c031cf26 Mon Sep 17 00:00:00 2001
From: "kernel-internal[bot]"
<260533166+kernel-internal[bot]@users.noreply.github.com>
Date: Tue, 13 Oct 2026 22:33:56 +0000
Subject: [PATCH 352/366] release: 0.124.0
---
.release-please-manifest.json | 2 +-
CHANGELOG.md | 13 +++++++++++++
pyproject.toml | 2 +-
src/kernel/_version.py | 2 +-
4 files changed, 16 insertions(+), 3 deletions(-)
diff --git a/.release-please-manifest.json b/.release-please-manifest.json
index 58ebf7f2..cadbcc6f 100644
--- a/.release-please-manifest.json
+++ b/.release-please-manifest.json
@@ -1,3 +1,3 @@
{
- ".": "0.123.0"
+ ".": "0.124.0"
}
\ No newline at end of file
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 7fdca44b..c786557b 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,18 @@
# Changelog
+## [0.124.0](https://github.com/kernel/kernel-python-sdk/compare/v0.123.0...v0.124.0) (2026-10-13)
+
+
+### Features
+
+* Accept intent creation without a fixed status ([e6e016a](https://github.com/kernel/kernel-python-sdk/commit/e6e016a5cea493c505b16a05cb8ad2fa6ed9f13b))
+* Complete Visa vault verification and transaction reporting ([fc4b0de](https://github.com/kernel/kernel-python-sdk/commit/fc4b0de79e3242361ef7b11acbd2cbaeb058265e))
+
+
+### Bug Fixes
+
+* seal browser pool acquire SDK results ([4d81af2](https://github.com/kernel/kernel-python-sdk/commit/4d81af2b6103571b59cd1bb520eb85e28488b51c))
+
## [0.123.0](https://github.com/kernel/kernel-python-sdk/compare/v0.122.0...v0.123.0) (2026-10-09)
diff --git a/pyproject.toml b/pyproject.toml
index 6e2be1a4..d86abd33 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -1,6 +1,6 @@
[project]
name = "kernel"
-version = "0.123.0"
+version = "0.124.0"
description = "The official Python library for the kernel API"
dynamic = ["readme"]
license = "Apache-2.0"
diff --git a/src/kernel/_version.py b/src/kernel/_version.py
index f0320775..e084d175 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.123.0" # x-release-please-version
+__version__ = "0.124.0" # x-release-please-version
From a77f648fb68c7b4e6f93826238e9ac21ab39a4dd Mon Sep 17 00:00:00 2001
From: "kernel-internal[bot]"
<260533166+kernel-internal[bot]@users.noreply.github.com>
Date: Tue, 13 Oct 2026 23:44:39 +0000
Subject: [PATCH 353/366] feat: chore(stlc): seal custom-code tracking files
Stainless-Generated-From: baf5ca9e7427c77e6f94cfaab42fcb4253e12a9b
---
src/kernel/resources/api_keys.py | 41 ++++++++++++++++----
src/kernel/types/api_key.py | 32 ++++++++++++++-
src/kernel/types/api_key_create_params.py | 47 +++++++++++++++++++++--
tests/api_resources/test_api_keys.py | 19 +++++++++
4 files changed, 126 insertions(+), 13 deletions(-)
diff --git a/src/kernel/resources/api_keys.py b/src/kernel/resources/api_keys.py
index 8cc1ed53..52e6e6f9 100644
--- a/src/kernel/resources/api_keys.py
+++ b/src/kernel/resources/api_keys.py
@@ -2,7 +2,8 @@
from __future__ import annotations
-from typing import Optional
+from typing import Union, Optional
+from datetime import datetime
from typing_extensions import Literal
import httpx
@@ -59,6 +60,8 @@ def create(
*,
name: str,
days_to_expire: Optional[int] | Omit = omit,
+ expires_at: Union[str, datetime, None] | Omit = omit,
+ policy: api_key_create_params.Policy | Omit = omit,
project_id: Optional[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.
@@ -67,13 +70,22 @@ def create(
extra_body: Body | None = None,
timeout: float | httpx.Timeout | None | NotGiven = not_given,
) -> CreatedAPIKey:
- """
- Create a new API key within the authenticated organization.
+ """Create a new API key.
+
+ API keys may be scoped by project and may include a
+ policy.
Args:
name: Label for the API key (1-255 characters). API keys are not addressable by name.
- days_to_expire: Number of days until expiry, up to 3650. Use null for never.
+ days_to_expire: Number of days until expiry, up to 3650. Use null for never. Cannot be combined
+ with expires_at.
+
+ expires_at: Exact expiry time. Must be in the future and no later than the issuing
+ credential's own expiry. Cannot be combined with days_to_expire.
+
+ policy: Issue a scoped key that may only perform the listed actions on the listed
+ resources.
project_id: Unique project identifier
@@ -91,6 +103,8 @@ def create(
{
"name": name,
"days_to_expire": days_to_expire,
+ "expires_at": expires_at,
+ "policy": policy,
"project_id": project_id,
},
api_key_create_params.APIKeyCreateParams,
@@ -377,6 +391,8 @@ async def create(
*,
name: str,
days_to_expire: Optional[int] | Omit = omit,
+ expires_at: Union[str, datetime, None] | Omit = omit,
+ policy: api_key_create_params.Policy | Omit = omit,
project_id: Optional[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.
@@ -385,13 +401,22 @@ async def create(
extra_body: Body | None = None,
timeout: float | httpx.Timeout | None | NotGiven = not_given,
) -> CreatedAPIKey:
- """
- Create a new API key within the authenticated organization.
+ """Create a new API key.
+
+ API keys may be scoped by project and may include a
+ policy.
Args:
name: Label for the API key (1-255 characters). API keys are not addressable by name.
- days_to_expire: Number of days until expiry, up to 3650. Use null for never.
+ days_to_expire: Number of days until expiry, up to 3650. Use null for never. Cannot be combined
+ with expires_at.
+
+ expires_at: Exact expiry time. Must be in the future and no later than the issuing
+ credential's own expiry. Cannot be combined with days_to_expire.
+
+ policy: Issue a scoped key that may only perform the listed actions on the listed
+ resources.
project_id: Unique project identifier
@@ -409,6 +434,8 @@ async def create(
{
"name": name,
"days_to_expire": days_to_expire,
+ "expires_at": expires_at,
+ "policy": policy,
"project_id": project_id,
},
api_key_create_params.APIKeyCreateParams,
diff --git a/src/kernel/types/api_key.py b/src/kernel/types/api_key.py
index b0d6eef2..5dd0b02c 100644
--- a/src/kernel/types/api_key.py
+++ b/src/kernel/types/api_key.py
@@ -1,11 +1,11 @@
# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
-from typing import Optional
+from typing import List, Optional
from datetime import datetime
from .._models import BaseModel
-__all__ = ["APIKey", "CreatedBy"]
+__all__ = ["APIKey", "CreatedBy", "Policy", "PolicyStatement"]
class CreatedBy(BaseModel):
@@ -19,6 +19,27 @@ class CreatedBy(BaseModel):
"""Display name of the creator, if available."""
+class PolicyStatement(BaseModel):
+ action: str
+ """Permitted action, such as browsers:use or vaults:read."""
+
+ resource: str
+ """Resource the action applies to, in the format projects/{project_id}/{kind}/{id}.
+
+ Wildcards are supported.
+ """
+
+
+class Policy(BaseModel):
+ """Permissions granted to a scoped API key.
+
+ Absent for an ordinary key, which keeps unrestricted access within its organization or project.
+ """
+
+ statements: List[PolicyStatement]
+ """Allow statements. Every action and resource not listed is denied."""
+
+
class APIKey(BaseModel):
id: str
"""Unique API key identifier"""
@@ -55,3 +76,10 @@ class APIKey(BaseModel):
Null means the key is org-wide or the project name is unavailable.
"""
+
+ policy: Optional[Policy] = None
+ """Permissions granted to a scoped API key.
+
+ Absent for an ordinary key, which keeps unrestricted access within its
+ organization or project.
+ """
diff --git a/src/kernel/types/api_key_create_params.py b/src/kernel/types/api_key_create_params.py
index 2909510e..86d0ccf3 100644
--- a/src/kernel/types/api_key_create_params.py
+++ b/src/kernel/types/api_key_create_params.py
@@ -2,10 +2,13 @@
from __future__ import annotations
-from typing import Optional
-from typing_extensions import Required, TypedDict
+from typing import Union, Iterable, Optional
+from datetime import datetime
+from typing_extensions import Required, Annotated, TypedDict
-__all__ = ["APIKeyCreateParams"]
+from .._utils import PropertyInfo
+
+__all__ = ["APIKeyCreateParams", "Policy", "PolicyStatement"]
class APIKeyCreateParams(TypedDict, total=False):
@@ -13,7 +16,43 @@ class APIKeyCreateParams(TypedDict, total=False):
"""Label for the API key (1-255 characters). API keys are not addressable by name."""
days_to_expire: Optional[int]
- """Number of days until expiry, up to 3650. Use null for never."""
+ """Number of days until expiry, up to 3650.
+
+ Use null for never. Cannot be combined with expires_at.
+ """
+
+ expires_at: Annotated[Union[str, datetime, None], PropertyInfo(format="iso8601")]
+ """Exact expiry time.
+
+ Must be in the future and no later than the issuing credential's own expiry.
+ Cannot be combined with days_to_expire.
+ """
+
+ policy: Policy
+ """
+ Issue a scoped key that may only perform the listed actions on the listed
+ resources.
+ """
project_id: Optional[str]
"""Unique project identifier"""
+
+
+class PolicyStatement(TypedDict, total=False):
+ action: Required[str]
+ """Permitted action, such as browsers:use or vaults:read."""
+
+ resource: Required[str]
+ """Resource the action applies to, in the format projects/{project_id}/{kind}/{id}.
+
+ Wildcards are supported.
+ """
+
+
+class Policy(TypedDict, total=False):
+ """
+ Issue a scoped key that may only perform the listed actions on the listed resources.
+ """
+
+ statements: Required[Iterable[PolicyStatement]]
+ """Allow statements. Every action and resource not listed is denied."""
diff --git a/tests/api_resources/test_api_keys.py b/tests/api_resources/test_api_keys.py
index 03d1ed8e..67005442 100644
--- a/tests/api_resources/test_api_keys.py
+++ b/tests/api_resources/test_api_keys.py
@@ -13,6 +13,7 @@
APIKey,
CreatedAPIKey,
)
+from kernel._utils import parse_datetime
from kernel.pagination import SyncOffsetPagination, AsyncOffsetPagination
base_url = os.environ.get("TEST_API_BASE_URL", "http://127.0.0.1:4010")
@@ -35,6 +36,15 @@ def test_method_create_with_all_params(self, client: Kernel) -> None:
api_key = client.api_keys.create(
name="staging",
days_to_expire=30,
+ expires_at=parse_datetime("2025-01-15T09:00:00Z"),
+ policy={
+ "statements": [
+ {
+ "action": "browsers:use",
+ "resource": "projects/proj_abc123/browsers/br_abc123",
+ }
+ ]
+ },
project_id="proj_abc123",
)
assert_matches_type(CreatedAPIKey, api_key, path=["response"])
@@ -319,6 +329,15 @@ async def test_method_create_with_all_params(self, async_client: AsyncKernel) ->
api_key = await async_client.api_keys.create(
name="staging",
days_to_expire=30,
+ expires_at=parse_datetime("2025-01-15T09:00:00Z"),
+ policy={
+ "statements": [
+ {
+ "action": "browsers:use",
+ "resource": "projects/proj_abc123/browsers/br_abc123",
+ }
+ ]
+ },
project_id="proj_abc123",
)
assert_matches_type(CreatedAPIKey, api_key, path=["response"])
From 11d911df22a924a778f424e77da7f209ece5d73d Mon Sep 17 00:00:00 2001
From: "kernel-internal[bot]"
<260533166+kernel-internal[bot]@users.noreply.github.com>
Date: Tue, 13 Oct 2026 23:49:34 +0000
Subject: [PATCH 354/366] release: 0.125.0
---
.release-please-manifest.json | 2 +-
CHANGELOG.md | 7 +++++++
pyproject.toml | 2 +-
src/kernel/_version.py | 2 +-
4 files changed, 10 insertions(+), 3 deletions(-)
diff --git a/.release-please-manifest.json b/.release-please-manifest.json
index cadbcc6f..f3343e21 100644
--- a/.release-please-manifest.json
+++ b/.release-please-manifest.json
@@ -1,3 +1,3 @@
{
- ".": "0.124.0"
+ ".": "0.125.0"
}
\ No newline at end of file
diff --git a/CHANGELOG.md b/CHANGELOG.md
index c786557b..fa87bc60 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,12 @@
# Changelog
+## [0.125.0](https://github.com/kernel/kernel-python-sdk/compare/v0.124.0...v0.125.0) (2026-10-13)
+
+
+### Features
+
+* chore(stlc): seal custom-code tracking files ([a77f648](https://github.com/kernel/kernel-python-sdk/commit/a77f648fb68c7b4e6f93826238e9ac21ab39a4dd))
+
## [0.124.0](https://github.com/kernel/kernel-python-sdk/compare/v0.123.0...v0.124.0) (2026-10-13)
diff --git a/pyproject.toml b/pyproject.toml
index d86abd33..a9a2b552 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -1,6 +1,6 @@
[project]
name = "kernel"
-version = "0.124.0"
+version = "0.125.0"
description = "The official Python library for the kernel API"
dynamic = ["readme"]
license = "Apache-2.0"
diff --git a/src/kernel/_version.py b/src/kernel/_version.py
index e084d175..7bf40e5e 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.124.0" # x-release-please-version
+__version__ = "0.125.0" # x-release-please-version
From f3a8871bc5120bbc097c079d6a1e73ccaf74dba8 Mon Sep 17 00:00:00 2001
From: "kernel-internal[bot]"
<260533166+kernel-internal[bot]@users.noreply.github.com>
Date: Wed, 14 Oct 2026 01:11:09 +0000
Subject: [PATCH 355/366] Build SDK
Stainless-Generated-From: 68dfe05dbb40ad22ef2da417f4362c26c8de242e
---
src/kernel/resources/vaults/items.py | 326 +++++++++++-------
...rize_vault_item_operation_request_param.py | 11 +-
.../types/vaults/card_vault_item_spec.py | 11 +-
.../vaults/card_vault_item_spec_param.py | 11 +-
...fill_vault_item_operation_request_param.py | 59 ++--
.../fill_vault_item_operation_result.py | 15 +-
.../vaults/item_perform_operation_params.py | 29 +-
src/kernel/types/vaults/item_update_params.py | 9 +-
src/kernel/types/vaults/item_upsert_params.py | 9 +-
src/kernel/types/vaults/vault_item.py | 12 +-
.../vaults/vault_item_operation_response.py | 12 +-
tests/api_resources/vaults/test_items.py | 208 ++---------
12 files changed, 349 insertions(+), 363 deletions(-)
diff --git a/src/kernel/resources/vaults/items.py b/src/kernel/resources/vaults/items.py
index 79ca2ced..0cdc5164 100644
--- a/src/kernel/resources/vaults/items.py
+++ b/src/kernel/resources/vaults/items.py
@@ -155,10 +155,17 @@ def update(
remain unchanged and explicit empty lists clear them. Wallet/provider binding
and unsupported fields cannot change after authorization starts. An uncertain
update enters recovery_required and must not be retried. Checkout cards may be
- edited between authorizations.
+ edited between authorizations. A Link card authorized with browser_id and
+ page_url cannot be changed; delete it and create a new card with the updated
+ details.
Args:
- spec: Live payment card. Test-mode card creation is not supported.
+ spec: Live Link card request. Test-mode card creation is not supported. The spec can
+ be changed with PATCH until the card is authorized at a checkout. Authorize it
+ with the browser at final checkout: Kernel inspects the checkout through WebMCP
+ and internally chooses a merchant-bound Link Pay Token when supported or a
+ virtual card otherwise. Callers never receive or choose how the credential is
+ delivered.
extra_headers: Send extra headers
@@ -199,7 +206,9 @@ def update(
remain unchanged and explicit empty lists clear them. Wallet/provider binding
and unsupported fields cannot change after authorization starts. An uncertain
update enters recovery_required and must not be retried. Checkout cards may be
- edited between authorizations.
+ edited between authorizations. A Link card authorized with browser_id and
+ page_url cannot be changed; delete it and create a new card with the updated
+ details.
Args:
version: Expected current item version from the latest read.
@@ -400,6 +409,8 @@ def perform_operation(
*,
id_or_name: str,
type: Literal["authorize"],
+ browser_id: str | Omit = omit,
+ page_url: 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,
@@ -412,10 +423,11 @@ def perform_operation(
`available_operations`, following its natural-language description. Availability
is rechecked at execution time; unavailable operations return 409. Authorization
and preparation may call an external provider and return updated state. Link
- cards advertise authorize without checkout context. Eligible unused AgentCard
- cards advertise prepare_checkout, which requires checkout context and obtains
- device approval before native Square Pay. Keep the returned approval page open,
- poll until ready_to_submit, then submit before preparation.expires_at. Unused
+ cards advertise authorize; pass browser_id and page_url from the final checkout
+ page so Kernel can inspect the checkout. Eligible unused AgentCard cards
+ advertise prepare_checkout, which requires checkout context and obtains device
+ approval before native Square Pay. Keep the returned approval page open, poll
+ until ready_to_submit, then submit before preparation.expires_at. Unused
preparations expire automatically and cannot be reused. If spend-request
creation is rejected with a non-retryable provider error, the card item is
deleted and the provider's error code and message are returned. Rate limits
@@ -430,6 +442,11 @@ def perform_operation(
a failure, an `unknown` outcome, or a transport error.
Args:
+ browser_id: Browser session ID, not a reusable browser name. The browser must have this
+ vault attached.
+
+ page_url: Exact current top-level URL of the final checkout page open in browser_id.
+
extra_headers: Send extra headers
extra_query: Add additional query parameters to the request
@@ -459,10 +476,11 @@ def perform_operation(
`available_operations`, following its natural-language description. Availability
is rechecked at execution time; unavailable operations return 409. Authorization
and preparation may call an external provider and return updated state. Link
- cards advertise authorize without checkout context. Eligible unused AgentCard
- cards advertise prepare_checkout, which requires checkout context and obtains
- device approval before native Square Pay. Keep the returned approval page open,
- poll until ready_to_submit, then submit before preparation.expires_at. Unused
+ cards advertise authorize; pass browser_id and page_url from the final checkout
+ page so Kernel can inspect the checkout. Eligible unused AgentCard cards
+ advertise prepare_checkout, which requires checkout context and obtains device
+ approval before native Square Pay. Keep the returned approval page open, poll
+ until ready_to_submit, then submit before preparation.expires_at. Unused
preparations expire automatically and cannot be reused. If spend-request
creation is rejected with a non-retryable provider error, the card item is
deleted and the provider's error code and message are returned. Rate limits
@@ -513,10 +531,11 @@ def perform_operation(
`available_operations`, following its natural-language description. Availability
is rechecked at execution time; unavailable operations return 409. Authorization
and preparation may call an external provider and return updated state. Link
- cards advertise authorize without checkout context. Eligible unused AgentCard
- cards advertise prepare_checkout, which requires checkout context and obtains
- device approval before native Square Pay. Keep the returned approval page open,
- poll until ready_to_submit, then submit before preparation.expires_at. Unused
+ cards advertise authorize; pass browser_id and page_url from the final checkout
+ page so Kernel can inspect the checkout. Eligible unused AgentCard cards
+ advertise prepare_checkout, which requires checkout context and obtains device
+ approval before native Square Pay. Keep the returned approval page open, poll
+ until ready_to_submit, then submit before preparation.expires_at. Unused
preparations expire automatically and cannot be reused. If spend-request
creation is rejected with a non-retryable provider error, the card item is
deleted and the provider's error code and message are returned. Rate limits
@@ -566,10 +585,11 @@ def perform_operation(
`available_operations`, following its natural-language description. Availability
is rechecked at execution time; unavailable operations return 409. Authorization
and preparation may call an external provider and return updated state. Link
- cards advertise authorize without checkout context. Eligible unused AgentCard
- cards advertise prepare_checkout, which requires checkout context and obtains
- device approval before native Square Pay. Keep the returned approval page open,
- poll until ready_to_submit, then submit before preparation.expires_at. Unused
+ cards advertise authorize; pass browser_id and page_url from the final checkout
+ page so Kernel can inspect the checkout. Eligible unused AgentCard cards
+ advertise prepare_checkout, which requires checkout context and obtains device
+ approval before native Square Pay. Keep the returned approval page open, poll
+ until ready_to_submit, then submit before preparation.expires_at. Unused
preparations expire automatically and cannot be reused. If spend-request
creation is rejected with a non-retryable provider error, the card item is
deleted and the provider's error code and message are returned. Rate limits
@@ -614,10 +634,11 @@ def perform_operation(
`available_operations`, following its natural-language description. Availability
is rechecked at execution time; unavailable operations return 409. Authorization
and preparation may call an external provider and return updated state. Link
- cards advertise authorize without checkout context. Eligible unused AgentCard
- cards advertise prepare_checkout, which requires checkout context and obtains
- device approval before native Square Pay. Keep the returned approval page open,
- poll until ready_to_submit, then submit before preparation.expires_at. Unused
+ cards advertise authorize; pass browser_id and page_url from the final checkout
+ page so Kernel can inspect the checkout. Eligible unused AgentCard cards
+ advertise prepare_checkout, which requires checkout context and obtains device
+ approval before native Square Pay. Keep the returned approval page open, poll
+ until ready_to_submit, then submit before preparation.expires_at. Unused
preparations expire automatically and cannot be reused. If spend-request
creation is rejected with a non-retryable provider error, the card item is
deleted and the provider's error code and message are returned. Rate limits
@@ -655,8 +676,8 @@ def perform_operation(
*,
id_or_name: str,
browser_id: str,
- fields: Iterable[VaultFillFieldParam],
type: Literal["fill"],
+ fields: Iterable[VaultFillFieldParam] | Omit = omit,
page_url: str | Omit = omit,
timeout_ms: int | Omit = omit,
# Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
@@ -671,10 +692,11 @@ def perform_operation(
`available_operations`, following its natural-language description. Availability
is rechecked at execution time; unavailable operations return 409. Authorization
and preparation may call an external provider and return updated state. Link
- cards advertise authorize without checkout context. Eligible unused AgentCard
- cards advertise prepare_checkout, which requires checkout context and obtains
- device approval before native Square Pay. Keep the returned approval page open,
- poll until ready_to_submit, then submit before preparation.expires_at. Unused
+ cards advertise authorize; pass browser_id and page_url from the final checkout
+ page so Kernel can inspect the checkout. Eligible unused AgentCard cards
+ advertise prepare_checkout, which requires checkout context and obtains device
+ approval before native Square Pay. Keep the returned approval page open, poll
+ until ready_to_submit, then submit before preparation.expires_at. Unused
preparations expire automatically and cannot be reused. If spend-request
creation is rejected with a non-retryable provider error, the card item is
deleted and the provider's error code and message are returned. Rate limits
@@ -691,13 +713,19 @@ def perform_operation(
Args:
browser_id: Browser session ID, not a reusable browser name.
- fields: Field bindings for this step. No two bindings may resolve to the same element.
+ fields: Field bindings, when the item's advertised fill operation requires them. Each
+ selector is searched in the main frame and all descendant frames and must
+ resolve to exactly one editable input or select, matched directly or through a
+ containing element; zero or multiple candidates fail, and no two bindings may
+ resolve to the same element. Select elements match an option by value, not
+ label. All bindings are validated before any write, then filled in request
+ order, stopping at the first failure. If the page navigates or a target
+ disappears, fill stops rather than choosing another page or element.
page_url: Exact current top-level page URL, including path, query, and fragment. Must
match exactly one open page in the browser; zero or multiple matches fail. No
- prefix or glob matching. Required for cards, which must use HTTPS without
- embedded credentials. Optional for credentials, where omission requires exactly
- one open page.
+ prefix or glob matching. When an item allows omitting it, the browser must have
+ exactly one open page.
timeout_ms: Total operation deadline in milliseconds, not a per-field timeout.
@@ -733,10 +761,11 @@ def perform_operation(
`available_operations`, following its natural-language description. Availability
is rechecked at execution time; unavailable operations return 409. Authorization
and preparation may call an external provider and return updated state. Link
- cards advertise authorize without checkout context. Eligible unused AgentCard
- cards advertise prepare_checkout, which requires checkout context and obtains
- device approval before native Square Pay. Keep the returned approval page open,
- poll until ready_to_submit, then submit before preparation.expires_at. Unused
+ cards advertise authorize; pass browser_id and page_url from the final checkout
+ page so Kernel can inspect the checkout. Eligible unused AgentCard cards
+ advertise prepare_checkout, which requires checkout context and obtains device
+ approval before native Square Pay. Keep the returned approval page open, poll
+ until ready_to_submit, then submit before preparation.expires_at. Unused
preparations expire automatically and cannot be reused. If spend-request
creation is rejected with a non-retryable provider error, the card item is
deleted and the provider's error code and message are returned. Rate limits
@@ -781,10 +810,11 @@ def perform_operation(
`available_operations`, following its natural-language description. Availability
is rechecked at execution time; unavailable operations return 409. Authorization
and preparation may call an external provider and return updated state. Link
- cards advertise authorize without checkout context. Eligible unused AgentCard
- cards advertise prepare_checkout, which requires checkout context and obtains
- device approval before native Square Pay. Keep the returned approval page open,
- poll until ready_to_submit, then submit before preparation.expires_at. Unused
+ cards advertise authorize; pass browser_id and page_url from the final checkout
+ page so Kernel can inspect the checkout. Eligible unused AgentCard cards
+ advertise prepare_checkout, which requires checkout context and obtains device
+ approval before native Square Pay. Keep the returned approval page open, poll
+ until ready_to_submit, then submit before preparation.expires_at. Unused
preparations expire automatically and cannot be reused. If spend-request
creation is rejected with a non-retryable provider error, the card item is
deleted and the provider's error code and message are returned. Rate limits
@@ -832,10 +862,11 @@ def perform_operation(
`available_operations`, following its natural-language description. Availability
is rechecked at execution time; unavailable operations return 409. Authorization
and preparation may call an external provider and return updated state. Link
- cards advertise authorize without checkout context. Eligible unused AgentCard
- cards advertise prepare_checkout, which requires checkout context and obtains
- device approval before native Square Pay. Keep the returned approval page open,
- poll until ready_to_submit, then submit before preparation.expires_at. Unused
+ cards advertise authorize; pass browser_id and page_url from the final checkout
+ page so Kernel can inspect the checkout. Eligible unused AgentCard cards
+ advertise prepare_checkout, which requires checkout context and obtains device
+ approval before native Square Pay. Keep the returned approval page open, poll
+ until ready_to_submit, then submit before preparation.expires_at. Unused
preparations expire automatically and cannot be reused. If spend-request
creation is rejected with a non-retryable provider error, the card item is
deleted and the provider's error code and message are returned. Rate limits
@@ -887,10 +918,11 @@ def perform_operation(
`available_operations`, following its natural-language description. Availability
is rechecked at execution time; unavailable operations return 409. Authorization
and preparation may call an external provider and return updated state. Link
- cards advertise authorize without checkout context. Eligible unused AgentCard
- cards advertise prepare_checkout, which requires checkout context and obtains
- device approval before native Square Pay. Keep the returned approval page open,
- poll until ready_to_submit, then submit before preparation.expires_at. Unused
+ cards advertise authorize; pass browser_id and page_url from the final checkout
+ page so Kernel can inspect the checkout. Eligible unused AgentCard cards
+ advertise prepare_checkout, which requires checkout context and obtains device
+ approval before native Square Pay. Keep the returned approval page open, poll
+ until ready_to_submit, then submit before preparation.expires_at. Unused
preparations expire automatically and cannot be reused. If spend-request
creation is rejected with a non-retryable provider error, the card item is
deleted and the provider's error code and message are returned. Rate limits
@@ -936,10 +968,11 @@ def perform_operation(
`available_operations`, following its natural-language description. Availability
is rechecked at execution time; unavailable operations return 409. Authorization
and preparation may call an external provider and return updated state. Link
- cards advertise authorize without checkout context. Eligible unused AgentCard
- cards advertise prepare_checkout, which requires checkout context and obtains
- device approval before native Square Pay. Keep the returned approval page open,
- poll until ready_to_submit, then submit before preparation.expires_at. Unused
+ cards advertise authorize; pass browser_id and page_url from the final checkout
+ page so Kernel can inspect the checkout. Eligible unused AgentCard cards
+ advertise prepare_checkout, which requires checkout context and obtains device
+ approval before native Square Pay. Keep the returned approval page open, poll
+ until ready_to_submit, then submit before preparation.expires_at. Unused
preparations expire automatically and cannot be reused. If spend-request
creation is rejected with a non-retryable provider error, the card item is
deleted and the provider's error code and message are returned. Rate limits
@@ -991,10 +1024,11 @@ def perform_operation(
`available_operations`, following its natural-language description. Availability
is rechecked at execution time; unavailable operations return 409. Authorization
and preparation may call an external provider and return updated state. Link
- cards advertise authorize without checkout context. Eligible unused AgentCard
- cards advertise prepare_checkout, which requires checkout context and obtains
- device approval before native Square Pay. Keep the returned approval page open,
- poll until ready_to_submit, then submit before preparation.expires_at. Unused
+ cards advertise authorize; pass browser_id and page_url from the final checkout
+ page so Kernel can inspect the checkout. Eligible unused AgentCard cards
+ advertise prepare_checkout, which requires checkout context and obtains device
+ approval before native Square Pay. Keep the returned approval page open, poll
+ until ready_to_submit, then submit before preparation.expires_at. Unused
preparations expire automatically and cannot be reused. If spend-request
creation is rejected with a non-retryable provider error, the card item is
deleted and the provider's error code and message are returned. Rate limits
@@ -1037,7 +1071,7 @@ def perform_operation(
["id_or_name", "type"],
["id_or_name", "amount", "currency", "occurred_at", "status", "transaction_type", "type"],
["id_or_name", "checkout", "type"],
- ["id_or_name", "browser_id", "fields", "type"],
+ ["id_or_name", "browser_id", "type"],
["id_or_name", "browser_id", "page_url", "type"],
["id_or_name", "access_token", "type"],
["id_or_name", "bindings", "browser_id", "input", "page_url", "tool_ref", "type"],
@@ -1059,6 +1093,8 @@ def perform_operation(
| Literal["1pw_recover"]
| Literal["1pw_update_access_token"]
| Literal["webmcp_invoke"],
+ browser_id: str | Omit = omit,
+ page_url: str | Omit = omit,
amount: int | Omit = omit,
currency: str | Omit = omit,
occurred_at: Union[str, datetime] | Omit = omit,
@@ -1068,9 +1104,7 @@ def perform_operation(
]
| Omit = omit,
checkout: VaultCheckoutContextParam | Omit = omit,
- browser_id: str | Omit = omit,
fields: Iterable[VaultFillFieldParam] | Omit = omit,
- page_url: str | Omit = omit,
timeout_ms: int | Omit = omit,
goal: str | Omit = omit,
keywords: SequenceNotStr[str] | Omit = omit,
@@ -1101,15 +1135,15 @@ def perform_operation(
body=maybe_transform(
{
"type": type,
+ "browser_id": browser_id,
+ "page_url": page_url,
"amount": amount,
"currency": currency,
"occurred_at": occurred_at,
"status": status,
"transaction_type": transaction_type,
"checkout": checkout,
- "browser_id": browser_id,
"fields": fields,
- "page_url": page_url,
"timeout_ms": timeout_ms,
"goal": goal,
"keywords": keywords,
@@ -1205,7 +1239,12 @@ def upsert(
checkout instead.
Args:
- spec: Live payment card. Test-mode card creation is not supported.
+ spec: Live Link card request. Test-mode card creation is not supported. The spec can
+ be changed with PATCH until the card is authorized at a checkout. Authorize it
+ with the browser at final checkout: Kernel inspects the checkout through WebMCP
+ and internally chooses a merchant-bound Link Pay Token when supported or a
+ virtual card otherwise. Callers never receive or choose how the credential is
+ delivered.
extra_headers: Send extra headers
@@ -1452,10 +1491,17 @@ async def update(
remain unchanged and explicit empty lists clear them. Wallet/provider binding
and unsupported fields cannot change after authorization starts. An uncertain
update enters recovery_required and must not be retried. Checkout cards may be
- edited between authorizations.
+ edited between authorizations. A Link card authorized with browser_id and
+ page_url cannot be changed; delete it and create a new card with the updated
+ details.
Args:
- spec: Live payment card. Test-mode card creation is not supported.
+ spec: Live Link card request. Test-mode card creation is not supported. The spec can
+ be changed with PATCH until the card is authorized at a checkout. Authorize it
+ with the browser at final checkout: Kernel inspects the checkout through WebMCP
+ and internally chooses a merchant-bound Link Pay Token when supported or a
+ virtual card otherwise. Callers never receive or choose how the credential is
+ delivered.
extra_headers: Send extra headers
@@ -1496,7 +1542,9 @@ async def update(
remain unchanged and explicit empty lists clear them. Wallet/provider binding
and unsupported fields cannot change after authorization starts. An uncertain
update enters recovery_required and must not be retried. Checkout cards may be
- edited between authorizations.
+ edited between authorizations. A Link card authorized with browser_id and
+ page_url cannot be changed; delete it and create a new card with the updated
+ details.
Args:
version: Expected current item version from the latest read.
@@ -1697,6 +1745,8 @@ async def perform_operation(
*,
id_or_name: str,
type: Literal["authorize"],
+ browser_id: str | Omit = omit,
+ page_url: 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,
@@ -1709,10 +1759,11 @@ async def perform_operation(
`available_operations`, following its natural-language description. Availability
is rechecked at execution time; unavailable operations return 409. Authorization
and preparation may call an external provider and return updated state. Link
- cards advertise authorize without checkout context. Eligible unused AgentCard
- cards advertise prepare_checkout, which requires checkout context and obtains
- device approval before native Square Pay. Keep the returned approval page open,
- poll until ready_to_submit, then submit before preparation.expires_at. Unused
+ cards advertise authorize; pass browser_id and page_url from the final checkout
+ page so Kernel can inspect the checkout. Eligible unused AgentCard cards
+ advertise prepare_checkout, which requires checkout context and obtains device
+ approval before native Square Pay. Keep the returned approval page open, poll
+ until ready_to_submit, then submit before preparation.expires_at. Unused
preparations expire automatically and cannot be reused. If spend-request
creation is rejected with a non-retryable provider error, the card item is
deleted and the provider's error code and message are returned. Rate limits
@@ -1727,6 +1778,11 @@ async def perform_operation(
a failure, an `unknown` outcome, or a transport error.
Args:
+ browser_id: Browser session ID, not a reusable browser name. The browser must have this
+ vault attached.
+
+ page_url: Exact current top-level URL of the final checkout page open in browser_id.
+
extra_headers: Send extra headers
extra_query: Add additional query parameters to the request
@@ -1756,10 +1812,11 @@ async def perform_operation(
`available_operations`, following its natural-language description. Availability
is rechecked at execution time; unavailable operations return 409. Authorization
and preparation may call an external provider and return updated state. Link
- cards advertise authorize without checkout context. Eligible unused AgentCard
- cards advertise prepare_checkout, which requires checkout context and obtains
- device approval before native Square Pay. Keep the returned approval page open,
- poll until ready_to_submit, then submit before preparation.expires_at. Unused
+ cards advertise authorize; pass browser_id and page_url from the final checkout
+ page so Kernel can inspect the checkout. Eligible unused AgentCard cards
+ advertise prepare_checkout, which requires checkout context and obtains device
+ approval before native Square Pay. Keep the returned approval page open, poll
+ until ready_to_submit, then submit before preparation.expires_at. Unused
preparations expire automatically and cannot be reused. If spend-request
creation is rejected with a non-retryable provider error, the card item is
deleted and the provider's error code and message are returned. Rate limits
@@ -1810,10 +1867,11 @@ async def perform_operation(
`available_operations`, following its natural-language description. Availability
is rechecked at execution time; unavailable operations return 409. Authorization
and preparation may call an external provider and return updated state. Link
- cards advertise authorize without checkout context. Eligible unused AgentCard
- cards advertise prepare_checkout, which requires checkout context and obtains
- device approval before native Square Pay. Keep the returned approval page open,
- poll until ready_to_submit, then submit before preparation.expires_at. Unused
+ cards advertise authorize; pass browser_id and page_url from the final checkout
+ page so Kernel can inspect the checkout. Eligible unused AgentCard cards
+ advertise prepare_checkout, which requires checkout context and obtains device
+ approval before native Square Pay. Keep the returned approval page open, poll
+ until ready_to_submit, then submit before preparation.expires_at. Unused
preparations expire automatically and cannot be reused. If spend-request
creation is rejected with a non-retryable provider error, the card item is
deleted and the provider's error code and message are returned. Rate limits
@@ -1863,10 +1921,11 @@ async def perform_operation(
`available_operations`, following its natural-language description. Availability
is rechecked at execution time; unavailable operations return 409. Authorization
and preparation may call an external provider and return updated state. Link
- cards advertise authorize without checkout context. Eligible unused AgentCard
- cards advertise prepare_checkout, which requires checkout context and obtains
- device approval before native Square Pay. Keep the returned approval page open,
- poll until ready_to_submit, then submit before preparation.expires_at. Unused
+ cards advertise authorize; pass browser_id and page_url from the final checkout
+ page so Kernel can inspect the checkout. Eligible unused AgentCard cards
+ advertise prepare_checkout, which requires checkout context and obtains device
+ approval before native Square Pay. Keep the returned approval page open, poll
+ until ready_to_submit, then submit before preparation.expires_at. Unused
preparations expire automatically and cannot be reused. If spend-request
creation is rejected with a non-retryable provider error, the card item is
deleted and the provider's error code and message are returned. Rate limits
@@ -1911,10 +1970,11 @@ async def perform_operation(
`available_operations`, following its natural-language description. Availability
is rechecked at execution time; unavailable operations return 409. Authorization
and preparation may call an external provider and return updated state. Link
- cards advertise authorize without checkout context. Eligible unused AgentCard
- cards advertise prepare_checkout, which requires checkout context and obtains
- device approval before native Square Pay. Keep the returned approval page open,
- poll until ready_to_submit, then submit before preparation.expires_at. Unused
+ cards advertise authorize; pass browser_id and page_url from the final checkout
+ page so Kernel can inspect the checkout. Eligible unused AgentCard cards
+ advertise prepare_checkout, which requires checkout context and obtains device
+ approval before native Square Pay. Keep the returned approval page open, poll
+ until ready_to_submit, then submit before preparation.expires_at. Unused
preparations expire automatically and cannot be reused. If spend-request
creation is rejected with a non-retryable provider error, the card item is
deleted and the provider's error code and message are returned. Rate limits
@@ -1952,8 +2012,8 @@ async def perform_operation(
*,
id_or_name: str,
browser_id: str,
- fields: Iterable[VaultFillFieldParam],
type: Literal["fill"],
+ fields: Iterable[VaultFillFieldParam] | Omit = omit,
page_url: str | Omit = omit,
timeout_ms: int | Omit = omit,
# Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
@@ -1968,10 +2028,11 @@ async def perform_operation(
`available_operations`, following its natural-language description. Availability
is rechecked at execution time; unavailable operations return 409. Authorization
and preparation may call an external provider and return updated state. Link
- cards advertise authorize without checkout context. Eligible unused AgentCard
- cards advertise prepare_checkout, which requires checkout context and obtains
- device approval before native Square Pay. Keep the returned approval page open,
- poll until ready_to_submit, then submit before preparation.expires_at. Unused
+ cards advertise authorize; pass browser_id and page_url from the final checkout
+ page so Kernel can inspect the checkout. Eligible unused AgentCard cards
+ advertise prepare_checkout, which requires checkout context and obtains device
+ approval before native Square Pay. Keep the returned approval page open, poll
+ until ready_to_submit, then submit before preparation.expires_at. Unused
preparations expire automatically and cannot be reused. If spend-request
creation is rejected with a non-retryable provider error, the card item is
deleted and the provider's error code and message are returned. Rate limits
@@ -1988,13 +2049,19 @@ async def perform_operation(
Args:
browser_id: Browser session ID, not a reusable browser name.
- fields: Field bindings for this step. No two bindings may resolve to the same element.
+ fields: Field bindings, when the item's advertised fill operation requires them. Each
+ selector is searched in the main frame and all descendant frames and must
+ resolve to exactly one editable input or select, matched directly or through a
+ containing element; zero or multiple candidates fail, and no two bindings may
+ resolve to the same element. Select elements match an option by value, not
+ label. All bindings are validated before any write, then filled in request
+ order, stopping at the first failure. If the page navigates or a target
+ disappears, fill stops rather than choosing another page or element.
page_url: Exact current top-level page URL, including path, query, and fragment. Must
match exactly one open page in the browser; zero or multiple matches fail. No
- prefix or glob matching. Required for cards, which must use HTTPS without
- embedded credentials. Optional for credentials, where omission requires exactly
- one open page.
+ prefix or glob matching. When an item allows omitting it, the browser must have
+ exactly one open page.
timeout_ms: Total operation deadline in milliseconds, not a per-field timeout.
@@ -2030,10 +2097,11 @@ async def perform_operation(
`available_operations`, following its natural-language description. Availability
is rechecked at execution time; unavailable operations return 409. Authorization
and preparation may call an external provider and return updated state. Link
- cards advertise authorize without checkout context. Eligible unused AgentCard
- cards advertise prepare_checkout, which requires checkout context and obtains
- device approval before native Square Pay. Keep the returned approval page open,
- poll until ready_to_submit, then submit before preparation.expires_at. Unused
+ cards advertise authorize; pass browser_id and page_url from the final checkout
+ page so Kernel can inspect the checkout. Eligible unused AgentCard cards
+ advertise prepare_checkout, which requires checkout context and obtains device
+ approval before native Square Pay. Keep the returned approval page open, poll
+ until ready_to_submit, then submit before preparation.expires_at. Unused
preparations expire automatically and cannot be reused. If spend-request
creation is rejected with a non-retryable provider error, the card item is
deleted and the provider's error code and message are returned. Rate limits
@@ -2078,10 +2146,11 @@ async def perform_operation(
`available_operations`, following its natural-language description. Availability
is rechecked at execution time; unavailable operations return 409. Authorization
and preparation may call an external provider and return updated state. Link
- cards advertise authorize without checkout context. Eligible unused AgentCard
- cards advertise prepare_checkout, which requires checkout context and obtains
- device approval before native Square Pay. Keep the returned approval page open,
- poll until ready_to_submit, then submit before preparation.expires_at. Unused
+ cards advertise authorize; pass browser_id and page_url from the final checkout
+ page so Kernel can inspect the checkout. Eligible unused AgentCard cards
+ advertise prepare_checkout, which requires checkout context and obtains device
+ approval before native Square Pay. Keep the returned approval page open, poll
+ until ready_to_submit, then submit before preparation.expires_at. Unused
preparations expire automatically and cannot be reused. If spend-request
creation is rejected with a non-retryable provider error, the card item is
deleted and the provider's error code and message are returned. Rate limits
@@ -2129,10 +2198,11 @@ async def perform_operation(
`available_operations`, following its natural-language description. Availability
is rechecked at execution time; unavailable operations return 409. Authorization
and preparation may call an external provider and return updated state. Link
- cards advertise authorize without checkout context. Eligible unused AgentCard
- cards advertise prepare_checkout, which requires checkout context and obtains
- device approval before native Square Pay. Keep the returned approval page open,
- poll until ready_to_submit, then submit before preparation.expires_at. Unused
+ cards advertise authorize; pass browser_id and page_url from the final checkout
+ page so Kernel can inspect the checkout. Eligible unused AgentCard cards
+ advertise prepare_checkout, which requires checkout context and obtains device
+ approval before native Square Pay. Keep the returned approval page open, poll
+ until ready_to_submit, then submit before preparation.expires_at. Unused
preparations expire automatically and cannot be reused. If spend-request
creation is rejected with a non-retryable provider error, the card item is
deleted and the provider's error code and message are returned. Rate limits
@@ -2184,10 +2254,11 @@ async def perform_operation(
`available_operations`, following its natural-language description. Availability
is rechecked at execution time; unavailable operations return 409. Authorization
and preparation may call an external provider and return updated state. Link
- cards advertise authorize without checkout context. Eligible unused AgentCard
- cards advertise prepare_checkout, which requires checkout context and obtains
- device approval before native Square Pay. Keep the returned approval page open,
- poll until ready_to_submit, then submit before preparation.expires_at. Unused
+ cards advertise authorize; pass browser_id and page_url from the final checkout
+ page so Kernel can inspect the checkout. Eligible unused AgentCard cards
+ advertise prepare_checkout, which requires checkout context and obtains device
+ approval before native Square Pay. Keep the returned approval page open, poll
+ until ready_to_submit, then submit before preparation.expires_at. Unused
preparations expire automatically and cannot be reused. If spend-request
creation is rejected with a non-retryable provider error, the card item is
deleted and the provider's error code and message are returned. Rate limits
@@ -2233,10 +2304,11 @@ async def perform_operation(
`available_operations`, following its natural-language description. Availability
is rechecked at execution time; unavailable operations return 409. Authorization
and preparation may call an external provider and return updated state. Link
- cards advertise authorize without checkout context. Eligible unused AgentCard
- cards advertise prepare_checkout, which requires checkout context and obtains
- device approval before native Square Pay. Keep the returned approval page open,
- poll until ready_to_submit, then submit before preparation.expires_at. Unused
+ cards advertise authorize; pass browser_id and page_url from the final checkout
+ page so Kernel can inspect the checkout. Eligible unused AgentCard cards
+ advertise prepare_checkout, which requires checkout context and obtains device
+ approval before native Square Pay. Keep the returned approval page open, poll
+ until ready_to_submit, then submit before preparation.expires_at. Unused
preparations expire automatically and cannot be reused. If spend-request
creation is rejected with a non-retryable provider error, the card item is
deleted and the provider's error code and message are returned. Rate limits
@@ -2288,10 +2360,11 @@ async def perform_operation(
`available_operations`, following its natural-language description. Availability
is rechecked at execution time; unavailable operations return 409. Authorization
and preparation may call an external provider and return updated state. Link
- cards advertise authorize without checkout context. Eligible unused AgentCard
- cards advertise prepare_checkout, which requires checkout context and obtains
- device approval before native Square Pay. Keep the returned approval page open,
- poll until ready_to_submit, then submit before preparation.expires_at. Unused
+ cards advertise authorize; pass browser_id and page_url from the final checkout
+ page so Kernel can inspect the checkout. Eligible unused AgentCard cards
+ advertise prepare_checkout, which requires checkout context and obtains device
+ approval before native Square Pay. Keep the returned approval page open, poll
+ until ready_to_submit, then submit before preparation.expires_at. Unused
preparations expire automatically and cannot be reused. If spend-request
creation is rejected with a non-retryable provider error, the card item is
deleted and the provider's error code and message are returned. Rate limits
@@ -2334,7 +2407,7 @@ async def perform_operation(
["id_or_name", "type"],
["id_or_name", "amount", "currency", "occurred_at", "status", "transaction_type", "type"],
["id_or_name", "checkout", "type"],
- ["id_or_name", "browser_id", "fields", "type"],
+ ["id_or_name", "browser_id", "type"],
["id_or_name", "browser_id", "page_url", "type"],
["id_or_name", "access_token", "type"],
["id_or_name", "bindings", "browser_id", "input", "page_url", "tool_ref", "type"],
@@ -2356,6 +2429,8 @@ async def perform_operation(
| Literal["1pw_recover"]
| Literal["1pw_update_access_token"]
| Literal["webmcp_invoke"],
+ browser_id: str | Omit = omit,
+ page_url: str | Omit = omit,
amount: int | Omit = omit,
currency: str | Omit = omit,
occurred_at: Union[str, datetime] | Omit = omit,
@@ -2365,9 +2440,7 @@ async def perform_operation(
]
| Omit = omit,
checkout: VaultCheckoutContextParam | Omit = omit,
- browser_id: str | Omit = omit,
fields: Iterable[VaultFillFieldParam] | Omit = omit,
- page_url: str | Omit = omit,
timeout_ms: int | Omit = omit,
goal: str | Omit = omit,
keywords: SequenceNotStr[str] | Omit = omit,
@@ -2398,15 +2471,15 @@ async def perform_operation(
body=await async_maybe_transform(
{
"type": type,
+ "browser_id": browser_id,
+ "page_url": page_url,
"amount": amount,
"currency": currency,
"occurred_at": occurred_at,
"status": status,
"transaction_type": transaction_type,
"checkout": checkout,
- "browser_id": browser_id,
"fields": fields,
- "page_url": page_url,
"timeout_ms": timeout_ms,
"goal": goal,
"keywords": keywords,
@@ -2502,7 +2575,12 @@ async def upsert(
checkout instead.
Args:
- spec: Live payment card. Test-mode card creation is not supported.
+ spec: Live Link card request. Test-mode card creation is not supported. The spec can
+ be changed with PATCH until the card is authorized at a checkout. Authorize it
+ with the browser at final checkout: Kernel inspects the checkout through WebMCP
+ and internally chooses a merchant-bound Link Pay Token when supported or a
+ virtual card otherwise. Callers never receive or choose how the credential is
+ delivered.
extra_headers: Send extra headers
diff --git a/src/kernel/types/vaults/authorize_vault_item_operation_request_param.py b/src/kernel/types/vaults/authorize_vault_item_operation_request_param.py
index d267b310..b2f9bee5 100644
--- a/src/kernel/types/vaults/authorize_vault_item_operation_request_param.py
+++ b/src/kernel/types/vaults/authorize_vault_item_operation_request_param.py
@@ -10,7 +10,16 @@
class AuthorizeVaultItemOperationRequestParam(TypedDict, total=False):
"""Authorize a Link or Kernel card using its existing purchase specification.
- Use only after explicit user approval and when the item advertises authorize. Do not automatically retry provider failures or indeterminate outcomes. Checkout context is not accepted.
+ Use only after explicit user approval and when the item advertises authorize; follow the advertised operation's description. For a Link card, pass browser_id and page_url together while the browser is on the final checkout page: Kernel inspects that checkout, chooses how the credential is delivered, and limits fill to that checkout's origin in any browser with this vault attached. Omitting both issues a virtual card that fills any page on the merchant origin. Once approval has been requested, browser_id and page_url are ignored and authorize only re-checks the request. Browser and checkout inspection errors occur before any provider request and can be retried once the browser is on the checkout page. Kernel cards do not accept browser_id or page_url.
"""
type: Required[Literal["authorize"]]
+
+ browser_id: str
+ """Browser session ID, not a reusable browser name.
+
+ The browser must have this vault attached.
+ """
+
+ page_url: str
+ """Exact current top-level URL of the final checkout page open in browser_id."""
diff --git a/src/kernel/types/vaults/card_vault_item_spec.py b/src/kernel/types/vaults/card_vault_item_spec.py
index 61f8fff4..9224fe34 100644
--- a/src/kernel/types/vaults/card_vault_item_spec.py
+++ b/src/kernel/types/vaults/card_vault_item_spec.py
@@ -57,12 +57,17 @@ class LinkCardVaultItemSpecTotal(BaseModel):
class LinkCardVaultItemSpec(BaseModel):
- """Live payment card. Test-mode card creation is not supported."""
+ """Live Link card request.
+
+ Test-mode card creation is not supported. The spec can be changed with PATCH until the card is authorized at a checkout. Authorize it with the browser at final checkout: Kernel inspects the checkout through WebMCP and internally chooses a merchant-bound Link Pay Token when supported or a virtual card otherwise. Callers never receive or choose how the credential is delivered.
+ """
amount: int
"""Integer amount in minor currency units.
- Link permits at most 50000 per spend request.
+ Link Pay Token execution permits at most 500000; virtual-card execution permits
+ at most 50000. Authorization rejects a larger amount when the checkout requires
+ a virtual card.
"""
context: str
@@ -83,7 +88,7 @@ class LinkCardVaultItemSpec(BaseModel):
provider: Literal["link"]
wallet: str
- """Wallet item key used to mint this card."""
+ """Connected Link wallet item key."""
expires_at: Optional[int] = None
diff --git a/src/kernel/types/vaults/card_vault_item_spec_param.py b/src/kernel/types/vaults/card_vault_item_spec_param.py
index 635bcdc1..08cd8545 100644
--- a/src/kernel/types/vaults/card_vault_item_spec_param.py
+++ b/src/kernel/types/vaults/card_vault_item_spec_param.py
@@ -57,12 +57,17 @@ class LinkCardVaultItemSpecTotal(TypedDict, total=False):
class LinkCardVaultItemSpec(TypedDict, total=False):
- """Live payment card. Test-mode card creation is not supported."""
+ """Live Link card request.
+
+ Test-mode card creation is not supported. The spec can be changed with PATCH until the card is authorized at a checkout. Authorize it with the browser at final checkout: Kernel inspects the checkout through WebMCP and internally chooses a merchant-bound Link Pay Token when supported or a virtual card otherwise. Callers never receive or choose how the credential is delivered.
+ """
amount: Required[int]
"""Integer amount in minor currency units.
- Link permits at most 50000 per spend request.
+ Link Pay Token execution permits at most 500000; virtual-card execution permits
+ at most 50000. Authorization rejects a larger amount when the checkout requires
+ a virtual card.
"""
context: Required[str]
@@ -83,7 +88,7 @@ class LinkCardVaultItemSpec(TypedDict, total=False):
provider: Required[Literal["link"]]
wallet: Required[str]
- """Wallet item key used to mint this card."""
+ """Connected Link wallet item key."""
expires_at: int
diff --git a/src/kernel/types/vaults/fill_vault_item_operation_request_param.py b/src/kernel/types/vaults/fill_vault_item_operation_request_param.py
index f00f5369..a86966d1 100644
--- a/src/kernel/types/vaults/fill_vault_item_operation_request_param.py
+++ b/src/kernel/types/vaults/fill_vault_item_operation_request_param.py
@@ -12,33 +12,18 @@
class FillVaultItemOperationRequestParam(TypedDict, total=False):
"""
- Fill selected fields from one ready credential or ready, unexpired Link or
- Kernel card into a browser linked to its vault.
- Only invoke when the item advertises `fill`. Browser and vault must belong
- to the same project. Kernel checks access and allowed destinations before
- filling; providing a page URL does not authorize a destination.
-
- Find exactly one open page matching `page_url`. Credential items may omit
- `page_url` to require exactly one open page; cards require an HTTPS page URL.
- Credentials have no destination allowlist. TOTP fields generate a current
- code immediately before writing; their seeds never enter the browser. For each selector, search
- the main frame and all descendant frames for editable inputs or selects
- matched directly or contained within matching elements. Each selector must
- resolve to one unique editable element across all frames; zero or multiple
- candidates fail. Count each element once, even if multiple matching
- containers contain it. Validate all bindings before filling.
- Select elements match an option by its value, not its label.
- If the page navigates or a target disappears during filling, stop rather
- than selecting a different page or element.
-
- Fill in request order and stop on the first failure. This operation is
- not atomic: previously filled fields are not rolled back. Never submit
- the form or click buttons, though input/change events may trigger site
- behavior. Link and Kernel cards use fill for browser checkout and do not expose
- aliases or support egress substitution. Fill does not consume the item, so
- a failed or indeterminate fill is safe to retry; a retry rewrites the same
- fields. A retry right after an indeterminate fill may wait up to 15 seconds
- for the earlier attempt's browser lock to expire.
+ Use a vault item in a browser session, for example to fill a login or
+ payment form. Only invoke when the item advertises `fill`; the advertised
+ operation describes the inputs that item needs. Requires `browser_id` and
+ usually `page_url`; some items also require `fields`. Browser and vault must
+ belong to the same project. Kernel checks access and allowed destinations
+ before filling; providing a page URL does not authorize a destination.
+
+ Fill never submits forms or clicks buttons, though input and change events
+ may trigger site behavior. It is not atomic: completed steps are not rolled
+ back. Fill does not consume the item, so a failed or indeterminate fill is
+ safe to retry. A retry right after an indeterminate fill may wait up to 15
+ seconds for the earlier attempt's browser lock to expire.
Secret values are never returned or included in operation logs, traces,
audit events, or error details. This does not prevent an agent with
@@ -49,18 +34,26 @@ class FillVaultItemOperationRequestParam(TypedDict, total=False):
browser_id: Required[str]
"""Browser session ID, not a reusable browser name."""
- fields: Required[Iterable[VaultFillFieldParam]]
- """Field bindings for this step. No two bindings may resolve to the same element."""
-
type: Required[Literal["fill"]]
+ fields: Iterable[VaultFillFieldParam]
+ """
+ Field bindings, when the item's advertised fill operation requires them. Each
+ selector is searched in the main frame and all descendant frames and must
+ resolve to exactly one editable input or select, matched directly or through a
+ containing element; zero or multiple candidates fail, and no two bindings may
+ resolve to the same element. Select elements match an option by value, not
+ label. All bindings are validated before any write, then filled in request
+ order, stopping at the first failure. If the page navigates or a target
+ disappears, fill stops rather than choosing another page or element.
+ """
+
page_url: str
"""Exact current top-level page URL, including path, query, and fragment.
Must match exactly one open page in the browser; zero or multiple matches fail.
- No prefix or glob matching. Required for cards, which must use HTTPS without
- embedded credentials. Optional for credentials, where omission requires exactly
- one open page.
+ No prefix or glob matching. When an item allows omitting it, the browser must
+ have exactly one open page.
"""
timeout_ms: int
diff --git a/src/kernel/types/vaults/fill_vault_item_operation_result.py b/src/kernel/types/vaults/fill_vault_item_operation_result.py
index 0f3f1855..8f63d2d7 100644
--- a/src/kernel/types/vaults/fill_vault_item_operation_result.py
+++ b/src/kernel/types/vaults/fill_vault_item_operation_result.py
@@ -11,17 +11,18 @@
class FillVaultItemOperationResult(BaseModel):
fields: List[VaultFillFieldResult]
- """Exactly one result per request binding, in request order.
-
- After the first failed or unknown field, all remaining fields are not_attempted.
+ """
+ One result per request binding, in request order; empty when the fill used no
+ bindings. After the first failed or unknown field, all remaining fields are
+ not_attempted.
"""
status: Literal["completed", "failed", "unknown"]
- """Completed only when all fields were filled.
+ """Completed when the fill finished.
- Failed when execution stopped with known outcomes. Unknown when any field's
- outcome cannot be determined. None of these statuses confirms payment or
- merchant acceptance.
+ Failed when execution stopped with known outcomes. Unknown when an outcome
+ cannot be determined. Completed means values were supplied to the page, not that
+ the site accepted or submitted them.
"""
type: Literal["fill"]
diff --git a/src/kernel/types/vaults/item_perform_operation_params.py b/src/kernel/types/vaults/item_perform_operation_params.py
index 3db75e75..9c3cdbf1 100644
--- a/src/kernel/types/vaults/item_perform_operation_params.py
+++ b/src/kernel/types/vaults/item_perform_operation_params.py
@@ -34,6 +34,15 @@ class AuthorizeVaultItemOperationRequest(TypedDict, total=False):
type: Required[Literal["authorize"]]
+ browser_id: str
+ """Browser session ID, not a reusable browser name.
+
+ The browser must have this vault attached.
+ """
+
+ page_url: str
+ """Exact current top-level URL of the final checkout page open in browser_id."""
+
class RestartVerificationVaultItemOperationRequest(TypedDict, total=False):
id_or_name: Required[str]
@@ -89,18 +98,26 @@ class FillVaultItemOperationRequest(TypedDict, total=False):
browser_id: Required[str]
"""Browser session ID, not a reusable browser name."""
- fields: Required[Iterable[VaultFillFieldParam]]
- """Field bindings for this step. No two bindings may resolve to the same element."""
-
type: Required[Literal["fill"]]
+ fields: Iterable[VaultFillFieldParam]
+ """
+ Field bindings, when the item's advertised fill operation requires them. Each
+ selector is searched in the main frame and all descendant frames and must
+ resolve to exactly one editable input or select, matched directly or through a
+ containing element; zero or multiple candidates fail, and no two bindings may
+ resolve to the same element. Select elements match an option by value, not
+ label. All bindings are validated before any write, then filled in request
+ order, stopping at the first failure. If the page navigates or a target
+ disappears, fill stops rather than choosing another page or element.
+ """
+
page_url: str
"""Exact current top-level page URL, including path, query, and fragment.
Must match exactly one open page in the browser; zero or multiple matches fail.
- No prefix or glob matching. Required for cards, which must use HTTPS without
- embedded credentials. Optional for credentials, where omission requires exactly
- one open page.
+ No prefix or glob matching. When an item allows omitting it, the browser must
+ have exactly one open page.
"""
timeout_ms: int
diff --git a/src/kernel/types/vaults/item_update_params.py b/src/kernel/types/vaults/item_update_params.py
index 136a04d0..33e3ad08 100644
--- a/src/kernel/types/vaults/item_update_params.py
+++ b/src/kernel/types/vaults/item_update_params.py
@@ -15,7 +15,14 @@ class CardVaultItemUpdateRequest(TypedDict, total=False):
id_or_name: Required[str]
spec: Required[CardVaultItemSpecParam]
- """Live payment card. Test-mode card creation is not supported."""
+ """Live Link card request.
+
+ Test-mode card creation is not supported. The spec can be changed with PATCH
+ until the card is authorized at a checkout. Authorize it with the browser at
+ final checkout: Kernel inspects the checkout through WebMCP and internally
+ chooses a merchant-bound Link Pay Token when supported or a virtual card
+ otherwise. Callers never receive or choose how the credential is delivered.
+ """
type: Literal["card"]
diff --git a/src/kernel/types/vaults/item_upsert_params.py b/src/kernel/types/vaults/item_upsert_params.py
index e77fbabf..3fa6c509 100644
--- a/src/kernel/types/vaults/item_upsert_params.py
+++ b/src/kernel/types/vaults/item_upsert_params.py
@@ -187,7 +187,14 @@ class CardVaultItemRequest(TypedDict, total=False):
id_or_name: Required[str]
spec: Required[CardVaultItemSpecParam]
- """Live payment card. Test-mode card creation is not supported."""
+ """Live Link card request.
+
+ Test-mode card creation is not supported. The spec can be changed with PATCH
+ until the card is authorized at a checkout. Authorize it with the browser at
+ final checkout: Kernel inspects the checkout through WebMCP and internally
+ chooses a merchant-bound Link Pay Token when supported or a virtual card
+ otherwise. Callers never receive or choose how the credential is delivered.
+ """
type: Required[Literal["card"]]
diff --git a/src/kernel/types/vaults/vault_item.py b/src/kernel/types/vaults/vault_item.py
index 697f56d9..1b72d59c 100644
--- a/src/kernel/types/vaults/vault_item.py
+++ b/src/kernel/types/vaults/vault_item.py
@@ -98,6 +98,9 @@ class WalletVaultItem(BaseModel):
action: Optional[VaultItemAction] = None
+ description: Optional[str] = None
+ """Server-generated provider and lifecycle guidance for agents."""
+
expanded: Optional[WalletVaultItemExpanded] = None
"""Live, non-persisted data requested through the item GET expand parameter."""
@@ -151,7 +154,14 @@ class CardVaultItem(BaseModel):
"""Immutable item key assigned when the item is created."""
spec: CardVaultItemSpec
- """Live payment card. Test-mode card creation is not supported."""
+ """Live Link card request.
+
+ Test-mode card creation is not supported. The spec can be changed with PATCH
+ until the card is authorized at a checkout. Authorize it with the browser at
+ final checkout: Kernel inspects the checkout through WebMCP and internally
+ chooses a merchant-bound Link Pay Token when supported or a virtual card
+ otherwise. Callers never receive or choose how the credential is delivered.
+ """
state: CardVaultItemState
"""Issued Link cards retain encrypted card material for the fill operation.
diff --git a/src/kernel/types/vaults/vault_item_operation_response.py b/src/kernel/types/vaults/vault_item_operation_response.py
index 0968dab7..4faf7487 100644
--- a/src/kernel/types/vaults/vault_item_operation_response.py
+++ b/src/kernel/types/vaults/vault_item_operation_response.py
@@ -100,6 +100,9 @@ class WalletVaultItem(BaseModel):
action: Optional[VaultItemAction] = None
+ description: Optional[str] = None
+ """Server-generated provider and lifecycle guidance for agents."""
+
expanded: Optional[WalletVaultItemExpanded] = None
"""Live, non-persisted data requested through the item GET expand parameter."""
@@ -153,7 +156,14 @@ class CardVaultItem(BaseModel):
"""Immutable item key assigned when the item is created."""
spec: CardVaultItemSpec
- """Live payment card. Test-mode card creation is not supported."""
+ """Live Link card request.
+
+ Test-mode card creation is not supported. The spec can be changed with PATCH
+ until the card is authorized at a checkout. Authorize it with the browser at
+ final checkout: Kernel inspects the checkout through WebMCP and internally
+ chooses a merchant-bound Link Pay Token when supported or a virtual card
+ otherwise. Callers never receive or choose how the credential is delivered.
+ """
state: CardVaultItemState
"""Issued Link cards retain encrypted card material for the fill operation.
diff --git a/tests/api_resources/vaults/test_items.py b/tests/api_resources/vaults/test_items.py
index f69942ef..8e565e2e 100644
--- a/tests/api_resources/vaults/test_items.py
+++ b/tests/api_resources/vaults/test_items.py
@@ -491,6 +491,18 @@ def test_method_perform_operation_overload_1(self, client: Kernel) -> None:
)
assert_matches_type(VaultItemOperationResponse, item, path=["response"])
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ def test_method_perform_operation_with_all_params_overload_1(self, client: Kernel) -> None:
+ item = client.vaults.items.perform_operation(
+ key="key",
+ id_or_name="id_or_name",
+ type="authorize",
+ browser_id="x",
+ page_url="https://example.com",
+ )
+ assert_matches_type(VaultItemOperationResponse, item, path=["response"])
+
@pytest.mark.skip(reason="Mock server tests are disabled")
@parametrize
def test_raw_response_perform_operation_overload_1(self, client: Kernel) -> None:
@@ -839,24 +851,6 @@ def test_method_perform_operation_overload_6(self, client: Kernel) -> None:
key="key",
id_or_name="id_or_name",
browser_id="browser-session-id",
- fields=[
- {
- "field": "number",
- "selector": "#card-number",
- },
- {
- "field": "exp_month",
- "selector": "#expiry-month",
- },
- {
- "field": "exp_year",
- "selector": "#expiry-year",
- },
- {
- "field": "cvc",
- "selector": "#security-code",
- },
- ],
type="fill",
)
assert_matches_type(VaultItemOperationResponse, item, path=["response"])
@@ -868,6 +862,7 @@ def test_method_perform_operation_with_all_params_overload_6(self, client: Kerne
key="key",
id_or_name="id_or_name",
browser_id="browser-session-id",
+ type="fill",
fields=[
{
"field": "number",
@@ -890,7 +885,6 @@ def test_method_perform_operation_with_all_params_overload_6(self, client: Kerne
"format": "MM/YY",
},
],
- type="fill",
page_url="https://shop.example/checkout",
timeout_ms=1,
)
@@ -903,24 +897,6 @@ def test_raw_response_perform_operation_overload_6(self, client: Kernel) -> None
key="key",
id_or_name="id_or_name",
browser_id="browser-session-id",
- fields=[
- {
- "field": "number",
- "selector": "#card-number",
- },
- {
- "field": "exp_month",
- "selector": "#expiry-month",
- },
- {
- "field": "exp_year",
- "selector": "#expiry-year",
- },
- {
- "field": "cvc",
- "selector": "#security-code",
- },
- ],
type="fill",
)
@@ -936,24 +912,6 @@ def test_streaming_response_perform_operation_overload_6(self, client: Kernel) -
key="key",
id_or_name="id_or_name",
browser_id="browser-session-id",
- fields=[
- {
- "field": "number",
- "selector": "#card-number",
- },
- {
- "field": "exp_month",
- "selector": "#expiry-month",
- },
- {
- "field": "exp_year",
- "selector": "#expiry-year",
- },
- {
- "field": "cvc",
- "selector": "#security-code",
- },
- ],
type="fill",
) as response:
assert not response.is_closed
@@ -972,24 +930,6 @@ def test_path_params_perform_operation_overload_6(self, client: Kernel) -> None:
key="key",
id_or_name="",
browser_id="browser-session-id",
- fields=[
- {
- "field": "number",
- "selector": "#card-number",
- },
- {
- "field": "exp_month",
- "selector": "#expiry-month",
- },
- {
- "field": "exp_year",
- "selector": "#expiry-year",
- },
- {
- "field": "cvc",
- "selector": "#security-code",
- },
- ],
type="fill",
)
@@ -998,24 +938,6 @@ def test_path_params_perform_operation_overload_6(self, client: Kernel) -> None:
key="",
id_or_name="id_or_name",
browser_id="browser-session-id",
- fields=[
- {
- "field": "number",
- "selector": "#card-number",
- },
- {
- "field": "exp_month",
- "selector": "#expiry-month",
- },
- {
- "field": "exp_year",
- "selector": "#expiry-year",
- },
- {
- "field": "cvc",
- "selector": "#security-code",
- },
- ],
type="fill",
)
@@ -2498,6 +2420,18 @@ async def test_method_perform_operation_overload_1(self, async_client: AsyncKern
)
assert_matches_type(VaultItemOperationResponse, item, path=["response"])
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ async def test_method_perform_operation_with_all_params_overload_1(self, async_client: AsyncKernel) -> None:
+ item = await async_client.vaults.items.perform_operation(
+ key="key",
+ id_or_name="id_or_name",
+ type="authorize",
+ browser_id="x",
+ page_url="https://example.com",
+ )
+ assert_matches_type(VaultItemOperationResponse, item, path=["response"])
+
@pytest.mark.skip(reason="Mock server tests are disabled")
@parametrize
async def test_raw_response_perform_operation_overload_1(self, async_client: AsyncKernel) -> None:
@@ -2846,24 +2780,6 @@ async def test_method_perform_operation_overload_6(self, async_client: AsyncKern
key="key",
id_or_name="id_or_name",
browser_id="browser-session-id",
- fields=[
- {
- "field": "number",
- "selector": "#card-number",
- },
- {
- "field": "exp_month",
- "selector": "#expiry-month",
- },
- {
- "field": "exp_year",
- "selector": "#expiry-year",
- },
- {
- "field": "cvc",
- "selector": "#security-code",
- },
- ],
type="fill",
)
assert_matches_type(VaultItemOperationResponse, item, path=["response"])
@@ -2875,6 +2791,7 @@ async def test_method_perform_operation_with_all_params_overload_6(self, async_c
key="key",
id_or_name="id_or_name",
browser_id="browser-session-id",
+ type="fill",
fields=[
{
"field": "number",
@@ -2897,7 +2814,6 @@ async def test_method_perform_operation_with_all_params_overload_6(self, async_c
"format": "MM/YY",
},
],
- type="fill",
page_url="https://shop.example/checkout",
timeout_ms=1,
)
@@ -2910,24 +2826,6 @@ async def test_raw_response_perform_operation_overload_6(self, async_client: Asy
key="key",
id_or_name="id_or_name",
browser_id="browser-session-id",
- fields=[
- {
- "field": "number",
- "selector": "#card-number",
- },
- {
- "field": "exp_month",
- "selector": "#expiry-month",
- },
- {
- "field": "exp_year",
- "selector": "#expiry-year",
- },
- {
- "field": "cvc",
- "selector": "#security-code",
- },
- ],
type="fill",
)
@@ -2943,24 +2841,6 @@ async def test_streaming_response_perform_operation_overload_6(self, async_clien
key="key",
id_or_name="id_or_name",
browser_id="browser-session-id",
- fields=[
- {
- "field": "number",
- "selector": "#card-number",
- },
- {
- "field": "exp_month",
- "selector": "#expiry-month",
- },
- {
- "field": "exp_year",
- "selector": "#expiry-year",
- },
- {
- "field": "cvc",
- "selector": "#security-code",
- },
- ],
type="fill",
) as response:
assert not response.is_closed
@@ -2979,24 +2859,6 @@ async def test_path_params_perform_operation_overload_6(self, async_client: Asyn
key="key",
id_or_name="",
browser_id="browser-session-id",
- fields=[
- {
- "field": "number",
- "selector": "#card-number",
- },
- {
- "field": "exp_month",
- "selector": "#expiry-month",
- },
- {
- "field": "exp_year",
- "selector": "#expiry-year",
- },
- {
- "field": "cvc",
- "selector": "#security-code",
- },
- ],
type="fill",
)
@@ -3005,24 +2867,6 @@ async def test_path_params_perform_operation_overload_6(self, async_client: Asyn
key="",
id_or_name="id_or_name",
browser_id="browser-session-id",
- fields=[
- {
- "field": "number",
- "selector": "#card-number",
- },
- {
- "field": "exp_month",
- "selector": "#expiry-month",
- },
- {
- "field": "exp_year",
- "selector": "#expiry-year",
- },
- {
- "field": "cvc",
- "selector": "#security-code",
- },
- ],
type="fill",
)
From f08136407428a84fd709d491d38694db026f70c8 Mon Sep 17 00:00:00 2001
From: rgarcia <72655+rgarcia@users.noreply.github.com>
Date: Wed, 14 Oct 2026 16:15:01 +0000
Subject: [PATCH 356/366] feat: support Link Pay Tokens when authorizing Link
cards at checkout
Link card authorize accepts browser_id and page_url so Kernel can deliver a merchant-bound Link Pay Token at supported checkouts, and card fill no longer requires fields.
From 440682106bff906e83d41ee289453044abf34510 Mon Sep 17 00:00:00 2001
From: "kernel-internal[bot]"
<260533166+kernel-internal[bot]@users.noreply.github.com>
Date: Wed, 14 Oct 2026 16:15:59 +0000
Subject: [PATCH 357/366] release: 0.126.0
---
.release-please-manifest.json | 2 +-
CHANGELOG.md | 7 +++++++
pyproject.toml | 2 +-
src/kernel/_version.py | 2 +-
4 files changed, 10 insertions(+), 3 deletions(-)
diff --git a/.release-please-manifest.json b/.release-please-manifest.json
index f3343e21..f38100a4 100644
--- a/.release-please-manifest.json
+++ b/.release-please-manifest.json
@@ -1,3 +1,3 @@
{
- ".": "0.125.0"
+ ".": "0.126.0"
}
\ No newline at end of file
diff --git a/CHANGELOG.md b/CHANGELOG.md
index fa87bc60..d69a64ae 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,12 @@
# Changelog
+## [0.126.0](https://github.com/kernel/kernel-python-sdk/compare/v0.125.0...v0.126.0) (2026-10-14)
+
+
+### Features
+
+* support Link Pay Tokens when authorizing Link cards at checkout ([f081364](https://github.com/kernel/kernel-python-sdk/commit/f08136407428a84fd709d491d38694db026f70c8))
+
## [0.125.0](https://github.com/kernel/kernel-python-sdk/compare/v0.124.0...v0.125.0) (2026-10-13)
diff --git a/pyproject.toml b/pyproject.toml
index a9a2b552..e13b78ea 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -1,6 +1,6 @@
[project]
name = "kernel"
-version = "0.125.0"
+version = "0.126.0"
description = "The official Python library for the kernel API"
dynamic = ["readme"]
license = "Apache-2.0"
diff --git a/src/kernel/_version.py b/src/kernel/_version.py
index 7bf40e5e..20748255 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.125.0" # x-release-please-version
+__version__ = "0.126.0" # x-release-please-version
From 84a69ad74c46ddcdd87acd88a23dd5330adf555b Mon Sep 17 00:00:00 2001
From: "kernel-internal[bot]"
<260533166+kernel-internal[bot]@users.noreply.github.com>
Date: Wed, 14 Oct 2026 23:56:23 +0000
Subject: [PATCH 358/366] feat: Add passkey credential fields and PATCH field
add/remove
Stainless-Generated-From: 8a555252c3a00ef277f917b1e70b8f3026866e77
---
api.md | 1 +
src/kernel/types/vaults/__init__.py | 3 +
...lect_vault_item_operation_request_param.py | 2 +-
.../vaults/credential_collection_action.py | 8 +--
.../credential_vault_field_definition.py | 21 ++++---
...tial_vault_field_definition_input_param.py | 55 +++++++++++++++++++
.../credential_vault_field_input_param.py | 27 +++++----
.../vaults/credential_vault_field_type.py | 2 +-
.../credential_vault_field_update_param.py | 5 +-
.../types/vaults/credential_vault_item.py | 49 +++++++++--------
...credential_vault_item_spec_update_param.py | 18 +++++-
...dential_vault_item_update_request_param.py | 11 ++--
.../managed_auth_credential_vault_field.py | 21 ++++---
.../vaults/vault_webmcp_binding_param.py | 3 +-
tests/api_resources/vaults/test_items.py | 20 +++++++
15 files changed, 181 insertions(+), 65 deletions(-)
create mode 100644 src/kernel/types/vaults/credential_vault_field_definition_input_param.py
diff --git a/api.md b/api.md
index f5243e5a..33b17d60 100644
--- a/api.md
+++ b/api.md
@@ -576,6 +576,7 @@ from kernel.types.vaults import (
CredentialAccountVaultItemRequest,
CredentialCollectionAction,
CredentialVaultFieldDefinition,
+ CredentialVaultFieldDefinitionInput,
CredentialVaultFieldInput,
CredentialVaultFieldState,
CredentialVaultFieldType,
diff --git a/src/kernel/types/vaults/__init__.py b/src/kernel/types/vaults/__init__.py
index ff4183c4..d72d730c 100644
--- a/src/kernel/types/vaults/__init__.py
+++ b/src/kernel/types/vaults/__init__.py
@@ -92,6 +92,9 @@
from .authorize_vault_item_operation_request_param import (
AuthorizeVaultItemOperationRequestParam as AuthorizeVaultItemOperationRequestParam,
)
+from .credential_vault_field_definition_input_param import (
+ CredentialVaultFieldDefinitionInputParam as CredentialVaultFieldDefinitionInputParam,
+)
from .kernel_credential_vault_item_spec_input_param import (
KernelCredentialVaultItemSpecInputParam as KernelCredentialVaultItemSpecInputParam,
)
diff --git a/src/kernel/types/vaults/collect_vault_item_operation_request_param.py b/src/kernel/types/vaults/collect_vault_item_operation_request_param.py
index 2ce3031c..4038b90e 100644
--- a/src/kernel/types/vaults/collect_vault_item_operation_request_param.py
+++ b/src/kernel/types/vaults/collect_vault_item_operation_request_param.py
@@ -12,7 +12,7 @@ class CollectVaultItemOperationRequestParam(TypedDict, total=False):
Supported for ready
and pending_collection credential items. Always render the same form from
- every form-supported field; totp fields have no form input and are omitted.
+ every form-supported field; totp and passkey fields have no form input and are omitted.
No caller-selected field subsets or form overrides are accepted.
Reuse an active Kernel-hosted session or renew an expired session atomically.
Customer-hosted forms use their own backend and ordinary item GET/PATCH. Opening
diff --git a/src/kernel/types/vaults/credential_collection_action.py b/src/kernel/types/vaults/credential_collection_action.py
index 331e255c..2f1cd48c 100644
--- a/src/kernel/types/vaults/credential_collection_action.py
+++ b/src/kernel/types/vaults/credential_collection_action.py
@@ -11,14 +11,14 @@
class CredentialCollectionAction(BaseModel):
"""
One schema-derived form for the item, available in ready or pending_collection
- state. Render every form-supported field as editable; omit totp fields and
- preserve their stored seeds. Prefill non-sensitive values,
+ state. Render every form-supported field as editable; omit totp and passkey
+ fields and preserve their stored values. Prefill non-sensitive values,
and allow existing sensitive values to be preserved or replaced without ever
revealing them. No field subsets or per-request form configuration exist.
Validate required fields against the resulting values, including preserved
secrets. Submit changed values only, using the version used to render the form.
- Scoped hosted submission rejects totp edits; seed writes require the ordinary
- authenticated item API. Customer forms likewise omit totp from their payloads.
+ Scoped hosted submission rejects totp and passkey edits; those writes require the ordinary
+ authenticated item API. Customer forms likewise omit them from their payloads.
Save edits atomically. A successful hosted submission increments the version,
marks ready, and consumes the session; an empty edit may complete collection
while preserving values. A customer form uses PATCH for changed values and
diff --git a/src/kernel/types/vaults/credential_vault_field_definition.py b/src/kernel/types/vaults/credential_vault_field_definition.py
index 8c60748d..c8317687 100644
--- a/src/kernel/types/vaults/credential_vault_field_definition.py
+++ b/src/kernel/types/vaults/credential_vault_field_definition.py
@@ -25,14 +25,19 @@ class CredentialVaultFieldDefinition(BaseModel):
type: CredentialVaultFieldType
"""
- Text, email, and password have form inputs; totp does not and is omitted from
- both Kernel-hosted and customer React forms. Password and totp must be
- sensitive. A totp value is an RFC 4648 Base32 generator seed (case-insensitive,
- optional trailing padding), not an otpauth URI or current code. Reject invalid
- or empty decoded seeds. Browser fill generates an RFC 6238 code at execution
- time using HMAC-SHA1, 6 digits, and a 30-second period. Preserve leading zeros;
- never fill the seed. Custom algorithms, digits, periods, and form enrollment are
- unsupported.
+ Text, email, and password have form inputs; totp and passkey do not and are
+ omitted from both Kernel-hosted and customer React forms. Password, totp, and
+ passkey must be sensitive. A totp value is an RFC 4648 Base32 generator seed
+ (case-insensitive, optional trailing padding), not an otpauth URI or current
+ code. Reject invalid or empty decoded seeds. Browser fill generates an RFC 6238
+ code at execution time using HMAC-SHA1, 6 digits, and a 30-second period.
+ Preserve leading zeros; never fill the seed. Custom algorithms, digits, periods,
+ and form enrollment are unsupported. A passkey value is a JSON-encoded FIDO
+ Credential Exchange Format passkey object with unpadded base64url
+ `credentialId`, `userHandle` (at most 64 bytes), and `key` (a PKCS#8 ECDSA P-256
+ private key), a lowercase `rpId` domain, and optional `username` and
+ `userDisplayName`. Other CXF members are ignored. Passkeys are never filled into
+ pages.
"""
label: Optional[str] = None
diff --git a/src/kernel/types/vaults/credential_vault_field_definition_input_param.py b/src/kernel/types/vaults/credential_vault_field_definition_input_param.py
new file mode 100644
index 00000000..f97cf911
--- /dev/null
+++ b/src/kernel/types/vaults/credential_vault_field_definition_input_param.py
@@ -0,0 +1,55 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from __future__ import annotations
+
+from typing_extensions import Required, TypedDict
+
+from .credential_vault_field_type import CredentialVaultFieldType
+
+__all__ = ["CredentialVaultFieldDefinitionInputParam"]
+
+
+class CredentialVaultFieldDefinitionInputParam(TypedDict, total=False):
+ """A field definition without a value.
+
+ Field updates add fields with this shape and set their values in `fields`.
+ """
+
+ name: Required[str]
+ """Unique stable field name used to key values, updates, and browser fills."""
+
+ type: Required[CredentialVaultFieldType]
+ """
+ Text, email, and password have form inputs; totp and passkey do not and are
+ omitted from both Kernel-hosted and customer React forms. Password, totp, and
+ passkey must be sensitive. A totp value is an RFC 4648 Base32 generator seed
+ (case-insensitive, optional trailing padding), not an otpauth URI or current
+ code. Reject invalid or empty decoded seeds. Browser fill generates an RFC 6238
+ code at execution time using HMAC-SHA1, 6 digits, and a 30-second period.
+ Preserve leading zeros; never fill the seed. Custom algorithms, digits, periods,
+ and form enrollment are unsupported. A passkey value is a JSON-encoded FIDO
+ Credential Exchange Format passkey object with unpadded base64url
+ `credentialId`, `userHandle` (at most 64 bytes), and `key` (a PKCS#8 ECDSA P-256
+ private key), a lowercase `rpId` domain, and optional `username` and
+ `userDisplayName`. Other CXF members are ignored. Passkeys are never filled into
+ pages.
+ """
+
+ label: str
+ """Optional human-readable display label.
+
+ It is returned as non-secret metadata and never affects value keys, updates, or
+ browser fills. Use single-line, trimmed display text without control or
+ formatting characters. The server enforces a 128-byte UTF-8 limit.
+ """
+
+ required: bool
+
+ sensitive: bool
+ """
+ Set false explicitly for ordinary usernames, email addresses, and other
+ non-secret identifiers. Reserve true for secrets such as passwords, API tokens,
+ and TOTP seeds. Password, totp, and passkey fields must be true. Omission
+ defaults to true for safety; do not rely on that default for every field. False
+ permits API reads and form prefilling.
+ """
diff --git a/src/kernel/types/vaults/credential_vault_field_input_param.py b/src/kernel/types/vaults/credential_vault_field_input_param.py
index 2f481706..5e26bb8d 100644
--- a/src/kernel/types/vaults/credential_vault_field_input_param.py
+++ b/src/kernel/types/vaults/credential_vault_field_input_param.py
@@ -15,14 +15,19 @@ class CredentialVaultFieldInputParam(TypedDict, total=False):
type: Required[CredentialVaultFieldType]
"""
- Text, email, and password have form inputs; totp does not and is omitted from
- both Kernel-hosted and customer React forms. Password and totp must be
- sensitive. A totp value is an RFC 4648 Base32 generator seed (case-insensitive,
- optional trailing padding), not an otpauth URI or current code. Reject invalid
- or empty decoded seeds. Browser fill generates an RFC 6238 code at execution
- time using HMAC-SHA1, 6 digits, and a 30-second period. Preserve leading zeros;
- never fill the seed. Custom algorithms, digits, periods, and form enrollment are
- unsupported.
+ Text, email, and password have form inputs; totp and passkey do not and are
+ omitted from both Kernel-hosted and customer React forms. Password, totp, and
+ passkey must be sensitive. A totp value is an RFC 4648 Base32 generator seed
+ (case-insensitive, optional trailing padding), not an otpauth URI or current
+ code. Reject invalid or empty decoded seeds. Browser fill generates an RFC 6238
+ code at execution time using HMAC-SHA1, 6 digits, and a 30-second period.
+ Preserve leading zeros; never fill the seed. Custom algorithms, digits, periods,
+ and form enrollment are unsupported. A passkey value is a JSON-encoded FIDO
+ Credential Exchange Format passkey object with unpadded base64url
+ `credentialId`, `userHandle` (at most 64 bytes), and `key` (a PKCS#8 ECDSA P-256
+ private key), a lowercase `rpId` domain, and optional `username` and
+ `userDisplayName`. Other CXF members are ignored. Passkeys are never filled into
+ pages.
"""
encrypted_value: str
@@ -55,9 +60,9 @@ class CredentialVaultFieldInputParam(TypedDict, total=False):
"""
Set false explicitly for ordinary usernames, email addresses, and other
non-secret identifiers. Reserve true for secrets such as passwords, API tokens,
- and TOTP seeds. Password and totp fields must be true. Omission defaults to true
- for safety; do not rely on that default for every field. False permits API reads
- and form prefilling.
+ and TOTP seeds. Password, totp, and passkey fields must be true. Omission
+ defaults to true for safety; do not rely on that default for every field. False
+ permits API reads and form prefilling.
"""
value: str
diff --git a/src/kernel/types/vaults/credential_vault_field_type.py b/src/kernel/types/vaults/credential_vault_field_type.py
index cb400e9c..67e5368e 100644
--- a/src/kernel/types/vaults/credential_vault_field_type.py
+++ b/src/kernel/types/vaults/credential_vault_field_type.py
@@ -4,4 +4,4 @@
__all__ = ["CredentialVaultFieldType"]
-CredentialVaultFieldType: TypeAlias = Literal["text", "email", "password", "totp"]
+CredentialVaultFieldType: TypeAlias = Literal["text", "email", "password", "totp", "passkey"]
diff --git a/src/kernel/types/vaults/credential_vault_field_update_param.py b/src/kernel/types/vaults/credential_vault_field_update_param.py
index 823b21aa..b33079bf 100644
--- a/src/kernel/types/vaults/credential_vault_field_update_param.py
+++ b/src/kernel/types/vaults/credential_vault_field_update_param.py
@@ -36,6 +36,7 @@ class CredentialVaultFieldUpdateParam(TypedDict, total=False):
immediately clear the stored value. Clearing a required form-supported field
reopens collection; clearing an optional field does not prevent readiness.
Values must satisfy the declared field type. For totp, value is the generator
- seed, never a current code. Clearing a required totp field returns 400 because
- it cannot be collected in a form.
+ seed, never a current code. For passkey, value is the JSON-encoded CXF passkey
+ object. Clearing a required totp or passkey field returns 400 because it cannot
+ be collected in a form.
"""
diff --git a/src/kernel/types/vaults/credential_vault_item.py b/src/kernel/types/vaults/credential_vault_item.py
index f9bd1158..91f6d8e1 100644
--- a/src/kernel/types/vaults/credential_vault_item.py
+++ b/src/kernel/types/vaults/credential_vault_item.py
@@ -116,28 +116,29 @@ class CredentialVaultItem(BaseModel):
action: Optional[Action] = None
"""
One schema-derived form for the item, available in ready or pending_collection
- state. Render every form-supported field as editable; omit totp fields and
- preserve their stored seeds. Prefill non-sensitive values, and allow existing
- sensitive values to be preserved or replaced without ever revealing them. No
- field subsets or per-request form configuration exist. Validate required fields
- against the resulting values, including preserved secrets. Submit changed values
- only, using the version used to render the form. Scoped hosted submission
- rejects totp edits; seed writes require the ordinary authenticated item API.
- Customer forms likewise omit totp from their payloads. Save edits atomically. A
- successful hosted submission increments the version, marks ready, and consumes
- the session; an empty edit may complete collection while preserving values. A
- customer form uses PATCH for changed values and does not send an empty PATCH
- when nothing changed. Kernel-hosted bearer sessions require no Kernel account
- and are bound to the item version. Expired, superseded, consumed, or
- deleted-item sessions cannot submit. Authenticated item GET renews expired
- active sessions for ready or pending items; pending items always receive an
- action. A ready item with no active session omits the action until collect is
- invoked. Concurrent renewals return the same link. Renewal changes neither
- values nor item version. An expired link cannot renew itself. The hosted form
- handles its collection protocol; callers only open the returned URL and do not
- extract or submit its token through the public API. For customer-hosted forms,
- use @onkernel/vault-react and an authenticated customer backend calling the
- ordinary item GET/PATCH API. Kernel does not store customer collection URLs or
- authenticate the customer's end users. Treat URLs and submitted values as
- secrets and exclude them from logs, traces, and errors.
+ state. Render every form-supported field as editable; omit totp and passkey
+ fields and preserve their stored values. Prefill non-sensitive values, and allow
+ existing sensitive values to be preserved or replaced without ever revealing
+ them. No field subsets or per-request form configuration exist. Validate
+ required fields against the resulting values, including preserved secrets.
+ Submit changed values only, using the version used to render the form. Scoped
+ hosted submission rejects totp and passkey edits; those writes require the
+ ordinary authenticated item API. Customer forms likewise omit them from their
+ payloads. Save edits atomically. A successful hosted submission increments the
+ version, marks ready, and consumes the session; an empty edit may complete
+ collection while preserving values. A customer form uses PATCH for changed
+ values and does not send an empty PATCH when nothing changed. Kernel-hosted
+ bearer sessions require no Kernel account and are bound to the item version.
+ Expired, superseded, consumed, or deleted-item sessions cannot submit.
+ Authenticated item GET renews expired active sessions for ready or pending
+ items; pending items always receive an action. A ready item with no active
+ session omits the action until collect is invoked. Concurrent renewals return
+ the same link. Renewal changes neither values nor item version. An expired link
+ cannot renew itself. The hosted form handles its collection protocol; callers
+ only open the returned URL and do not extract or submit its token through the
+ public API. For customer-hosted forms, use @onkernel/vault-react and an
+ authenticated customer backend calling the ordinary item GET/PATCH API. Kernel
+ does not store customer collection URLs or authenticate the customer's end
+ users. Treat URLs and submitted values as secrets and exclude them from logs,
+ traces, and errors.
"""
diff --git a/src/kernel/types/vaults/credential_vault_item_spec_update_param.py b/src/kernel/types/vaults/credential_vault_item_spec_update_param.py
index d7933b9b..06dde966 100644
--- a/src/kernel/types/vaults/credential_vault_item_spec_update_param.py
+++ b/src/kernel/types/vaults/credential_vault_item_spec_update_param.py
@@ -2,15 +2,25 @@
from __future__ import annotations
-from typing import Dict
+from typing import Dict, Iterable
from typing_extensions import TypedDict
+from ..._types import SequenceNotStr
from .credential_vault_field_update_param import CredentialVaultFieldUpdateParam
+from .credential_vault_field_definition_input_param import CredentialVaultFieldDefinitionInputParam
__all__ = ["CredentialVaultItemSpecUpdateParam"]
class CredentialVaultItemSpecUpdateParam(TypedDict, total=False):
+ add_fields: Iterable[CredentialVaultFieldDefinitionInputParam]
+ """Field definitions to append, with the same defaults as creation.
+
+ Set their values in `fields` in the same update. Names must be unique after
+ removals; at most 32 fields may result. A required totp or passkey field needs a
+ value in `fields`.
+ """
+
description: str
"""
Recognizable site or service name used as the form title, without suffixes such
@@ -19,3 +29,9 @@ class CredentialVaultItemSpecUpdateParam(TypedDict, total=False):
"""
fields: Dict[str, CredentialVaultFieldUpdateParam]
+
+ remove_fields: SequenceNotStr[str]
+ """Names of fields to delete with their values.
+
+ Unknown names return 400. At least one field must remain.
+ """
diff --git a/src/kernel/types/vaults/credential_vault_item_update_request_param.py b/src/kernel/types/vaults/credential_vault_item_update_request_param.py
index ca34cbc9..5c45f9df 100644
--- a/src/kernel/types/vaults/credential_vault_item_update_request_param.py
+++ b/src/kernel/types/vaults/credential_vault_item_update_request_param.py
@@ -10,10 +10,13 @@
class CredentialVaultItemUpdateRequestParam(TypedDict, total=False):
- """Atomically update description and selected values.
-
- Omitted properties are
- preserved. Field names, types, required flags, and sensitivity cannot change.
+ """
+ Atomically update description, field definitions, and selected values.
+ Omitted properties are preserved. `remove_fields` deletes definitions and
+ their values, then `add_fields` appends definitions, then `fields` sets
+ values, including for added fields. An existing field's definition cannot
+ be edited in place; to change it, remove the field and add it again, in
+ the same update or a later one. The old value is deleted with it.
Unknown field names return 400; stale versions or mismatched item types
return 409 without changing the item. A successful update increments version
and invalidates outstanding Kernel-hosted collection sessions. If required
diff --git a/src/kernel/types/vaults/managed_auth_credential_vault_field.py b/src/kernel/types/vaults/managed_auth_credential_vault_field.py
index 5554832b..e13171c5 100644
--- a/src/kernel/types/vaults/managed_auth_credential_vault_field.py
+++ b/src/kernel/types/vaults/managed_auth_credential_vault_field.py
@@ -9,12 +9,17 @@
class ManagedAuthCredentialVaultField(BaseModel):
type: CredentialVaultFieldType
"""
- Text, email, and password have form inputs; totp does not and is omitted from
- both Kernel-hosted and customer React forms. Password and totp must be
- sensitive. A totp value is an RFC 4648 Base32 generator seed (case-insensitive,
- optional trailing padding), not an otpauth URI or current code. Reject invalid
- or empty decoded seeds. Browser fill generates an RFC 6238 code at execution
- time using HMAC-SHA1, 6 digits, and a 30-second period. Preserve leading zeros;
- never fill the seed. Custom algorithms, digits, periods, and form enrollment are
- unsupported.
+ Text, email, and password have form inputs; totp and passkey do not and are
+ omitted from both Kernel-hosted and customer React forms. Password, totp, and
+ passkey must be sensitive. A totp value is an RFC 4648 Base32 generator seed
+ (case-insensitive, optional trailing padding), not an otpauth URI or current
+ code. Reject invalid or empty decoded seeds. Browser fill generates an RFC 6238
+ code at execution time using HMAC-SHA1, 6 digits, and a 30-second period.
+ Preserve leading zeros; never fill the seed. Custom algorithms, digits, periods,
+ and form enrollment are unsupported. A passkey value is a JSON-encoded FIDO
+ Credential Exchange Format passkey object with unpadded base64url
+ `credentialId`, `userHandle` (at most 64 bytes), and `key` (a PKCS#8 ECDSA P-256
+ private key), a lowercase `rpId` domain, and optional `username` and
+ `userDisplayName`. Other CXF members are ignored. Passkeys are never filled into
+ pages.
"""
diff --git a/src/kernel/types/vaults/vault_webmcp_binding_param.py b/src/kernel/types/vaults/vault_webmcp_binding_param.py
index b7a18292..cdb94f49 100644
--- a/src/kernel/types/vaults/vault_webmcp_binding_param.py
+++ b/src/kernel/types/vaults/vault_webmcp_binding_param.py
@@ -11,7 +11,8 @@ class VaultWebmcpBindingParam(TypedDict, total=False):
field: Required[str]
"""A declared, populated credential field or supported card field.
- A TOTP field supplies a fresh code, never its seed.
+ A TOTP field supplies a fresh code, never its seed. Passkey fields cannot be
+ filled.
"""
input_path: Required[str]
diff --git a/tests/api_resources/vaults/test_items.py b/tests/api_resources/vaults/test_items.py
index 8e565e2e..36ded6bc 100644
--- a/tests/api_resources/vaults/test_items.py
+++ b/tests/api_resources/vaults/test_items.py
@@ -255,6 +255,15 @@ def test_method_update_with_all_params_overload_2(self, client: Kernel) -> None:
key="x",
id_or_name="id_or_name",
spec={
+ "add_fields": [
+ {
+ "name": "name",
+ "type": "text",
+ "label": "label",
+ "required": True,
+ "sensitive": True,
+ }
+ ],
"description": "description",
"fields": {
"foo": {
@@ -262,6 +271,7 @@ def test_method_update_with_all_params_overload_2(self, client: Kernel) -> None:
"value": "value",
}
},
+ "remove_fields": ["x"],
},
type="credential",
version=1,
@@ -2184,6 +2194,15 @@ async def test_method_update_with_all_params_overload_2(self, async_client: Asyn
key="x",
id_or_name="id_or_name",
spec={
+ "add_fields": [
+ {
+ "name": "name",
+ "type": "text",
+ "label": "label",
+ "required": True,
+ "sensitive": True,
+ }
+ ],
"description": "description",
"fields": {
"foo": {
@@ -2191,6 +2210,7 @@ async def test_method_update_with_all_params_overload_2(self, async_client: Asyn
"value": "value",
}
},
+ "remove_fields": ["x"],
},
type="credential",
version=1,
From eed3e7ba06c46d3cfa6048d9f02c3d8fff6046e6 Mon Sep 17 00:00:00 2001
From: "kernel-internal[bot]"
<260533166+kernel-internal[bot]@users.noreply.github.com>
Date: Thu, 15 Oct 2026 00:25:55 +0000
Subject: [PATCH 359/366] feat: Deliver Kernel card details through fill
Stainless-Generated-From: 9e1188cd7d4ff2411cf480d9989938bb90840560
---
src/kernel/resources/vaults/items.py | 72 +++++++++++++++++++
src/kernel/types/vaults/kernel_card_state.py | 5 +-
.../vaults/kernel_card_vault_item_spec.py | 2 +-
.../kernel_card_vault_item_spec_param.py | 2 +-
.../vaults/kernel_wallet_vault_item_spec.py | 2 +-
.../kernel_wallet_vault_item_spec_param.py | 2 +-
6 files changed, 79 insertions(+), 6 deletions(-)
diff --git a/src/kernel/resources/vaults/items.py b/src/kernel/resources/vaults/items.py
index 0cdc5164..63991b12 100644
--- a/src/kernel/resources/vaults/items.py
+++ b/src/kernel/resources/vaults/items.py
@@ -434,6 +434,9 @@ def perform_operation(
return HTTP 429 and retain the card item; stop, back off, and retry the same
authorize operation.
+ Ready Kernel cards advertise fill. Fill delivers card details for a few minutes;
+ submit checkout promptly after filling. A repeated fill is safe.
+
Fill returns a value-free execution result. Validation failures before writing
return 400 (invalid request or targets), 403 (access or destination denied), 404
(resource not found), or 409 (item or browser not ready). Once writing starts,
@@ -487,6 +490,9 @@ def perform_operation(
return HTTP 429 and retain the card item; stop, back off, and retry the same
authorize operation.
+ Ready Kernel cards advertise fill. Fill delivers card details for a few minutes;
+ submit checkout promptly after filling. A repeated fill is safe.
+
Fill returns a value-free execution result. Validation failures before writing
return 400 (invalid request or targets), 403 (access or destination denied), 404
(resource not found), or 409 (item or browser not ready). Once writing starts,
@@ -542,6 +548,9 @@ def perform_operation(
return HTTP 429 and retain the card item; stop, back off, and retry the same
authorize operation.
+ Ready Kernel cards advertise fill. Fill delivers card details for a few minutes;
+ submit checkout promptly after filling. A repeated fill is safe.
+
Fill returns a value-free execution result. Validation failures before writing
return 400 (invalid request or targets), 403 (access or destination denied), 404
(resource not found), or 409 (item or browser not ready). Once writing starts,
@@ -596,6 +605,9 @@ def perform_operation(
return HTTP 429 and retain the card item; stop, back off, and retry the same
authorize operation.
+ Ready Kernel cards advertise fill. Fill delivers card details for a few minutes;
+ submit checkout promptly after filling. A repeated fill is safe.
+
Fill returns a value-free execution result. Validation failures before writing
return 400 (invalid request or targets), 403 (access or destination denied), 404
(resource not found), or 409 (item or browser not ready). Once writing starts,
@@ -645,6 +657,9 @@ def perform_operation(
return HTTP 429 and retain the card item; stop, back off, and retry the same
authorize operation.
+ Ready Kernel cards advertise fill. Fill delivers card details for a few minutes;
+ submit checkout promptly after filling. A repeated fill is safe.
+
Fill returns a value-free execution result. Validation failures before writing
return 400 (invalid request or targets), 403 (access or destination denied), 404
(resource not found), or 409 (item or browser not ready). Once writing starts,
@@ -703,6 +718,9 @@ def perform_operation(
return HTTP 429 and retain the card item; stop, back off, and retry the same
authorize operation.
+ Ready Kernel cards advertise fill. Fill delivers card details for a few minutes;
+ submit checkout promptly after filling. A repeated fill is safe.
+
Fill returns a value-free execution result. Validation failures before writing
return 400 (invalid request or targets), 403 (access or destination denied), 404
(resource not found), or 409 (item or browser not ready). Once writing starts,
@@ -772,6 +790,9 @@ def perform_operation(
return HTTP 429 and retain the card item; stop, back off, and retry the same
authorize operation.
+ Ready Kernel cards advertise fill. Fill delivers card details for a few minutes;
+ submit checkout promptly after filling. A repeated fill is safe.
+
Fill returns a value-free execution result. Validation failures before writing
return 400 (invalid request or targets), 403 (access or destination denied), 404
(resource not found), or 409 (item or browser not ready). Once writing starts,
@@ -821,6 +842,9 @@ def perform_operation(
return HTTP 429 and retain the card item; stop, back off, and retry the same
authorize operation.
+ Ready Kernel cards advertise fill. Fill delivers card details for a few minutes;
+ submit checkout promptly after filling. A repeated fill is safe.
+
Fill returns a value-free execution result. Validation failures before writing
return 400 (invalid request or targets), 403 (access or destination denied), 404
(resource not found), or 409 (item or browser not ready). Once writing starts,
@@ -873,6 +897,9 @@ def perform_operation(
return HTTP 429 and retain the card item; stop, back off, and retry the same
authorize operation.
+ Ready Kernel cards advertise fill. Fill delivers card details for a few minutes;
+ submit checkout promptly after filling. A repeated fill is safe.
+
Fill returns a value-free execution result. Validation failures before writing
return 400 (invalid request or targets), 403 (access or destination denied), 404
(resource not found), or 409 (item or browser not ready). Once writing starts,
@@ -929,6 +956,9 @@ def perform_operation(
return HTTP 429 and retain the card item; stop, back off, and retry the same
authorize operation.
+ Ready Kernel cards advertise fill. Fill delivers card details for a few minutes;
+ submit checkout promptly after filling. A repeated fill is safe.
+
Fill returns a value-free execution result. Validation failures before writing
return 400 (invalid request or targets), 403 (access or destination denied), 404
(resource not found), or 409 (item or browser not ready). Once writing starts,
@@ -979,6 +1009,9 @@ def perform_operation(
return HTTP 429 and retain the card item; stop, back off, and retry the same
authorize operation.
+ Ready Kernel cards advertise fill. Fill delivers card details for a few minutes;
+ submit checkout promptly after filling. A repeated fill is safe.
+
Fill returns a value-free execution result. Validation failures before writing
return 400 (invalid request or targets), 403 (access or destination denied), 404
(resource not found), or 409 (item or browser not ready). Once writing starts,
@@ -1035,6 +1068,9 @@ def perform_operation(
return HTTP 429 and retain the card item; stop, back off, and retry the same
authorize operation.
+ Ready Kernel cards advertise fill. Fill delivers card details for a few minutes;
+ submit checkout promptly after filling. A repeated fill is safe.
+
Fill returns a value-free execution result. Validation failures before writing
return 400 (invalid request or targets), 403 (access or destination denied), 404
(resource not found), or 409 (item or browser not ready). Once writing starts,
@@ -1770,6 +1806,9 @@ async def perform_operation(
return HTTP 429 and retain the card item; stop, back off, and retry the same
authorize operation.
+ Ready Kernel cards advertise fill. Fill delivers card details for a few minutes;
+ submit checkout promptly after filling. A repeated fill is safe.
+
Fill returns a value-free execution result. Validation failures before writing
return 400 (invalid request or targets), 403 (access or destination denied), 404
(resource not found), or 409 (item or browser not ready). Once writing starts,
@@ -1823,6 +1862,9 @@ async def perform_operation(
return HTTP 429 and retain the card item; stop, back off, and retry the same
authorize operation.
+ Ready Kernel cards advertise fill. Fill delivers card details for a few minutes;
+ submit checkout promptly after filling. A repeated fill is safe.
+
Fill returns a value-free execution result. Validation failures before writing
return 400 (invalid request or targets), 403 (access or destination denied), 404
(resource not found), or 409 (item or browser not ready). Once writing starts,
@@ -1878,6 +1920,9 @@ async def perform_operation(
return HTTP 429 and retain the card item; stop, back off, and retry the same
authorize operation.
+ Ready Kernel cards advertise fill. Fill delivers card details for a few minutes;
+ submit checkout promptly after filling. A repeated fill is safe.
+
Fill returns a value-free execution result. Validation failures before writing
return 400 (invalid request or targets), 403 (access or destination denied), 404
(resource not found), or 409 (item or browser not ready). Once writing starts,
@@ -1932,6 +1977,9 @@ async def perform_operation(
return HTTP 429 and retain the card item; stop, back off, and retry the same
authorize operation.
+ Ready Kernel cards advertise fill. Fill delivers card details for a few minutes;
+ submit checkout promptly after filling. A repeated fill is safe.
+
Fill returns a value-free execution result. Validation failures before writing
return 400 (invalid request or targets), 403 (access or destination denied), 404
(resource not found), or 409 (item or browser not ready). Once writing starts,
@@ -1981,6 +2029,9 @@ async def perform_operation(
return HTTP 429 and retain the card item; stop, back off, and retry the same
authorize operation.
+ Ready Kernel cards advertise fill. Fill delivers card details for a few minutes;
+ submit checkout promptly after filling. A repeated fill is safe.
+
Fill returns a value-free execution result. Validation failures before writing
return 400 (invalid request or targets), 403 (access or destination denied), 404
(resource not found), or 409 (item or browser not ready). Once writing starts,
@@ -2039,6 +2090,9 @@ async def perform_operation(
return HTTP 429 and retain the card item; stop, back off, and retry the same
authorize operation.
+ Ready Kernel cards advertise fill. Fill delivers card details for a few minutes;
+ submit checkout promptly after filling. A repeated fill is safe.
+
Fill returns a value-free execution result. Validation failures before writing
return 400 (invalid request or targets), 403 (access or destination denied), 404
(resource not found), or 409 (item or browser not ready). Once writing starts,
@@ -2108,6 +2162,9 @@ async def perform_operation(
return HTTP 429 and retain the card item; stop, back off, and retry the same
authorize operation.
+ Ready Kernel cards advertise fill. Fill delivers card details for a few minutes;
+ submit checkout promptly after filling. A repeated fill is safe.
+
Fill returns a value-free execution result. Validation failures before writing
return 400 (invalid request or targets), 403 (access or destination denied), 404
(resource not found), or 409 (item or browser not ready). Once writing starts,
@@ -2157,6 +2214,9 @@ async def perform_operation(
return HTTP 429 and retain the card item; stop, back off, and retry the same
authorize operation.
+ Ready Kernel cards advertise fill. Fill delivers card details for a few minutes;
+ submit checkout promptly after filling. A repeated fill is safe.
+
Fill returns a value-free execution result. Validation failures before writing
return 400 (invalid request or targets), 403 (access or destination denied), 404
(resource not found), or 409 (item or browser not ready). Once writing starts,
@@ -2209,6 +2269,9 @@ async def perform_operation(
return HTTP 429 and retain the card item; stop, back off, and retry the same
authorize operation.
+ Ready Kernel cards advertise fill. Fill delivers card details for a few minutes;
+ submit checkout promptly after filling. A repeated fill is safe.
+
Fill returns a value-free execution result. Validation failures before writing
return 400 (invalid request or targets), 403 (access or destination denied), 404
(resource not found), or 409 (item or browser not ready). Once writing starts,
@@ -2265,6 +2328,9 @@ async def perform_operation(
return HTTP 429 and retain the card item; stop, back off, and retry the same
authorize operation.
+ Ready Kernel cards advertise fill. Fill delivers card details for a few minutes;
+ submit checkout promptly after filling. A repeated fill is safe.
+
Fill returns a value-free execution result. Validation failures before writing
return 400 (invalid request or targets), 403 (access or destination denied), 404
(resource not found), or 409 (item or browser not ready). Once writing starts,
@@ -2315,6 +2381,9 @@ async def perform_operation(
return HTTP 429 and retain the card item; stop, back off, and retry the same
authorize operation.
+ Ready Kernel cards advertise fill. Fill delivers card details for a few minutes;
+ submit checkout promptly after filling. A repeated fill is safe.
+
Fill returns a value-free execution result. Validation failures before writing
return 400 (invalid request or targets), 403 (access or destination denied), 404
(resource not found), or 409 (item or browser not ready). Once writing starts,
@@ -2371,6 +2440,9 @@ async def perform_operation(
return HTTP 429 and retain the card item; stop, back off, and retry the same
authorize operation.
+ Ready Kernel cards advertise fill. Fill delivers card details for a few minutes;
+ submit checkout promptly after filling. A repeated fill is safe.
+
Fill returns a value-free execution result. Validation failures before writing
return 400 (invalid request or targets), 403 (access or destination denied), 404
(resource not found), or 409 (item or browser not ready). Once writing starts,
diff --git a/src/kernel/types/vaults/kernel_card_state.py b/src/kernel/types/vaults/kernel_card_state.py
index 047b5f9e..e2bb6ad3 100644
--- a/src/kernel/types/vaults/kernel_card_state.py
+++ b/src/kernel/types/vaults/kernel_card_state.py
@@ -32,8 +32,9 @@ def __getattr__(self, attr: str) -> str: ...
class KernelCardState(BaseModel):
- """
- A ready Kernel card retains its encrypted network token and one-time code for the fill operation until the item's expires_at. Fill and submit checkout before then. Visa purchases require a spend_approval action before the code is issued; Mastercard purchases need no hosted approval. masks.last4 is the enrolled card's last four digits; masks.token_last4 is the network token's last four digits shown to the merchant. Kernel cards do not expose aliases or support egress substitution. Kernel does not observe whether the merchant charged the card.
+ """A ready Kernel card supports authorize, fill, then submit checkout.
+
+ Card details are delivered for a few minutes after fill; submit promptly. Repeated fill is safe and never submits checkout. Visa purchases require a spend_approval action before details are issued; Mastercard purchases need no hosted approval. masks.last4 is the enrolled card's last four digits; masks.token_last4, when present, is the token's last four digits shown to the merchant. Kernel does not observe whether the merchant charged the card.
"""
provider: Literal["kernel"]
diff --git a/src/kernel/types/vaults/kernel_card_vault_item_spec.py b/src/kernel/types/vaults/kernel_card_vault_item_spec.py
index 1581c68c..73fd646b 100644
--- a/src/kernel/types/vaults/kernel_card_vault_item_spec.py
+++ b/src/kernel/types/vaults/kernel_card_vault_item_spec.py
@@ -11,7 +11,7 @@
class KernelCardVaultItemSpec(BaseModel):
"""One live purchase with a Kernel-enrolled card.
- Authorization obtains an agentic network token number, expiry and one-time 3-digit code. They are stored encrypted for the fill operation, which types them only on merchant_url's origin; the merchant's own checkout submits the payment. The one-time code is valid until the item's expires_at; fill and submit checkout before then. Mastercard purchases need no cardholder approval. A Visa purchase returns a spend_approval action: the cardholder approves it with a Visa passkey, and Kernel registers a Visa intent for one transaction up to the amount before issuing the code. Card updates are not supported; delete and create a new item instead.
+ Authorization obtains an agentic network token number, expiry and one-time 3-digit code. They are stored encrypted for the fill operation, which types them only on merchant_url's origin; the merchant's own checkout submits the payment. The one-time code is valid until the item's expires_at; fill and submit checkout before then. Mastercard purchases need no cardholder approval. A Visa purchase returns a spend_approval action: the cardholder approves it with a Visa passkey, and Kernel registers a Visa intent for one transaction up to the amount before issuing the code. For supported cards whose network cannot be enrolled for agentic tokens, authorize prepares checkout details without issuing a one-time code. The card can be filled into one checkout before its expires_at, one hour after authorization. Fill types those details on merchant_url's origin. The amount, merchant_url and merchant_country are recorded but do not restrict payment requests sent by the browser. Card updates are not supported; delete and create a new item instead.
"""
amount: int
diff --git a/src/kernel/types/vaults/kernel_card_vault_item_spec_param.py b/src/kernel/types/vaults/kernel_card_vault_item_spec_param.py
index 72d6dae8..d18c4d8a 100644
--- a/src/kernel/types/vaults/kernel_card_vault_item_spec_param.py
+++ b/src/kernel/types/vaults/kernel_card_vault_item_spec_param.py
@@ -10,7 +10,7 @@
class KernelCardVaultItemSpecParam(TypedDict, total=False):
"""One live purchase with a Kernel-enrolled card.
- Authorization obtains an agentic network token number, expiry and one-time 3-digit code. They are stored encrypted for the fill operation, which types them only on merchant_url's origin; the merchant's own checkout submits the payment. The one-time code is valid until the item's expires_at; fill and submit checkout before then. Mastercard purchases need no cardholder approval. A Visa purchase returns a spend_approval action: the cardholder approves it with a Visa passkey, and Kernel registers a Visa intent for one transaction up to the amount before issuing the code. Card updates are not supported; delete and create a new item instead.
+ Authorization obtains an agentic network token number, expiry and one-time 3-digit code. They are stored encrypted for the fill operation, which types them only on merchant_url's origin; the merchant's own checkout submits the payment. The one-time code is valid until the item's expires_at; fill and submit checkout before then. Mastercard purchases need no cardholder approval. A Visa purchase returns a spend_approval action: the cardholder approves it with a Visa passkey, and Kernel registers a Visa intent for one transaction up to the amount before issuing the code. For supported cards whose network cannot be enrolled for agentic tokens, authorize prepares checkout details without issuing a one-time code. The card can be filled into one checkout before its expires_at, one hour after authorization. Fill types those details on merchant_url's origin. The amount, merchant_url and merchant_country are recorded but do not restrict payment requests sent by the browser. Card updates are not supported; delete and create a new item instead.
"""
amount: Required[int]
diff --git a/src/kernel/types/vaults/kernel_wallet_vault_item_spec.py b/src/kernel/types/vaults/kernel_wallet_vault_item_spec.py
index 728d54f2..fb2f039e 100644
--- a/src/kernel/types/vaults/kernel_wallet_vault_item_spec.py
+++ b/src/kernel/types/vaults/kernel_wallet_vault_item_spec.py
@@ -10,7 +10,7 @@
class KernelWalletVaultItemSpec(BaseModel):
"""One card stored with Kernel-managed credentials.
- Creation returns a card_enrollment action: the cardholder enters the card and their email on a Kernel-hosted page, and the wallet connects once the card is stored. Kernel then enrolls it for an agentic network token when the issuer supports it. The card number never reaches Kernel. The connected wallet's payment_methods expansion lists the card; capabilities.single_use_card.eligible is false, with a network_token_* reason, until the card has a network token, and authorize returns 400 for such a card. Visa purchases require the cardholder to complete a spend_approval action before Kernel issues a one-time code; Mastercard purchases need no hosted approval.
+ Creation returns a card_enrollment action: the cardholder enters the card and their email on a Kernel-hosted page, and the wallet connects once the card is stored. Kernel then enrolls it for an agentic network token when the issuer supports it. The card number never reaches Kernel. The connected wallet's payment_methods expansion lists the card; capabilities.single_use_card.eligible is false, with a network_token_* reason, until the card has a network token, and authorize returns 400 for such a card unless Kernel supports checkout delivery without a network token for that card. In that case, fill types the checkout details. Visa purchases require the cardholder to complete a spend_approval action before Kernel issues a one-time code; Mastercard purchases need no hosted approval.
"""
provider: Literal["kernel"]
diff --git a/src/kernel/types/vaults/kernel_wallet_vault_item_spec_param.py b/src/kernel/types/vaults/kernel_wallet_vault_item_spec_param.py
index c29066ce..b0e87117 100644
--- a/src/kernel/types/vaults/kernel_wallet_vault_item_spec_param.py
+++ b/src/kernel/types/vaults/kernel_wallet_vault_item_spec_param.py
@@ -10,7 +10,7 @@
class KernelWalletVaultItemSpecParam(TypedDict, total=False):
"""One card stored with Kernel-managed credentials.
- Creation returns a card_enrollment action: the cardholder enters the card and their email on a Kernel-hosted page, and the wallet connects once the card is stored. Kernel then enrolls it for an agentic network token when the issuer supports it. The card number never reaches Kernel. The connected wallet's payment_methods expansion lists the card; capabilities.single_use_card.eligible is false, with a network_token_* reason, until the card has a network token, and authorize returns 400 for such a card. Visa purchases require the cardholder to complete a spend_approval action before Kernel issues a one-time code; Mastercard purchases need no hosted approval.
+ Creation returns a card_enrollment action: the cardholder enters the card and their email on a Kernel-hosted page, and the wallet connects once the card is stored. Kernel then enrolls it for an agentic network token when the issuer supports it. The card number never reaches Kernel. The connected wallet's payment_methods expansion lists the card; capabilities.single_use_card.eligible is false, with a network_token_* reason, until the card has a network token, and authorize returns 400 for such a card unless Kernel supports checkout delivery without a network token for that card. In that case, fill types the checkout details. Visa purchases require the cardholder to complete a spend_approval action before Kernel issues a one-time code; Mastercard purchases need no hosted approval.
"""
provider: Required[Literal["kernel"]]
From 766dd6e961fd36f580540a535cad76e659ecafee Mon Sep 17 00:00:00 2001
From: "kernel-internal[bot]"
<260533166+kernel-internal[bot]@users.noreply.github.com>
Date: Thu, 15 Oct 2026 17:53:56 +0000
Subject: [PATCH 360/366] feat: Remove Link expires_at, return 400 for missing
credential values, and log fill failures
Stainless-Generated-From: 88ca5c4e49b3c7f3efdec7aedee539be511afa02
---
src/kernel/types/vaults/card_vault_item_spec.py | 2 --
src/kernel/types/vaults/card_vault_item_spec_param.py | 2 --
tests/api_resources/vaults/test_items.py | 4 ----
3 files changed, 8 deletions(-)
diff --git a/src/kernel/types/vaults/card_vault_item_spec.py b/src/kernel/types/vaults/card_vault_item_spec.py
index 9224fe34..f6b471f8 100644
--- a/src/kernel/types/vaults/card_vault_item_spec.py
+++ b/src/kernel/types/vaults/card_vault_item_spec.py
@@ -90,8 +90,6 @@ class LinkCardVaultItemSpec(BaseModel):
wallet: str
"""Connected Link wallet item key."""
- expires_at: Optional[int] = None
-
line_items: Optional[List[LinkCardVaultItemSpecLineItem]] = None
metadata: Optional[Dict[str, str]] = None
diff --git a/src/kernel/types/vaults/card_vault_item_spec_param.py b/src/kernel/types/vaults/card_vault_item_spec_param.py
index 08cd8545..6e3ad521 100644
--- a/src/kernel/types/vaults/card_vault_item_spec_param.py
+++ b/src/kernel/types/vaults/card_vault_item_spec_param.py
@@ -90,8 +90,6 @@ class LinkCardVaultItemSpec(TypedDict, total=False):
wallet: Required[str]
"""Connected Link wallet item key."""
- expires_at: int
-
line_items: Iterable[LinkCardVaultItemSpecLineItem]
metadata: Dict[str, str]
diff --git a/tests/api_resources/vaults/test_items.py b/tests/api_resources/vaults/test_items.py
index 36ded6bc..96363302 100644
--- a/tests/api_resources/vaults/test_items.py
+++ b/tests/api_resources/vaults/test_items.py
@@ -120,7 +120,6 @@ def test_method_update_with_all_params_overload_1(self, client: Kernel) -> None:
"payment_method_id": "x",
"provider": "link",
"wallet": "wallet",
- "expires_at": 0,
"line_items": [
{
"name": "name",
@@ -1617,7 +1616,6 @@ def test_method_upsert_with_all_params_overload_2(self, client: Kernel) -> None:
"payment_method_id": "x",
"provider": "link",
"wallet": "wallet",
- "expires_at": 0,
"line_items": [
{
"name": "name",
@@ -2059,7 +2057,6 @@ async def test_method_update_with_all_params_overload_1(self, async_client: Asyn
"payment_method_id": "x",
"provider": "link",
"wallet": "wallet",
- "expires_at": 0,
"line_items": [
{
"name": "name",
@@ -3556,7 +3553,6 @@ async def test_method_upsert_with_all_params_overload_2(self, async_client: Asyn
"payment_method_id": "x",
"provider": "link",
"wallet": "wallet",
- "expires_at": 0,
"line_items": [
{
"name": "name",
From 483f3312824d07e5f315afe0ce5058d4fc4a6e99 Mon Sep 17 00:00:00 2001
From: "kernel-internal[bot]"
<260533166+kernel-internal[bot]@users.noreply.github.com>
Date: Thu, 15 Oct 2026 20:13:18 +0000
Subject: [PATCH 361/366] feat: Raise Kernel card amount cap to $10,000
Stainless-Generated-From: d575dc948e013eab030ba967ec036407d469a387
---
src/kernel/types/vaults/kernel_card_vault_item_spec.py | 2 +-
src/kernel/types/vaults/kernel_card_vault_item_spec_param.py | 2 +-
2 files changed, 2 insertions(+), 2 deletions(-)
diff --git a/src/kernel/types/vaults/kernel_card_vault_item_spec.py b/src/kernel/types/vaults/kernel_card_vault_item_spec.py
index 73fd646b..77912828 100644
--- a/src/kernel/types/vaults/kernel_card_vault_item_spec.py
+++ b/src/kernel/types/vaults/kernel_card_vault_item_spec.py
@@ -16,7 +16,7 @@ class KernelCardVaultItemSpec(BaseModel):
amount: int
"""
- Integer amount in minor currency units (at most 50000), bound to the one-time
+ Integer amount in minor currency units (at most 1000000), bound to the one-time
code.
"""
diff --git a/src/kernel/types/vaults/kernel_card_vault_item_spec_param.py b/src/kernel/types/vaults/kernel_card_vault_item_spec_param.py
index d18c4d8a..77d3084d 100644
--- a/src/kernel/types/vaults/kernel_card_vault_item_spec_param.py
+++ b/src/kernel/types/vaults/kernel_card_vault_item_spec_param.py
@@ -15,7 +15,7 @@ class KernelCardVaultItemSpecParam(TypedDict, total=False):
amount: Required[int]
"""
- Integer amount in minor currency units (at most 50000), bound to the one-time
+ Integer amount in minor currency units (at most 1000000), bound to the one-time
code.
"""
From 9869c911414a8be186a1a64682487fd38423fb92 Mon Sep 17 00:00:00 2001
From: "kernel-internal[bot]"
<260533166+kernel-internal[bot]@users.noreply.github.com>
Date: Thu, 15 Oct 2026 22:08:11 +0000
Subject: [PATCH 362/366] feat: Add atomic preconditions to pool network
updates
Stainless-Generated-From: e2681cdb0a54cbf44fecfc3096d09059ceca72f7
---
src/kernel/resources/browser_pools.py | 18 +++++++++++++
.../types/browser_pool_update_params.py | 11 ++++++++
tests/api_resources/test_browser_pools.py | 26 +++++++++++++++++++
3 files changed, 55 insertions(+)
diff --git a/src/kernel/resources/browser_pools.py b/src/kernel/resources/browser_pools.py
index 645c0d27..141f806e 100644
--- a/src/kernel/resources/browser_pools.py
+++ b/src/kernel/resources/browser_pools.py
@@ -254,6 +254,7 @@ def update(
*,
chrome_policy: Dict[str, object] | Omit = omit,
discard_all_idle: bool | Omit = omit,
+ expected_network: BrowserNetworkConfigParam | Omit = omit,
extensions: Iterable[BrowserExtension] | Omit = omit,
fill_rate_per_minute: int | Omit = omit,
headless: bool | Omit = omit,
@@ -302,6 +303,13 @@ def update(
configuration until it is discarded (by this flag on a later update, or by
flushing the pool).
+ expected_network: Optional optimistic concurrency precondition. Send the network configuration
+ from the last pool response, or {} when it has no network configuration. The
+ entire update is rejected with 409 stale_network if the saved network differs,
+ checked atomically with the write. Omit for unconditional updates. Explicit
+ empty lists differ from omitted lists; route targets must match the saved
+ references (resolved IDs, not equivalent proxy names).
+
extensions: If provided, replaces the extension list. Empty array clears all
previously-selected extensions. Omit this field to leave extensions unchanged.
@@ -397,6 +405,7 @@ def update(
{
"chrome_policy": chrome_policy,
"discard_all_idle": discard_all_idle,
+ "expected_network": expected_network,
"extensions": extensions,
"fill_rate_per_minute": fill_rate_per_minute,
"headless": headless,
@@ -911,6 +920,7 @@ async def update(
*,
chrome_policy: Dict[str, object] | Omit = omit,
discard_all_idle: bool | Omit = omit,
+ expected_network: BrowserNetworkConfigParam | Omit = omit,
extensions: Iterable[BrowserExtension] | Omit = omit,
fill_rate_per_minute: int | Omit = omit,
headless: bool | Omit = omit,
@@ -959,6 +969,13 @@ async def update(
configuration until it is discarded (by this flag on a later update, or by
flushing the pool).
+ expected_network: Optional optimistic concurrency precondition. Send the network configuration
+ from the last pool response, or {} when it has no network configuration. The
+ entire update is rejected with 409 stale_network if the saved network differs,
+ checked atomically with the write. Omit for unconditional updates. Explicit
+ empty lists differ from omitted lists; route targets must match the saved
+ references (resolved IDs, not equivalent proxy names).
+
extensions: If provided, replaces the extension list. Empty array clears all
previously-selected extensions. Omit this field to leave extensions unchanged.
@@ -1054,6 +1071,7 @@ async def update(
{
"chrome_policy": chrome_policy,
"discard_all_idle": discard_all_idle,
+ "expected_network": expected_network,
"extensions": extensions,
"fill_rate_per_minute": fill_rate_per_minute,
"headless": headless,
diff --git a/src/kernel/types/browser_pool_update_params.py b/src/kernel/types/browser_pool_update_params.py
index 8374568e..4d366884 100644
--- a/src/kernel/types/browser_pool_update_params.py
+++ b/src/kernel/types/browser_pool_update_params.py
@@ -44,6 +44,17 @@ class BrowserPoolUpdateParams(TypedDict, total=False):
flushing the pool).
"""
+ expected_network: BrowserNetworkConfigParam
+ """Optional optimistic concurrency precondition.
+
+ Send the network configuration from the last pool response, or {} when it has no
+ network configuration. The entire update is rejected with 409 stale_network if
+ the saved network differs, checked atomically with the write. Omit for
+ unconditional updates. Explicit empty lists differ from omitted lists; route
+ targets must match the saved references (resolved IDs, not equivalent proxy
+ names).
+ """
+
extensions: Iterable[BrowserExtension]
"""If provided, replaces the extension list.
diff --git a/tests/api_resources/test_browser_pools.py b/tests/api_resources/test_browser_pools.py
index 863c39c2..5f2e3b47 100644
--- a/tests/api_resources/test_browser_pools.py
+++ b/tests/api_resources/test_browser_pools.py
@@ -188,6 +188,19 @@ def test_method_update_with_all_params(self, client: Kernel) -> None:
id_or_name="id_or_name",
chrome_policy={"foo": "bar"},
discard_all_idle=False,
+ expected_network={
+ "allowed_hosts": ["example.com", "*.example.com"],
+ "private_hosts": ["*.example.ts.net", "100.64.0.0/10"],
+ "proxy_routes": [
+ {
+ "hosts": ["string"],
+ "proxy": {
+ "id": "x",
+ "name": "x",
+ },
+ }
+ ],
+ },
extensions=[
{
"id": "id",
@@ -744,6 +757,19 @@ async def test_method_update_with_all_params(self, async_client: AsyncKernel) ->
id_or_name="id_or_name",
chrome_policy={"foo": "bar"},
discard_all_idle=False,
+ expected_network={
+ "allowed_hosts": ["example.com", "*.example.com"],
+ "private_hosts": ["*.example.ts.net", "100.64.0.0/10"],
+ "proxy_routes": [
+ {
+ "hosts": ["string"],
+ "proxy": {
+ "id": "x",
+ "name": "x",
+ },
+ }
+ ],
+ },
extensions=[
{
"id": "id",
From 30f88e18b045f3fedbe1bd79d3125db105dd725f Mon Sep 17 00:00:00 2001
From: "kernel-internal[bot]"
<260533166+kernel-internal[bot]@users.noreply.github.com>
Date: Thu, 15 Oct 2026 22:52:10 +0000
Subject: [PATCH 363/366] feat: Allow replacing vault cards whose provider
outcome is unknown
Stainless-Generated-From: eb64d6365c4ab1f4031fb1c725d53c3486e45882
---
src/kernel/resources/vaults/items.py | 54 ++++++++++---------
.../types/vaults/card_vault_item_state.py | 14 ++---
src/kernel/types/vaults/kernel_card_state.py | 10 ++--
3 files changed, 41 insertions(+), 37 deletions(-)
diff --git a/src/kernel/resources/vaults/items.py b/src/kernel/resources/vaults/items.py
index 63991b12..209c9e86 100644
--- a/src/kernel/resources/vaults/items.py
+++ b/src/kernel/resources/vaults/items.py
@@ -154,10 +154,10 @@ def update(
request, subject to atomic provider approval checks; omitted optional fields
remain unchanged and explicit empty lists clear them. Wallet/provider binding
and unsupported fields cannot change after authorization starts. An uncertain
- update enters recovery_required and must not be retried. Checkout cards may be
- edited between authorizations. A Link card authorized with browser_id and
- page_url cannot be changed; delete it and create a new card with the updated
- details.
+ update enters recovery_required; delete the card and create a new one rather
+ than retrying the update. Checkout cards may be edited between authorizations. A
+ Link card authorized with browser_id and page_url cannot be changed; delete it
+ and create a new card with the updated details.
Args:
spec: Live Link card request. Test-mode card creation is not supported. The spec can
@@ -205,10 +205,10 @@ def update(
request, subject to atomic provider approval checks; omitted optional fields
remain unchanged and explicit empty lists clear them. Wallet/provider binding
and unsupported fields cannot change after authorization starts. An uncertain
- update enters recovery_required and must not be retried. Checkout cards may be
- edited between authorizations. A Link card authorized with browser_id and
- page_url cannot be changed; delete it and create a new card with the updated
- details.
+ update enters recovery_required; delete the card and create a new one rather
+ than retrying the update. Checkout cards may be edited between authorizations. A
+ Link card authorized with browser_id and page_url cannot be changed; delete it
+ and create a new card with the updated details.
Args:
version: Expected current item version from the latest read.
@@ -321,11 +321,12 @@ def delete(
Unresolved payment operations normally block deletion, including operations on
child cards of a wallet. Deleting a connected Kernel wallet first blocks new
payments on it, then removes its enrolled card. If that fails, the wallet is
- kept and keeps refusing payments; retry the deletion. An AgentCard card in
- recovery_required whose checkout create response returned no authorization ID
- may be explicitly abandoned by deleting that card directly; deleting its wallet
- or vault remains blocked. Deleting or recreating an item is not proof that a
- payment did not occur. Deleting a managed auth credential item leaves the
+ kept and keeps refusing payments; retry the deletion. A Link or Kernel card in
+ recovery_required for issuance, or an AgentCard card in recovery_required whose
+ checkout create response returned no authorization ID, may be explicitly
+ abandoned by deleting that card directly; deleting its wallet or vault remains
+ blocked until the card is deleted. Deleting or recreating an item is not proof
+ that a payment did not occur. Deleting a managed auth credential item leaves the
connection and its saved credential unchanged.
Args:
@@ -1526,10 +1527,10 @@ async def update(
request, subject to atomic provider approval checks; omitted optional fields
remain unchanged and explicit empty lists clear them. Wallet/provider binding
and unsupported fields cannot change after authorization starts. An uncertain
- update enters recovery_required and must not be retried. Checkout cards may be
- edited between authorizations. A Link card authorized with browser_id and
- page_url cannot be changed; delete it and create a new card with the updated
- details.
+ update enters recovery_required; delete the card and create a new one rather
+ than retrying the update. Checkout cards may be edited between authorizations. A
+ Link card authorized with browser_id and page_url cannot be changed; delete it
+ and create a new card with the updated details.
Args:
spec: Live Link card request. Test-mode card creation is not supported. The spec can
@@ -1577,10 +1578,10 @@ async def update(
request, subject to atomic provider approval checks; omitted optional fields
remain unchanged and explicit empty lists clear them. Wallet/provider binding
and unsupported fields cannot change after authorization starts. An uncertain
- update enters recovery_required and must not be retried. Checkout cards may be
- edited between authorizations. A Link card authorized with browser_id and
- page_url cannot be changed; delete it and create a new card with the updated
- details.
+ update enters recovery_required; delete the card and create a new one rather
+ than retrying the update. Checkout cards may be edited between authorizations. A
+ Link card authorized with browser_id and page_url cannot be changed; delete it
+ and create a new card with the updated details.
Args:
version: Expected current item version from the latest read.
@@ -1693,11 +1694,12 @@ async def delete(
Unresolved payment operations normally block deletion, including operations on
child cards of a wallet. Deleting a connected Kernel wallet first blocks new
payments on it, then removes its enrolled card. If that fails, the wallet is
- kept and keeps refusing payments; retry the deletion. An AgentCard card in
- recovery_required whose checkout create response returned no authorization ID
- may be explicitly abandoned by deleting that card directly; deleting its wallet
- or vault remains blocked. Deleting or recreating an item is not proof that a
- payment did not occur. Deleting a managed auth credential item leaves the
+ kept and keeps refusing payments; retry the deletion. A Link or Kernel card in
+ recovery_required for issuance, or an AgentCard card in recovery_required whose
+ checkout create response returned no authorization ID, may be explicitly
+ abandoned by deleting that card directly; deleting its wallet or vault remains
+ blocked until the card is deleted. Deleting or recreating an item is not proof
+ that a payment did not occur. Deleting a managed auth credential item leaves the
connection and its saved credential unchanged.
Args:
diff --git a/src/kernel/types/vaults/card_vault_item_state.py b/src/kernel/types/vaults/card_vault_item_state.py
index cead6d53..37dbdecd 100644
--- a/src/kernel/types/vaults/card_vault_item_state.py
+++ b/src/kernel/types/vaults/card_vault_item_state.py
@@ -47,13 +47,13 @@ class LinkCardState(BaseModel):
status: Literal[
"requested", "pending_authorization", "ready", "consumed", "expired", "declined", "recovery_required"
]
- """recovery_required means an original provider operation has an unresolved
- outcome.
-
- Do not retry, delete, or replace it. Known references may be observed safely,
- but unknown creation without an ID and uncertain card-material retrieval require
- manual reconciliation with the provider or support. There is no reset or
- caller-asserted reconciliation operation.
+ """
+ recovery_required means an original provider operation has an unresolved outcome
+ and the card cannot be used. Reads keep observing a known provider request and
+ may clear the recovery. To continue, delete this card and create a new one;
+ deleting cancels the original request when its ID is known and keeps the
+ recovery evidence. The user may see an approval request for the original card
+ and should approve only the new one.
"""
domains: Optional[List[str]] = None
diff --git a/src/kernel/types/vaults/kernel_card_state.py b/src/kernel/types/vaults/kernel_card_state.py
index e2bb6ad3..b2daa14f 100644
--- a/src/kernel/types/vaults/kernel_card_state.py
+++ b/src/kernel/types/vaults/kernel_card_state.py
@@ -51,10 +51,12 @@ class KernelCardState(BaseModel):
fresh local verification link; this does not approve spending or replay any
provider request. Unclassified legacy recovery and uncertain intent creation
cannot restart. Kernel never approves again or issues another code for the item
- automatically, and the item cannot be deleted or replaced until the original
- attempt is reconciled with support. When status_reason says the provider refused
- retrieval before acceptance, no code was issued and a later read retries.
- declined means the card network refused to issue a code.
+ automatically. To continue, delete this card and create a new purchase; the
+ cardholder approves the new purchase separately. Recovery while reporting a
+ merchant transaction cannot be deleted and must be reconciled with support. When
+ status_reason says the provider refused retrieval before acceptance, no code was
+ issued and a later read retries. declined means the card network refused to
+ issue a code.
"""
domains: Optional[List[str]] = None
From 84f71b89f84dea25464cbc22ed92786d73083419 Mon Sep 17 00:00:00 2001
From: "kernel-internal[bot]"
<260533166+kernel-internal[bot]@users.noreply.github.com>
Date: Thu, 15 Oct 2026 23:21:11 +0000
Subject: [PATCH 364/366] feat: Classify text captchas as token tasks and
Binance as a provider
Stainless-Generated-From: 583b7c7b7f489c6ba20a300c9a1cacc6c60a2e4e
---
.../browsers/browser_captcha_challenge_result_event.py | 6 ++++--
.../types/browsers/browser_captcha_solve_result_event.py | 9 +++++++--
.../browsers/browser_captcha_solve_started_event.py | 9 +++++++--
3 files changed, 18 insertions(+), 6 deletions(-)
diff --git a/src/kernel/types/browsers/browser_captcha_challenge_result_event.py b/src/kernel/types/browsers/browser_captcha_challenge_result_event.py
index bed8fa7c..00580a46 100644
--- a/src/kernel/types/browsers/browser_captcha_challenge_result_event.py
+++ b/src/kernel/types/browsers/browser_captcha_challenge_result_event.py
@@ -43,14 +43,16 @@ class Data(BaseModel):
"""
captcha_provider: Optional[
- Literal["hcaptcha", "recaptcha_v2", "recaptcha_v3", "turnstile", "geetest", "arkose", "human", "other"]
+ Literal[
+ "hcaptcha", "recaptcha_v2", "recaptcha_v3", "turnstile", "geetest", "arkose", "human", "binance", "other"
+ ]
] = None
"""Captcha product the challenge belongs to, not the service that solved it.
Enterprise reCAPTCHA variants are grouped into their version bucket
(recaptcha_v2 or recaptcha_v3), FunCaptcha uses arkose, press-and-hold
challenges served by HUMAN (formerly PerimeterX) use human, and unlisted
- products use other.
+ products, including text captchas a site draws itself, use other.
"""
challenge_id: Optional[str] = None
diff --git a/src/kernel/types/browsers/browser_captcha_solve_result_event.py b/src/kernel/types/browsers/browser_captcha_solve_result_event.py
index 1feb5d77..5d7a7360 100644
--- a/src/kernel/types/browsers/browser_captcha_solve_result_event.py
+++ b/src/kernel/types/browsers/browser_captcha_solve_result_event.py
@@ -29,14 +29,16 @@ class Data(BaseModel):
"""
captcha_provider: Optional[
- Literal["hcaptcha", "recaptcha_v2", "recaptcha_v3", "turnstile", "geetest", "arkose", "human", "other"]
+ Literal[
+ "hcaptcha", "recaptcha_v2", "recaptcha_v3", "turnstile", "geetest", "arkose", "human", "binance", "other"
+ ]
] = None
"""Captcha product the challenge belongs to, not the service that solved it.
Enterprise reCAPTCHA variants are grouped into their version bucket
(recaptcha_v2 or recaptcha_v3), FunCaptcha uses arkose, press-and-hold
challenges served by HUMAN (formerly PerimeterX) use human, and unlisted
- products use other.
+ products, including text captchas a site draws itself, use other.
"""
challenge_id: Optional[str] = None
@@ -53,6 +55,9 @@ class Data(BaseModel):
ERROR_CAPTCHA_UNSOLVABLE). Absent on success.
"""
+ media: Optional[Literal["image", "video"]] = None
+ """Captured hCaptcha recognition media, when known."""
+
task_id: Optional[str] = None
"""Opaque identifier shared with the matching captcha_solve_started."""
diff --git a/src/kernel/types/browsers/browser_captcha_solve_started_event.py b/src/kernel/types/browsers/browser_captcha_solve_started_event.py
index 12dd56e6..c17b9097 100644
--- a/src/kernel/types/browsers/browser_captcha_solve_started_event.py
+++ b/src/kernel/types/browsers/browser_captcha_solve_started_event.py
@@ -19,14 +19,16 @@ class Data(BaseModel):
"""Deprecated: use captcha_provider and task_kind."""
captcha_provider: Optional[
- Literal["hcaptcha", "recaptcha_v2", "recaptcha_v3", "turnstile", "geetest", "arkose", "human", "other"]
+ Literal[
+ "hcaptcha", "recaptcha_v2", "recaptcha_v3", "turnstile", "geetest", "arkose", "human", "binance", "other"
+ ]
] = None
"""Captcha product the challenge belongs to, not the service that solved it.
Enterprise reCAPTCHA variants are grouped into their version bucket
(recaptcha_v2 or recaptcha_v3), FunCaptcha uses arkose, press-and-hold
challenges served by HUMAN (formerly PerimeterX) use human, and unlisted
- products use other.
+ products, including text captchas a site draws itself, use other.
"""
challenge_id: Optional[str] = None
@@ -37,6 +39,9 @@ class Data(BaseModel):
continues. It does not indicate task ordering or challenge completion.
"""
+ media: Optional[Literal["image", "video"]] = None
+ """Captured hCaptcha recognition media, when known."""
+
task_id: Optional[str] = None
"""Opaque identifier shared with the matching captcha_solve_result."""
From 5a621135bad2661c79068027773ff32b4e14e0c9 Mon Sep 17 00:00:00 2001
From: "kernel-internal[bot]"
<260533166+kernel-internal[bot]@users.noreply.github.com>
Date: Fri, 16 Oct 2026 17:56:36 +0000
Subject: [PATCH 365/366] feat: chore(stlc): seal custom-code tracking files
Stainless-Generated-From: 0ff55d1efda4dd5e76891ee5c5aeb2d2ce83db55
---
src/kernel/resources/browser_pools.py | 22 ++++++++++---------
src/kernel/resources/browsers/browsers.py | 8 +++++--
src/kernel/types/browser_create_params.py | 5 ++++-
src/kernel/types/browser_memory.py | 2 +-
src/kernel/types/browser_memory_request.py | 2 +-
.../types/browser_pool_create_params.py | 5 ++++-
.../types/browser_pool_update_params.py | 10 ++++-----
tests/api_resources/test_browser_pools.py | 4 ++--
8 files changed, 35 insertions(+), 23 deletions(-)
diff --git a/src/kernel/resources/browser_pools.py b/src/kernel/resources/browser_pools.py
index 141f806e..61ad0977 100644
--- a/src/kernel/resources/browser_pools.py
+++ b/src/kernel/resources/browser_pools.py
@@ -119,7 +119,8 @@ def create(
kiosk_mode: If true, launches the browser in kiosk mode to hide address bar and tabs in live
view. Defaults to false.
- memory: Memory requested for headful browsers in this pool.
+ memory: Memory for browsers in this pool. Headless: 1GiB (default), 2GiB, or 4GiB.
+ Headful: 8GiB (default) or 16GiB.
name: Optional name for the browser pool. Must be unique within the project.
@@ -259,7 +260,7 @@ def update(
fill_rate_per_minute: int | Omit = omit,
headless: bool | Omit = omit,
kiosk_mode: bool | Omit = omit,
- memory: Literal["8GiB", "16GiB"] | Omit = omit,
+ memory: Literal["1GiB", "2GiB", "4GiB", "8GiB", "16GiB"] | Omit = omit,
name: str | Omit = omit,
network: BrowserNetworkConfigParam | Omit = omit,
profile: browser_pool_update_params.Profile | Omit = omit,
@@ -321,9 +322,9 @@ def update(
kiosk_mode: If provided, replaces whether browsers launch in kiosk mode.
- memory: Memory requested for newly-warmed headful browsers in this pool. Existing
- browsers retain their original allocation. Use discard_all_idle to replace idle
- browsers.
+ memory: Memory requested for newly-warmed browsers in this pool: 1GiB, 2GiB, or 4GiB for
+ headless, 8GiB or 16GiB for headful. Existing browsers retain their original
+ allocation. Use discard_all_idle to replace idle browsers.
name: If provided, replaces the pool name. Empty string is a no-op; the pool name
cannot be cleared or reset to empty once assigned.
@@ -785,7 +786,8 @@ async def create(
kiosk_mode: If true, launches the browser in kiosk mode to hide address bar and tabs in live
view. Defaults to false.
- memory: Memory requested for headful browsers in this pool.
+ memory: Memory for browsers in this pool. Headless: 1GiB (default), 2GiB, or 4GiB.
+ Headful: 8GiB (default) or 16GiB.
name: Optional name for the browser pool. Must be unique within the project.
@@ -925,7 +927,7 @@ async def update(
fill_rate_per_minute: int | Omit = omit,
headless: bool | Omit = omit,
kiosk_mode: bool | Omit = omit,
- memory: Literal["8GiB", "16GiB"] | Omit = omit,
+ memory: Literal["1GiB", "2GiB", "4GiB", "8GiB", "16GiB"] | Omit = omit,
name: str | Omit = omit,
network: BrowserNetworkConfigParam | Omit = omit,
profile: browser_pool_update_params.Profile | Omit = omit,
@@ -987,9 +989,9 @@ async def update(
kiosk_mode: If provided, replaces whether browsers launch in kiosk mode.
- memory: Memory requested for newly-warmed headful browsers in this pool. Existing
- browsers retain their original allocation. Use discard_all_idle to replace idle
- browsers.
+ memory: Memory requested for newly-warmed browsers in this pool: 1GiB, 2GiB, or 4GiB for
+ headless, 8GiB or 16GiB for headful. Existing browsers retain their original
+ allocation. Use discard_all_idle to replace idle browsers.
name: If provided, replaces the pool name. Empty string is a no-op; the pool name
cannot be cleared or reset to empty once assigned.
diff --git a/src/kernel/resources/browsers/browsers.py b/src/kernel/resources/browsers/browsers.py
index ec5f0f42..0333bfdd 100644
--- a/src/kernel/resources/browsers/browsers.py
+++ b/src/kernel/resources/browsers/browsers.py
@@ -239,7 +239,9 @@ def create(
kiosk_mode: If true, launches the browser in kiosk mode to hide address bar and tabs in live
view.
- memory: Memory for a headful, non-GPU browser session. Defaults to 8GiB.
+ memory:
+ Memory for a non-GPU browser. Headless: 1GiB (default), 2GiB, or 4GiB. Headful:
+ 8GiB (default) or 16GiB.
name: Optional human-readable name for the browser session, used to find it later in
the dashboard. Must be unique among active sessions within the project. Can be
@@ -954,7 +956,9 @@ async def create(
kiosk_mode: If true, launches the browser in kiosk mode to hide address bar and tabs in live
view.
- memory: Memory for a headful, non-GPU browser session. Defaults to 8GiB.
+ memory:
+ Memory for a non-GPU browser. Headless: 1GiB (default), 2GiB, or 4GiB. Headful:
+ 8GiB (default) or 16GiB.
name: Optional human-readable name for the browser session, used to find it later in
the dashboard. Must be unique among active sessions within the project. Can be
diff --git a/src/kernel/types/browser_create_params.py b/src/kernel/types/browser_create_params.py
index 3fe435d5..d773f8cc 100644
--- a/src/kernel/types/browser_create_params.py
+++ b/src/kernel/types/browser_create_params.py
@@ -63,7 +63,10 @@ class BrowserCreateParams(TypedDict, total=False):
"""
memory: BrowserMemoryRequest
- """Memory for a headful, non-GPU browser session. Defaults to 8GiB."""
+ """Memory for a non-GPU browser.
+
+ Headless: 1GiB (default), 2GiB, or 4GiB. Headful: 8GiB (default) or 16GiB.
+ """
name: str
"""
diff --git a/src/kernel/types/browser_memory.py b/src/kernel/types/browser_memory.py
index 31f362ee..ccdb8fd4 100644
--- a/src/kernel/types/browser_memory.py
+++ b/src/kernel/types/browser_memory.py
@@ -4,4 +4,4 @@
__all__ = ["BrowserMemory"]
-BrowserMemory: TypeAlias = Literal["1GiB", "2GiB", "6GiB", "8GiB", "12GiB", "16GiB"]
+BrowserMemory: TypeAlias = Literal["1GiB", "2GiB", "4GiB", "6GiB", "8GiB", "12GiB", "16GiB"]
diff --git a/src/kernel/types/browser_memory_request.py b/src/kernel/types/browser_memory_request.py
index f2363b71..8f853631 100644
--- a/src/kernel/types/browser_memory_request.py
+++ b/src/kernel/types/browser_memory_request.py
@@ -4,4 +4,4 @@
__all__ = ["BrowserMemoryRequest"]
-BrowserMemoryRequest: TypeAlias = Literal["8GiB", "16GiB"]
+BrowserMemoryRequest: TypeAlias = Literal["1GiB", "2GiB", "4GiB", "8GiB", "16GiB"]
diff --git a/src/kernel/types/browser_pool_create_params.py b/src/kernel/types/browser_pool_create_params.py
index 6850502a..bf05dd92 100644
--- a/src/kernel/types/browser_pool_create_params.py
+++ b/src/kernel/types/browser_pool_create_params.py
@@ -62,7 +62,10 @@ class BrowserPoolCreateParams(TypedDict, total=False):
"""
memory: BrowserMemoryRequest
- """Memory requested for headful browsers in this pool."""
+ """Memory for browsers in this pool.
+
+ Headless: 1GiB (default), 2GiB, or 4GiB. Headful: 8GiB (default) or 16GiB.
+ """
name: str
"""Optional name for the browser pool. Must be unique within the project."""
diff --git a/src/kernel/types/browser_pool_update_params.py b/src/kernel/types/browser_pool_update_params.py
index 4d366884..4b5c6a0f 100644
--- a/src/kernel/types/browser_pool_update_params.py
+++ b/src/kernel/types/browser_pool_update_params.py
@@ -75,11 +75,11 @@ class BrowserPoolUpdateParams(TypedDict, total=False):
kiosk_mode: bool
"""If provided, replaces whether browsers launch in kiosk mode."""
- memory: Literal["8GiB", "16GiB"]
- """Memory requested for newly-warmed headful browsers in this pool.
-
- Existing browsers retain their original allocation. Use discard_all_idle to
- replace idle browsers.
+ memory: Literal["1GiB", "2GiB", "4GiB", "8GiB", "16GiB"]
+ """
+ Memory requested for newly-warmed browsers in this pool: 1GiB, 2GiB, or 4GiB for
+ headless, 8GiB or 16GiB for headful. Existing browsers retain their original
+ allocation. Use discard_all_idle to replace idle browsers.
"""
name: str
diff --git a/tests/api_resources/test_browser_pools.py b/tests/api_resources/test_browser_pools.py
index 5f2e3b47..04734119 100644
--- a/tests/api_resources/test_browser_pools.py
+++ b/tests/api_resources/test_browser_pools.py
@@ -210,7 +210,7 @@ def test_method_update_with_all_params(self, client: Kernel) -> None:
fill_rate_per_minute=0,
headless=False,
kiosk_mode=True,
- memory="8GiB",
+ memory="1GiB",
name="my-pool",
network={
"allowed_hosts": ["example.com", "*.example.com"],
@@ -779,7 +779,7 @@ async def test_method_update_with_all_params(self, async_client: AsyncKernel) ->
fill_rate_per_minute=0,
headless=False,
kiosk_mode=True,
- memory="8GiB",
+ memory="1GiB",
name="my-pool",
network={
"allowed_hosts": ["example.com", "*.example.com"],
From 14852b514d091c14368fcad7a3fa6383b3b63e1e Mon Sep 17 00:00:00 2001
From: "kernel-internal[bot]"
<260533166+kernel-internal[bot]@users.noreply.github.com>
Date: Fri, 16 Oct 2026 18:01:23 +0000
Subject: [PATCH 366/366] release: 0.127.0
---
.release-please-manifest.json | 2 +-
CHANGELOG.md | 14 ++++++++++++++
pyproject.toml | 2 +-
src/kernel/_version.py | 2 +-
4 files changed, 17 insertions(+), 3 deletions(-)
diff --git a/.release-please-manifest.json b/.release-please-manifest.json
index f38100a4..47775e91 100644
--- a/.release-please-manifest.json
+++ b/.release-please-manifest.json
@@ -1,3 +1,3 @@
{
- ".": "0.126.0"
+ ".": "0.127.0"
}
\ No newline at end of file
diff --git a/CHANGELOG.md b/CHANGELOG.md
index d69a64ae..dcd39316 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,19 @@
# Changelog
+## [0.127.0](https://github.com/kernel/kernel-python-sdk/compare/v0.126.0...v0.127.0) (2026-10-16)
+
+
+### Features
+
+* Add atomic preconditions to pool network updates ([9869c91](https://github.com/kernel/kernel-python-sdk/commit/9869c911414a8be186a1a64682487fd38423fb92))
+* Add passkey credential fields and PATCH field add/remove ([84a69ad](https://github.com/kernel/kernel-python-sdk/commit/84a69ad74c46ddcdd87acd88a23dd5330adf555b))
+* Allow replacing vault cards whose provider outcome is unknown ([30f88e1](https://github.com/kernel/kernel-python-sdk/commit/30f88e18b045f3fedbe1bd79d3125db105dd725f))
+* chore(stlc): seal custom-code tracking files ([5a62113](https://github.com/kernel/kernel-python-sdk/commit/5a621135bad2661c79068027773ff32b4e14e0c9))
+* Classify text captchas as token tasks and Binance as a provider ([84f71b8](https://github.com/kernel/kernel-python-sdk/commit/84f71b89f84dea25464cbc22ed92786d73083419))
+* Deliver Kernel card details through fill ([eed3e7b](https://github.com/kernel/kernel-python-sdk/commit/eed3e7ba06c46d3cfa6048d9f02c3d8fff6046e6))
+* Raise Kernel card amount cap to $10,000 ([483f331](https://github.com/kernel/kernel-python-sdk/commit/483f3312824d07e5f315afe0ce5058d4fc4a6e99))
+* Remove Link expires_at, return 400 for missing credential values, and log fill failures ([766dd6e](https://github.com/kernel/kernel-python-sdk/commit/766dd6e961fd36f580540a535cad76e659ecafee))
+
## [0.126.0](https://github.com/kernel/kernel-python-sdk/compare/v0.125.0...v0.126.0) (2026-10-14)
diff --git a/pyproject.toml b/pyproject.toml
index e13b78ea..d2de2e3b 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -1,6 +1,6 @@
[project]
name = "kernel"
-version = "0.126.0"
+version = "0.127.0"
description = "The official Python library for the kernel API"
dynamic = ["readme"]
license = "Apache-2.0"
diff --git a/src/kernel/_version.py b/src/kernel/_version.py
index 20748255..d351e0a2 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.126.0" # x-release-please-version
+__version__ = "0.127.0" # x-release-please-version